Quantcast
Channel: ++C++; // 未確認飛行 C ブログ
Viewing all 483 articles
Browse latest View live

.NET 5 Preview 2 と C# Next

$
0
0

先週末に .NET 5 Preview 2 が出ています。

昨日、これ絡みでまたライブ配信したりしてました。

ブログ的にはあんまり長くなってもしんどいので2回に分けようかと思います。 今日は前半の話で、「ひそかに C# Next がちょっと動いてた」という話になります。

.NET 5 Preview 2

アナウンス上は、Preview 1 との差分がほぼパフォーマンス改善だけになっています。

が、まあ、アナウンスするほどでもない細かい修正はいろいろあると思います。 ひそかに C# コンパイラーも更新されてるみたいですね。

Visual Studio 16.6 の方は2週間くらい前に Preview 2 になってるんですけど、 最近の C# コンパイラーは .NET SDK に同梱されているものが使われているようなので、 更新が掛かるとしたら .NET SDK の方に合わせて。 で、大体は、roslyn リポジトリ中の Language Feature Statusのページの State が Merged になっているものは動くはずなので、 物は試しにやってみたらやっぱり入ってたという感じ。

まあ、アナウンスに含まれないくらいなのでお察しだとは思いますが、小さな機能が3つほど追加されただけです。

LangVersion preview

ちなみに、まだ一応正式には「C# 9.0」とは言っていません。

まあ、Pull Request タイトルとかにはすでに普通に 9.0 の文字が入っていたりする (例: #42368)ので、 雰囲気的にはもう次のバージョンは 9.0 になるかと思いますが、正式決定ではないです。

なので、コンパイラー オプション的にもまだ LangVersion 9 とか 9.0 というものはない状態です。 一方で、LangVersionpreviewを指定すると、今日話すような新機能が使えるようになります。

ちなみに、このpreview指定ができるようになったことが、7.X 世代みたいな細かいアップデートをしなくなった要因の1つだと思われます。 「新しい機能を早期に試してほしい」というのが目的だったわけで、そのためにはマイナーバージョンを積み重ねるよりも、単にpreviewチャンネルを用意する方が好ましいということなんだと思います。

まあ、現状、9.0 らしい機能は1つも入ってないんですが。 今入っている3つの機能は「8.0 の時に間に合わなかっただけ」みたいな感じのものです。 ちなみに、本格的に 9.0 っぽい機能が入り出すのは、マイルストーンを覗き見してる感じでは Visual Studio 16.7 のタイミングみたいです。

.NET 5 Preview 2 での C# 新機能

ということで、今回入った機能は以下の3つ。

  • Lambda discard parameters
  • Attributes on local functions
  • Skip locals init

Lambda discard parameters

C# 8.0 では discard (値の破棄)という機能が入ったわけですが、 これを使える場所がちょっと増えて、ラムダ式の引数でも使えるようになっています。

Func<int, int, int> f = (_, _) => 0;

ぱっと見ではわかりにくいかもしれませんが、2つある引数が両方同じ _ です。 普通の変数だと (x, x) => 0 みたいな書き方はできないんですが(名前被りはダメ)、 _ は「一切使われない引数」という意味になって、 2か所以上の場所でも使えるようになります。 当然、一切使われないことが前提なので、(_, _) => _ みたいなコードを書くとコンパイル エラーになります。

ちなみに、C# 8.0 以前のコードが壊れないように、「1引数」の場合には discard ではなく通常の変数扱いになります。 要するに、_ => _ というコードは今まで通り合法です。

Attributes on local functions

これも名前通りで、ローカル関数に属性を付けれるようになります。

static void Main()
{
    [return: NotNullIfNotNull("s")]
    int? f(string? s) => s?.Length;
 
    // f(null).Value だと警告が出る
    Console.WriteLine(f("").Value);
}

メソッドの内側に属性を書く構文がこれまでの C# にはなかったので、 ローカル関数の実装当初(C# 7.0 時点)では保留になっていました。

C# 8.0 で null 許容参照型が追加されたことで、ローカル関数に対しても属性を付けたいという欲求が急に膨れ上がったので、9.0 で実装することにしたみたいです。

Skip locals init

最後は unsafe 限定機能です。 値が不定になることをほとんど認めない C# にしては珍しく、 初期化をさぼるための機能。 SkipLocalsInit という属性をメソッドに付けることで、 その中ではローカル変数の0初期化が行われなくなります。 影響があるのは stackalloc を使うときくらいです。

void safe()
{
    Span<byte> span = stackalloc byte[4];
 
    // 通常、 stackalloc した時点で確保した領域は 0 初期化される。
    // なので、以下のコードは常に 0。
    Console.WriteLine(span[0]);
}
 
// unsafe 限定
[System.Runtime.CompilerServices.SkipLocalsInit]
void skipInit()
{
    Span<byte> span = stackalloc byte[4];
 
    // SkipLocalsInit 属性を付けた場合、0 初期化を行わない。
    // span の中身はプログラマーが責任を持って初期化しないとダメ。
    // 今、初期化をさぼってしまったので、以下のコードは不定な値を表示する。
    Console.WriteLine(span[0]);
}

不定な値を返すというのはそれだけで安全性を脅かすので unsafe オプション必須です。

0初期化をさぼりたいというのはパフォーマンス改善のためです。 プログラマーが責任を持ってちゃんと初期化するのであれば、 コンパイラーが自動的に挿入する 0 初期化コードは無駄にしかならないので。 その安全性を得るためのちょっとしたペナルティすら避けたい場合にこの属性を使います。


ピックアップRoslyn 4/8

$
0
0

前回話したことの続きライブ配信で話したことの後半

C# Language Design Meetingの議事録がいくつか上がっています。

この前後に2月頃の議事録も一気に上がっていて、 C# 9.0 がらみのデザイン決定も大詰めなんだなぁという感じがしています。 (実装期間を考えると、デザイン決定は今の時期が最も活発。)

ちなみに、パターン マッチングの改善の話とかは全然出てきませんが、 この辺りはデザインがすでに割と固まってるという話であって、立ち消えたとかではありません。

トリアージ

いくつか、そもそも採用するかどうかの検討。

ref struct 制約

今、ref 構造体にはインターフェイスを実装できないわけですが、 これは、ref 構造体は box 化してはいけないという制約を守るためです。

一方で、ジェネリック型引数に ref struct 制約を付けれるなら、ref 構造体がインターフェイスを実装していても box 化を避けれるんじゃないかという話が上がっています。

ただ、検討の結果、ref struct 制約は必要として、それだけでも不十分だろうとのこと。 .NET ランタイムの方にも手を入れないと安全性を確保できないし、 Shapesっていう C# 10.0 (次の次)で検討している機能とも領域が被るので、 ref struct 制約は 10.0 のタイミングで改めて検討しようという流れ。

デリゲートのオーバーロード解決の改善

C# 7.3 でメソッドのオーバーロード解決の改善をやってるんですが、 デリゲートの場合にはこの手の解決をしてないみたいです。

とりあえず C# 9.0 でデリゲートに対しても同様の改善を入れたいとのこと。 あと、同じく C# 9.0 目標で作業が進んでいる関数ポインター (この後、今日のブログでも少し話題あり)でも同様のオーバーロード解決をしたいとのこと。

拡張メソッドの GetEnumerator

C# の foreachGetEnumerator メソッドの呼び出しに展開されるわけですが、 現状だと、インスタンス メソッドしか認めていなかったりします。 この手の構文一覧を見てもらえるとわかるんですが、新しめの構文との一貫性が取れなくなっていたりします。 (新しいほど、拡張メソッドでも OK になってる。)

ただ、検討に上がってなかったわけではなくて、 終始「破壊的変更になりかねないからうかつに触りたくない」という雰囲気。

今回もそういう姿勢です。 「破壊的変更を起こさない限りにおいて歓迎」。 (ちなみに、外部からの Pull Request が来ているので、これに関する検討だと思います。 で、今、破壊的変更を起こしてないかの確認中。)

native int

前々から、いわゆる「native int」(32ビットCPU上では32ビット、64ビットCPU上では64ビットになるような整数型)が欲しいという話があります。

実のところ、今でもIntPtr型がそういう挙動をしているんですが、 こいつは名前通りポインター扱いなので、算術・論理演算とかを全面的には認めていなかったりします。 そこで、算術・論理演算をちゃんと認めた native int 型として nintnuint を足す流れになっています。

ただ、内部的には nintIntPtr 構造体で、nuintUIntPtr 構造体に展開する (似て非なる型を追加することはしない)という方向で決定済み。 破壊的変更を起こさないように、 nint とかのキーワードを使って宣言しているときは算術・論理演算を認めて、 そうでないときはこれまで通り認めないという、セマンティクスを見た挙動変化をします。

今回は、じゃあ、nint を LangVersion 8 以前で動かす(IntPtr に見える)ときにどういう挙動をすべきかという話。 まあ、普通にコンパイル エラーにすべきだろうという感じ。

あと、nint定数はどう扱うべきかという話も。 32ビットCPUでもちゃんと動くようにするために、以下のようにするみたいです。

  • 32ビットに収まる値は const 扱い
  • それを超える場合は readonly static 的なものに展開

target-typed new

target-typed new、要するに、List<T> x = new (); みたいな書き方でコンストクター呼び出しできるようにしようという話です。

この構文、「ライブラリ側で新たにコンストラクターが足された時に、利用側に破壊的変更が起きる可能性がある」っていう意味ではちょっと怖いんですが。 それを言うと、今でも null とか default が似たような状態なのでしょうがないだろうと。

new () 自体は型を持っていないものの、default とかと同じで、ターゲットの型を見て具体的な型を決定して、その型のコンストラクターを呼びたいとのこと。 機械的にこの作業をすると、enum に対して new () も書けてしまうことになるけども、 それは認めようという感じ。

あと、null 許容値型に対して T? x = new (); と書いたときには null 扱いじゃなくて new T() 扱い(T? の元になる型(underlying type) T の既定値)扱いすべきだろうという話も。

あと、dynamicに関しては、 今も new dynamic() を認めていないんだし、dynamic x = new (); は認めないようです。

Record

Record 型周りは、以前書いたんですが、 init-only プロパティというのを足す方向で議論が進んでいます。 これに対して、init-only よりも、ビルダーパターンを使った実装にしてほしいという話が出ていたのでそれを検討。 結局は、init-only で行くみたいです。

あと、Record 型の仕様には EqualsGetHashCodeメソッドの自動実装も含まれるわけですが、どういうコードを生成すべきかというのも議題になっています。 等値比較は複雑なカスタマイズをしたい要件が結構あります。 (例えば、配列だったら SequenceEquals すべきかとか、 文字列だったらカルチャー配慮した比較をすべきかとか。) まあ、Record 的には、「手書きの Equals があればそっちを使う」という仕様なので、 カスタマイズが必要なら手書きしてほしい。 コンパイラーが自動生成する比較はシンプルな shallow 比較にしたいとのこと。

関数ポインター

元々は static delegate とか言われていたやつなんですが、 今はもう、完全に「関数ポインター」扱いになっています。 ポインターという名があらわす通り、native 相互運用向けで、unsafe な機能になります。

相互運用の際には呼び出し規約(引数とか戻り値を、どういう順番でどこに置くかみたいなルール)の指定が必要になるんですが、cdecl とか stdcall とか fastcall みたいな、今現在明確に決まっていて名前があるものだけじゃなく、 将来の規約追加にも耐えれる作りにしたいので、属性での指定もできるようにしたいとのこと。

あと、関数ポインター自体に属性を付けたい場合、delegate* cdecl[属性]<void> ptr; みたいな文法にするようです。

プロパティ内の field キーワード

以下のようなやつ。

public int P
{
    get => field ;
    set
    {
        PropertyChanged();
        field  = value;
    }
}

プロパティに対するバック フィールドにアクセスするためのキーワードが欲しいと。

これ、Source Generatorが入れば需要が激減するだろうし、 キーワード追加する割にはそこまで利便性が上がるわけでもないしで、 提案が出ては「今のところ取り組むつもりはない」みたいな返答を繰り返してた機能なんですが。

「重複提案があまりにも多い」と言って、今回ついに Design Meeting の議題に乗ったという。 ちなみに、僕が把握してる範囲でも重複 issue は20個くらいあります。 (その中の一番古い issue: #133。 「これと重複してるよ」リンクがあまりに多いので、「被リンク」通知だらけです。)

とはいえ、「取り組みます」と言っているわけではなくて、 「誰も取り組んでない機能の中で最も頻繁に要求が来るものであることを認めた」みたいなノリ。

ピックアップRoslyn 4/19: C# 9.0 機能の仕様ドキュメントいくつか

$
0
0

ここ1週間くらいで、C# 9.0 で入るであろう機能がちゃんとした仕様ドキュメントに起こされ始めました。

パターン マッチング v3

パターン マッチングも3世代目になります。 最初 C# 7.0 に入ったころは単に「is がちょっと便利になった」、 「switch で型分岐ができるようになった」程度の機能でしたが、 3世代目ともなるとずいぶんいろいろなものが増えています。

詳細は動くものが出てきてからちゃんとした記事化しようかと思いますが、以下のようなパターンが追加される予定です。

  • 型パターン
    • 今、int i とか int _ とか書かないといけないものを int だけで型パターン扱いする修正
  • 括弧パターン
    • パターンを () でくくれるようにする
    • 下記 andor の結合優先度がよくわからなくなるのを防ぐために追加
  • conjunctive and (論理積) パターン
    • and で繋いだ2つのパターンのどちらにもマッチする
  • disjunctive or (論理和) パターン
    • or で繋いだ2つのパターンの少なくともどちらか片方にマッチする
  • negated not (論理否定) パターン
    • not の後ろのパターンを満たさない時にマッチする
  • relational (比較) パターン
    • 数値(int 系、float 系、decimal)型に対して、<><=>= で大小比較する

パターン マッチングv3がらみは割とすでに実装が動いてるんですが、 いくつか最近検討されたばかり・まだ検討中の項目もあります。

  • x is byte and < 100 みたいに書くと、and の左側の型に合わせて右側の型が決まる (roslyn #42207)
    • この場合、< 100 判定は byte 扱いで比較
  • or の場合は左右の型の間の暗黙的な型変換(派生型 → 基底型みたいなやつ)だけ考慮 (roslyn #43419)
  • x is not string ns みたいに、not パターンを使った場合でもその後 ns 変数を使える (csharplang #3369)

target-typed 条件演算子

Base x = b ? new A() : new B(); みたいな条件演算子を書いた時、 左辺の Base 型から型決定して、A, B 違う型でもこの式が有効になるようにしようという話です。

switchの場合、導入時の C# 8.0 の時点からこの手の型決定機構が働いています。 switch 式でだけ有効なのも変な話なので条件演算子でも同様のことをしたいという案はずっとあったんですが、 問題は、以下のような場合に既存のコードを壊してしまうこと。

// 既存のルールだと long の方が選ばれる。
// switch 式と同じルールの target-typed を導入すると short が優先されるようになる。
M(b ? 1 : 2);

void M(short x) { }
void M(long x) { }

ということで、

  • switch 式の場合は target-typed による型決定 → 共通型の判定
  • 条件演算子の場合は 共通型の判定 → target-typed による型決定

という不整合は起こすけどしょうがなく、このルールで実装するとのこと。

ちなみに、こういう特殊な実装をしてもなお、M(b ? 1 : 2, 1); みたいなメソッド呼び出しに対して破壊的変更になる可能性は残っているけども、これくらいなら許容範囲だろうということで破壊的変更を認める方向だそうです。

Records のロードマップ

最近何度か書いてますが(例えば 2/3 のブログ)、 Records として検討されている機能は結構たくさんあります。

パターン マッチング同様、最終形を意識しつつも段階的に実装して行こうということで、 何を優先的に実装するかの検討に入ったみたいです。 現状、以下のような順序。

今取り組む(= C# 9.0 時点で入る):

  • nominal だけ
    • 要するに、init-only プロパティだけまず実装して、プライマリ コンストラクターは入らない
  • 派生は認めない(object からの直派生だけにする)
  • with 式は Clone メソッドからの init-only プロパティの書き換えという決め打ち実装だけ認める
  • value equality はちゃんと実装する

次の段階として以下のものを検討:

  • 派生
  • プライマリ コンストラクター
  • with 式をカスタマイズできるようにする

同時に要検討:

  • ファクトリ メソッドの生成
  • validator
  • 任意のプロパティを Records と同じ value quality/with 等のコード生成に含める
  • Records 外でのプライマリ コンストラクターを認めるかどうか

ということで、直近(= C# 9.0)ではまず init-only プロパティってものが主役になりそうです。 (逆に、プライマリ コンストラクターはまた流れました。) これ単体の提案ドキュメントも上がりました。

内部的には set アクセサーに対して modreq (ちょっと強制力の強い属性みたいなもの)を付ける方向で実装するみたいです。

(C# 7.2 移行、ちょくちょくこの modreq ってやつの話が出てくるので、 そろそろちゃんとこの話も記事化しようかと画策中: tracking issue。)

C# Source Generator (first preview)を試してみる

$
0
0

C# で、ビルド時にソースコード生成してくれる仕組みが入りそうです。

案自体は C# 6.0 時代からある話です。 まあ、ソースコード生成はいろいろと大変なのでなかなか手付かずだったんですが、 やっと最初の1歩が公開されたという段階。

とりあえず、昨日、軽く試してみるライブ配信してました:

ほんとに「first preview」(最初だからこんなもんでしょ)という状態ではあります。 あんまりブログに「まだあれもない、これもない」と書いてもしょうがないので、 ここでは「そんな状態のものを試して貢献したい」という人柱の募集だけしておきます。

ピックアップRoslyn: .NET 5 への Xamarin 統合に向けて

$
0
0

1つ前のブログで話した Source Generator の動機の1つは、 リフレクションが使えない環境でのコード生成をある程度カバーできるようにというものです。

今、 .NET Core と Xamarin の統合作業真っ最中で、 その結果、iOS とか Web Assembly とかで使いにくい機能をどうしようかという話になっていて、リフレクションもその1つです。

その他にもいくつか、対応プラットフォームが増えることで必要な作業がちらほらと。

Feature Switch

iOS とかの AOT (事前ネイティブ コンパイル)シナリオだと使わない機能はコンパイル時に削ってしまいたいわけですが、 そのためのスイッチを用意したいみたいな話も出ていたりします。

RegexRegexOptions.Compiled とか、めったに使わず、使わない場合削ってしまえればコード量が結構多いみたいです。 あと、暗号系のアルゴリズムとかは、アプリごとに1度「これ」と決めてしまえばほとんどの場合その1個だけしか使わないと思いますが、この場合、使わないものを軒並み決してしまいたかったり。

こういう特定機能のスイッチは、 Target Framework の亜種を増やすのではなくて、 以下のような感じの bool フラグでソースコード中で切り替え処理をしてほしそうです。 (Hardware Intrinsicsとかでやっているのと同じ方式です。)

internal static class FeatureDefiningType
{
    [FeatureSwitch("System.Runtime.OptionalFeatureBehavior")]
    internal static bool IsOptionalFeatureEnabled { get; }
}

国際化対応

カレンダーや時刻とかの国ごとに違う書式(2020年5月3日 か 3 May, 2020 か)とか、 文字ごとの Unicode カテゴリー、文字列のどこで改行していいかとか、 アルファベットの大文字・小文字の対応付けとか、 どれも結構大きなテーブルデータを必要とします。

これに対して、.NET Core の国際化対応は ICU (International Components for Unicode) に依存していたりします。

で、これも iOS とかでどうしようかという話に。

デスクトップやサーバーOSには今どき大体 ICU が同梱されていたりするんですが。 Android も、API level 24 移行は ICU4J を持っていたりします。 問題が iOS と Web Assembly で、この辺りは OS/プラットフォーム側に ICU がないのでどうしようかという状態。

案としては、いっそ国際化対応を捨てるという方法。 どのカルチャーを取得しようとしても常に InvariantCultureを返してしまうというモードがあるみたいです。

別案としては、ICU をアプリ単位で同梱する方法。 これは、「OS のバージョンに依らず、所望の ICU バージョン(≒ Unicode バージョン)に依存したい」という場合にも使えます。

ちなみに、ICU はテーブルデータを全部持とうとすると16MBくらいのサイズになるらしく、 アプリ1個1個に全部含めてしまうには少々でかいです。 ICU 自体には「必要な分だけ残してテーブルを削る」みたいな仕組みもあるらしくて、 iOS とか Web Assembly みたいな AOT シナリオのコンパイルにもテーブル削減の仕組みを組み込みたいというような話もあります。

ピックアップRoslyn: C# 9.0 向け Design Notes

$
0
0

C# Language Design Notes が3件に、

提案ドキュメントのアップロードが3件。

特に partial メソッドの拡張の話は Source Generator 関連です。 最初、Source Generator ネタでまとめて投稿しようかと思っていたものの、ちょっとまとまり悪くなりそうだったので1日3件ブログに。

partial メソッドの拡張

コンパイル時のソースコード生成ができるようになると、

  • 手書きで int M(); みたいなメソッド宣言だけを書く
  • それに反応して、何らかの M の実装をコード生成したい

みたいな要件が出てきます。

こういう、コード生成物と手書きコードのつなぎ用の機能として、 C# には 2.0 の頃から partial type、 3.0 の頃から partial method という機能があったりします。 ですがこれは、

  • Windows Forms とかが生成する中身が空っぽのメソッドが先にある
  • 手書きコードを足す必要がないのであれば、一切痕跡を残さず消す
  • 手書きコードを足した場合、コード生成物の中から呼ばれるようになる

みたいな機能で、以下のような制限があります。

これに対して、今出ている要件は逆方向のつなぎ(手書きが先にあって、コード生成物が後)になります。 微妙に挙動に差があるんですが、同じ partial というキーワードを使いまわしたいみたいです。

  • 手書きコードで中身が空っぽのメソッドを先に書く
  • コード生成でメソッドの中身を埋める、埋めないとコンパイル エラー
  • アクセス修飾子を付けたら(private であっても)この挙動になる
  • この場合は void 以外の戻り値と、out 引数を持てる

という感じ。 partial void M();private partial void M(); で挙動が違うのがなかなか気持ち悪いですけども、 新しいキーワードを足すよりはこの方がマシだろいうという判断みたいです。

トップレベルのステートメント

Program.Main を書かなくても、ステートメントを直接ファイル直下に書けるようにするという話。 今回の議題は、

  • スクリプト方言みたいに式だけ書く(; を付けない式を書く)とそれが戻り値になるべきか
    • → やらない
  • トップレベルに書いた変数などの名前はどう扱うべきか
    • プログラム全域がその名前のスコープになる(= 同名別変数を定義するとエラーになる)
    • かといって、今の仕様だとその変数を参照して使おうとするとエラーになる
    • 使えない変数はスコープから外すべき?
    • → 今は使ってないけども将来はわからないのでいったんこの仕様で行きたい
  • コマンドライン引数の受け取り方
    • void Main(string[] args) みたいな部分が消えるので、コマンドライン引数の args はどう受け取ればいいか
    • 案1: 暗黙の変数 (プロパティの set 中の value みたいなもの)を用意する(名前はおそらく args 変数になる)
    • 案2: Environment.Args なりなんなり、何らかの API を用意する
    • → もうちょっと要検討。どっちの案にするかはともかく、こういう手段は必要
  • await の扱い
    • 今の仕様
      • トップレベル ステートメント中に await が1つでもあれば、生成されるのは Task Main()
      • 1つもなければ、生成されるのは void Main()
    • これでいい?常に Task Main() で生成するとかしなくていい?
    • → とりあえず今の仕様で行く

Records がらみ

with 式の Clone

with 式ではオブジェクトの Clone → 特定のプロパティだけ書き換え みたいな処理になります。 で、このクローンをどうするか。

  • 現状、ユーザー定義の Clone メソッドには対応していない
  • 将来的にはユーザー定義で Clone の挙動を自由に変えれるようにしたい
  • C# 9.0 時点では、将来の拡張性だけつぶさないように気を付けつつ、いったん現状の実装で進める

Records: positional

4/13の Design Notes での検討によれば、 positional な Records (プライマリ コンストラクター)の優先度は低めになったんですが。 それでも、これに関していろいろ検討しているみたいです。

プライマリ コンストラクター

  • プライマリ コンストラクターは常に呼ばれないといけない
    • 手書きのコンストラクターがある場合、Point () : this(0, 0) みたいにプライマリ コンストラクター呼び出しが必要
  • positional な Records にはコピー コンストラクターも自動生成したい
    • Point(Point other) : this(other.X, other.Y) { } みたいなプライマリ コンストラクター呼び出しをしたい
  • プライマリ コンストラクターでは、
    • class MyClass(int x, int y) { public int P => x + y; } みたいにキャプチャが発生したときには自動的にフィールドを生成する
    • そうでないときは何も生成しない(単なるコンストラクターの引数扱い)
  • positional な Records では、init-only なプロパティを生成する
  • Records でない普通のクラスでプライマリ コンストラクターを認める場合と、positional な Records とで不整合は起きないか
    • → 生成されるものがフィールドか init-only プロパティかの差があるけど、それくらいは許容する

プライマリ コンストラクター本体とバリデーター

プライマリ コンストラクターに対してコンストラクター本体を持ちたい場合、 以下のような構文になるみたいです。

class TypeName(int X, int Y) // プライマリ コンストラクター
{
    // 引数リストなしの型名 = プライマリ コンストラクターの本体
    public TypeName
    {
        // new TypeName(x, y) の時点で呼ばれる処理
    }
 
    // init キーワード = バリデーター
    init
    {
        // new TypeName { X = x, Y = y } みたいな、初期化子での初期化の後に呼ばれる
    }
}

元々はこの2つを区別していなかったものの、結局は両方必要そうで、両方を認めそう。

プライマリ コンストラクターからの基底クラス コストラクター呼び出し

2種類の書き方ができるけども、どちらも認めてしまえとのこと。

class TypeName(int X, int Y) : BaseType(X, Y);
 
class TypeName(int X, int Y) : BaseType
{
    public TypeName : base(X, Y) { }
}

共変戻り値

要は以下のようなやつ。

class Compilation ...
{
    virtual Compilation WithOptions(Options options)...
}
class CSharpCompilation : Compilation
{
    // 戻り値の型が Compilation.WithOptions と違う
    // けど、派生型で共変なので問題ないはず。
    override CSharpCompilation WithOptions(Options options)...
}

これも需要は非常に高いものの、 「.NET ランタイム側の修正が必要なので C# だけでできなくて重たい」みたいに言われ続けてたやつ。

.NET 5 で実装するみたいです。今まさに作業中:

ということで、C# 的にも C# 9.0 で入ります。 ただ、 .NET Core 3.1 以前のバージョンでは動かないです。

モジュール初期化子

モジュール(dll とか)が読み込まれたタイミングで1回だけ呼ばれる処理を書きたいという要望が以前からあります。

似たようなものとして、クラスの静的コンストラクターがあるんですが…

class Initializer
{
    static Initializer()
    {
        // 初めてこのクラスの何らかのメンバーを使おうとしたタイミングで呼ばれる
    }
}

こいつだと、このクラスに一切触れなかった場合には全く呼ばれませんし、 「初めて触ったとき」という読めないタイミングでの呼び出しになります。

なのでモジュール読み込み時に確定で呼ばれるメソッドを用意したいというのがモジュール初期化子(module initializer)です。

今のところ、属性 + 静的コンストラクターで実装したいみたいです。

using System.Runtime.CompilerServices;
 
[module: ModuleInitializer(typeof(MyModuleInitializer))]
 
internal static class MyModuleInitializer
{
    static MyModuleInitializer()
    {
        // モジュール読み込み時に実行される
    }
}

見た目で区別できない変数

$
0
0

ちょっとしたネタ投稿をしてみたわけですが。

https://paiza.io/projects/WMu7W_PPTqkZRV5iGztJtg

import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception {
        int a = 1;
        int a︀ = 2;
        int a︁ = 4;
        int a︂ = 8;
        int a︃ = 16;
        int a︄ = 32;
        int a︅ = 64;
        int a︆ = 128;
        int a︇ = 256;
        int a︈ = 512;
        int a︉ = 1024;
        int a︊ = 2048;
        int a︋ = 4096;
        int a︌ = 8192;
        int a︍ = 16384;
        int a︎ = 32768;
        int a️ = 65536;

        System.out.println(a + a︀ + a︁ + a︂ + a︃ + a︄ + a︅ + a︆ + a︇ + a︈ + a︉ + a︊ + a︋ + a︌ + a︍ + a︎ + a️);
    }
}

ぱっと見は同じ「a」という名前の変数が17個並んでいますが、これ、全部別変数です。 名前被りでコンパイル エラーになったりもしません。

まあ、種を明かすと異字体セレクターっていう不可視の文字をくっつけてるだけで、この異字体セレクターが16種類あるので(何もつけてない1個と併せて)17個の「a」を作れているという状態。

元々同じ見た目で別の文字

特に難しいことをしなくても、元々、見た目が同じ別の文字は結構あります。 分かりやすい例で言うと、

とか。 全部出自が同じだし、発音も同系統(「あ」系統)なので似てて当然なんですが。 「出自が同じで字形も近い文字をまとめるかどうか」と言うのは結構根が深い問題になります。 Unicode ではこれらの文字を別文字扱いしています。

(別の文字なので当然字形が違ってもいいんですけども、 たいていのフォントでは同じ字形で表示されて区別がつかないと思います。)

こいつらを使えば、割と簡単に「ぱっと見で区別がつかない変数」を作れます。

using System;
 
class Program
{
    static void Main()
    {
        var A = 1;
        var Α = 2;
        var А = 4;
 
        Console.WriteLine(A + Α + А); // 7
        Console.WriteLine((int)nameof(A)[0]); // U+0041 (10進 65)
        Console.WriteLine((int)nameof(Α)[0]); // U+0391 (10進 913)
        Console.WriteLine((int)nameof(А)[0]); // U+0410 (10進 1040)
    }
}

今時、意図的に排除でもしていない限り non-ASCII な文字も変数名に使えるので、たいていの言語で同じことができると思います。 まあ、わざわざ non-ASCII な文字でソースコードを書く人も少ないので、悪意を持ってやらない限りは起きないと思いますが。

( 例えば Rust は ASCII 文字しか受け付けない意思を感じる。)

アクセント

もう少し悪意を高めてみます。

Unicode はそれ以前の各国個別の文字コードとの互換性のために、 いくつか、全く同じ文字を別のコードで表現できてしまいます。 分かりやすいのがアクセント記号の類(ダイアクリティカルマーク)で、 例えば、「á」の文字は以下の2通りの表現方法があります。

  • á (U+00E1) : 元々西欧向け文字コードに入っていた文字
  • á (U+0061 U+0301) : 無印の a の上に acute アクセントを結合

Unicode にはこの手の「同じ文字の別表現」を互いに変換するための仕様も入っているんですが、 その手の変換まで書けるプログラミング言語はまずありません。

結果的に、この手の文字も「ぱっと見で区別がつかない変数」に使えます。

using System;
 
class Program
{
    static void Main()
    {
        var á = 1;
        var  = 2;
 
        Console.WriteLine(á + ); // 3
        Console.WriteLine((int)nameof(á)[0]); // U+00E1 (10進 225)
        Console.WriteLine((int)nameof()[0]); // [0] は `a` が拾える。U+0061 (10進 97)
        Console.WriteLine(nameof().Length); // 実は2文字なので Length が 2
    }
}

アクセント記号(この例で言うと U+0301)を変数名に使えるかどうかはプログラミング言語によります。 例えば Go は letter (a とかの可読文字)しか受け付けませんJava とか C# とかは、2文字目以降にはアクセント記号の類を受け付けます。

0幅不可視文字

Unicode にはいくつか、以下のような、0幅でまったく見えない文字が存在します。

まあでも、この手の文字はほとんどのプログラミング言語が受け付けません。 Java と C# はちょっと特殊で、「受け付けるけど完全に無視」という挙動をしたりします。

いずれにしても、0幅不可視な文字がソースコードに紛れていて困ることはほとんどない… と思っていたんですけども…

異字体セレクター

そういえばあったわ、Java と C# が変数名として受け付けて、無視もせず、0幅不可視の文字… というのが異字体セレクター

数字の 0 を斜線付きで表示したりするために使う文字なんですけども、カテゴリーが Nonspacing Mark (アクセント記号とかと同じカテゴリー)だったりします。

ほとんどの文字が異字体なんて持ってないので、異字体セレクターがくっついていても、テキスト レンダリング上は単に無視されます。 要するに、実質、0幅不可視文字。 なのに、カテゴリー的には変数として受け付けられるし、別の文字として扱われます。

このうち U+FE00〜U+FE0F の16文字を使って書いたコードが冒頭の Java コードになります。

再掲: https://paiza.io/projects/WMu7W_PPTqkZRV5iGztJtg

ちなみにこの文字、後ろにいくらでもつなげられるので、16個と言わず、文字列長を増やしていけばいくらでも「a」を増やせます。

ちなみに、背景

これ、単体テストのテストケースを検討しているときに思いついたものの、 以下の2つの理由で没ったものだったりします。

  • 簡単に入力できない
    • 普通の状況で絶対に現れない文字列
    • 日本語 IME を使って入力できちゃう「葛󠄀」(葛󠄀城市の葛󠄀。葛飾区の葛の異字体。U+845B U+E0100)の方が適任
  • プログラム的には難しい処理をしていない
    • 厄介なのは人の目に見えないという点だけ
    • プログラム的には á (U+0061 U+0301) と同じパターンなので、á だけで十分

というか、「葛󠄀」と「葛」は区別されるのかどうかが気になったのが先で、 異字体セレクター (U+E0100 とか)のカテゴリーがアクセント記号の類と同じ(Nonspacing Mark)なことに気付いたというのが実際の流れ。 (しかも、この Nonspacing Mark カテゴリーの文字一覧を眺めてみてるに、完全に不可視なのは異字体セレクターだけっぽい。)

ちなみに、どうやって上記 Java コードを書いたかと言うと… "a\uFE00a\uFE01a\uFE02a\uFE03..." みたいなエスケープ シーケンスを使ってプログラムで出力して、それをコピペして書いています。

C# 9.0 in .NET 5 Preview 4 (Build でのリリース)

$
0
0

一昨日、C# 9.0 の話を動画配信してたわけですが。

Microsoft Build に合わせていろいろとブログ公開が公開されてました。

結構いろんな方向に手を出してるなぁという感じで、あんまり事細かに全部は見れていないんですが。 というか、全部見てるといくらでも時間が溶けるというか。 C# 9.0の話だけでもお腹いっぱいというか。 動画配信でも当然のようにほぼ C# 9.0 の話だけで2時間くらいになっています。

C# チームによる C# 9.0 のブログも投稿されています。

ちなみに、C# チームのブログは「C# 9.0 は最終的にこうなる予定」という内容で、 今現在は動かない機能が結構あります。 (というか、9.0 の主役なのでブログのボリュームを割かれている Records が、 まだまだ絶賛作業中で仕様レベルでも完成形になっていないです。)

一方で、配信で話した&今日ここで書くのは .NET 5 Preview 4 に merge された新機能についてです:

  • native int
  • target-typed new
  • pattern V3

これらを動かすためには相変わらず LangVersion preview 指定が必要になります。 (9.0 指定はまだできません。)

配信前に用意したコード:

配信中に書きちらかした結果

native int

native int は、 32ビットCPUでは32ビット整数(int, uint)、 64ビットCPUでは64ビット整数(long, ulong) になる整数型です。

これまでも IntPtrUIntPtr がそういう性質を持つ型だったんですが、 名前通りポインター用であって、 普通の整数演算(加減乗除とか)には使いにくかったです。 それが C# 9.0 で、

  • nintnuint というキーワードを用意
  • このキーワードを使って作った変数の場合、int とかと同じ整数演算が使えるようになる

という状態になります。 ちなみに、実体(コンパイル結果)としては IntPtrUIntPtr に翻訳されるみたいです。

まあ、CPU の違いを意識したコードを書かないといけない人と言うのはあまり多くないはずなので、 nintnuint もあまり多くの人が使う機能ではないと思います。

target-typed new

ターゲットの型から推論できる場合、new T()T を省略できて、new() と書ける機能です。 ちょっとした機能ですが、もしかするとこれからお世話になる度合いでいうと C# 9.0 の中で一番多いかもしれません。 (C# 7 世代でも、個人的にはtarget-typed defaultの利用頻度が非常に高かったです。)

var (ソース側からの推論)よりもターゲット側からの推論が有効なのは以下のような場面です。

1つはフィールドとかプロパティの初期化子。

using System.Collections.Generic;
 
class Sample
{
    private static Dictionary<int, string> _cache = new();
}

もう1つはメソッドの引数。

static void M(Dictionary<string, string> options) { }
 
static void Main()
{
    M(new()
    {
        { "define", "DEBUG" },
        { "o", "true" },
        { "w", "4" },
    });
}

特に、ジェネリックな型でフルネームを書くと長ったらしくなるものに対して有効で、 ここで挙げた例みたいに Dictionary に対して使うことが多くなるんじゃないかなと思います。

pattern V3

C# 7.0 から脈々と、ちょっとずつ拡充されてきたパターン マッチングなんですが、一応、9.0 で最終形になります。

まあ、7.0 と 8.0 では見送られる程度には複雑な割に需要が低いパターンなので、 そんなに使う機会はないかもしれません。

そもそも、パターン自体が Roslyn チームの「内需」なんじゃないかという感じもありますし。 (コンパイラーとか書いてるとパターン マッチが欲しい場面が多々あります。 is T 分岐だらけですし、[Azcase が並んでいる嫌な感じの switch] とかが平然と出てくるので。)

ただ、C# 9.0 の主役である Records の発展形として、

みたいな話があって、これが入るとパターン マッチングの需要がちょっと上がるかもしれません。

また、複雑なパターンはたぶん書かないという人にとっても、以下の2つのパターンは便利だと思います。

  • simplified type pattern
  • not pattern

前者は、以下のように、_ なしで型パターンを使えるというもの。

static int M(object obj) => obj switch
{
    // C# 8.0 までだと _ が必須だった。
    // 型名だけだと定数パターンとの区別がつかないため。
    short _ => 1,
    int _ => 1,
    long _ => 1,
 
    // C# 9.0 では型名だけで型パターンにできるようになった。
    // 文脈を読んでくれてる(型名だったら型パターン、型がなければ定数パターンとして解釈)。
    ushort => 2,
    uint => 2,
    ulong => 2,
};

後者は名前通り「パターンを満たさないとき」用の構文です。 たぶん、not null が一番使うと思います。

static int M(object obj) => obj switch
{
    // string 型のインスタンスじゃないとき
    not string => 1,
    // null じゃないとき
    not null => 2,
};

ちなみに、「こんな変な文法を追加しなくても、既存の構文で if とか when とか並べればいいんじゃないのか?」と思うかもしれませんが、パターン マッチングは結構賢くて、 ちゃんと条件が網羅的かどうかの判定をやってくれます。

static int Invalid(object obj)
{
    if (!(obj is string)) return 1;
    if (!(obj is null)) return 2;
 
    // null の時は1つ目の if に引っかかっているはずで、
    // 2つめ if と合わせると全条件網羅してる。
    // なので、ここには到達できないはずだけど、if だと到達判定が効かない。
    // このメソッドはコンパイル エラーを起こす。
}
 
// パターンを使うと網羅性の判定が正しく動く。
// このメソッドはエラーにもならないし警告も出ない。
// 逆に、網羅できてない場合は警告が出る。
static int Valid(object obj) => obj switch
{
    not string => 1,
    not null => 2,
};

あと、数値の範囲を表すパターンとして min..max を使いたいという意見も結構あるんですが… 「両端を含む・含まない」の区別が紛らわしすぎるということで愚直に <<=>>= を使うということになりました。

static int M(byte b) => b switch
{
    >= 0 and <= 10 => 1, // 0 も 10 も含んで [0, 10] の範囲
    > 10 and <= 20 => 2, // 10 は含まず 20 は含んで (10, 20] の範囲
    > 20 and < 30 => 3, // 20 も 30 も含まず (20, 30) の範囲
    >= 30 and < 40 => 4, // 30 は含んで 40 は含まず [30, 40) の範囲
    _ => 0,
};

ちなみに、比較パターンでも網羅性のチェックが働いています。

// これは無警告
static int M(byte b) => b switch
{
    >= 0 and <= 250 => 1,
    251 or 252 or 253 or 254 => 2,
    255 => 3,
};
 
// 例えば以下の3つには警告が出る
static int M1(byte b) => b switch
{
    >= 0 and < 250 => 1, // <= と間違えて < を書いて、250 が漏れてる
    251 or 252 or 253 or 254 => 2,
    255 => 3,
};
static int M2(byte b) => b switch
{
    >= 0 and <= 250 => 1,
    251 or 253 or 254 => 2, // 252 が漏れてる 
    255 => 3,
};
static int M3(byte b) => b switch
{
    >= 0 and <= 250 => 1,
    251 or 252 or 253 or 254 => 2,
    // 255 が漏れてる
};

この辺りのパターンの最適化の掛け方とか網羅性のチェックは、 先達となるプログラミング言語があって割と十分に検証されているらしく、 「需要は高くないけど、検討コストも低いから go サインが出た」という類になります。


partial メソッドの拡張 (C# 9.0 候補機能)

$
0
0

もう1週間近く経過しちゃってるんですけども、Visual Studio 16.7 Preview 2 が出ています。

で、今日はこの Preview 2 で追加された C# 9.0 候補の話です。 先月追加された Srouce Generator関連の機能で、 partial メソッドの亜種が追加されました。

先月、Design Notesの時点で軽く触れていた機能が、この度実際に触れる状態になっています。

(既存の) partial メソッド

意外と知られていない機能みたいなので改めて、既存の partial メソッド自体についても説明。

一番の用途としては、自動生成されているコードに、一部分だけ手動で処理をカスタマイズしたいみたいなときに使います。 例えば、以下のようなソースコードを T4 テンプレートなどを使って生成していると考えてください。

// T4 テンプレートとか XAML とかから自動生成されている想定のコード
partial class Sample
{
    public int X
    {
        get => _x;
        set
        {
            OnXChanging();
            _x = value;
            OnXChanged();
        }
    }
    private int _x;
 
    // 既定動作としては何もしたくない。
    // 人手で、何かしら _x = value; の前後に処理を挟みたいときにはこれに実装を持たせる。
    partial void OnXChanging();
    partial void OnXChanged();
}

一部分だけカスタマイズしたいからといって、このコードに手作業で修正を加えてしまうと、 再度自動生成がかかったタイミングで上書きされて消えてしまいます。 そこで、カスタマイズする可能性のある部分に partial メソッド(partial 修飾子を付けて、中身を持たない空っぽのメソッド)を挟んでおきます。

もしも、手作業カスタマイズが必要なら、以下のように、別ファイルで partial メソッドに実装を与えます。

// 手書きする想定のコード
partial class Sample
{
    partial void OnXChanged()
    {
        System.Console.WriteLine("X: " + X);
    }
}

今回上げた例では、OnXChangingOnXChanged という2つの partial メソッドがありますが、 そのうち OnXChanged の方にだけ実装を与えています。 virtual とか abstract とかとは違って、以下のような挙動をします。

  • 必要ないなら実装を与えなくていい
  • 実装を与えていない場合、コンパイル結果から完全に消滅する
    • メタデータ(リフレクションで取れるメソッド情報)すら残さないのでノーコスト
  • 実装を与えた場合でも、通常のメソッド扱い
    • virtual を付けた場合と違って、インライン展開が効いたりして負担が少ない

C# 2.0 の頃からある機能なんですけども、 言われてみると利用場面が少ないというか。 T4 なりなんなり、コード生成を多用している人にしか目につかないのかなと思います。 Entity Framework の Scaffolding とかで使われているはず…

ただ、まあ、dotnet/runtimeとかで用例を探してみたものの、テストを除けば数十件くらいしか出てこないんですよね。確かにレア機能。 しかも、自動生成のコードと手書きコードの橋渡しと言うよりも、プラットフォーム依存な処理の分離に使われてることの方が多そう。

partial class Sample
{
    public void M()
    {
        OnMBegin();
 
        // 全プラットフォーム共通処理
    }
 
    partial void OnMBegin();
}
 
// Sample.Windows.cs みたいなファイル
partial class Sample
{
    partial void OnMBegin()
    {
        // Windows 限定処理
    }
}

新 partial メソッド

で、C# 9.0 で追加されるのは逆向きの用途で使うものです。 手書きの方が先にあって、その実装は Source Generator に埋めてもらうという想定。

例えば以下のような書き方をします。

// 手書きコード側
using System;
 
partial class Sample
{
    [Utf8("abcd")]
    public partial ReadOnlySpan<byte> M();
}

2.0 の頃からある partial との違いは以下の通りです。

  • アクセシビリティの指定をする
    • アクセシビリティの有無でどちらの partial メソッドなのかの分岐をしてる
  • 戻り値が void でなくてもいい
    • アクセシビリティなしの partial メソッドは void しか受け付けない
  • ref 引数out 引数を持てる
    • 同上
  • 必ず1個、実装を与えないといけない
    • 手書きの C# コードを起点にして、Source Generator で実装を生成する想定

例えば上記の例は、Source Generator を使って以下のようなコードを生成する想定で書いています。

// Source Generator で生成する想定のコード
partial class Sample
{
    public partial ReadOnlySpan<byte> M()
        => new byte[] { 0x41, 0x42, 0x43, 0x44, };
}

アクセシビリティの有無で挙動が違うっていうのはそこそこ気持ち悪くはあるんですが。 元々の partial メソッド自体がほとんど使われていませんし、 このためだけに新しいキーワードを追加するほどではないのかなと思います。

おまけ

ちなみに、今回例に挙げている Source Generator のコードなんですが、 実際に作ってみたもので、 文字列を .NET の文字列リテラル(UTF-16 になる)ではなくて、UTF-8 のバイト列としてプログラムに埋め込むための Source Generator です。 (ちょっとコード整理して、GitHub に上げたら改めてブログに書こうかと思っています。)

昨日、こいつの紹介でちょっと動画配信したりしてたんですけども。

三人寄れば文殊の知恵というか、主にあえとすさんがすごい気もするものの、 ライブ配信中にこの新 partial メソッドのバグを見つけたり

みんなで寄ってたかって新機能を試してみるっていうのはライブ配信に対して求めていたことの1つなので、 非常によい回になったのではないかと思います。

ピックアップRoslyn 6/9: record

$
0
0

先月くらいからじわじわと、C# Language Design Meeting で Records がらみの議題が上がっています。 最近やっとまとまってきた感じがするのでまとめて紹介。

record 型の新設

まず、基本方針として、record は class/struct に対する修飾子ではなくて、enum とか delegate とかと同じく1種の型みたいな扱いにしたみたいです。 なので、以下のような書き方に。

record Point(int X, int Y);

とりあえず初期実装としては結構やることを絞るみたいで、

  • record は参照型
    • 値型なものは既存の struct に手を入れるか、"record struct" を新設するかになると思うもののまだ未定
  • プライマリ コンストラクターを持てるのは record だけ
    • class Point(int X, int Y) とか struct Point(int X, int Y) とかは未実装
    • 検討はされてるものの、record と同じコード生成をすべきかどうかでまだ迷ってそう
      • record の場合はプライマリ コンストラクター引数から public int X { get; init; } プロパティを作ることが決まってる
      • 通常の class, struct の場合はプロパティまでは作らない、キャプチャが掛からない限りフィールドにすらしないという案あり

みたいな実装のようです。

この辺りは issue のコメントでの反発も結構大きいんですが… 修飾子じゃなくて型のカテゴリーの新設な点とか、当初実装に値型版がない点とか…

構造体との一貫性

今はいったん未定な状態になってるんですが、 仮に、普通の class/struct にもプライマリ コンストラクターを持てて、 record のものと近いコード生成をすることになったとします (1案としてはそういう実装も考えられます)。

じゃあ、class と record の本質的な差は何になるかと言うと、

  • メンバーごとの(shallow な)比較による Equasl/GetHashCode が生成される
  • メンバーごとの(shallow な)コピーによる clone メソッドが生成される

という点になります。 で、この2つ、struct の場合は標準で作られます。

using System;
 
struct Point
{
    public int X;
    public int Y;
}
 
class Program
{
    static void Main()
    {
        var p1 = new Point { X = 1, Y = 2 };
        var p2 = new Point { X = 1, Y = 2 };
        Console.WriteLine(p1.Equals(p2)); // true
 
        p2.X = 3;
        Console.WriteLine(p1.Equals(p2)); // false
    }
}

ということで、コンセプト上は、「record は struct のような振る舞いを持つ参照型」みたいに考えることもできます。 なので、今の struct の挙動とあまりに違うものにはしたくないし、 今の struct が非効率な実装になっちゃってる部分は record に合わせて struct の方にも改善を入れてもいいかもとか、 そういう感じの話は出ています。

data 修飾子

プライマリ コンストラクター前提の構文は「positional record」と呼ばれています。 引数の並びに意味があって、new Point(1, 2) みたいに、positional(位置指定) で初期化ができるためこう呼びます。

一方で、プロパティを元にして、new Point { X = 1, Y = 2 } みたいに書く想定のものを「nominal record」と呼びます。 nominal record のために、data 修飾子も用意する流れのようです。 以下のような書き方ができます。一見、data 修飾子を付けたフィールドっぽい書き方ですが、get; init; な public プロパティが生成されます。

record Point
{
    data int X;
    data int Y;
}

base 呼び出しとか、プライマリ コンストラクター引数のスコープとか

あとは細かい話。 record 型は派生もできるんですが、その場合、以下のような書き方ができます。

record Person(string FirstName, string LastName)
{
    public string Fullname => $"{FirstName} {LastName}";
    public override string ToString() => $"{FirstName} {LastName}";
}
 
record Student(string FirstName, string LastName, int Id)
    : Person(FirstName, LastName)
{
    public override string ToString() => $"{FirstName} {LastName} ({ID})";
}

このとき、以下のような点が検討に上がっています。

  • コンストラクター引数に対して、それと同名のプロパティと、引数からプロパティへの代入コードが自動生成される
    • 代入のタイミングは base コンストラクターより前であるべきか後であるべきか
    • 今のところ「前」案優勢
  • 基底クラスのコンストラクターを呼んでいる部分(この例だと Person(FirstName, LastName) の引数の部分のスコープはどうなるべきか
    • クラス内の全メンバーがスコープ
    • ただ、通常コンストラクターのbase アクセスと同様に、インスタンス メンバーに触わろうとするとエラー
  • 自動生成されるのと同名のメンバーを手書きすると、手書きの方を優先して使う
    • Equals とか
    • その手書き Equals とかが sealed だったりするとエラーにする
  • object.Equals(object) じゃなくて Equals(T) は作るべきか? → そうする予定だし、IEquatable<T> の実装も需要が高いことは認識してて検討の範囲内

C# 9.0 in Visual Studio 16.7 preview 3

$
0
0

先週の話にはなってしまうんですが、Visual Studio 16.7 が Preview 3.1 になっています。 .NET 5 も Preview 6 に。

で、今回も C# 9.0 の新機能がいくつか入っています。

  • Records
  • Top-level statements
  • Function pointers

昨日、このネタでライブ配信してたりするんですが、 今回、ついに配信時間が3時間の大台に…

配信時間が長くなっているのはチャット欄での応答が盛り上がりすぎたというのが原因です。 (「新機能が多くて長引いた」みたいなのではなく。) 特に Function pointers の話とか、そもそも付いてくる人がいると思っていなかったので、 Function pointers だけで相当な時間しゃべることになったのがだいぶ意外…

インタラクションが欲しくて始めたライブ配信だし、 タイム キーピングを気にしなくていいのも楽なので、 うれしい悲鳴なんですけども、

!is 問題

最初に取り上げるのは C# 9.0 でもなんでもないんですけども。

VS 16.7 p3 のリリース ノートを見ていて、null 抑制演算子の ! に対するリファクタリングが入っていてなんとも言えない気持ちになったという話。

以下のコード、解釈の仕方を間違う人があまりにも多いらしく。

if (x !is 0) { }

!x が「x の否定」、not x の意味なせいで、それで勘違いしちゃう人がいたりします。 上記のコードは正しくは (x!) is 0 の意味で、この ! は後置きの x!。 否定の意味の ! ではなくて、C# 8.0 の null 許容参照型 がらみの機能で、効果としては単なる null 警告の抑止です。

挙句の果てに、is 0 と書いた時点で「null ではないことが確定」なので、null 警告の抑止をする意味すらないという。

つまるところ、!is を not is と勘違いして使ってしまうと、真逆の挙動になるというひどいバグを生む原因です。 なのでしょうがないので…

リファクタリング1: 意味がないので ! を消す

if (x is 0) { }

リファクタリング2: “ちゃんと”真逆に直す

if (x is not 0) { }

is not T x

not パターン自体は先月の時点で入っていたんですが、 微妙に今回の 16.7 Preview 3 リリースで入った修正もあります。 以下のようなコードが有効になりました。

static void M(object x)
{
    // not パターンでも変数宣言できる
    if (x is not string s)
    {
        // ちなみに、ここで s の中身を読もうとすると「未初期化」エラー
 
        s = "";
 
        // s を読めるのはこの行以降
    }
 
    // ここは絶対 s が初期化されている保証あり
    Console.WriteLine(s);
}

Top-level statements

Program.Main が要らなくなります。 C# スクリプト モードじゃなくても以下のように、Top-level (クラスとか名前空間の外)にコードが書けます。

using System;
 
Console.WriteLine("Hellow World!");

ちなみに、「C# スクリプト モード」とはまたちょっと挙動が違ったりします。

Top-level の場合

  • あくまで Main メソッドの自動生成
    • 変数やメソッドを書くと、ローカル変数、ローカル関数の意味になる
    • args (暗黙的変数)でコマンドライン引数を受け取れるし、return で終了コードを返せる
  • #r#load などのスクリプト専用機能は使えない

スクリプトの場合

  • ラッパー クラスが作られる
    • 変数はフィールドに、メソッドはインスタンス メソッドになる
  • 逆に、return とか namespace とか「通常モード」専用の構文は使えない

ちなみに、Top-level に普通に await も書けます。 await があれば async Task Main、なければ void Main みたいな扱いです。

Top-level ステートメントを書いた上で、さらにどこかのクラスに Main メソッドを書いてしまった場合、 Top-level ステートメントの方が優先されます(書いてしまった Main は呼ばれない)。 警告だけは出ます。

また、複数のファイルに Top-level ステートメントを書いたり、 名前空間やクラスよりも後ろに書いた場合はコンパイル エラーになります。 あくまで、1ファイルの先頭にだけ Top-level ステートメントを書けます。

Function pointer

関数ポインターを C# 上で書けるようになりました。

まあ、正直、大多数の人にとって直接触れる機能ではないです。 実質的には P/Invoke 専用機能になると思います(どうひねり出そうと思っても他の用途が思いつかない)。

以下のように、delegate* で「関数ポインター型」を作って、 & でメソッドのアドレスを取得できる機能です。

using System;
 
class Program
{
    unsafe static void Main()
    {
        delegate*<int, void> f = &M;
        f(1);
    }
 
    static void M(int x) => Console.WriteLine(x);
}

.NET の仕様上は上記コードに相当する命令(ldftn, calli)が元々あったりします。 ただ、C# からこれらの命令を使う手段が全くなくて、 これまでは IL アセンブラーや Reflection.Emit を使う必要がありました。

今、iOS や WebAssembly 対応のために、 Reflection.Emit による実行時コード生成を、 Source Generator によるコンパイル時コード生成に置き換えたいという話もあったりします。 P/Invoke の類も、 .NET Runtime の中で特殊対応するよりも、事前にソースコード生成したいみたいな話もあって、 そのために必要になる機能です。

「ないと困るから入れた」という類であって、 「便利に使いたい、簡単に使いたい」という動機はないので、 構文的に結構複雑だったりします。

ただ、関数ポインターで <void> を認めてくれるんなら、普通のデリゲートでも同じように書きたい… (書けないし、今後もたぶんずっと無理)

using System;
 
class Program
{
    static void Main()
    {
        Func<int, void> f = M; // こう書きたい(無理)
        Action<int> a = M; // 戻り値が void かどうかで型が違う
    }
 
    static void M(int x) => Console.WriteLine(x);
}

Records

待望(?)の Records。 概ね、6月9日にブログに書いた状態で実装されていそうな感じ。

一番シンプルな書き方をすると以下のようになります。 プライマリ コンストラクター構文。 引数の順序(position, 位置)に意味があるので positional record と言ったりもします。

using System;
 
// 一番シンプルな書き方はこうなる
record Point(int X, int Y);
 
class Program
{
    static void Main()
    {
        var p = new Point(1, 2);
        Console.WriteLine(p.X);
    }
}

classstruct と並んで、record という型定義用のキーワードが増えます。

ちなみに上記コードは以下のコードとほぼ同じ意味になります。

// プライマリ コンストラクターの展開結果
record Point
{
    public int X { get; init; }
    public int Y { get; init; }
 
    public Point(int X, int Y)
    {
        this.X = X;
        this.Y = Y;
    }
}

残念ながら、今のところ(というか、おそらく C# 9.0 リリース時点では)、 プライマリ コンストラクターを書けるのは record だけになりそうです。

一方、initclassstruct でも使えます。 例えば、以下のようなコードは有効な C# 9.0 コードになります。

// init に関しては çlass でも struct でも使える
class Point
{
    public int X { get; init; }
    public int Y { get; init; }
 
    public Point(int X, int Y)
    {
        this.X = X;
        this.Y = Y;
    }
}

ということで、Records の説明をする上での本質は以下の2点からなります。

  • じゃあ、 classrecord は何が違うか
  • プロパティの init は何か

補足: IsExternalInit 属性

init プロパティを表現するために IsExternalInit という名前の属性を使っているんですが、 これは、

  • 現時点では .NET 5 にも入っていない
  • リリース時点では .NET 5 には入る予定
  • 古い .NET ランタイムに対するポーティングとかは提供せず「C# 9.0 をフルにサポートするのは .NET 5 のみ」という扱いにしたい

ということになっています。 ただ、以下のコードを自前で用意すれば、現時点の .NET 5 Preview や古い .NET ランタイムでも recordinit を使えます。

namespace System.Runtime.CompilerServices
{
    internal class IsExternalInit : Attribute { }
}

ちなみにこの型、Attribute から派生している立派な「属性」ですが、 実際の使われ方は modreq になります。

後述する init が、古い C# コンパイラーから触られるとまずい機能なので、触れなくするために modreq を使っています。

record と class

現状だと、プライマリ コンストラクターを書けるのは record だけなんですが、 classstruct に対しても後々追加される可能性は結構高いです。

で、前述の通り、init プロパティを使うのであれば、classstructrecord でほぼ同じ書き方ができます。 じゃあ、classstructrecord の何が違うかと言うと…

  • struct みたいな、全フィールドの memberwise 比較を元にした EqualsGetHashCodeClone がコンパイラー生成される
  • record は参照型

みたいな感じです。 要するに、struct 的な「値セマンティクス」を持つ参照型が record

これだけ書いてしまうと大したことをしていないように聞こえますけども、 immutable なデータ構造を簡単に書けるようにするという意義があります。

参照型の「値比較」(memberwise に EqualsGetHashCode 実装)は immutable に作らないとまずいです。 一方で、immutable なクラスを真面目に書くのはすさまじく大変で、その負担を減らしてくれるのが record です。

あと、派生が絡んだ時の Equals 実装も意外とめんどくさくて、そこも record からのコード生成が頑張ってくれています。

with 式

immutable なクラスの部分書き換えをする場合、基本的には Clone してから所望のメンバーだけを上書きと言う処理が必要になります。

それをやってくれるのが with 式で、record に対して以下のような書き方ができます。

using System;
 
record Point(int X, int Y);
 
class Program
{
    static void Main()
    {
        var p = new Point(1, 2);
 
        // p を部分書き換え(この場合 X だけ書き換え)
        var p1 = p with { X = 3 };
 
        Console.WriteLine((p.X, p.Y));   // (1, 2)。元の p は不変
        Console.WriteLine((p1.X, p1.Y)); // (3, 2)。p を Clone した上で X だけ書き換えてる
    }
}

これと同じことを class の手書きでやろうとすると、以下のように、Clone 後の書き換えで immutable であることが破たんします。

using System;
 
class Point
{
    // 本当は set できるとまずいけど、Clone 後の書き換えのために必須になってしまう。
    // これを回避するために init アクセサー(後述)がある。
    public int X { get; set; }
    public int Y { get; set; }
    public Point(int x, int y) => (X, Y) = (x, y);
    public Point Clone() => new Point(X, Y);
}
 
class Program
{
    static void Main()
    {
        var p = new Point(1, 2);
 
        // p を部分書き換え(この場合 X だけ書き換え)
        var p1 = p.Clone();
 
        // Clone 後の書き換えのためにやむを得ず public set。
        // init とか with とか、新構文が必要になる理由。
        p1.X = 3;
 
        Console.WriteLine((p.X, p.Y));   // (1, 2)。元の p は不変
        Console.WriteLine((p1.X, p1.Y)); // (3, 2)。p を Clone した上で X だけ書き換えてる
    }
}

(もっと複雑で、実行時コストも高い方法でよければもうちょっとやり様はあるんですが。 後述する init プロパティで一応、実行時コストは掛けずにこの問題を解決できるので、それを採用することになりました。)

ちなみに、原理的には with 式は init プロパティを持つ classstruct に対しても使えるはずなんですが、 C# 9.0 時点では record 専用構文になりそうです。 (スケジュールの問題。あとで classstruct に対する wth 式追加が検討される。)

init アクセサー

プロパティのアクセサーに、set の代わりに init を使うことで、 初期化子や with 式でだけ書き換え可能なプロパティができます。

class InitOnly
{
    public int X { get; init; }
}
 
class Program
{
    static void Main()
    {
        var p = new InitOnly
        {
            X = 1, // 初期化子を使える
        };
 
        p.X = 2; // これはコンパイル エラー
 
        // with 式での書き換え(Clone 後の書き換え)もできる
        var p1 = p with { X = 3 };
    }
}

init アクセサーは以下の場所からだけ呼び出せる制限付きの set みたいなものです。

  1. そのクラスのコンストラクター内
  2. オブジェクト初期化子
  3. with
  4. 他の init アクセサー内

1だけでよければ get-only プロパティでも実現できるんですが、残りの3つのために init アクセサーが新設されました。

ちなみに、setinit を同時に書くことはできません。 というか、init アクセサーは内部的には「特殊な属性を付けた set アクセサー」として実現されています。

ピックアップRoslyn 7/19: そろそろ C# 9.0 機能は fix、10.0 向けトリアージ

$
0
0

Visual Studio 16.7 Preview 4 が出てるのと、Design Meeting 議事録を1件紹介。

16.7 Preview 4 で、LangVersion9.0 が入りました。 また、7月13日の Meeting 議事録は C# 9.0 よりも先の話が出てきています。 一部のちょっとした修正を除けば、「C# 10.0 候補として採用」、「C# 10.0 のタイミングで再検討」みたいな結論のものが多いです。

16.7 Preview 4 と C# 9.0

16.7 Preview 3 のときから目立った新機能はないんですが、Preview 3 の時に気になっていたバグは治っていました。

特に、Top-level statements が安心して使えるようになったのは結構な嬉しさがあります。 (Preview 3/3.1 の時は、誤判定で「未使用 private メソッド扱い」されて挙動不審だった。)

以下のような、class Programstatic void Main も要らないコードが書けます。

using System;

foreach (var r in "🥺😍🙄".EnumerateRunes())
{
    Console.WriteLine($"{r.Value:X}");
}

ぴえん

これまで、C# 9.0 候補の機能は Preview としてだけ提供されていて、LangVersionpreview を指定しないと使えませんでした。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
    <LangVersion>preview</LangVersion>
  </PropertyGroup>

</Project>

これに対して、VS 16.7 Preview 4 で、「9.0」が追加され、以下のように言語バージョンを明示できるようになりました。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
    <LangVersion>9.0</LangVersion>
  </PropertyGroup>

</Project>

また、 .NET 5 がターゲット(TargetFrameworknet5.0)の場合、 デフォルト挙動(LangVersion を省略、もしくは、default 指定)が C# 9.0 になりました。 なので、以下の書き方(net5.0LangVersion 省略)でも C# 9.0 になります。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

</Project>

10.0 向けトリアージ

スケジュール的に、今年11月リリースを予定されている .NET 5/C# 9.0 にはこれ以上大き目の機能は追加されない状態になりました。 (いくつか小さめの機能が入る可能性や、現状入っている機能に対して修正が加わる可能性はまだまだあります。) なので、C# Language Design Meeting ではそろそろ「10.0」(来年11月)を見据えた話になっています。

Generics and generic type parameters in aliases

  • using MyList<T> = System.Collections.Generic.List<T>; みたいに、using エイリアスに型引数を書きたいという話
  • その他、タプルとかにもエイリアスを書けるようにしたい
  • (ファイル単位じゃなく)グローバルに影響する using も検討の対象
  • C# 10.0 に向けて検討

"closed" enum types

  • enum に対して、switch で網羅性チェックが効く(その代わり、後からのメンバー追加が破壊的変更(switch に警告・エラーが出る)ようにしたいという話
  • 元々 C# 10.0 として検討されていた Discriminated Union (Record の延長戦上)と一緒に検討したい

Allow ref assignment for switch expressions

  • switch 式で ref 戻り値を返したい
  • ref var r = ref x switch { 0 => ref a, _ => ref b }; みたいなの
  • ちなみに、条件演算子の場合は今でも ref var r = ref (x ? ref a : ref b); と書ける
  • 「Any Time」(優先度低め。コミュニティ貢献があれば実現するかも、くらい)扱い

Null suppressions nested under propagation operators

  • null 抑止の後起き ! の挙動がちょっと怪しいらしい
  • ! の有無によって実行時の挙動は変わらない」(あくまで警告抑止の効果しかない)ということになっているのに、挙動を変えちゃうことがあるみたい
  • a?.b.c!.d.e(a?.b.c)!.d.e として解釈されてて、このせいで、?. のショートサーキットの掛かり方が変わるとのこと
  • この挙動はまずそうなので、破壊的変更になってでも直したい
  • C# 9.0 のタイミングで検討

Relax rules for trailing whitespaces in format specifier

  • $"{date:yyyy-MM-dd}$"{date:yyyy-MM-dd } みたいに、文字列補間のフォーマット指定で、{} 内の空白の有無で挙動が変わるのは変じゃないか
  • Rejcted。変じゃない。意図的。元から、date.ToString("yyyy-MM-dd")date.ToString("yyyy-MM-dd ") で出力が変わるので

Private field consideration in structs during definite assignment analysis

  • struct Result { public object Value { get; } } みたいに、自動実装プロパティから生成されてるはずの private フィールドが、構造体の「確実な初期化」解析から漏れるバグがあるらしい
  • native compiler (C# 5.0 以前の、C++ 実装の C# コンパイラー)時代からのバグで、「バグまで含めて破壊的変更を起こさないように移植した結果」とのこと
  • でも、結構まずいバグなので変更することに前向き
  • 「来週しっかり検討」という扱い(たぶん、C# 9.0 で修正しそう)

Remove restriction that optional parameters must be trailing

  • void M(int x, int y = 0, int z) みたいに、末尾以外にオプション引数を認めたいという話
  • .NET のメタデータ的には許される構造で、C# が禁止してるだけ
  • まあ、制限を緩めてもいいかも
  • Any Time (あまり積極的ではない)

List patterns

  • パターン マッチングで、x is [1, 2, 3] みたいな書き方で配列/リストに対するマッチングをしたいという話
  • コミュニティ貢献ですでにプロトタイプがある
  • それを元に詳細を検討する必要がある
  • C# 10.0 に向けて検討

Property-scoped fields and the field keyword

  • プロパティの get/set/init 内でだけアクセスできるフィールドを定義したいという話
  • ものすごく昔からたびたび似た要求が上がっては「そのうちね」な空気感だったやつ
  • ついに C# 10.0 で検討

File-scoped namespaces

  • namespace X { ... } みたいに {} でくくってインデントが1段下がる書き方じゃなく、namespace X; みたいな1行でインデントを下げずに名前空間定義したいという話
  • 同上、これもついに C# 10.0 で検討

Allowing ref/out on lambda parameters without explicit type

  • (ref x, ref y) => {} みたいなラムダ式を書きたいという話
  • 実装できない大きな問題もなさそうだけど、優先度が付くほど需要もない
  • Any Time

Using declarations with a discard

  • using var _ = new X(); とか using (new X()) { } とかは書けるんだから、using _ = new X(); とか using new X(); を認めてほしいという話
  • X.X (「いつかは取り組む」くらい。Any Time よりは多分前向き)

Allow null-conditional operator on the left hand side of an assignment

  • x?.Value = 1; みたいなので、if (x != null) x.Value = 1; 扱いしてほしいという話
  • X.X

Top level statements and functions

  • Top level statements 自体は C# 9.0 に入る
  • まだ検討課題として残っているものとして、top level に書いたメソッドはプロジェクト全体から呼べるべきかどうかという話がある
  • 9.0 ではあくまで「生成される Main メソッド内のローカル関数扱い」、「top level メソッドは top level statement からしか呼べない」という実装で行く
  • 「プロジェクト全体から呼べるかどうか」は「top level statements part 2」としてC# 10.0 で再度ディスカッションを設ける

Implicit usings

  • プロジェクト全体にしたして影響する using ディレクティブが欲しい
  • C# スクリプト構文だとこれに類するものが認められてて、例えば Visual Studio の C# Interactive では using System; はなくても System 名前空間の型を使える
  • top level statements 文法とスクリプト文法の差異を少しでも減らすためには、通常 C# でもこの手の暗黙的な using があった方がいいのではないか
  • 現状、csc のオプションとして csc -using:System みたいな与え方を検討
  • C# 10.0 のタイミングでディスカッション

ピックアップRoslyn 7/21: 引き続き C# 10.0 向けトリアージ

$
0
0

先週に続きトリアージ。

あと、records がらみの残作業整理。

こちらも 9.0 (今年11月リリース)よりも先の話になります。 records の「残作業」というのも「9.0 からは削ったけども」という話です。

トリアージ

Extend with expression to anonymous type

  • 匿名型は record 的な性質がある(immutable で、オブジェクト初期化子を使ってインスタンス生成)型なので、record と同じく with 式を使いたい
  • そもそも「with 式の汎用化」自体「records の残作業」なので、それと合わせて C# 10.0 に向けて考えたい

Required properties

  • init プロパティ(var a = new A { X = 1 } みたいに初期化子での書き換えはできるけど、その後 a.X = 2; みたいな書き換えは認めないプロパティ)に対して、「初期化子での初期化を義務付け」みたいな制約を足したい
  • C# 10.0 で考える

Shebang support

  • Shebang = スクリプトでよく見る #!/bin/sh みたいなやつのこと(sharp + bang を縮めた造語) ‐ シェルに解釈してもらって、「このスクリプトファイルをどのインタープリターに掛けるか」みたいなのの指定に使う ‐ インタープリター側からすると単に無視してる
  • これまで C# は、ランタイムのバージョンとか、依存するパッケージ、ツールとかの指定は C# ソースコード内には書かず、外部(csproj とか)に置いてた
    • C# のスクリプト用途を次のステップに進めるために、C# でも「単一 cs ファイルでこの手の情報を持てるようにしたい」という話に
  • C# 10.0 のタイミングでディスカッションしたい ‐ 10.0 に入れれないかもしれないけど、「ディスカッション開始自体に何年もかかる」みたいにはしたくなくて、「何か月かかかる」程度に収めたい

Private fields in structs with SkipLocalsInit

  • 前回も書いた「private フィールド持ちの構造体の確実な初期化が実は漏れてる」問題、やっぱりまずいという話に
  • SkipLocalsInitと組み合わさると、セキュリティ的にもガベコレ的にも危ないコードになる
  • native compiler (C# 5.0 以前の、C++ 実装の C# コンパイラー)時代からの負の遺産が残っているだけで、ちゃんと未初期化を検知して警告にできる実装にはなってる
    • 警告の追加も破壊的変更になるので、警告ウェーブ(言語バージョンとは別に警告のバージョニングを追加する)とともに有効にする予定だった -でも、SkipLocalsInit 指定があるときには有無を言わさず警告を出すように修正することにした

records がらみの残作業

More initialization functionality

reocrds は、これまでの C# の「immutable なオブジェクトの初期化がとにかくめんどくさい」という問題に対する解決策だったりするので、初期化回りの機能が多いです。 9.0 で入れれなくて引き続き課題になっているものも結構残っていたり。

Init fields

  • init-only プロパティ(T Property { get; init; } みたいなやつ)だけじゃなく、フィールドにも init 修飾をつけて、オブジェクト初期化子で書き換え可能にしたい

Init members

  • init-only プロパティはコンストラクターか他の initアクセサーからも書き換えできるけど、9.0 時点ではその他のメソッドからは書き換えできない
  • メソッドに init 修飾を付けることで、コンストラクターか init アクセサー内からだけ呼び出せて、init-only プロパティの書き換えができるメソッドを定義したい

Final initializers

  • オブジェクト初期化子での初期化が一通り終わったあとに呼ばれるメソッドが欲しい
    • コンストラクターはオブジェクト初期化子よりも前になっちゃう
  • 今のところ init { ... } みたいな構文を考えてる
  • この機能自体は欲しい。初期化が一通り終わったあとに、オブジェクトの状態が不正じゃないかの確認をしたいことは多々ある
  • ただ、「オブジェクト初期化子の後に呼ぶ」っていうタイミングは古いコンパイラーには強制できない

Required members

トリアージでも出てきてるので省略。

Factories

  • ファクトリーメソッドも records の一部として考慮 ‐ 「必ず新しいインスタンスを作って返す」みたいなのを強制する仕組みが要る

What about collection initializers?

  • init-only なコレクション初期化子は可能?
    • new T { a, b } みたいなのは、var x = new T(); x.Add(a); x.Add(b); に展開しちゃうし、通常、この Add は mutation (状態の書き換え)を起こしちゃう ‐ 前述の init members を使って、init-only (初期化子のタイミングまでは呼び出し可能)な Add メソッドを作れればいける?

Generalizing away record magic

今、records (record キーワードを使って型を宣言)専用になってしまっている機能が結構あるものの、普通のクラスや構造体に対しても適用できそうな文法も結構あります。 records と他の複合型の差は小さい方が、互いに乗り換えがしやすくて好ましいので、 できる限り「records 専用な構文」は作りたくありません。

Allowing users to define cloning

  • with 式は「クローン → 部分書き換え」を行う構文
  • 現状、このクローンは records から生成される通常定義・通常呼び出し不可の専用メソッドでやってる
  • 前述の Factories も入れた上で、ユーザー定義の Clone メソッドを受け付けるようにしたい

Cross-inheritance between records and non-records

  • 現状、records は records から、クラスはクラスからしか派生できない
  • 「records の基底クラスとして使える条件」みたいなのをしっかりと定義することで、「異種派生」を認められるようにしたい

Primary constructors

  • record T(int X, int Y); みたいな書き方、クラスと構造体でも使えるようにしたい ‐ records の場合はこのコンストラクター引数からプロパティ XY の生成までやっちゃってるけど ‐ クラス、構造体ではコンストラクターの簡易記法としてだけ使って、プロパティの生成まではやらない

Bodies and attributes for primary constructors

  • 現状、プライマリ コンストラクターには属性を付けれないし、本体を持てない
  • record T(int X) { T { ... } } みたいな記法(引数なしコンストラクター)で、プライマリ コンストラクターの本体を与えたい
  • Final initializers (init { ... }) と用途がちょっと被り気味だけど、完全に一本化できなくて、どっちも必要

Automatic with-ing on all structs?

  • 構造体は常に with 式で使える要件を満たせてはいる ‐ 暗黙的にメンバーごとのコピー機能を持ってる ‐ これをそのまま使って with 式を認めるべき?

More record functionality

いくつか、records として予定されていた機能は C# 9.0 に間に合ってなくて未実装。

Struct records

  • 現状、records は参照型
  • records のセマンティクスは構造体にインスパイアされてるものなのに、records の機能のいくつかは当の構造体では使えない ‐ positional members (プライマリ コンストラクターからのプロパティ生成)とか、strongly-typed な Equals/IEquatable<T> 生成したりとか
  • 「値型の records」みたいなのを定義する構文が別途必要

Data properties

  • data string Name みたいな書き方で public string Name { get; init; } を生成したい

Discriminated unions

#113 とか #2962 とか参照。

  • records が落ち着いてからその先の機能として検討するつもりでいた
  • その時が来た

Visual Studio 16.7 & 16.8 Preview 1 リリース / C# 9.0 の新機能3つ(module initializers, static lambda, target-typed conditional)

$
0
0

5日に、Visual Studio 2019 の 16.7 と、16.8 Preview 1 がリリースされました。

ということで、先週、ライブ配信もしていました。

16.7 が正式リリースになった記念に、Preview の頃に触れてた話題を改めてちょこっと振り返ったのと、16.8 Preview 1 で新たに追加された C# 9.0 の3つの機能の話でした。

C# 9.0 に今回追加されたのは以下の3つです。

今日は主にこの3つについて説明。

Module Initializers

モジュール(exe (アプリ)や dll (ライブラリ))が読み込まれた時点で必ず1回呼ばれるメソッドを書けるようになりました。

以下のように、ModuleInitializer 属性を付けた静的メソッドが、モジュール読み込み時に呼ばれます。

using System;
using System.Runtime.CompilerServices;

class Init
{
    [ModuleInitializer]
    internal static void M1() => Console.WriteLine("Init.M1");

    [ModuleInitializer]
    internal static void M2() => Console.WriteLine("Init.M2");
}

静的コンストラクターでも近いことができるんですが、

  • 静的コンストラクター
    • そのクラスのメンバーに触れた時点で初めて呼ばれる ‐ 1度も使っていないクラスの静的コンストラクターは結局呼ばれない
    • 1つのクラスに1つ限り
  • Module Initializers ‐ クラスのメンバーを使っていようが使っていまいが、モジュール読み込み時に必ず呼ばれる ‐ 1クラスに複数持てる

みたいな差があります。 確実に、確定タイミングで呼ばれるというのもメリットですし、個別に静的コンストラクターを持つよりはちょっとだけパフォーマンス的にも都合がいいみたいです。

今、Source Generatorって機能の実装も進められていて、これが入ると、たぶん「各クラスについて1回限り走らせたい処理」みたいなものは結構あると思います。 例えば自分が必要に迫られているものだと、リフレクションが使えない環境で自前でリフレクションに代わる型情報を持つみたいなコードなんですけども、 これが「確実に、確定タイミング」になってくれるのは結構ありがたかったりします。

Static anonymous functions

匿名関数 (ラムダ式匿名メソッド式)に対して static 修飾を付けて、キャプチャの抑止ができるようになりました。

using System;

// OK
Action staticLambda = static () => { };
Action staticAnonymousMethod = static delegate () { };

// コンパイル エラー
int local = 1;
Action badStaticLambda = static () => Console.WriteLine(local);
Action badStaticAnonymousMethod = static delegate () { Console.WriteLine(local); };

これは割かし、「工数的な問題で 8.0 に入らなかっただけ」系の機能です。 C# 8.0 時点でローカル関数に関しては同様の機能が入っていて、 匿名関数でも同様の需要があることはわかっていましたが、 文法的にちょっとめんどくさいので後回しになっていたものです。

Target-Typed Conditional Expression

条件演算子 (? :)で、第2項と第3項で共通の型を決められないときに、ターゲット型を見て型を決定できるようになりました。

void targetTypedConditional(bool x)
{
    // target-typed で、1 : null の部分がちゃんと int? になる。
    int? v1 = x ? 1 : null;

    // あくまで target-typed で判定してるので、以下のような推論は働かない(コンパイル エラー)。
    // 1 と null の「共通型」は確定できない。
    //var v2 = x ? 1 : null;
}

switchの場合には C# 8.0 時点であった機能です。新しい文法である switch 式と違って、既存の文法に手を入れるのはリスクもある(というか、実際、ちょっと破壊的変更を起こしてる)ので 8.0 には間に合わなかった機能です。

C# 9.0 最終版

$
0
0

いくつかライブ配信では言ってたんですが、C# 9.0 がそろそろ機能確定しそうな感じ。 11月リリースと言ってるわけなので、まあ、時期的にもこの辺りで確定していないとまずいでしょう。

ということで、先日、 What's new in C# 9.0 もドキュメント化されて docs 上に公開されました。

見出しに載るようなレベルでの機能の増減はもうありません。

Records とか Function pointers とか、一部の機能はまだちょっと修正が入るかと思います。 それに関しては9月9日の Design Meeting 議事録にまとまっています。 (同日の議題には C# 10.0 の話題というか、C# 10.0 に流れてしまったものの話もあり。)

ちなみに先日のライブ配信:

Visual Studio 16.8 Preview 2

16.8 Preview 2 が出た時点でのライブ配信では気づいてなかったんですが、 以下の2つの機能、この時点で入っていました。 (これらが最後の C# 9.0 機能です。)

  • Covariant return types
  • Extension GetEnumerator

Covariant return types

いわゆる共変戻り値。virtual メソッドの override 側で、戻り値の型を共変にできるようになりました。 要するに、以下のようなやつです。

class Base
{
    public virtual Base Clone() => new Base();
}

class Derived : Base
{
    // これの戻り値、C# 8.0 までは Base でないとダメだった
    public override Derived Clone() => new Derived();
}

デリゲートや、out 修飾付きのジェネリック型引数などではこれまでもできていたことですし、 認めてまずいことは何1つありません。 これができないことは割かしずっと問題として認識はされていて、 今になってようやく実装されたのは単に優先度の問題です。

C# 9.0 の機能のメジャーな機能の中では唯一、 .NET Runtime 側の修正が必須 (C# コンパイラーによる小手先のトリックだけでは実現不能)な機能です。 要するに、「.NET Core への移行だけで手一杯(C# 7.0 付近)」 → 「インターフェイスのデフォルト実装の方が優先(C# 8.0)」 → 「共変戻り値に着手(今ここ)」という感じ。

(インターフェイスのデフォルト実装同様、というかそれよりさらにだいぶ昔から、Java にはこの機能があったり。 Android での Java との相互運用のためもあって、.NET Core と Xamarin (Mono) との統合を目指している今このタイミングで共変戻り値も採用になりました。)

Extension GetEnumerator

GetEnumerator が拡張メソッドであっても foreach ステートメントで使えるようになりました。

例えば以下のような拡張メソッドを用意することで、2-tuple に対する foreach が使えます。

using System;
using System.Collections.Generic;
 
foreach (var i in (1, 2))
{
    Console.WriteLine(i);
}
 
static class TupleExtensions
{
    public static Tuple2Enumerator<T> GetEnumerator<T>(this (T, T) t) => new(t);
 
    public struct Tuple2Enumerator<T> : IEnumerator<T>
    {
        private int _i;
        private (T, T) _tuple;
 
        public Tuple2Enumerator((T, T) tuple)
        {
            _i = 0;
            _tuple = tuple;
        }
 
        public T Current => _i switch
        {
            1 => _tuple.Item1,
            2 => _tuple.Item2,
            _ => default!,
        };
 
        public bool MoveNext() => ++_i < 3;
 
        object System.Collections.IEnumerator.Current => Current!;
        void System.Collections.IEnumerator.Reset() => throw new NotImplementedException();
        void IDisposable.Dispose() { }
    }
}

まあ、実用途があるかというとそこまで有益な使い道は思いつかないんですが…

配信ではしゃべってるんですが、 タプルに対しては arity ごとに別拡張メソッドが必要だったりRange に対しては inclusive/exclusive 問題がやっぱりだいぶ混乱しそうとかあり。

これは、他の新しめの文法との一貫性を取るためです。 パターン ベースな構文一覧にある通り、 クエリ式とか分解await では認めていることなので、 それと揃えたいという話が前々からありました。

(確かそれも、実用性が低めということで着手されず、最終的にはコミュニティ貢献(C# チーム外の人の実装)だったと思います。)

null 許容参照型の改善

#3297のうち、たぶん、制約なしジェネリック型に対する T? は 16.8 Preview 2 で入ったはず。

class C<T>
//where T :class // これがあれば前からOK
//where T :struct // これがあれば前からOK
// 制約なしは今回から初めてOK
{
    // これだとエラー。 
    // T? と言いつつ、C<int> とかを渡すと int。int? ではない
    //public static T? M() => null;
 
    // 実は nullable じゃなくて、defaultable
    // LINQ の FirstOrDefault 的な奴
    // あまりにきもいから、当初 T?? にしようという案もあった
    // ? になったのは、 x ?? y の ?? と区別つかなくて困ったかららしい
    public static T? M() => default;
}

ただこれ、少々クセはありまして。 上記コメントにもありますが、この場合の T? は nullable じゃなくて「defaultable」と呼んだ方がいいかもしれないようなものです。 以下のように、型引数として非 null 値型を渡すと nullable にはなりません。

string? x1 = C<string?>.M();
string? x2 = C<string>.M(); // 順当に string?
int?    x3 = C<int?>.M();   // 順当に int?
int     x4 = C<int>.M();    // これの戻り値は int? にならない。default(int)、つまり、0 が返る。

「実は nullable じゃなくて defaultable」という挙動が気持ち悪すぎて C# 8.0 時点では見送られたし、 9.0 でも T?? みたいな他の文法が検討されたりしたんですが、他の文法にもいろいろ問題があって、 結局単に「制約なしの T? は defaultable」ということになったみたいです。

C# 9.0 最終トリアージ

C# Language Design Meeting for September 9th, 2020は、C# 9.0 のタイミングでやる作業の最終判断みたいな感じになっています。

冒頭で言った通り、 「What's new in C# 9.0」みたいな記事が docs に並ぶ時点でもう、大きな変更はないんですけども、 いくつか細かい議題が。

とりあえず今日は 9.0 の残作業の話のみ。

「10.0 行き」みたいな感じで分類されているものも、 言い回しとしては「10.0 までの期間で再検討」みたいなふわっとしたものが多いので、 もうちょっと固まってきたら改めて。 その他、「Anytime (いつやるか不明。相当低優先度)」行きなものも省略。

! と .? の組み合わせがおかしい

#3393

現在、a?.b.c!.d.e(a?.b.c)!.d.e として解釈されてしまうという問題があります。

null 条件演算子 ?.のショートサーキットの性質上、! の有無によって挙動が変わります。 null 抑止演算子 !の理念としては、! の有無で挙動は変えたくないそうで、これは完全に想定外の仕様バグです。

ただ、C# 8.0 で1度この仕様で実装してしまったものはしょうがないので、「破壊的変更を許容してでも直すべきバグかどうか」が争点にはなっていました。 まあ、それでも「9.0 で直す」判定になりそうです。

インターフェイスの静的メソッドが共変注釈をちゃんと見てない

#3275

これもほぼバグ。こっちは破壊的変更をするわけでもなく、単に深刻度が低くて優先度が低い状態。 Pull Request はすでに出ていて間に合うかどうかだけの問題で、 一応まだ C# 9.0 候補だそうです。

record がらみ

#3226#3213#3137 など

結構、10.0 行きになった機能はあります。

ただ、いくつかは 16.8 Preview 2 時点で実装されていないけども正式リリースまでに実装されるということになっているものがあります。

  • ToString で単に型名だけじゃなく、Point { X = 1, Y = 2 } みたいな文字列化されるようになる
  • == 演算子が生成されるようになる
    • reference equal じゃなくて、Equals メソッド呼び出しの「値による比較」になる
  • EqualsGetHashCode (コンパイラーが自動生成してくれるものの、手書きで挙動を上書き可能)のうち、片方だけ手動上書きすると警告になる

ピックアップRoslyn: C# 10.0 でのレコード話

$
0
0

先月書いた通り、C# 9.0 がらみはほぼ確定(バグ修正レベルの変更しかしない状態)になっています。

(そういえばライブ配信はやったもののブログ化していなかった話題として、.NET 5.0 の RC 1 到達というのもあります。 RC (リリース候補)が付くと、もう大きな変更はできません。 あと、.NET Conf のページに「.NET Conf 2020 は11月10日開始」、「.NET 5 launch」の文字が入ったので、.NET 5.0 のリリース日も決まりました。アメリカ西海岸時間で11月10日なので、日本だと11月11日に朝起きたらリリースされているくらいのタイミング。)

そうなると、今デザイン作業が行われているのはすでにその次、C# 10.0 の話になります。

ということでここ2週間ほどの C# の Language Design Meeting はC# 10.0 がらみが議題になります。

一番大きな話題は C# 9.0 で導入されるレコードに関するものです。 C# 9.0 時点では仕様を詰め切れていなくて「9.0 リリース後に再検討」となっていた項目がいくつかあって、 この2週ほどのミーティングではまさにその再検討な話が結構な割合を占めています。

ちょっと長くなりそうなので、今日はこのレコード関連の話だけを書こうかと思います。

他に、構造体(特に ref フィールド、ref 構造体の改善など)の話題とか、細かいトリアージ作業とかもあったりするんですが、この辺りは後日改めて。

前提知識: C# 9.0 レコード型

レコード型は、以下のように record キーワードを使って宣言する新しい型で、

record Point(int X, int Y);

内部的には以下のようなクラスの生成になります。

class Point
{
    public int X { get; init; }
    public int Y { get; init; }
    public Point(int X, int Y) // X, Y プロパティに代入
    public void Deconstruct(out int X, out int Y) // X, Y プロパティから値取得
    public override bool Equals(object? obj) // X, Y の値の比較
    public override int GetHashCode() // X, Y からハッシュ値生成
    public override string ToString() // Point { X = ... } の書式で文字列化
    public Point Clone() // shallow コピー (実際には通常の C# から参照できない名前で生成)
}

いくつかのとらえ方がありますが、以下のようなものとして説明されます。

  • プレーンなデータを簡潔に書けるようにするための型
  • 匿名型 (new { X = 1, Y = 2 } みたいなやつ)の名前付き版
  • value semantics (値による比較やコピー生成)を持つクラス
    • C# の場合は構造体が元から value semantics 的な挙動を持っているので、「レコードは構造体的な性質を持つ参照型」ともいえる

ちなみに、この手の型は immutable にしないとまずかったりします。 わかりやすくまずいのは例えば以下のような場合。 ハッシュ値が変わってしまうことで HashSetDictionary の挙動を壊します。

using System;
using System.Collections.Generic;
 
var p = new Point { X = 1, Y = 2 };
 
// HashSet (ハッシュ値で等値比較してる)にインスタンスを渡す
HashSet<Point> set = new();
set.Add(p);
 
// その後、値を書き換え
p.X = 3;
 
// ハッシュ値が変わってしまってるので判定が狂う
Console.WriteLine(set.Contains(p)); // false
 
// Remove もできなくなる
set.Remove(p);
Console.WriteLine(set.Count); // Remove できてないので 1 が返る
 
class Point
{
    public int X { get; set; }
    public int Y { get; set; }
    public bool Equals(Point other) => (X, Y) == (other.X, other.Y);
    public override bool Equals(object? obj) => obj is Point other && Equals(other);
    public override int GetHashCode() => X ^ Y;
}

レコード型から生成されるクラスの例に init というキーワードが入っていますが、 これも C# 9.0 の新機能で、プロパティがオブジェクト初期化子までは書き換え可能、その後は書き換え不能になるという機能です。 既存のプロパティと比べて、

  • set 可能プロパティ(int X { get; set; } みたいなの): どこでも書き換えできる
  • get-only プロパティ (int X { get; } みたいなの): コンストラクター内でだけ書き換えできる
  • init プロパティ (int X { get; init; } みたいなの): コンストラクター内とオブジェクト初期化子でだけ書き換えできる
var p = new Point
{
    Settable = 1, // OK
    GetOnly = 1,  // ✖
    Init = 1,     // OK
};
 
p.Settable = 1; // OK
p.GetOnly = 1; // ✖
p.Init = 1; // ✖
 
class Point
{
    public int Settable { get; set; }
    public int GetOnly { get; }
    public int Init { get; init; }
 
    public Point()
    {
        Settable = 1; // OK
        GetOnly = 1;  // OK
        Init = 1;     // OK
    }
}

というものです。 C# の場合、初期化子が C# 3.0 からの後付けなせいでちょっと使いにくかったんですが、その改善案になります。

immutable なデータを書き換えて使いたい場合、 shallow コピーを作ってからそのコピーの方を書き換えるというのが推奨される方式になります。 これに関しても C# 9.0 で「with 式」という新しい文法が追加されていて、 以下のような書き方でコピー& init プロパティの書き換えができます。

using System;
 
var p1 = new Point(1, 2);
var p2 = p1 with { X = 3 };
 
Console.WriteLine(p1); // Point { X = 1, Y = 2 } (元のまま)
Console.WriteLine(p2); // Point { X = 3, Y = 2 } (新インスタンスで X が書き換わってる)
 
record Point(int X, int Y);

C# 10.0 に持ち越されたレコード関連議題

いくつか、レコードにはいくつか議題が残っていて、「10.0 で改めて検討」となっているものがあります。

  • レコードは参照型である
    • 値型版 (仮称 record struct)をどうするか
  • 既存の構造体との兼ね合い
    • 「record struct」を新設すべきなのか、既存の構造体に手を入れるべきなのか
    • プロパティ生成とかはしないとしても、既存の構造体の時点で with 式を使える条件はそろってるはず
  • プライマリ コンストラクター
    • 通常のクラスにも class Point(int X, int Y) みたいな書き方を認めたい ‐ その場合、単にコンストラクターの簡易記法であってプロパティなどのコンパイラー生成はしない

以下、 9/3010/5での検討事項。

構造体の等値比較

レコードでは「クラスに対して Equals メソッドをコンパイラー生成する」という仕組みで「値比較」を実現しています。

構造体の場合、object.Equals(object) の中で、.NET ランタイムが「値比較」に相当する処理を行っています。 なので、挙動としてはレコードと構造体の Equals はどちらも同じ「値比較」なんですが、 現状の構造体の Equals はちょっとパフォーマンスが悪いです。 これは、Equals(object)を介しているせいでボックス化が起こるのと、.NET ランタイム内での処理がリフレクション的になっているからです。

そこで、レコードと同じく構造体に対してもコンパイラー生成で「値比較」の Equals メソッドを生成すべきかどうかというのが議題になっていました。

これに関しては以下のような結論。

  • コンパイラー生成の Equals は作らない方がいい
    • コンパイラー生成してしまうとコンパイル結果のバイナリ サイズが膨らむ
    • 既存の構造体に対して Equals 生成すると既存コードを壊す ‐ かといって、新しい「record struct (仮)」だけが高パフォーマンスみたいな状態になると、既存の構造体が忌むべきものになってしまう(それは望まない)
  • 既存の構造体と record struct (仮)は明確に別
    • プライマリ コンストラクターからのプロパティ生成、型付きの == 演算子・Equalsメソッド生成、IEquatable<T> 実装するのは record struct (仮)だけ
  • .NET ランタイムのレベルで構造体の Equals(object) を最適化すべき

構造体に対する with 式

構造体は元から shallow コピーを持っている(単に代入するだけでコピー発生。.NET の中間言語的にも dup 命令ってのを持ってて、1命令でコピーになる)ので、with 式を使える条件を満たしています。

また、C# 9.0 で入るレコード(クラスで生成されるやつ)は、現状、コピーのカスタマイズ性がない(通常の C# からは参照できない($Clone<>みたいな)名前でコピー メソッドが生成されていて、手書きでの上書きができないようにしてある)状態です。 これは「将来改めて検討する」ということになっていて、とりあえず、カスタマイズ性がある状態からない状態には戻せないけれど、できないものをできるようにすることは簡単だからいったん「ない」仕様にしてあります。

これに対して、C# 10.0 では以下のような方針(決定ではない)で進めていきたいようです。

  • すべての構造体は with 式利用可能にする
  • ただ、既存の構造体は with 時のコピーのカスタマイズ性は提供しない
    • デフォルト動作の「dup 命令でコピー」を常に使う
  • record struct (仮) の場合は、レコード (9.0 で入るクラスのやつ)と合わせて再検討することになるけども… ‐ record struct (仮)のコピーのカスタマイズは認めない方がよさそう
    • でないと、ジェネリック型引数で where T : struct なものの挙動がおかしくなりそう

プライマリ コストラクター

C# 9.0 のレコードでは、プライマリ コンストラクターの引数(record Point(int X, int Y)XY)から public な init プロパティ(public int X { get; init; } とか)が生成されます。

C# 10.0 で検討している record struct (仮) でも同様であるべきかという話があります。

  • record (参照型のレコード) と record struct (値型のレコード)という見方をすると、 同じ挙動であった方がいい
  • 「record は名前付きの匿名型」に対して、「タプルの名前付き版」が欲しいという話もあって、record struct (仮) をその位置に据えたいという見方もある
    • この場合、タプルのメンバーは public フィールドになっているので、record struct (仮)は public フィールドを生成した方が合う
  • immutable でないと問題を起こすのは参照型だけ
    • 値型の場合は代入で常にコピーが作られるので前述の HashSet みたいな問題を起こさない

そして検討の結果、現状、record struct (仮)に関しては以下のような方向性になりそうみたいです。

  • デフォルトで public で mutable なプロパティ(public int X { get; set;} みたいなの)を生成する
  • 手書きでカスタマイズ可能なので必要であれば `public int X { get; init; } を自分で足してもらう
  • あるいは構造体の場合元からreadonly structがあるので、immutable にしたければ readonly record struct Point(int X, int Y) みたいに書いてもらう
  • C# 9.0 時点のレコードには「プロパティかフィールドか」のカスタマイズ権はない(プロパティでないとダメ)ので、フィールドでも上書きできるように変更する

data メンバー

プライマリ コンストラクター (record Point(int X, int Y) みたいなの)からのプロパティ生成は、 常にコンストラクター生成がセットで、コンストラクターでの初期化が前提になります。

必然的に以下のような書き方になって、コンストラクター呼び出しには引数順序に意味があるので、 これを「位置によるレコード」(positional record)と呼んだりします。

var p = new Point(1, 2);

これに対して、init プロパティだけを書いて、

record Point
{
    public int X { get; init; }
    public int Y { get; init; }
}

オブジェクト初期化子を前提にした書き方をすることもできます。 こちらはプロパティ名指定が必須で、逆に順序には意味がなくなるので、「名前によるレコード」(nominal record)と呼んだりします。

var p = new Point { X = 1, Y = 2 };

これはこれで便利なんですが、レコード型の「プレーンなデータを簡潔に書けるようにする」という目的からすると、 public int X { get; init; }という書き方はちょっと煩雑過ぎます。

そこで提案されているのが data メンバーで、以下のようなコードから public int X { get; init; } をコンパイラー生成したいというものです。

record Point
{
    data int X;
    data int Y;
}

この案自体はちょっと前からあって、単純に案が出たのがギリギリ過ぎて C# 9.0 には入れなかったという状態です。

ここで改めて record struct (仮)が議題になるんですが、

  • プライマリ コンストラクターからのプロパティ生成の仕方が違うけど、data メンバーの場合はどうするべきか
    • C# 9.0 の参照型レコードは immutable (get; init;)
    • record struct (仮) は mutable (get; set;)

という問題があります。

ここはまだだいぶ悩んでいるようで、以下の3案が全部まだ候補だそうです。

  • positional に合わせるべきで、値型の場合は mutable (get; set;)、参照型の場合は immutable (get; init;)
  • data の挙動は一致しているべきで、値型だろうと参照型だろうと immutable (get; init;)
  • data メンバーという提案自体をあきらめる

ピックアップRoslyn: C# 10.0 での低水準機能改善(ref フィールドなど)

$
0
0

前回書いたのの続き。

ここ数週間くらいの Language Design Meeting 議事録は C# 10.0 (今年11月に正式リリースされるのが 9.0 で、すでにその次のデザイン)話になっていています。

このうち、前回はレコード型関連の話を書きましたが、 今日は低水準機能の改善の話です。(9/23 議事録分)

(残り、細かいトリアージ話もあるんですが、それはまたさらに回を改めて。)

低水準 (low level) 機能

.NET Core 2.1 くらいの世代で .NET のパフォーマンスが劇的に向上したわけですが、その原動力になっているのはSpan<T>構造体です。 関連する C# の言語機能としては以下のものがあります。

C# のような「生産性と安全性重視」なプログラミング言語にとっては珍しく、パフォーマンス優先の機能です。 C# だと普段あまり意識しなくていいはずのメモリ管理を強く意識した機能なので、低水準(low-level: ハードウェア寄りという意味)機能というくくりになります。

今回、C# 10.0 向けに検討されているのもこの手の低水準機能で、以下のようなものです。

  • ref 構造体が ref フィールドを持てるようにする
  • ByReferenct<T> という特殊な型を使うのをやめて、ref フィールドに移行する
  • 構造体が、そのフィールドを ref 戻り値で返せるようにする
  • safe な文脈で、managed な型に対しても固定長バッファーを使えるようにする

Span 構造体の内部と「ref フィールド」

Span<T>構造体は、論理的には以下のような構造体だと説明されます。

struct Span<T>
{
    ref T _pointer;
    int _length;
}

フィールドとして T への参照と長さを持っています。 この「フィールドとして T への参照を持っている」(以下、これを「ref フィールド」と呼びます)というのが Span<T> 構造体の肝で、 パフォーマンス向上のポイントになっています。

ただ、「論理的には」と書いたのは、これまでの .NET (.NET 5.0/ C# 9.0 時点でも)にはこの ref フィールド機能がなくて、 .NET Core 2.1 の Span<T> 実装当時には、以下のような特殊処理をすることにしました。

struct ByReference<T>
{
    // .NET ランタイムが特別扱いする前提なので、C# では書けない
}
 
struct Span<T>
{
    ByReference<T> _pointer;
    int _length;
}

ByReference<T> がやりたいことはまさに ref フィールドなんですが、 .NET に本格的に ref フィールドを導入するよりは、この特殊処理で実装した方が楽だったそうです。

そして、ref フィールドが欲しくなるような状況の大半は Span<T> がカバーしているので、 この時点では以下のような方針になりました。

  • 将来、本格的に ref フィールドを導入するときまで ByReference<T> は public にしない
  • Span<T> に関する escape analysis (メソッド外に漏らしてまずいものを return してないかのフロー解析)だけ実装する

そして、この先送りにしていた ref フィールドの話が本格的に検討される段階になったみたいです。 .NET ランタイムにも手を入れる必要がありますし、 C# コンパイラー的にも escape analysis の改善が必要で、 9/23 の議事録では ref フィールドに対する escape analysis の案が書かれています。

当然、ref フィールドが入れば、ByReference<T> という特殊な構造体は必要なくなるので、 Span<T> も素直に ref フィールドで実装したいという話にもなります。

readonly ref struct Span<T>
{
    ref readonly T _field;
    readonly int _length;
 
    // 今までありそうでなかったコンストラクター。
    // 今回の提案はこれをできるようにするのも目標の1つ。
    public Span(ref T value)
    {
        ref _field = ref value;
        _length = 1;
    }
}

構造体のフィールドを ref 戻り値で返す

ref 戻り値では、構造体のフィールドの参照を返せなかったりします。 例えば、以下のコードはコンパイル エラーになります。

struct S
{
    int _field;
    public ref int Prop => ref _field; // _field の参照を返せない
}

以下のような、インターフェイス実装とジェネリックなメソッド呼び出しをしたときに、 外に漏れてはいけない参照を漏らしてしまうことがあるので禁止されています。

interface I1
{
    ref int Prop { get; }
}
 
struct S1 : I1
{
    int _field;
    public ref int Prop => ref _field;
 
    // p の寿命は M 内で閉じてるはずなものの、その p の中身が ref 戻り値で返ってしまう。
    static ref int M<T>(T p) where T : I1 => ref p.Prop;
}

これに対する対処は、結局、

  • フィールドの参照を返すメソッドには ThisRefEscapes 属性を付ける
    • 専用の C# 文法を用意してコンパイラー生成にするか、明示的にこの属性を書かせるかはまだ要検討
  • この属性が付いているメソッドには制限を掛ける
    • インターフェイス実装できなくする
    • 以下のようなメソッド呼び出しも制限する
struct S1
{
    public ref int GetValue() => ...
}
 
class Example
{
    ref int M()
    {
        // このコードは今現在有効 (破壊的変更したくないので今後も有効)
        S1 local = default;
        return ref local.GetValue();
    }
}

safe な固定長バッファー

Span<T> が出た当初にも、この構造体があれば固定長バッファーを safe に実装できるんじゃないかという話は出ていたんですが。

実際には、固定長バッファーを safe にしたいときに問題になるのは前節の「構造体のフィールドの参照を返す」の方なので、これまで実装が止まっていました。 前述の ThisRefEscapes 属性があれば問題が解決するので、一緒に検討に上がっているみたいです。

ピックアップRoslyn: C# 10.0 トリアージ

$
0
0

前回前々回の続きというか、大きくなりすぎたので分けたのの続き。

ここ数週、C# 10.0 向けの検討が続いていて、 そのうち大きなものは前々回の record struct前回の低水準機能で、残りはこまごまとしたトリアージ作業になります。

今回でやっと最後、その残りのトリアージの話。

NaN 比較

C# では、というか、IEEE 754 (浮動小数点数の標準規格)では、 NaN (Not a Number)との比較は常に false ということになっています。

bool m(double x) => x == double.NaN;
 
Console.WriteLine(m(1.0)); // 当然 false
Console.WriteLine(m(double.NaN)); // これですら false

最近の C# では「常に false な式」に対して警告を出すことが結構あるんで、 過去の文法に対しても「常に false 警告」を足してもいいんじゃないかという話があります。

ただ、これまでの C# だと、「警告であっても追加すると破壊的変更になりうる」ということで消極でした。

これに対して C# 9.0/.NET 5.0 では警告ウェーブ(AnalysisLevel オプション。RC 1 記念ライブ配信のときに口頭説明はしてる)が入るので、今後は警告の追加もしていきたいということになっています。

で、NaN との比較の話に戻りますが、 実はすでに FxCop Analyzer (Roslyn 標準ではないものの、Visual Studio ではデフォルトで有効になっているアナライザー)が NaN 比較に対する修正を提案してきます。 「Roslyn 標準に置き換えるほどではない」ということで、「特に何もしない」とのこと。

null 許容参照型の改善

C# 8.0 で null 許容参照型が入りましたが、最初から完全なものを作るのは無理なので段階的に改善していくという計画になっていて、C# 9.0 でもいくつか改善が入っています。

  • MemberNotNull 属性
class X
{
    public string NotNull;
    public X() => Init();
 
    // このメソッドの呼び出し後、NotNull プロパティの非 null を保証
    [MemberNotNull(nameof(NotNull))]
    private void Init() => NotNull = "";
}
  • 制約なしジェネリック型に対する T?
#nullable enable
 
class X
{
    // where T を書かないときも T? が利用できるように。
    // ただし、意味的には nullable というよりも "defaultable" で…
    static T? M<T>() => default;
 
    static void Main()
    {
        string? s1 = M<string?>(); // string? → string?
        string? s2 = M<string>();  // string → string?
        int?    i1 = M<int?>();    // int? → int?
        int     i2 = M<int>();     // int → int で 0 が返る
    }
}

で、C# 9.0 にも漏れたものがいくつかあって、引き続き 10.0 向けに検討していくとのこと。

  • Task<T> の改善
    • 共変性を認めたい(Task<T>Task<T?> に代入できるようにしたい>)
  • LINQ の改善
    • 特に source.Where(x => x != null).Select(x => xは非null扱い) ができるようにできないものか
  • 未初期フィールド(今のところ良案なし)

required プロパティ

前々回、少し nominal record (オブジェクト初期化子で初期化する前提のレコード型)の話をしましたが、 C# 9.0 時点では nominal に(プロパティで)定義したメンバーは初期化を必須にできません。 常に省略可能で、省略した場合は 0/null に自動的に初期化されます。

var p = new Point
{
    // X, Y ともに何も書かなくても別に構わない
};
 
record Point
{
    public int X { get; init; }
    public int Y { get; init; }
}

これに対して、明示的な初期化を義務付けたいという話があって、 1案としては以下のような書き方が提案されています。 これを required プロパティといいます。

var p = new Point
{
    X = 1, // X は書かないとコンパイル エラー
    // Y はなくてもいい
};
 
record Point
{
    public int X { get; req; }
    public int Y { get; init; }
}

元々「間に合う物なら C# 9.0 で」くらいの感じで提案が出ていたものなので、引き続き 10.0 候補として検討していくとのこと。

匿名型に対する with 式

これも前々回書きましたが、 レコード型は「名前付きの匿名型」という側面があります。

となると逆に、「匿名型は名前なしのレコード型」という扱いになっている方が自然で、 この一貫性を取るために、匿名型にも with 式を認めたいという話が出ています。

var a = new { X = 1, Y = 2 };
var b = a with { X = 3 }; // 9.0 時点ではできないものの、10.0 で検討

discussionでは「匿名型自体どうなの?」とか言われたりもしますが…

C# チーム的には前向き(たぶん、変更コストがそんなに高くなく、レコード型との一貫性を重要視してる)みたいで、10.0 候補になっています。 元々 with 式には 10.0 向け残作業(ユーザー定義の Clone メソッドとか)があるので、それと合わせて検討。

shebang

C# でも shebang (Unix シェルでよくある、1行目に #! を書いてスクリプトを何で実行するか指定するやつ)を認めよう(C# コンパイラー的には単にコメント扱いで無視)という話があります。

#! dotnet run
 
System.Console.WriteLine("Hello");

ただ、これはどちらかというと donet CLI側の問題なので、C# チーム的には「X.0」(いつやるか未定)扱い。 「CLI 側が dotnet run でスクリプト実行できるようになったら本気出す」みたいな感じみたいです。

リスト パターン

配列とか List<T> とか(あるいはもしかしたら汎用に IEnumerable<T> も)を [] を使ったパターンでマッチングできるようにしたいという話があります。


var x = new[] { 1, 2, 3 };
 
if (x is [1, 2, var i])
{
    ...
}

(すでにコミュニティ貢献でプロトタイプ実装があったりします。)

これに対して C# チーム的には「リスト パターンは辞書パターンと一緒に考えたい」、 「ただ、プロパティ パターンの {} と明確に区別がついて、かつ、辞書らしい文法を思いつかない」という感じ。

「C# 10.0 に入れれる気はしないけども」という補足付きで「10.0 で検討」とのこと。

global using

今、マイクロソフトによる公式 C# チュートリアルとかでは、「ブラウザーでコードを試す」という機能があって、ブラウザー内で C# コードを書いてその場で実行できたりします。

ここでは C# のスクリプト文法を使えるので、例えば、以下のような1行のコードが「実行」ボタン1つで実行できます。

Console.WriteLine("Hellow World!");

これ、実は using System; なしで Console クラスにアクセスできたりします。 スクリプト文法限定なんですが、いくつかの名前空間は「デフォルトで using 済み扱い」みたいにする機能があるということだったりします。

一方で、C# 9.0 からはトップ レベル ステートメントという機能が入ります。 プロジェクト(csproj)を作ってコンパイルする通常の C# 文法とスクリプト文法の差を縮めたいという意図で、 ファイル直下にステートメントを書いて Main メソッドを省略できるという機能です。

ここで、「通常文法とスクリプト文法の差を縮めたい」という意図があるので、 前述の「デフォルトで using 済み扱い」も通常文法に入れたいという議題が上がります。 これを指して global using といっていて、コンパイラー オプションとか csproj 中のタグで、プロジェクト全体に「using した状態にする」というオプションを提供したいそうです。

.NET Notebooksとか、 .NET 6 辺りをターゲットにした「C# インタラクティブ実行環境」があったりするので、その辺りのユーザーの使用感のフィードバックをもらいつつ、C# 10.0 で検討とのこと。

closed enum

enum 型に対して、「メンバー定義してない値は取らない」という保証を与えて、 switch の網羅性チェックが働くようにしたいという話があります。

例えば以下のコードは現状では警告が出るんですが、「警告をなくせる enum が欲しい」というのが closed enum です(ここでいう close (閉じる)というのは、「これ以上のメンバー追加はない」という意味です)。

int m(X x) => x switch
{
    X.A => 1,
    X.B => 2,
    X.C => 4,
    // 今の enum の仕様だと (X)100 とか書けるので、A, B, C だけでは「網羅した」判定を受けない。
    // 警告が出る。
};
 
enum X
{
    A, B, C
}

この辺りの網羅性のロジックは、別途 C# 10.0 で検討されている discriminated union でも同様なので、それと一緒に考えたいとのこと。

トップ レベル関数

C# 9.0 で入ったトップ レベル ステートメントで、トップ レベルにメソッドを書いた場合、 それはトップ レベルからのみアクセスできます。

using System;
 
// トップ レベルでメソッドを書く。
void m() => Console.WriteLine("m");
 
// トップ レベルから呼ぶのは OK。
m();
 
class Program
{
    // トップじゃない場所から呼ぶとコンパイル エラー。
    // ちなみにエラー内容は「m が見つからない」じゃなくて、
    // 「トップ レベルの m はトップ レベルからだけ呼べる」。
    static void M() => m();
}

少なくとも C# 9.0 時点では意図的にこういう仕様になっているんですが、 「将来、この m をプロジェクト内のどこからでも呼んでいい global 関数的なものとして認めてもいいんじゃないか」という議題は残っていました (今エラーになるものを将来エラーじゃなくすというのは破壊的変更にはならないので検討の余地がある)。

とはいえ元々「可能性はある」と言っていただけなので、あまり積極的ではなく。 「もし C# を1から再設計するんなら入れるけど、今から入れるのはちょっと」みたいな意見の人が多いそうです。 今回やっぱりばっさりと「rejected」とのことです。

プライマリ コンストラクター

前々回触れたとおり。 今、レコード型にだけ許されている record Point(int X, int Y) みたいな書き方(型名直後に () で引数リスト)をクラス、構造体にも認めようという話。

引き続き 10.0 目標で検討。

パラメーターの null 検証の簡素化

null 許容参照型による null 検証はあくまでコンパイル時の検証で、 unsafe とか抑止演算子の !とかを使うとコンパイル時検証をすり抜けられます。 また、構造体や配列要素の規定値とか、フロー解析がしにくくて、今のところ検証をすり抜けてしまう穴があります。

そこで、必要であればやっぱり実行時の検証、要するに以下のようなコードも必要だろうという空気感。

void M(string s)
{
    if (s is null)
        throw new ArgumentNullException(nameof(s));
 
    ...
}

これを、string s! とかで簡素化したいという案も出ています。 「文法は ! でいいのか」みたいな部分で合意が取れておらず 9.0 では流れましたが、10.0 で再検討とのこと。

generic type alias

using エイリアスで以下のような書き方をしたいという話はずっと昔からたびたび出ています。

using List<T> = System.Collections.Generic.List<T>;

「欲しいけど、他にたくさんある C# 10.0 候補を押しのけてまでは…」という感じみたいで、 「X.0」(いつやるか不明)行き。

パラメーターに対する nameof

null 許容参照型の NutNullIfNotNull とかの登場で急に需要が高まったんですが、 属性内で、メソッドの引数を nameof 参照したいという要求があります。

using System.Diagnostics.CodeAnalysis;
 
class Path
{
    // 今、nameof(path) とは書けない。
    [return: NotNullIfNotNull("path")]
    public static string? GetFileName(string? path);
}

まあ、C# 8.0 時点でこれの需要が急増することはわかっていて、 単に優先度的に 9.0 に入らなかっただけです。 すでに実装は始めているそうなので、10.0 候補。

Span パターン

今や普通に stringSpan<char>ReadOnlySpan<char> を比較することがあるわけで、 だったら、Span<chat>switch 式に掛けたいという要求が当然あります。

// string に対してこんな感じの switch していたものを…
int M(string s) => s switch
{
    "Id" => 1,
    "Name" => 2,
    "Age" => 3,
    _ => 0,
};
 
// Span や ReadOnlySpan でもやりたい。
int M(ReadOnlySpan<char> s) => s switch
{
    "Id" => 1,
    "Name" => 2,
    "Age" => 3,
    _ => 0,
};

これは「Any Time」(C# チーム的には乗り気じゃないけど、コミュニティ貢献は受け付ける)扱いなんですが、 実際にコミュニティ貢献の Pull Request が出ていたりします。 それに対する細かい判断:

  • Span<char>ReadOnlySpan<char> に対する特殊対応なので気持ち悪いものの…
    • 実のところ Span に対しては foreach とかですでに特別扱いしているので今更
  • 後から足すと破壊的変更にならないか…
    • Spanref 構造体object に代入できないとかの制限が幸いして、破壊的変更を避けれそう
  • ReadOnlySpan<char> だけ?
    • ReadOnlySpan<char> を受け付けるんなら Span<char> も受け付けてよさそう
  • Memory<char>ReadOnlyMemory<char> は?
    • それはなしで。m.Span と書くだけでいいし、Span 限定で
  • switch だけ認める?
    • パターンを掛ける任意のコンテキスト(is とかでも)で認めてよさそう
  • ジャンプ テーブル化
    • 内部実装的なことをいうと、今、string に対する switchcase が6個以上あるときハッシュ値を使ったジャンプ テーブル化する最適化を掛けてる
    • Span<char>ReadOnlySpan<char> でも同様の最適化が要る。アロケーション除けによるメリットを打ち消すくらい遅くなる実装は避けたい

ピックアップRoslyn 10/31: csharplang の運営方針とかトリアージとか

$
0
0

また何件かまとめて、C# Language Design Meeting 議事録を紹介。

主に、csharplang の運営方針に関する話と、こまごまとトリアージ話。

(この他に、 October 21st, 2020ではプライマリ コンストラクターの話があったり、 Meeting 議事録とは別に派生型の網羅性の話が出てたりするんですが、 またちょっと話が大きくなりそうなので別の回で改めて。)

csharplang の運営方針

C# コミュニティ大使

先週の配信:

これの割と冒頭で話してるんですけども、csharplangで、Community Ambassador (コミュニティ大使)を設けようという話が出ていました。 (実際、ほぼ即日、何名か任命。)

C# はオープンソース開発されているといっても、マイクロソフトの C# チームが責任を負ってどの機能をいつまでに実装するかなどは独裁的に決定しています。 バグ修正の類などは roslyn (コンパイラー実装に関するリポジトリ)に直接 Pull Request を出して通ったりしますが、 言語仕様に関しては csharplang (言語仕様だけに絞ったリポジトリ)でのディスカッションを経て、 C# チームが承認したものだけが受け付けられます。

あるいは、C# チームが「微妙なラインなので、もし実装してくれる人がいるなら受け付けるけども、C# チーム内では実装しない」と判定した言語機能であれば外部貢献が受け付けられたりはします。 ただ、これも、「例え外部貢献だろうとリジェクト」と判定される言語機能もあって、その場合は誰かがその機能を実装したとしても Pull Request が承認されることはありません。

とはいえ、そういう運営体制だとしてコミュニティ(要するにマイクロソフト外の協力者)側でサポートできることがあるだろうというのが今回の流れです。

csharplang には、Design Meeting でこの話題が出た12日時点で1500件を超える issue が立っていたわけですが…

  • そもそも GitHub に Discussions 機能が追加される前からあるもので、今であれば discussion にすべきものが大半
  • ほとんどが重複
  • 残る issue も具体的なメリットや、逆にそれを実装した場合のリスクなどの観点が抜けている

というような状態です。

これに対して、

  • issue から discussion への移行
  • disucussion の answered 判定
  • 重複 issue の close
  • 具体性のある提案ドキュメント化(に向けた誘導)

などは、最終決定権を持たないコミュニティ メンバーでも可能なわけです。 そこで、csharplang 内で特にアクティブに活動していて、その辺りの整理作業を任せられそうな信頼のおける数名に「大使」として issue/discussion の編集権限を与えることになったそうです。

そして効果のほどなんですが、その後3週間弱となる現在、csharplang の issue はついに900ほどまで減っています。 ほぼ1日30個くらいの一定ペースで減少中。

マイルストーンの整理

前節の通り、C# の言語機能に関しては C# チームの決定権が絶対的なんですが、 その決定結果は「やる/やらない」の2択ではなくて、以下のように積極度に段階があります。

  • Working Set: やりたいし、C# チーム自身が活発に手がける
  • Backlog: やりたいけども、ちょっと決め手に欠けていて、言語設計のレベルで何かいいアイディアが欲しい(いきなりコミュニティ実装を受け付けられるというレベルでもない)
  • Any Time: やってもいいけども、優先度低めで C# チームのリソースを割けない(コミュニティ実装は受け付けられる)
  • Likely Never: よっぽどのことがないとやらない

今まで 9.0 とか 10.0 とかの C# のバージョンをそのままマイルストーンにしていましたが、 「活発に手掛けているからと言っても短期間で完成するものではなくて数バージョン先になる」みたいな機能もあれば、 「そんなに優先度が高くなかったけども、コミュニティ貢献の質が良くて採用」みたいな機能もあるので、 取り組みの活発さとマイルストーンの温度感に差がありました。

ということで、Working Set と Backlog マイルストーンを新設したとのこと (Any Time と Likely Never は元からあったものの、ここで改めて意図を明文化)。

これからはまず Working Set か Backlog かに分類された上で、 具体的に実装が進んできてリリースに含められそうかどうかが見えてきてから初めて C# バージョン番号を冠したマイルストーンに移動という流れになりそうです。 また、バージョン番号も実際にリリースされる(マイルストーンが close される)までは、「予定ではそのバージョンで入れるけども、重大な問題が発覚したらそのバージョンからは外すこともある」みたいな状態です。

トリアージ

いくつか抜粋。

Repeated Attributes in Partial Members

C# 9.0 で追加する新しい partial method ですが、以下のようなコードを書くとコンパイル エラーを起こします。

partial class C
{
    [return: MaybeNull]
    public partial string M();
}
 
partial class C
{
    [return: MaybeNull]
    public partial string M() => "";
}

AttributeUsage で重複不可(AllowMultiple = false) になっている属性が partial の宣言側と実装側の両方についている場合、「重複」判定を受けてしまっているという状態。 この例のように null 許容関連の属性は宣言側と実装側の両方に付けたいことが結構あって、これをエラーにされると結構困りそうです。 ということで、重複不可になっている属性でも、全く同じパラメーターで指定されている場合は両側に同じ属性が付いている状態を認めたいとのこと。 working set 判定。

params Span

現状、可変長引数は配列が作られてしまうのでアロケーションが発生します。 これを、Span<T> で受け付けられるようにしてアロケーションをなくしたいという話は前々からあります。

11日に書いた低水準機能改善の一環として「safe な固定長バッファー」みたいな話もあって、 これがあればそんなに難なく params Span<T> ができるはずなので、今このタイミングで working set 判定。

Sequence Expressions

(var x = Read(); x * x) みたいな書き方で、複数のステートメントを並べつつ、最後の値を返す「」にできる文法が欲しいという話。

初期化子とか switchとか、 式しか受け付けない便利な文法が結構あります。

これと関連しそうないくつかの提案 (303830373086)がすでに working set 判定済みなので、この Sequence Expressions も working set 入り。

utf8 string literals

C# に UTF-8 関連の特殊対応文法を入れるよりもまず 「Utf8String 型の実装](https://github.com/dotnet/corefxlab/issues/2350)が先だろという話でして。

そっちは nightly ビルドの NuGet パッケージを入れて試してみてフィードバックが欲しいらしいですよ。

ということで、C# 側としては Backlog 判定だそうです。

File scoped namespaces

今までの、

namespace N
{
    class C
    {
    }
}

これを、

namespace N;
 
class C
{
}

こうじゃ。 (1ライン名前空間の導入。)

最近の C# はトップ レベル ステートメントしかり、「見栄えがすっきりすること」に割と前向きです。

もちろん、「主張が弱くなりすぎていて、書くのはいいとしても、読むときには逆に見逃してしまうリスクが高くて読みづらい」みたいなこともあります (プログラムは書いている時間よりも読んでいる時間の方が長いので、「読みづらい」というのは思った以上のデメリット)。 とはいえ、この辺りは慣れの問題もあって、すっきり書けるプログラミング言語が増えてきた昨今、「読みづらい」判定の基準もずいぶん変わってきたと思います。

また、C# は基本的には空白文字の有無によって挙動を変えない言語なので、 その発想でいうと「インデント1段の差は些細な差」ではあります。 例えばまあ、別に、1ライン名前空間がなくても、以下のように書けばほぼ同じ見た目になります。

namespace N
{
class C
{
}
}

ただ、最近は

  • Visual Studio に限らずツールでの自動整形が当たり前
  • 改行位置とか空白の数まで含めて、スタイルまできっちり決めて、スタイル違反を警告にすることもざら
  • 何だったら CI テストでスタイル違反は merge できないようにはじく人までいる

みたいな状態なので、「挙動が変わらないはずの言語でも空白の有無が結構大きい」みたいな感じになっています。

ということで C# でもついに1ライン名前空間に前向きな判定が出ていて、working set 入り(というか、すでに 10.0 (バージョン番号マイルストーン)入り)。

Efficient params and string formatting

文字列補間は便利な構文なんですけども、 アロケーションが掛かっちゃうタイミングが早すぎてロギングとかの用途では使えなかったりします。 (例えば、ログ レベルによってはログ書き出ししない場合が大部分になるのに、書き出しもしない文字列のアロケーションをしまくってしまう。)

ILogger.Log メソッドformatter 引数を持つ長ったらしくて使いにくそうなシグネチャになってるのもそのせいでして。

前述の params Span<T> とかを使ったりでアロケーションを減らそう見たいな提案はいくつか出ているんですが、今あげたロギングの例とかを考えると多分不十分じゃないかなと思います。

ということで「working set としてキープし続ける。都度色々な提案を見ていく」みたいな空気感。

readonly classes and records

readonly structの参照型版。 class, record に対しても readonly 修飾(フィールド全部が readonly であることを求める)を付けたいという話。

今までやってない理由は大体以下の2点。

  • あくまで shallow な read-only 性しか担保していなくて、階層すべての immutability を保証できない
  • 構造体ほど切羽詰まってない(構造体の場合は隠れたコピー発生問題がある)

とはいえ、あって困るものではなく、working set 入り。

Target typed anonymous type initializers

以下のような話。

// C# 9.0 で、ターゲットからの型推論で new() と書けるように
Point a = new(1, 2);
 
// 逆に(3.0 からある)ソース型推論だとこうなる
var b = new Point(1, 2);
var c = new Point { X = 1, Y = 2 };
 
// ターゲット型推論で nominal (プロパティ名指定)な初期化をするならこうなる。
Point d = new() { X = 1, Y = 2 };
 
// ↑ この () は邪魔じゃない?
// とはいえ…
// これは「匿名型」になる。
var e = new { X = 1, Y = 2 };
 
// ターゲット型があるときはターゲット型推論扱い
// (ターゲットが object とか dynamic、var の時だけ匿名型扱いするような分岐)
// も可能なんじゃないか?
// (C# 9.0 時点ではエラー。たぶん、破壊的変更にはならず上記挙動が可能)
Point f = new { X = 1, Y = 2 };
 
record Point(int X = 0, int Y = 0);

new() {}() が邪魔というか、ソース型推論の場合との整合性があんまりよくなくていまいちなのは確かだったり。

その一方で、匿名型と混ざる怖さを払しょくできるほど欲しい構文かと言われると微妙で。 Backlog 入り。

Static local functions in base calls

以下のように、初期化子でローカル関数を呼びたいという話が出ていまして。

class Base
{
    public Base(int x) { }
}
 
class Derive : Base
{
    public C() : base(init())
    {
        static int init()
        {
            // 何かそれなりの処理
        }
    }
}

まあ割と「わからなくはない」という感じで Working set 入りしてるんですが。 ただ、C# チーム的にはもうちょっと汎用的に、「スコープの拡張」みたいなのを考えているみたいです。

例えば、

  • 上記のようにローカル関数のスコープを広げるのであれば、メソッド内で定義した const も同様に使いたい場面はある
  • 初期化子だけじゃなくて、属性とかからも参照したい
    • [return: NotNullIfNotNull(nameof(p))] string? M(string? p) みたいなのとか、実際、スコープを広げたい要求は出てきてる

祝 .NET 5.0 リリース: .NET Core 3.1 からの移行話

$
0
0

祝 .NET 5.0 GA。

一応注釈なんですが、 .NET は以下のような状態です。

  • .NET 5.0 からは単に「.NET」になります
    • .NET Framework, Standard, Core の統合結果です
    • TargetFramework 名、 net5.0 で、 netstandard2.1 と netcoreapp3.1 の後続扱い(後方互換あり)です
  • 年次リリース&偶数バージョンにだけ長期サポート(LTS: Long Term Support)あり
    • 5.0 は長期サポートなし(6.0 が出たら移行を推奨)
      • この状態を指して GA (General Availability) と呼んでる
    • 6.0 は来年同時期にリリース予定
    • LTS は「3年もしくは次の LTS リリース後1年間のうち、いずれかのより長い方」がサポート期間

一応リリースの日の夜に「記念雑談」をしてました(割かし本当に記念だけして、雑談です。話題それまくり)。

.NET Core 3.1 からの移行

前回、 .NET Core 2.2 から 3.0 の時は ASP.NET に破壊的変更があってそこでコード修正が必須だったりしたんですが。 今回、3.1 から 5.0 の以降では ASP.NET の破壊的変更もなさそうで、自分がかかわっているコードで大きな修正が必要なものはなかったです。

小さい修正はちょっとだけあって、自分のコードでも以下の2点は踏みました:

  • WPF (WinForms でも同様のはず)で、csproj の書き方がちょっと変わる
  • 警告がアグレッシブに追加される

あと、「コンパイルは通るけど挙動が変わっている可能性がある」という警戒を要する点もあります(自分では今のところは踏んでいない):

  • 国際化対応が NLS (Windows が元々持っていたライブラリ)から ICU (Unicode 標準に沿ったオープンな実装)に切り替わった

Windows 向けプロジェクト

元:

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
 
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFrameworks>netcoreapp3.1</TargetFrameworks>
    <UseWPF>true</UseWPF>
  </PropertyGroup>
 
</Project>

後:

<Project Sdk="Microsoft.NET.Sdk">
 
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net5.0-windows</TargetFramework>
    <UseWPF>true</UseWPF>
  </PropertyGroup>
 
</Project>

警告がアグレッシブに追加される

C# はこれまで警告の追加も破壊的変更になりうるということで、追加には消極的でした。 今では「仕様の穴だった」と認識されている問題のあるコードでも、 かつて警告なしでコンパイルできてしまっていたものに新たに警告を足すということは避けていました。 (世の中にはコンパイルはできてしまう上でたまたま問題を踏まず動いてしまっていたコードがたくさんあります。)

.NET 5.0 世代ではこの方針に変更があって、以下のようになっています。

  • TargetFramework を変えない限りには警告の追加はないものの、net5.0 に上げた場合には警告が出るようにする -C# 言語バージョン(LangVersion) とは別に「警告バージョン」(AnalysisLevel)を指定できるようにする
  • この条件下で、これまでだったら足さなかったようなレベルの警告を大幅に追加する

.NET 5.0 をターゲットにしたいし、C# 9.0 の新機能も使いたいけども警告だけは増やしたくないという人は、以下の2つのオプションを csproj に追加してください。

<Project Sdk="Microsoft.NET.Sdk">
 
  <PropertyGroup>
    <EnableNETAnalyzers>false</EnableNETAnalyzers>
    <AnalysisLevel>4.0</AnalysisLevel>
  </PropertyGroup>
 
</Project>

ちなみに、自分が踏んだ「追加警告」は以下のような警告だけでした(CS8881 の追加)。

// エラーにならなくなってた条件
// - X と Y が別 DLL (別 csproj)
// - X が参照型だけを含む
public struct X<T>
{
    private string _x;
}

struct Y
{
    X<int> _x;
 
    public Y(bool x)
    {
        // 一定の条件下で、 _x を初期化しなくてもエラーが出なかった。
        // C# 9.0 (AnalysisLevel 5.0) では警告だけは出るようになった。
        if (x) return;
 
        _x = new X<int>();
    }
}

ICU (International Components for Unicode) 化

もしかしたら踏むかもしれない地雷として話題になっているのが、国際化対応の ICU 化です。

.NET Core では今まで以下のような状態でした。

  • Windows 上では NLS (National Language Support) という Windows 組み込みの国際化ライブラリを使っていた
  • 非 Windows 環境では (ICU)を使っていた
  • なので、カルチャー依存の文字列処理などが、Windows とそうでない環境に差があった

.NET 5.0 では、Windows 上でも ICU を使う方針に変わりました。 「ICU を使えるオプション」(opt-in)にするか「デフォルトは ICU で、NLS に戻せるオプション」(opt-out)にするかは迷っていたみたいなんですが、結局は後者、デフォルト動作は ICU になりました。

「同じバージョンの .NET が Windows とその他で挙動が違う」という問題はなくなった一方で、 「Windows 上では .NET Core 3.1 と .NET 5.0 で挙動が違う」ということが一部起こります。

カルチャー依存な API を使わなければ問題ないんですが… .NET の string は歴史的背景で、一部はカルチャー依存、一部は非依存みたいになっているので注意が必要です。 例えば、IndexOf はカルチャー依存で、Contains は非依存みたいなことがあったりします。

¥ 記号

日本語 Windows がやっている余計なお世話として有名なのが、「いまだに ¥ 記号と \ (逆スラッシュ)を同一視する」というのがあります。

using System;
using System.Globalization;
using System.Threading;
 
var s1 = @"\";
var s2 = "¥";
 
var culture = CultureInfo.GetCultureInfo("ja-jp");
Thread.CurrentThread.CurrentCulture = culture;
Console.WriteLine(s1.Contains(s2)); // CurrentCulture 非依存。今までもこれからも False。
Console.WriteLine(s1.IndexOf(s2)); // CurrentCulture 依存。今まで 0。これから -1。

昔、逆スラッシュのコード(U+005C)は国ごとに解釈を変えてもいいという扱いになっていて、 日本語では¥(円記号)、韓国語では₩(ウォン記号)に使っていたという時代の名残りです。 というか、日本語 Windows 上ではフォントによってはいまだに U+005C が円記号で表示されますし…

今(Unicode)では、¥ (U+00A5)、₩ (U+20A9)にはちゃんと別コードが割当たっていますが、NLS は今でも \ (U+005C) と同一視する処理が入っていたりします。

ちなみに、それぞれ日本語カルチャー(ja-jp)、韓国語カルチャー(ko-kr)でだけこの処理が起こります。

改行

ICU では CR LF は「分割不可」らしく、CR LF と LF は別文字扱いになっています。 NLS はこの処理をしていないので、以下のコードは NLS と ICU で結果が変わります。

using System;
 
var s1 = "\r\n";
var s2 = "\n";
 
Console.WriteLine(s1.IndexOf(s2)); // NLS だと 1。ICU だと -1。

対処法

もしこの手の問題を踏んだ場合、対処方法としては2つあります。

  • NLS に戻す
  • カルチャー依存をやめる

NLS に戻すなら、csproj に以下の設定を追加します。

  <ItemGroup>
    <RuntimeHostConfigurationOption Include="System.Globalization.UseNls" Value="true" />
  </ItemGroup>

一方、カルチャー依存するようなメソッドは大体、第2引数にオプション指定できるので、それを Ordinal にしてしまえばカルチャー問題は踏まなくなります。

Console.WriteLine(s1.IndexOf(s2)); // CurrentCulture になってるのが問題
Console.WriteLine(s1.IndexOf(s2, StringComparison.CurrentCulture)); // \ と ¥ の問題を踏む
Console.WriteLine(s1.IndexOf(s2, StringComparison.InvariantCulture)); // \r\n と \n の問題を踏む
Console.WriteLine(s1.IndexOf(s2, StringComparison.Ordinal)); // カルチャー依存したくなければこれを指定すればいい

「デフォルトが CurrentCulture なことが問題」とは認識されていて、 近いうちにこの手の API に対して StringComparison を指定していなかった場合に警告を出すようなアナライザーを提供しようかという話になっていたりもします。

Viewing all 483 articles
Browse latest View live