Visual Studio 2019 (16.0)が RC までいってちょっと落ち着いたのか、csharplang にちょっと動きが。 (もう、次に C# 8.0 絡みの新機能実装がマージされるとしたら 16.1 になるので、C# チーム的には今ちょっと落ち着ける時期のはず。) Designe Notes 3件追加。

いくつかの話題はすでに個別の issue が立っています。

インターフェイスのデフォルト実装

半分くらいはインターフェイスのデフォルト実装がらみ。 base 呼び出しをどうしようかという話と、アクセシビリティをどうしようかという話。

base

base 呼び出しって言うのは以下のようなやつのこと。

using System;
 
class A
{
    protected virtual void M() => Console.WriteLine("A");
}
 
class B : A
{
    // この、B の M は後から足したり消したり
    //protected override void M() => Console.WriteLine("B");
}
 
// A, B とは別アセンブリにあるとして
class C : B
{
    // C から基底クラスの M() を呼ぶ
    protected override void M() => base.M();
}

この書き方で、C.M から基底クラスの M を呼び出せるわけですが、

  • 基底クラスを1つずつたどっていって、最初に見つかったやつが呼ばれる
    • コンパイル時に B.M があったけど、実行時に読み込んだものからは消えていたら A.M が呼ばれる
    • コンパイル時に B.M はなかったけど、実行時に読み込んだものには足されていたら B.M が呼ばれる

みたいな挙動です。 これは C# の仕様というか、 .NET ランタイム(IL 命令)のレベルでそういう仕様だそうです。

で、インターフェイスの場合はダイアモンド継承があり得るので、この仕様便りだと、規定をたどっていく経路が複数あって困る。 なので、base(B).M() みたいに、具体的にどの型の M を呼びたいのかを明示できる構文が導入される予定です。これについて、

  • この書き方、(C# 8.0 でこれから実装される)インターフェイスのデフォルト実装だけじゃなく、クラスに対しても認める
  • フィールドだろうがなんだろうが、この base(BaseClass).Member みたいな書き方が使える
    • ただし、(overrideさえなければ)元々 this.Member でアクセスできるものに限る
  • base(B).M() と書いたら B だけを見る。実行時に消えて時たら実行時エラーを起こす
    • B になければ B から上をたどって探す」みたいなのは現状の .NET ランタイムでは不可能
    • 将来的に、 .NET ランタイム自体に改修を入れる余地はある

とのこと。

アクセシビリティ

アクセシビリティpublic とか private とかのこと。

C# 7.3 までのインターフェイスは無条件に全部のメンバーが public (明示的に指定はできない)でしたが、 デフォルト実装とともに、アクセシビリティの指定ができるようになります。 これまでも、publicprotectedprivate は提供するつもりでしたが、 残りの internalprotected internalprivate protected も提供することに決めたそうです。

あと、インターフェイスの明示的実装、↓みたいなやつもあるわけですが。

interface I
{
    void M();
}
 
class A : I
{
    void I.M() { }
}

デフォルト実装が入ることで、「インターフェイスが基底のメンバーを明示的実装」という状況が発生します。 この場合、その明示的実装は protected 扱いにするそうです。 (前節の「base は基底をたどって最初に見つかったものを呼ぶ」挙動との兼ね合いだそうです。)

null 許容参照型

値型の default

null 許容“参照型”と言っているわけですから、名前通り、参照型に関する機能です。 でも、じゃあ、クラスとか参照型を含む構造体が絡んだとき、default(T) はどうするんだという問題が残ります。 (default 既定値は 0/null での初期化になります。nullが絡む。)

なんか今のところ、var y = default(参照型をフィールドとして持つ構造体) みたいなのに対する警告は出さないみたいです。 (もちろん、「null を認めてないつもりのものに null が混ざる」という落とし穴でしかないので、相当な妥協。)

ただ、C# 8.0 では無理としても、構造体の “defaultability” については今後取り組みたい姿勢はある模様。 3年前(roslyn リポジトリ側に文法に関する提案も混ざってた頃含む)に自分が書いた「default を認めない値型を作らせてくれ」という issue:

が急に championed (C# チームの誰かが興味を持って取り組む)状態に変わりました。 ある意味こいつは「値型版の null 許容参照型」です。

利用調査

null 許容参照型のフロー解析をオンにしてどうなるか、 それなりの規模なライブラリを調べてみたみたいです(作者に直接聞いたのか、クローンしてきて自分たちでやってみたのか、ブログとかを見ただけなのかとかはわからず)。 Telegram botとかJilとか。 そこで起きてた問題のまとめ。

  • メンバー定義で困ることが多い。メソッド内部での問題はむしろ少ない
  • インターフェイス側の定義を変えたときの、実装を全部変えて回る作業がやばい
  • 初期化子での初期化を前提としているものに対して警告が消せない
  • コンストラクター連鎖 (A() : this(0) { } みたいなやつ)もフロー解析しきれてない
  • null 許容値型(既存の、値型に対する ?) が null 許容参照型との挙動差でよく問題起こす
  • 診断のクオリティがまだまだ。ジェネリックな型でよく混乱するし、特にタプルに対してつらい
  • 自動 code fix 機能欲しい

プロパティの get/set に Obsolete

プロパティの get/set アクセサーには、それぞれ属性が付けれます(メソッド扱い。AttributeTargets.Method が入ってる属性だけ)。 でも、ObsoleteConditionalCLSComliant の3つは get/set に付けることは禁止されていました。

で、まあ、Obsoleteだけは認めてもいいんじゃないかという話に。 (あと、Xamarin iOS のやつかな、たぶん、Deprecated 属性も。) ConditionalCLSComliant は今後もノータッチとのこと。

params と文字列補間の効率

提案ドキュメントの背景説明に「MSBuildログ最小限にしてもstringで236MBメモリ食っててその半分がFormatがらみ」とか書かれてて、あっ、はい…そうですよね…

文字列の整形絡みは一時的な小さい文字列インスタンスが大量にできちゃって、結構遅かったりします。 なので、自分も結構仕事で、string.Format とかを避けて、stackalloc したりプールした char[] とかを使って自前で文字列整形することがあったり。

あと、params が必ず配列を new しちゃうのも、string.Format を重たくしている原因。

ということで、corefxlab の方で文字列整形をアロケーションなしでできないか、みたいな実験コードが出ていまして。

C# 側で対応しないといけないこともあるので Design Meeting でも議題に。 概ね、

  • paramsSpan<T> を認めて、スタックに値を置いて可変長引数呼び出ししたい
  • 値型のボックス化避けたいから Variant 型作るか

みたいな話。

まあでも、付いたコメント的には

  • なんで stackalloc に参照型使えないの? → それを認めようとするとガベコレのコードがだいぶ複雑になる(パフォーマンスにも悪影響)
  • Span<TypedReference> を認められるようにしようよ

みたいな感じ。

switch 式の優先度

今回の Designe Notes にはないんですけど、もう1個、割と最近立った提案 issue:

現時点 (Visual Studio 2019 RC でのことなので、たぶん、RC が外れても)での switchの結合優先度は関係演算子(== とか)と同じだそうです。 関係演算子って結構優先度が低くいですし、 & よりは上で + よりは下みたいな位置です。

  • x switch { ... } + 1 みたいなものが、} + 1 の方を先に見ちゃってエラーに
  • a + b switch {...}+ が先だけど、a & b switch {...}switch が先

とかいう嫌な状態。

なので、プライマリな優先度(x.M(). とか、[] とかと同じ)に変えようかという提案。

大体、x switch { ... } が後置き演算子みたいな見た目ですからね。 [] とか ++ とか . とか、後ろに置くものは大体がプライマリ。 それに合わせた方が自然だろうという意図もあり。

Index/Range の実装再考

もう1つは C# 側じゃなくて、corefx 側から出ている要望。

今現在の仕様だと、C# の姿勢としては、

  • ^a とか とか a..b とかの構文はそれぞれ Index/Range 構造体を作るだけ
  • x[a..b] みたいなのを使いたければ、コレクション クラスの実装側に Index/Range を受け付けるオーバーロードを増やせ

です。

これに対して、要するに、

  • インデクサーを持つコレクション クラス全部に対してオーバーロードを増やして回るのは大変だし、パフォーマンスが出ない実装になることがある
  • それよりは、collection[i.GetOffset(collection.Count)] みたいな、int のオーバーロードを使うコードに展開する実装に変えてほしい

という流れ。 まあ、そりゃ、コレクション作ってる側からするとそうですよね。 corefx (中の人同士)ですらそうなんだから、ましてコミュニティ実装なコレクションだとなおのこと。