ネコのために鐘は鳴る

寺院に住み着くパソコ〇好き

(C#) Source Generator で typeof の型名を取得する

C# 9.0 から使える Source Generator、良いですね。アプリケーションを作る側の人が Source Generator を作ることは少ないと思いますが、ライブラリを作る側の人には便利ですよね。

基本的な使い方・作り方はここでは解説しません。公式の説明公式サンプルを見たり、調べたりすれば出てきます。少ないのが残念なのですけれども。

Source Generator で属性を作って、属性の情報からコードを生成したりするというのは典型的な使い方ですね。

// ユーザーコード
[GenerateProperty(typeof(SomeType), "Prop1")]
partial class TargetType
{
}
// ------------------------
// 生成されたコード
partial class TargetType
{
    public SomeType Prop1 { get; set; }
}

上記のサンプルコードに特に意味はありません、ただの説明用の例です。GeneratePropertyという属性を使ってプロパティを自動生成してくれるソースジェネレーターを想定してください。(GeneratePropertyという属性自体もソースジェネレーターが生成した属性です。BCL にこんな属性があるわけではないです。)

シンタックスの読み取りはISourceGeneratorから提供される Roslyn の API を使うしかないのですが、ネットで検索しても情報が少なすぎて非常に苦労します。日本語で検索してもほとんど出てきません。英語でもかなり情報は少ないです。

このソースジェネレータ―を作るためには、typeof(SomeType)の部分から"SomeType"を読み取ってくる必要があるのですが、その方法の情報がなさ過ぎて苦労したので覚書です。

以下、ソースジェネレータ―の実装の一部。(例なので雑です。実際はもうちょっとちゃんと書いてください)

[2021/02/01 追記] サンプルコードと説明が色々と間違っていました。完全に別のものと勘違いしていました。以下、正しく書きなおしました。

private const string AttributeDef = 
@"namespace Sample
{
    internal sealed class GeneratePropertyAttribute : System.Attribute
    {

    }
}
";

private readonly Regex _attrRegex =
    new Regex(@"^(global::)?(Sample\.)?GenerateProperty(Attribute)?$");

public void Execute(GeneratorExecutionContext context)
{
    // ここで最初に属性自体を出力する (略)

    // シンタックスを総なめして属性を探す
    var compilation = context.Compilation;
    var attrs = compilation
        .SyntaxTrees
        .SelectMany(s => s.GetRoot().DescendantNodes())
        .Where(s => s.IsKind(SyntaxKind.Attribute))
        .OfType<AttributeSyntax>()
        .Where(s => _attrRegex.IsMatch(s.Name.ToString()))
        .ToArray();
    foreach(AttributeSyntax attr in attrs) {
        var attrSemantic = compilation.GetSemanticModel(attr.SyntaxTree);
        var expr = attr.ArgumentList.Arguments[0].Expression;

        // これが typeof の中身の型のフルネーム "SomeNamespace.SomeType"
        string typeName = attrSemantic
            .GetSymbolInfo((expr as TypeOfExpressionSyntax).Type)
            .Symbol!.ToString();

        // ↓ 間違い
        // string typeName = attrSemantic.GetConstantValue(expr).ToString();

        // 以下略 (生成コードの出力)
    }
}

SyntaxReceiverを使う方法もありますが、私はサクッと LINQ で syntax tree を全部なめるのが好きなので LINQ を使いました。どっちが良いのかはわかりませんが、特に問題はないでしょう。

ここで取得できる型名の文字列は、書いたソースコードの expression がそのまま取得できるので、typeof(SomeNamespace.SomeType)と書いた場合は"SomeNamespace.SomeType"となります。

セマンティクスをきちんと考慮してくれるため、typeof(SomeType)と書かれていた場合でも、きちんと名前空間付きのフルネーム"SomeNamespace.SomeType"を取得できます。

属性引数にtypeofを指定するのはけっこう頻出パターンだと思うので、日本語で情報があると助かる人が世の中に1人ぐらいはいるんじゃないかな。(いてほしいなぁ)

(C#) 型の外部から疑似的に interface を追加する

interface を後付けしたい

interface は C#ポリモーフィズムを実現するために、メソッド・プロパティの宣言だけを決めておき、実際の挙動は型の実装者に任せるというものです。今更説明するまでもないですね、普段みなさんが使ってるやつです。

普通は型定義の段階で interface を継承させて使うのですが、例えば外部ライブラリで定義されている型に自作の interface を継承させることはできません。

public class Apple  // 外部ライブラリのクラス
{
    public string Name => "apple";
}

public class Orange  // 外部ライブラリのクラス
{
    public string Name => "orange";
}

public interface IName  // 自作の interface
{
    string Name { get; }
}

上記のようなAppleOrangeクラスが外部ライブラリにあって、これらを統一的に扱うためにINameという自作の interface をつけたいとしても、当然外部ライブラリ内にあるクラスに勝手に継承を追加させることはできません。

 

今回は、これをローカル関数と関数ポインタ (と必要ならジェネリクス) を使って疑似的に実現させます。

public unsafe readonly struct IName
{
    private readonly object _obj;
    private readonly delegate*<object, string> _func;

    public string Name => _func(_obj);

    private IName(Apple apple)
    {
        _obj = apple;
        _func = &GetName;
        static string GetName(object obj) => Unsafe.As<Apple>(obj).Name;
    }
    
    private IName(Orange orange)
    {
        _obj = orange;
        _func = &GetName;
        static string GetName(object obj) => Unsafe.As<Orange>(obj).Name;
    }

    public static implicit operator IName(Apple apple) => new IName(apple);
    public static implicit operator IName(Orange orange) => new IName(orange);
    public static explicit operator Apple(in IName iName) => (Apple)iName._obj;
    public static explicit operator Orange(in IName iName) => (Orange)iName._obj;
    // サンプルなので Equals とか GetHashCode とかの override は省略
}

コンストラクタ内で、元になるオブジェクトと実装を呼ぶローカル関数の関数ポインタをフィールドに格納しておき、疑似インターフェースの定義したプロパティの呼び出し時に、関数ポインタ経由で処理を呼びます。ついでに、対象となる型とのキャストを作っておきます。

ローカル関数内で使っているUnsafe.As<Apple>(obj)(Apple)objと同義ですが、一切の安全チェックを省いた最速のキャストです。本来は危険なのですが、今回の場合はobjは絶対に対象の型であることが静的に保証できているため問題ありません。

一見意味不明ですが、短いですのでよーくコードを眺めてください。理解できるはずです。(というかこれ以上説明することがない)

 

原理としては、元になったオブジェクトの型によって関数ポインタ経由で処理を変えています。ここでよく考えてみてください、これって仮想関数テーブルから間接参照で処理に飛ぶというインターフェースの原理そのものですよね。

あたかも本物の interface のように振舞い、しかも原理まで同じなのでパフォーマンスもほぼ同等です。

Apple apple = new Apple();
IName iName = apple;    // 暗黙的キャスト

一応、INameという型名にしましたが、これは interface のプレフィックスである Iを冠していいのかは不明です。何しろ interface ではなく構造体ですので。というか、こんな実装方法はたぶん想定されていないため、命名方法の慣習も何もありません、無敵です。

ジェネリクス型にジェネリクス挙動をさせる

ここからは、先ほどまでとは別のことをします。記事のタイトルとして「型の外部から interface を追加する」というタイトルをつけましたが、本来この記事で言いたかった内容は「ローカル関数×関数ポインタ×ジェネリクスによるハックな実装」です。型の外部から interface を追加するのはその一例です。

では、先ほどと似た原理を使って、非ジェネリクス型にジェネリクス (のような) 挙動をさせてみましょう。

public unsafe class MyClass
{
    private readonly delegate*<void> _say;

    private MyClass(delegate*<void> say)
    {
        _say = say;
    }

    public void SayGenericsType() => _say();

    public static MyClass Create<T>()
    {
        if(typeof(T) == typeof(int))
            return new MyClass(&SayInt);
        else if(typeof(T) == typeof(double))
            return new MyClass(&SayDouble);
        else
            return new MyClass(&SayOtherTypes);

        static void SayInt() => Console.WriteLine("I am int");
        static void SayDouble() => Console.WriteLine("I am double");
        static void SayOtherTypes() => Console.WriteLine($"I am {typeof(T).FullName}");
    }
}

上記のMyClassジェネリクス型ではないですが、SayGenericsType()メソッドを呼ぶとジェネリクス挙動をします。

var obj1 = MyClass.Create<int>();
var obj2 = MyClass.Create<double>();
var obj3 = MyClass.Create<string>();

obj1.SayGenericsType();   // "I am int"
obj2.SayGenericsType();   // "I am double"
obj3.SayGenericsType();   // "I am System.String"

コンストラクタに型引数<T>を宣言することはできないため、Createという静的メソッドをファクトリーメソッドとしています。ファクトリーメソッド内で型によって関数ポインタを変えることで、挙動を操っています。そして、このたくさん並んだif分岐は JIT コンパイル時に分岐が消えます。これについては以前の記事 (3種類の定数と JIT コンパイル時最適化) で解説しているため、気になった方は読んでください。

これのすごい所は実行時型Typeを保存しているわけではなく、ジェネリクス<T>を静的型制約として復元できる点です。上記のobj3は生成時にstringを指定していますが、後でメソッドを呼ぶときにはどこにもstringと指定していません。にもかかわらず、SayGenericsType()メソッドの呼び出し時にジェネリクス型が復元されます。状態をインスタンスに保持するという点で、これはラムダ式の変数キャプチャに似ています。

実際、これはCreateメソッドの<T>がローカル関数のSayOtherTypesに入り込んでいる (これをキャプチャと呼んでいいのかどうかはわかりません) のですが、<T>のキャプチャがラムダ式の変数キャプチャと違う点は、パフォーマンス上のコストがないことです。変数キャプチャが動的状態 (値) をインスタンスに保存するのに対し、<T>のキャプチャは静的状態 (型制約) をインスタンスに保存するのです。

ローカル関数にジェネリクス<T>をキャプチャしつつ関数ポインタに保持し、オブジェクト生成時の<T>インスタンスに保存する、という C# の言語機能をパズルのように組み合わせた何とも珍妙な実装です。記事タイトルを「ローカル関数×関数ポインタ×ジェネリクスによるハックな実装」と題したかった意味が分かっていただけますでしょうか……?

ISpan<T>を作る

この記事の本命です。配列T[]やリストList<T>Span<T>を生成することができます。(公式にList<T>からSpan<T>を生成できるのは .NET5 以降からですが、ちょっとUnsafeを使うとそれ以前のバージョンでもできます。詳しくは過去記事を参照)

もし、C# 1.0 からSpan<T>が存在していたら、おそらく以下のようなISpan<T>というインターフェイスが存在したことでしょう。(少なくとも、私が C# の言語設計者なら作ります。)

public interface ISpan<T>
{
    Span<T> AsSpan();
}

ところが、Span<T>自体が割と最近登場した型で、残念ながらそんなものは存在しません。そこで、ここまで解説してきた外付け疑似インターフェースを作ります。

public unsafe readonly struct ISpan<T>
{
    private readonly object _obj;
    private readonly delegate*<object, Span<T>> _asSpan;

    public Span<T> AsSpan() => _asSpan(_obj);

    private ISpan(T[] array)
    {
        _obj = array;
        _asSpan = &AsSpan;
        static Span<T> AsSpan(object obj) => Unsafe.As<T[]>(obj).AsSpan();
    }

    private ISpan(List<T> list)
    {
        _obj = list;
        _asSpan = &AsSpan;
        static Span<T> AsSpan(object obj)
        {
            return CollectionsMarshal.AsSpan(Unsafe.As<List<T>>(obj));
        }
    }

    public static implicit operator ISpan<T>(T[] array) => new ISpan<T>(array);
    public static implicit operator ISpan<T>(List<T> list) => new ISpan<T>(list);
    public static explicit operator T[](in ISpan<T> iSpan) => (T[])iSpan._obj;
    public static explicit operator List<T>(in ISpan<T> iSpan) => (List<T>)iSpan._obj;
    // サンプルなので Equals とか GetHashCode とかの override は省略
}

原理は先ほどまでと同じなので分かるかと思います。これで、配列とリストを「Span<T>を生成することができる」という意味のインターフェースで共通に扱えますね。やったね。

 

そして、今回のオチです。このコード、本記事執筆現在の最新のコンパイラ (Visual Studio 2019 16.8.2) でコンパイルエラーになります。文法、実装内容ともに完全に正しい C# のコードです。まさかのコンパイラのバグを踏みました。

エラー内容は驚きの「CS8751 C# コンパイラで内部エラーが発生しました。」です。初めて見た。

おそらく、関数ポインタdelegate*の戻り値としてSpan<T>を指定しているのがまずいようです。(現状、言語仕様としては禁止されていません。) 関数ポインタもref structもどちらも最近 C# に入った新しめの機能なので枯れておらず、言語設計の未検討の仕様を突いてしまったと思われます。腐ってやがる。早すぎたんだ。

一応 roslyn のリポジトリに issue を出しました。

github.com

以上、外付けできる疑似 interface の作り方と、ローカル関数×関数ポインタ×ジェネリクスなミラクル実装でISpan<T>を作ろうとしたらコンパイラのバグを踏んだ話でした。。。

[追記] Memory<T>について

ご存知かと思いますが「Span<T>を生成するもの」という意味において、似たような機能のものとしてSystem.Memory<T>が公式で存在しています。しかし、私はあれが嫌いです。オーバーヘッドが大きくて遅い。独自型をMemory<T>対応させるためにはMemoryManager<T>を使うのですが実装がイケてなさすぎる。

私のライブラリで UnmanagedArray というライブラリがあるのですが、あれがMemory<T>に対応していないのもそんな理由です。

なら、私ならどう実装するのか?と言われると、目的に応じてケースバイケースなので回答を1つには出せませんが、答えの一つが先程の疑似ISpan<T>です。アンマネージドメモリの生ポインタとかも考慮に入れて考えると、あれでは boxing を挟んでしまうのでやや不十分なのですが、そこはケースバイケースという便利な単語でお茶を濁します。

(C#) 3種類の定数と JIT コンパイル時最適化

この記事は「C# Advent Calendar 2020」の24日目の記事です。

3種類の定数と分岐削除

C# には3種類の定数があります。とはいえ、実際に C#ソースコードとして目に見えるのは1種類だけでconstとして書くものです。それぞれ順に見ていきましょう。

C#定数 (const)

最も一般的な定数はconstキーワードで定義するものです。

void Hoge()
{
    const int a = 5;
    if(a == 5)
        Console.WriteLine("a is 5");
    else
        Console.WriteLine("a is not 5");
}

constC# の静的なソースコードの状態で定数であることを表現します。上の例ではconstは変数として定義されますが実際にはコンパイルすると変数はなくなり、aリテラル5が直接埋め込まれます。静的に5であることをコンパイラが認識できるため、上記のifの分岐はなくなり、Console.WriteLine("a is 5");だけが残ります。

IL 定数

次に IL 定数です。const キーワードをつけることはできませんが、コンパイル時に定数であることが確定しているものです。

string Foo()
{
    const string a = nameof(Foo);
    string b = $"{a}{a}";  // not 'const' but IL const
    return b;
}

一番わかりやすい IL 定数は string interpolation ($文字列) です。まずnameof演算子は右辺値constですので、上記のaC#定数です。そしてbconstではありません。(constにするとコンパイルエラー)

しかし、bコンパイル時に"FooFoo"であることが確定しています。したがって、コンパイル後の IL を見ると、直接定数で"FooFoo"が埋め込まれていることが確認できます。コンパイル後の IL は以下の通りです。

// IL
.method private hidebysig instance string Foo () cil managed 
{
    .maxstack 8
    IL_0000: ldstr "FooFoo"
    IL_0005: ret
}

みごとに string interpolation が消えて"FooFoo"が IL 内で定数となっているのがわかります。

では、C# 定数の時と同じように分岐が消えるのかどうか確認してみましょう。以下のコードを見てください。

int Bar()
{
    return $"{nameof(Bar)}{nameof(Bar)}" == "BarBar" ? 1 : 0;
}

このメソッドは必ず1を返すことは見て分かるかと思います。 ではこれをコンパイルした IL を見てましょう。

// IL
.method private hidebysig instance int32 Bar () cil managed
{
    .maxstack 8
    IL_0000: ldstr "BarBar"
    IL_0005: ldstr "BarBar"
    IL_000a: call bool [System.Private.CoreLib]System.String::op_Equality(string, string)
    IL_000f: brtrue.s IL_0013    // if 分岐

    IL_0011: ldc.i4.0
    IL_0012: ret

    IL_0013: ldc.i4.1
    IL_0014: ret
    }

あれ……?分岐が消えていませんね。上記の IL_000f のbrtrue.s命令が C#ifに相当します。一応 JIT コンパイル結果の x86-64 アセンブリまで見ておきましょう。

アセンブリを見てスラスラ理解できる宇宙人の方は良いですが、人間には厳しいので隣に高級言語的な疑似コードをつけておきました。

; x86-64 asm (on 64 bits)
Bar()
    L0000: sub rsp, 0x28
    L0004: mov rcx, 0x287a79f8938
    L000e: mov rdx, [rcx]
    L0011: mov rcx, rdx
    L0014: call System.String.Equals(System.String, System.String)
    L0019: test eax, eax            ; if("BarBar" == "BarBar")
    L001b: jne short L0024          ;     goto L0024 ┐
    L001d: xor eax, eax             ;                |
    L001f: add rsp, 0x28            ;                |
    L0023: ret                      ; return 0       |
    L0024: mov eax, 1               ;             <--┘
    L0029: add rsp, 0x28            ;
    L002d: ret                      ; return 1;

やっぱり分岐が消えていませんね。理想的にはただ1を返すだけのアセンブリになっていてほしいのですが、そうなっていません。これはおそらく単にC#コンパイラ、あるいは JIT コンパイラの最適化性能が不十分なだけです。どちらのレイヤーでこの最適化を解決するかは議論の余地がありますが、次期C#バージョン (少なくとも C# 10 以降) では C# コンパイラのレイヤーで解決するようです。

次期バージョンでは const のみを含む string interpolation はconstにできるようです。つまり、上記のBar()メソッドを JIT コンパイルすると

; x86-64 asm (on 64 bits)
Bar()
    L0000: mov eax, 1
    L0005: ret

となり、完全にリテラルの1を返すメソッドに最適化されます。

余談

現状の C# では string interpolation を使わずに+演算子const stringを繋ぐと、完全な最適化がされて1だけを返すメソッドになります。

int Bar()
{
    return nameof(Bar) + nameof(Bar) == "BarBar" ? 1 : 0;
}

つまり、これは string interpolation の最適化がただ甘いというだけの話です。

JIT 定数

JIT 定数は C# 定数や IL 定数よりもさらに制約が緩いもので、静的には定数として扱われません。実行時に JIT コンパイラが解釈し、実行環境の機械語になって初めて定数扱いされます。実際に見て見ましょう。

int Get3PointerSize()
{
    return IntPtr.Size * 3;
}
// IL
.method private hidebysig instance int32 Get3PointerSize () cil managed 
{
    .maxstack 8
    IL_0000: call int32 [System.Private.CoreLib]System.IntPtr::get_Size()
    IL_0005: ldc.i4.3        // 3
    IL_0006: mul             // IntPtr.Size * 3
    IL_0007: ret             // return
}
; x86-64 asm (on 64 bits)
Get3PointerSize()
    L0000: mov eax, 0x18
    L0005: ret                    ; return 24

上記のGet3PointerSize()メソッドはポインタ3つ分のバイトサイズを返すメソッドです。 IntPtr.Sizeは 64 bit 環境では8、32 bit 環境では4です。 つまり、64 bit 環境では24を、32 bit 環境では12を返します。

しかし、これは実行時に初めてその実行環境の値が決まるため、静的にはその値はわかりません。 したがって、IL 上では値を取得して乗算が行われているのがわかります。JIT コンパイルが行われて初めて定数が確定し、乗算を省略してリテラル値を返すメソッドになります。

では、JIT 定数についても分岐省略の最適化を見てみます。

void DumpRuntimeBits()
{
    if(IntPtr.Size == 4)
        Console.WriteLine("This is 32 bits runtime");
    else if(IntPtr.Size == 8)
        Console.WriteLine("This is 64 bits runtime");
    else
        Console.WriteLine("Unknown runtime");
}
; x86-64 asm (on 64 bits)
DumpRuntimeBits()
    L0000: mov rcx, 0x287a79f8970
    L000a: mov rcx, [rcx]        ; "This is 64 bits runtime"
    L000d: jmp System.Console.WriteLine(System.String)

JIT 後のアセンブリでは分岐が綺麗に消えていますね。 これは 64 bit 環境の JIT 結果なので、32 bit 環境 JIT の場合はif(IntPtr.Size == 4)の処理だけが残ります。

型と JIT と最適化

C# の実行時最適化を制御してこそ、JIT コンパイルを採用している C# の真の力を発揮できます。 JIT 定数としてIntPtr.Size以外に代表的な物はtypeof演算子です。 JIT 定数に焦点を当ててジェネリクスと組み合わせて使う方法について見ていきます。

 

ジェネリクス<T>は IL の状態では型引数<T>が抽象的なままメタな状態です。これが、実行時に実際に使われた型に合わせて機械語が生成されます。

例えば、List<T>の場合、List<int>.AddList<double>.AddJIT 後は完全に別のメソッドで、その関数ポインタの値は異なります。つまり、実行時にはあたかもList_intList_doubleという別の型が存在しているかのように解釈されます。

これと JIT 定数である typeofと組み合わせてみます。

public class Sample<T>
{
    public void DumpType()
    {
        if(typeof(T) == typeof(int))
            Console.WriteLine("int");
        else if(typeof(T) == typeof(double))
            Console.WriteLine("double");
        else
            Console.WriteLine("other types");
    }
}

// ------------------------
var intSample = new Sample<int>();
intSample.DumpType();        // >> int

JIT 後のアセンブリは省略しますが、この分岐は実行時には消え、一致する部分の処理だけが残ります。 したがって、ジェネリクスクラスあるいはジェネリクスメソッドの中で、特定の型に対してだけ処理を行う場合、typeofで if 文を大量に列挙するのが最適解です。

 

このように、JIT の仕組みと JIT 時最適化読みで施せる C# のパフォーマンスハックはいろいろとあります。 特に、ジェネリクスJIT は相性がいいため、いろいろとできます。また機会があれば記事を書こうかなと思います。

(例えば、分岐削除という観点ではないですが、ジェネリクスJIT の組み合わせとしては Static Type Caching なんかが有名ですね)

速すぎる最適化に意味はない、などとよく言われますが、そもそも知らない最適化は書けません。 より速い最適化方法を知っていてこそ、それを使うか使わないかの選択肢を選べます。

たまには速すぎる最適化で最高性能を出してみませんか?

それではよいクリスマスを。

(C#) Fast Span と Slow Span の挙動の不一致の罠

System.Span<T> および System.ReadOnlySpan<T>はメモリの連続領域を表すための配列ライクなオブジェクトです。 配列T[]およびそのスライス、スタックメモリstackalloc T[]、ポインタ (マネージドとアンマネージドの両方)を統一的に扱える便利なものです。

これらは .NET Core の時代に後から導入されたもので、これによって得られるパフォーマンス効果を100%使うためにはランタイムに修正が必要でした (Fast Span)。しかし、ランタイムに修正を加える前の古いランタイム (.NET Framework) およびランタイムを指定せず API が一致していれば使えるというライブラリ用の API 仕様である .NET Standard ではこれが使えず、本来のパフォーマンスを得られないが挙動だけ合わせたものとして提供されます (Slow Span)。

System.Span<T> および System.ReadOnlySpan<T>は Fast Span 環境では BCL に始めから入っており、Slow Span 環境では System.Memory の Nuget package で提供されています。本来ライブラリなのでパフォーマンスが劣る以外どちらも同じ挙動をすることが望まれるのですが、実は微妙に違いまして、Slow Span で使うと例外が発生するものがあります。

public unsafe struct Data
{
    public double* Pointer;
}

// -------------------------------------
// Create span (error !!!)
Span<Data> span = stackalloc Data[1] { new Data() };

実は上記のコードは Fast Span では実行できますが、Slow Span では例外を吐きます。 Span<T>に入れる型がポインタのフィールドを持っているとSpan<T>を作れません。ポインタの型は関係ありません。 これは再帰的に適用されるため、例えば上記のData構造体が別の構造体のフィールドを中に持っていて、それがポインタを持っている場合でも例外が出ます。

なら、Slow Span の環境ではポインタを持つ構造体をSpan<T>に入れられないのかというとIntPtrを使うと回避できます。

public unsafe struct Data2
{
    private IntPtr _pointer;
    public double* Pointer
    {
        get => (double*)_pointer;
        set => _pointer = (IntPtr)value;
    }
}

// -------------------------------------
// Create span (OK)
Span<Data2> span = stackalloc Data2[1] { new Data2() };

フィールドとしてポインタを直接持っていなければいいので上記のようにIntPtrに置き換えると例外が出ません。 ここでIntPtrの内部を知っている人からすれば、逆に混乱します。IntPtrは内部にvoid*のフィールドを持っているだけのラッパー構造体にすぎないため、先ほどの再帰的に適用されるという説明に矛盾します。

これは Slow Span のソースコードを確認したわけじゃないため推測ですが、おそらくIntPtrだけを特別扱いしていると思います。

個人的には Slow Span のレガシー環境を使わないので知ったこっちゃない!むしろ早く世の中全員ランタイム移行してくれ~って感じなんですが、まあそういうわけにもいかないので。

ランタイムを指定しない場合のライブラリなんかを作る場合は注意が必要ですね。

(C#) ランタイムが Blazor WebAssembly か否かを取得する

実行中のOS が何であるかを取得する時はSystem.Environment.OSVersion.Platformを使えそうかと思ってました。

using System;

// Windows 10 なら "Win32NT"
Console.WriteLine(Environment.OSVersion.Platform);

ところが、これを Mac .NET Core で取得すると"MacOSX"ではなく"Unix"と返ってきます。 それはともかく、MSDNを見ると何とも説明が古臭いです。SilverlightとかXboxとかはまだしも、"Win32S"っていつの時代だ……私生まれてない。(知らなくてググったら16bitのOSがうんぬんかんぬん)

deprecated が多すぎる。

時代は進んで Blazor なんていうものが出てきて、そもそもそんなものは想定されていない。Blazor WebAssembly (client-side のやつ) でこれを実行するとUnixと出てきます。自分のことを Unix だと思い込んでいる一般人かな。

 

代わりにSystem.Runtime.InteropServices.RuntimeInformation.OSDescriptionを使います。

using System.Runtime.InteropServices;

// Blazor WebAssembly で実行すると "web"
Console.WriteLine(RuntimeInformation.OSDescription);

ちゃんと"web"と出てきました。WebAssembly か否かが取得できました。

(C#) エンディアン固定でシリアライズ

今回のオチ

  • System.Buffers.Binary.BinaryPrimitives クラスを使いましょう

エンディアンを固定でシリアライズしたい

longの値をbyte[]にしたい時、普通はSystem.BitConverter.GetBytes(long)で困らないんですが、BitConverterエンディアンは固定じゃないんですよね。ランタイム依存です。普通はリトルエンディアンなんですが、一部 mono 環境なんかでビッグエンディアンな環境があるらしいんですよ、奥さん。

実行時にリトルエンディアン環境なのか、ビッグエンディアン環境なのかはBitConverter.IsLittleEndianプロパティで確認できます。ついでにこのプロパティは Intrinsic なので JIT 定数になり、if文を書くと JIT 時に分岐が消えます。愛してる定数。

たとえばファイルフォーマットなんかでエンディアンが決まってるもの扱う時に、BitConverterを使うと厳密にはランタイム依存で正しく動きません。雑に動けばいいならばBitConverterでいいんです、だって普通はリトルエンディアンだもん。

逆に TCP/IP なんかはビッグエンディアン固定です。(まあここは普通ライブラリ使うので直に書くことはないですが。)

 

長々と書いて何が言いたいのかというと、System.Buffers.Binary.BinaryPrimitivesを使うと、エンディアン固定で直列化できますよという話です。使い方はメソッド名見れば一目瞭然なんですが、例えばlongをリトルエンディアンでSpan<byte>にする時は

long value = 100L;
Span<byte> buf = stackalloc byte[8];
BinaryPrimitives.WriteInt64LittleEndian(buf, value);

です。ビッグエンディアン用のメソッドもちゃんとあります。

1つだけ残念なことは、BinaryPrimitivesクラスは .NET Standard 2.1, .NET Core 2.1 以降でしか使えないことです。 .NET Framework にはありません。だって Windows はリトルエンディアンだもん (2回目)。

(C#) static コンストラクタを手動で呼ぶ

方法だけ知りたい生き急いでいる人用

// using System.Runtime.CompilerServices;
RuntimeHelpers.RunClassConstructor(typeof(Hoge).TypeHandle);

以下説明

static コンストラクタが呼ばれない状況

C# の構造体やクラスには、インスタンスのコンストラクタとは別に、static コンストラクタを書くことができます。

public class Hoge
{
    // インスタンスコンストラクタ
    public Hoge() { ... }

    // static コンストラクタ
    static Hoge() { ... }
}

通常、static コンストラクタは、初めてインスタンスコンストラクタが呼ばれた時に最初の1回だけ実行されます。つまり、型に対して静的な初期化処理を書くことができます。

さらに、言語仕様として必ず1度のみ呼ばれることが保証されているためマルチスレッドにおいても排他処理などをする必要がなく、スレッドセーフに実行されます。

クラスの場合は最初のインスタンスnewされた時に必ず呼ばれますが、構造体の場合、実はインスタンスがあるにもかかわらず static コンストラクタが実行されない場合があります。

public struct MyData
{
    public int Value;

    // static コンストラクタ
    static MyData() => Console.WriteLine("static ctor called !!");
}

上記のような構造体があったとして、以下のコードは static コンストラクタが呼ばれません。

// Case 1
var array = new MyData[10];
Console.WriteLine(array[3].Value);  // Value : 0

// Case 2
var data = new MyData();
Console.WriteLine(data.Value);  // Value : 0

Case1 と Case 2 のどちらも、コンパイルが通って実行でき、MyData型のインスタンスが存在しているにもかかわらず static コンストラクタは実行されません。構造体なので、どちらのケースもメモリのゼロ初期化が行われているだけだからです。

ちなみにクラスの場合は、Case1は null で、インスタンスが存在していないため static コンストラクタが呼ばれないのは当然ですし、Case2 はきちんと呼ばれます。

構造体の静的な初期化処理を static コンストラクタで行っている場合、インスタンスを使う前に呼ばれていてほしいのに、これでは困るので手動で呼びたい。

手動で呼ぶ

以下の方法で手動で呼ぶことができます。

// using System.Runtime.CompilerServices;
RuntimeHelpers.RunClassConstructor(typeof(MyData).TypeHandle);

メソッド名がRunClassConstructorとか書かれていますが、構造体にも使えます。

この手動で呼ぶ方法は自動で呼ばれる条件と同じように、ただ1度のみスレッドセーフに実行されます。複数回呼んでも2回目以降は無視されます。また、既に自動で呼ばれた状態で実行しても、何も実行されません。

つまり、必ず static コンストラクタが呼ばれていてほしい場所の前にこれを書いておくと、実行されていることが保証できます。