先日 C# 8.0 予告なブログで書いた通り、 C# 8.0 で入るインターフェイスのデフォルト実装は .NET ランタイム側の修正が必要な機能です。

今日は、そういう「ランタイム側機能」についての話を少し。

ランタイム側機能

C# の言語機能は、C# コンパイラーがちょこっと頑張ってよい具合にコード生成して、 古い .NET Framework ランタイム上でも動くものが多いです。 「古いランタイム上では動かない新機能」というと、実は .NET Framework 2.0 でのジェネリクスの導入まで遡ります。 .NET Framework 2.0 は2005年リリースですし、 C# 8.0 には実に13年ぶりにランタイムの方に修正を求める機能が入ったことになります。

契機となったのはやっぱり .NET Core の存在です。 .NET Core は、オープンソースで開発ペースも速く、 side by side (1台のPCに複数バージョンを同時にインストール可能)なのでランタイムの更新がしやすいという利点があります。 ランタイム更新しやすいからこその、13年ぶりの新機能を追加です。

登場直後の .NET Core は .NET Framework の下位互換のような存在でしたが、 .NET Core 2.0 くらいから互換性が増してきて、 .NET Core 3.0 ではついに Windows 限定な WPF や UWP などの GUI フレームワークも .NET Core 上で動くようになりました。 一応、完全新規の案件であれば .NET Framework の方をわざわざ使う理由がないくらいにはなっています。 ようやく、次のステップに進む段階に入ったといえます。 これも、このタイミングで13年ぶりの新機能追加に至った要因でしょう。

RuntimeFeature クラス

しかし、ランタイムの新旧によって使えない機能があるとなると、 それを検知・コンパイル時にエラーにする仕組みが必要になります。 コンパイルできたはいいけど、実際に動かそうとした段階で無理だったというのでは困ります。

その検知機構として用意されたのが、RuntimeFeatureクラス(System.Runtime.CompilerServices名前空間)です。 以下のようなクラスになっていて、const stringなメンバーが存在するかどうかで、その機能を使えるかどうかを判定します。

public static partial class RuntimeFeature
{
#if FEATURE_DEFAULT_INTERFACES
    public const string DefaultImplementationsOfInterfaces = "DefaultImplementationsOfInterfaces";
#endif
    public const string PortablePdb = "PortablePdb";
    public static bool IsSupported(string feature);
}

今のところ生えているメンバーは、

  • DefaultImplementationsOfInterfaces … 今回追加されたインターフェイスのデフォルト実装の可否
  • PortablePdb … 動的コード生成で Portable PDB を解釈できるかどうか

の2つです。

PortablePdb

ちなみに、PortablePdbの方について補足。 まず、PDB はデバッグ情報が掛かれたファイルで、 Visual C++ の頃から同名の拡張子のファイルは作られていました。 C# でも、ビルド時に dll や exe と一緒に拡張子が pdb のファイルが作られていると思います。

拡張子自体はずっと同じ pdb ですが、内部の形式については最近ガラッと変わりました。 以前の pdb は、仕様がオープンになっておらず、 pdb を読み込めるデバッガーが Windows 依存でした(なので、通称 "Windows PDB")。 そこで、.NET Core では、せっかくなので仕様自体をオープンにした Portable な pdb 形式を作ることにしたそうです。 それが「Portable PDB」。

PDB は基本的に C# コンパイラーが生成するものなので、RuntimeFeature (ランタイム側)とは無関係そうに見えます。 では PortablePdb は何のためにあるかと言うと、動的コード生成です。 例えば C# スクリプト実行であっても、内部的にはちゃんと PDB を生成して、 デバッグ情報が取れるようにしてあります。 このとき、生成する PDB を Portable PDB にしていいか、クラシックな Windows PDB でないとダメかを判別するための機構がないと困るので、RuntimeFeature.PortablePdbがあります。

その他の RuntimeFeature 機能

その他に、RuntimeFeatureクラスには (.NET Standard 2.1 から)以下のような bool 型の静的プロパティもあります。

public static partial class RuntimeFeature
{
    public static bool IsDynamicCodeSupported { get; }
    public static bool IsDynamicCodeCompiled { get; }
}

先ほどの2つとは違って、こちらは実行時に値を確認して使う用みたいです。 そのランタイムで、

  • IsDynamicCodeSupported … そもそも動的コード実行は可能か
  • IsDynamicCodeCompiled … 動的コード実行はコンパイルされているか(= インタープリター実行ではないか = パフォーマンスよく実行できるか)

の判別に使います。 例えば、動的コンパイルができない環境で式ツリーCompile メソッドを使ったりすると、 高速化のためにやってることなのにかえって破滅的に遅いコードになってしまいます。 それを避けるために分岐に使うのがこれらのプロパティ。

今後入るかもしれないランタイム側機能

とりあえず、DefaultImplementationsOfInterfaces は最初の一歩です。 これからは定期的に、こういう .NET ランタイム側の修正が必要な機能が追加されていくものと思われます。 (おそらく、基本的にはメジャー バージョンアップのタイミングでの追加。 そんなに高頻度で追加はしないと思われます。)

例えば、以下のような issue ページがあります。 .NET ランタイム(CLR)に修正を入れれるなら実現できそうな C# 機能の一覧。

挙がっている内容は、以下のようなものです。

CLR unification of types across assemblies

別アセンブリで定義された「同名で同じメンバーを含む型」を同一視したいというもの。

DI 用途でほしかったり。 あと、匿名クラス(内部的に匿名クラスを生成してる)が public なところで使えない理由にもなっているので回避方法が欲しいという要望があります。

Make void a first-class type

要するに、Func<void>とかTask<void>と書かせろ、 Actionや非ジェネリックTaskとの分岐がめんどい、というあれ。

Covariance and contravariance for classes

現状、変性が認められているのはインターフェイスとデリゲートだけなわけですが、どうにかしてクラスでも認めてほしいというやつ。

Task<object>Task<string> を代入したいとか、そういうやつ。

|, &, and ~ operators on a type parameter with the enum constraint

where T : Enum が付いてるとき、その型の変数に対してビット演算したいというやつ。 今、列挙型とジェネリクスの相性が悪すぎてつらく。

Union and intersection types

最近 TypeScript に入ったあれ。 string | int で「stringint のどちらか」みたいな型を作ったり、 IA & IB で「2つのインターフェイスIAIBの両方を実装した(両方のメソッドを使える)型」を作ったり。

ある程度は C# コンパイラーのレベルでできるんですが、 ランタイムに手を入れて型システムのレベルで対応した方がよいという話。

Support generic indexers

インデクサーに型引数を取りたいと。 ↓みたいな。(今は、このTがどうやっても使えない。)

public interface IOptions
{
    T this<T>[OptionKey<T> key]
    {
        get;
        set;
    }
}

Higher-kinded polymorphism

ジェネリクスの型制約に複雑な条件を付けたいというやつ。 例として挙がってるのは↓みたいなコード。

public static T<A> To<T, A>(this IEnumerable<A> xs)
    where T : <>, new(), ICollection<>
{
    var ta = new T<A>();
    foreach (var x in xs)
    {
        ta.Add(x);
    }
    return ta;
}

methods in enums

列挙型に直接メソッドを持ちたいというやつ。 拡張メソッドでの実装だとちょっとつらいこともあり。