Visual Studio 2019 Preview 1 が出て、 さすがに C# 8.0 に入る機能・入らない機能がある程度見えてきたので、 今日からしばらくその辺りの紹介をしていこうかと。

とりあえず今日は、「1記事使うほどでもないような小さい奴」をまとめて紹介。

  1. 文字列補完、$@ の順序緩和
  2. ??= (null 合体代入)演算子
  3. 構造体の宣言時、refpartialの順序緩和
  4. 分解の右辺に default
  5. 入れ子の{}内での stackalloc
  6. unmanaged 制約付きの型引数に、ジェネリックな型を渡す

ちなみに、VS 2019 Preview 1 で実装されているのは上の2つだけです。

順序緩和

C# のキーワードには、並び順を自由に変えられるものがいくつかあります。 代表的なのはクラスやメソッドに対する修飾子ですが、例えば以下の3行は全く同じ意味になります。

static public readonly int x;
public readonly static int x;
readonly static public int x;

一見するとこれらと同じように順序不問そうに見えるのに、なぜか順序に厳しいものもあります。 やむを得ない理由があってそうなっているものもあるんですが、 例えば、部分クラスpartialは、 「C# 2.0 から追加したキーワードなので、1.0 時代のコードを壊さないように、順序を厳しくした」という理由で「classまたはstructの直前でないといけない」という制限が付いています。C# 7.2 で入ったref構造体も同様に、refキーワードはstructの直前にないといけません。

しかし、いくつかは理不尽、あるいは、過剰で、

  • 参照引数な拡張メソッドref this Tでないとダメだった
  • 構造体に対する refpartial の両方付けたければref partial structの順でないとダメ
    • partial ref struct でもいいはず
  • 文字列補間 の($)と逐語的リテラルの(@)を同時に指定したければ$@の順でないとダメ
    • @$ でもいいはず

とかいうものもあったりします。 こいつらはほんとにどっちが正しいのかわからず、よく間違います。

拡張メソッドの ref thisthis refは、 今は順序緩和されていてどちらでも使えます。 しかもこの修正、パッチ リリースでこっそりと入っていたり。

ということで、この度、C# 8.0 では後者2つも順序緩和されるみたいです。 どちらも最初から認めてくれててもいいレベルなんですけどね…

null 合体代入

「null だったら何か適当な既定値で上書き」みたいな処理は結構頻出かと思います。

static void M(string x = null)
{
    if (x == null) x = "default string";
    // x に対して何か処理
}

あるいは、遅延初期化のために、「初期値にnullを入れておいて、初回アクセス時に有効な値で上書き」みたいなことも結構書きます。

public string Name => _name ?? (_name = GetName());
private string _name = null;

後者の例では null 合体演算子 ?? と代入 = を組み合わせていますが、まあ、まさにやりたいことはこれ。 + に対する += のように、?? に対する ??= が欲しいという要望は結構あります。

ということで、その??=演算子が C# 8.0で入ります。

static void M(string x = null)
{
    x ??= "default string";
    // x に対して何か処理
}
 
public string Name => _name ??= GetName();
private string _name = null;

これ、VS 2019 Preview 1ですでに実装されていますけども、 取り組むことになったの、そこそこ最近なんですよね。 あと、地味な機能なのでそんなに話題にも登らず、アピールもされず。 なんか気が付いたら決まっていて、 気が付いたら実装されてて、 気が付いたらマージされた印象。

大した機能じゃなくて実装が簡単とは言え、ちょっとびっくり…

分解の右辺に default

C# 7.1 で defaultってのが入ったわけですが。 要は、左辺から推論が効く限りには、default(T)(T)を省略してdefaultだけで掛けるようになるというやつ。

このdefaultの型推論、C# 7.x までは、以下のような状況では利きませんでした。

(int x1, int y1) = default; // ダメ
(int x2, int y2) = default((int, int)); // これならOK
(int x3, int y3) = (default, default); // これでもOK

この、1行目の「ダメ」ってなっている方を、C# 8.0からはOKにするみたいです。

確かに、なんかきわどい… (int x, int y) に対して分解代入できる型は別にタプルに限らないわけで、 じゃあ、このdefaultは何に推論されたのか…的な不思議さは一瞬ちょっと感じます。 (まあ、でも、便利さ優先でほしい機能。)

入れ子の{}内での stackalloc

C# 7.2 で安全に使える stackallocが入りました。 ですが、ref構造体の制限から、非同期メソッド内ではこの機能が使えませんでした。

Span<int> x = stackalloc int[32];
 
// ここで x を使うのは安全なはずだけど、今は問答無用でエラー。
 
await Task.Delay(1);
 
// await をまたいで stackalloc を使おうとするのは明確にまずい。
// これは制限されていてもしょうがない。

これに対して、C# 8.0では、以下のように一段{}でくくればOKになります。 要するに、{}でくくることによって、絶対にawaitをまたがないことが保証されれば({}内にawaitがなければ)認めても安全ということです。

{
    // {} でくくったのでこれが書けるようになる。
    Span<int> x = stackalloc int[32];
}
await Task.Delay(1);

unmanaged 制約付きの型引数に、ジェネリックな型を渡す

C# 7.3 でunmanaged制約が入りましたが、微妙に使いにくい点がありました。

Unmanaged<int> x; // int は unmanaged なので OK
 
// 以下のものは C# 7.3 ではダメ
Unmanaged<(int, int)> y; // int しか含まないはずなのに…
Unmanaged<Unmanaged<int>> z; // 再帰的に unmanaged 制約を満たしてそうなのに…

要は、「ジェネリック型は問答無用ではじく」という状態です。

ちなみに、同様の事情はref構造体にもありまして。 ただ、ref構造体の方は、かなり厳密にチェックしてはじかないとまずい(セキュリティ ホールの原因になりかねない危険性あり)ので、 こちらは絶対にジェネリック型を使えないそうです。

そして、C# の仕様書上、ポインターにも同様の制限があります。 C# 2.0の頃からずっと、ジェネリックな型に対してポインターを使えませんでした。

しかしどうも、ポインターに関しては別にこの制限は要らなかったらしいです。 あくまで、「1つでも参照型を含んでいたらダメ」にすべきで、 再帰的にunmanaged制約を満たしているのならジェネリックかどうかは関係ないはずです。

なので、C# 8.0で、unmanaged制約でのジェネリック型の利用制限は撤廃するし、 仕様書のアンマネージ型に関する記述も修正すべきという話になっています。