はじめに
第1回目 .NET 5 を使いたい理由6選
第2回目 .NET 5 を使いたい理由6選(C#編)
この記事は第3回目となります。
unsafe
を使いこなせているでしょうか、みなさん。
私は使っていません。(←?!)
でも、C#9.0 のunsafe
は久しぶりに少し刺激的な内容になっています。パフォーマンスの欲求に従ってよいと言われたら使ってしまうかもしれませんね。
使いたい理由1 : delegate*
(関数ポインタ)が使える
https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/proposals/csharp-9.0/function-pointers
素直な関数ポインタの実装がとうとう使えるようになりました。
unsafe class Program
{
public static void Error(string text) => Console.WriteLine($"ERROR : {text}");
public static void Warning(string text) => Console.WriteLine($"WARNING : {text}");
public static void Information(string text) => Console.WriteLine(text);
static void Main(string[] args)
{
var funcs = stackalloc delegate* managed<string, void>[3] { &Error, &Warning, &Information };
funcs[1]("test"); // WARNING : test
}
}
このように配列も可能ですし、ポインタなのでインクリメント/デクリメントや加減算も可能です。これはC/C++のように動作します。
delegate
というものはこれまでは MulticastDelegate
クラスが実体で、ラムダ式も突き詰めるとこれになるという状態でした。static
メソッドのActionやFuncのパフォーマンスが悪いと言われた理由もすべてこれです。
これは delegate
の特性として、+= / -= (つまり add/remove)を受け付けなければならないという言語上大切な理由があってのことでした。
それに対して、delegate*
は素直な関数ポインタです。なので staticメソッドにしか使えません。また、リンク先の規約を見ていただければわかりますが、unmanaged
で呼び出し規約指定までできるようになっています。ありがたいことです。
managed Thiscall
は無いのか…と少し思いました。(unmanaged Thiscall
はあるのですが)
使いたい理由2 : スタックメモリを初期化しない[SkipLocalsInit]
https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/proposals/csharp-9.0/skip-localsinit
ローカル変数は初期化しないとそもそもエラーになりますが、stackalloc
で確保された領域はゼロfillを行います。
これを省略してくれるのが[SkipLocalsInit]
です。
using System.Runtime.CompilerServices;
[SkipLocalsInit]
void Func()
{
var values = stackalloc int [3]; // 初期化が抑止されるので中身が不定になる
}
真にパフォーマンスが必要なコアコードでのみ役立つようなところですが、ループの都度呼ばれるようなメソッドでは強い威力を発揮します。
使う際には、すべてのスタックメモリの初期化に責任を持つことが肝要です。
使いたい理由3 : ネイティブ整数型のenumを定義できる
これは制約の緩和です。
https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/proposals/csharp-9.0/native-integers
nint
nuint
が定義されました。int
が内部的にはInt32
であるように、nint
はIntPtr
であり、nuint
はUIntPtr
です。拡張のポイントは enum で定義できるようになったことです。
便利と言えば便利、そうでもないといえばそうでもないですね。ただ、ネイティブなライブラリを使う際に、ネイティブな値をそのまま enum にできることは安全性を高めるに違いありません。C#の実装がない場合、C/C++で書かれたものを使う必要はいまだに無くなっていないため助かります。
それと同時に、案外とネイティブライブラリをC#に移植するのを促進する一助とはなるかもしれません。
使いたい理由4 : partialメソッドが大幅に拡張された
これも規制の緩和です。もはや別物ですね。
https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/proposals/csharp-9.0/extending-partial-methods
上記リンクからの引用となりますが、C#9より前のpartialメソッドの仕様はトリッキーな仕様を持っていました。
partial class D
{
partial void M(string message);
void Example()
{
M(GetIt()); // Call to M and GetIt erased at compile time
}
string GetIt() => "Hello World";
}
定義されていないのはM()なのに、GetIt()まで消されてしまいます。
これは特定の処理をフォームのデザイン時と実行時でON/OFFするようなシナリオで重宝されたようです。
そのため partial メソッドは「戻り値はvoidのみ」「アクセシビリティを持てず常にprivateメソッドとなる」「outパラメーター不可」などの制約も持っていました。
今回は細部に触れませんが、C#9から使えるようになったソースジェネレーターの拡張に合わせて partial メソッドは大いに拡張されました。
- 戻り値を持つことができる
- アクセシビリティを持つことができる
- outパラメーター可
これだけの変更でできることはかなり広がります。クラス内には宣言のみ記述し、ソースジェネレーターに実装を任せることなどが可能です。
使いたい理由5 : 共変戻り値が採用された
これも規制の緩和です。
https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/proposals/csharp-9.0/covariant-returns
class A { public virtual A Create() => new A(); }
class B : A { public override B Create() => new B(); } // C#9より前はエラーだった
基幹コードを作る側には少しありがたい。使う側にはさほどメリットは無い。そんな立場の違いが分かる変更と言えます。戻り値の型が固定されていたために内部にXxxxInternalとかXxxxxImplのようなものを作っていたことがあったなら、それらはある程度削除できそうです。
使いたい理由6 : [ModuleInitializer]
属性が使える
https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/proposals/csharp-9.0/module-initializers
静的コンストラクタのコストについて考えたことがあるでしょうか。
クラスAの静的コンストラクタがクラスBを参照しており、そこにも静的コンストラクタがあった場合、クラスB、クラスAの順でコンストラクタが動かなければなりません。
こういった判定はランタイムで実行されるのでコストがあります。
実行順序が決まっている初期化ルーチンをこの[ModuleInitializer]
属性を使って書けば、ロードにかかるコストを抑えることが可能です。
まさに起動時間を縮めるための処方箋と言えます。
まとめ
.NET 5を使いたい理由(C#編)の続きでした。
私は最近 C#9.0 を使うようになってから .NET 5 より前に戻りたくなくなってしまいました。
レガシーなものを触るときを除いて、 .NET 5 を使うべき理由は沢山あると思います。
unsafeを使う現場は少ないと思いますが、制約緩和系の拡張は魅力的なものが多いと感じています。
感謝
.NET と C# の発展に携わるすべてのエンジニアの皆様に感謝いたします。