C# Language Design Notes アップロード祭りの続き。

Notes のうち3・4割くらいは、C# 7.3の機能の「最後の詰め」みたいな感じの検討でした。

C# 7.3 の状況

ちなみに、先日のブログでも書きましたが、C# 7.3に含める機能はもう概ね固まっているみたいです。進捗状況を見ると全項目Merged。

(大半は、nightly buildのパッケージに反映済み。まだなのは「Custom fixed」と「Indexing movable fixed buffers」の2つ。)

うちのC# によるプログラミング入門には、Visual Studio 15.7 Previewで拡張なしで C# 7.3を使えるようになる頃にはページを追加したいなぁとか思いながら、nightly build版でちらほら試しに触ってみています。 とりあえずは、今のnightly buildに含まれていない上記2機能を除いて、書きたい内容のサンプル コードを整備:

Design Notes で検討されていたこと

とりあえず具体的な機能説明はC# によるプログラミング入門の方にページ追加するときに頑張ります。

今日は、先日上がった Design Notes で触れられていた内容を軽く紹介。

式中の変数宣言

C# 7.0でいわゆる「out var」が入って、変数宣言を式中でできるようになりました。 C# 7.3では、7.0の頃には使えなかったいくつかの場所でこの機能を使えるよう拡充することになっています。

Notes での検討事項は、主にその変数のスコープをどうするか:

  • コンストラクター初期化子: C(int i) : base(out var x) { x } ← この x{} 内で有効
  • フィールド初期化子: その初期化子内でだけ有効
    • int X = M(out var x).N(x);Nのとこでxは有効
    • 続けて int Y = x;X の方で作った x は、Y の方では無効
    • C# スクリプト実行(変数っぽく宣言したものは、暗黙的に見えないクラスのフィールドになってる)の時の挙動とは一貫性ないんだけど
  • クエリ式: 句単位でスコープ切れてる
    • from x in e where M(x, out var y) select y;selectの方のyは無効
    • ラムダ式の挙動に準じてる。 e.Where(x => M(x, out var y)).Select(x => y);Select内のyは無効
    • 句をまたぎたければ let 句を使って

ジェネリクスの型制約

Enum, Delegate

where T : Enumwhere T : Delegate (または where T : MulticastDelegate)が指定できるように。

このEnumDelegateは、それぞれSystem.Enumクラス、System.Delegate クラスのことです。using System;がないとwhere T : Enumとは書けず。

また、キーワードのenumdelegateを使ったwhere T : enumwhere T : delegate は書けません。 将来的にそういうのも検討するかもしれないけども、少なくとも C# 7.3 では実装しません。

ValueType, Array

同じような「.NETのランタイムが特別扱いしてる型」にSystam.ValueTypeSystem.Arrayがありますが、 これらは現状では有意義な用途が見つかっておらず、「もし用途が見つかれば改めて考える」とのこと。

unmanaged

もう1個、こっちは文脈キーワードの追加で、where T : unmanaged という制約が追加されます。 これは要するに、「ポインター化可能な型」。 「unsafe コード限定機能」のとこで説明していますが、 ポインター化しても安全な型にはちょっとした制約があって、 その制約を満たす型を元々 unmanaged 型と呼びます。

これまで、ジェネリック型引数に対して unmanaged かどうかの判定はできなかったんですが、C# 7.3 でこの制約が入ったことで、ジェネリックなポインターを作れるようになります。

unmanaged の条件の1つが「構造体であること」なので、where T : unmanaged はこの時点で暗黙的に where T : struct の意味も含みます。 なので、where T : struct, unmanaged みたいな併用は認めません。

あと、「ポインター化可能かどうか」の判定には参照アセンブリ問題っていう問題があったりするんですが、 unmanaged 制約もまったく同じ問題を起こします。 (といっても、これは C# コンパイラーの問題ではないので C# チームとしてはどうしようもない。)

ちなみに、var の時にもそうだったんですが、「後からの文脈キーワードの追加」なので、互換性維持のために「varと言う名前の型、unmanagedという名前の型がすでに存在するときは、キーワードではなくてその型の方が優先される」という挙動になっています。 意図的にclass unmanaged { } みたいなクラスを作ると、where T : unmanagedT は「この unmanaged クラスの派生型」という扱いになります。

ref reassignment

ref var r に対して、「参照先の振り替え」ができるようになります。 ref var r = ref x; とした後、r = ref y; で振り替え。その後は r = value; とすると、y の中身が書き換わります。

普通の代入と同じく、この r = ref y も式にできます。 例えば、連結リスト操作で while ((l = ref l.Next) != null) みたいなコードを書けます。

あと、C# 7.3 では foreach (ref var x in source) みたいな、foreach の反復変数も参照にできます。 これも、通常の反復変数が書き換え不可なので、参照の場合の「参照先の振り替え」は不可です。

ref int r; みたいに未初期化の状態で変数宣言した後、r = ref x; と後から参照先を割り当てることも考えられますが、C# 7.3 の時点ではやらないみたい。 「安全性チェック」(戻り値として返せるかどうか)が大変になるからとのこと。

stackalloc

C# 7.2 で「安全な stackalloc」が入ります。 そのため、これまでなら「unsafe 限定のレア機能」だったstackallocが、C# 7.2 以降利用頻度が上がる可能性があります。

ということで、stackallocの使い勝手を上げてもいいだろうという話に。 具体的には、配列のnewに対してできることはstackallocにもできてもよいという話。 要するに、stackallocに初期化子を付けれるようになります。 stackalloc[] { 1, 2, 3 }とか。

パターン ベースの fixed ステートメント

Span<T>はパフォーマンス向上のために導入された型で、 こいつに対してポインター操作をしたいという需要は結構高いようです。 しかし、Span<T>からポインター取得する際にfixed (GC によってポインターが指してる先が移動しないように、オブジェクトを「固定」する)をどうやって掛けようかと言うので悩んでいました。

今は、fixed (T* p = MemoryMarshal.GetReference(span))とか書いたり、 ちょっと前まではfixed (T* p = span.DangerousGetPinnableReference())とかだったりしました。 これを、fixed (T* p = span) と書いたら何らかのメソッド呼び出しに置き換えたいという話。

展開結果は、まあ、「Dangerous はねぇわ」という感じみたいで、 結局、今現在 marge されているコードを見るにGetPinnableReferenceと言う名前を使いそう。

stringとかの標準ライブラリ中の型に関してはこのGetPinnableReferenceをインスタンス メソッドとして追加予定。 一方で、(ref thisin thisを含む)拡張メソッドもOKにする予定。

safe な手段ではできないんですが「null 相当する参照」を返すことで、fixed (T* p = s) の結果のpがnullポインターになることも認めるとのこと。 nullチェックは使う側の責任。

タプルの ==, !=

C# 7.3 から (x, y) == (1, 2) みたいな、==!=を使ったタプルの等値判定ができるようになります。

C# のタプルの実体はSystem.ValueTuple型なわけですが、これの==演算子を呼べばいいという話ではないです。 タプルは「メンバーごとの暗黙の型変換」を認めているので、比較でも型変換を考慮する必要があるとのこと。 例えば、(int x, int y)なタプル変数tに対して、t == (123L, 1E10D) とかができます(それぞれlongdoubleとの比較)。

ということで、タプルの==は、「メンバーごとの==呼び出しを&&で繋いだもの」として解釈。 ユーザー定義の==で、boolではなくユーザー定義型を返す場合、それのoperator.trueoperator.falseが呼ばれます。

オーバーロード解決

C# 6.0くらいの頃からずっと「bestest betterness」とかいうふざけた言い方をしてたやつをついに実装するそうで。 ちなみに、betternessルールの改良はたびたびやっているので、 「better betterness」→「best betterness」→「bestest betterness」とかポケモン進化みたいなふざけた名前になりました。

今まで、「まず引数の型の一致を確認」→「制約等を満たすか調べて、満たしてなければコンパイル エラー」という順の処理をしていました。 制約を調べる方があとなので、解決できないオーバーロードが結構あったという。 それを、引数の一致を調べるより先に、以下のものを調べるように変わります。

  • ジェネリック型制約
  • 静的メンバーかインスタンス メンバーか
  • (メソッドをデリゲートに渡すときに)メソッドの戻り値の型

これは bestest だわー。 (さすがにふざけた名前なので、最近、Proposal のタイトルも「Improve overload candidates」に変更されました。)