ネコのために鐘は鳴る

寺院に住み着くパソコ〇好き

(C#) interface の静的仮想メンバーでジェネリックコンストラクタ

interface の静的仮想メンバー

C# 11 から interface に静的仮想メンバーを持たせられるようになりました。これは別名 "Generic Math" と呼ばれており、数値の演算がジェネリクスを通して呼べるようになりました。

しかし、本記事では interface の静的仮想メンバーがもたらす別の使い方を解説します。 主たる動機である Generic Math の方の解説はググってください、ここでは説明しません。

「interface の静的仮想メンバー」が何かを簡単に言うと、interface に staitc メソッドを定義することで、そのインターフェースを継承した具象型に static メソッドの実装を強制させることができるのです。

public interface IFoo
{
    static abstract void Foo();
}

上記のように interface に静的メソッドを定義でき、この IFoo を継承した型はかならず static void Foo() を実装するようにできるということです。

ジェネリックコンストラク

本題に入る前に、ジェネリックスなメソッドの new() 制約について確認します。

public static void Bar<T>() where T : new()
{
    var obj = new T();
    // ... 何か処理
}

上記の Bar メソッドにはジェネリックス引数 T があり、その制約として new() 制約があります。 これは、型 T が引数のないコンストラクタを持っていることが必要で、この制約によってメソッド内で new T() を使うことができます。

しかし、この new() 制約は引数ありの制約、例えば where T : new(string) のようにすることができません。 また、new() 制約は内部的にはリフレクションを使ってインスタンスを生成するように作られており、微々たるものですがパフォーマンスが良くないことも知られています。

これを、C# 11 からの interface の静的仮想メンバーをつかって解決します。

public interface IConstruct<TSelf, T>
{
    static abstract TSelf New(in T item);
}

public class Dog : IConstruct<Dog, string>
{
    public string Name { get; }
    public Dog(string name) => Name = name;

    public static Dog New(in string item)
        => new Dog(item);
}

public class Person : IConstruct<Person, string>
{
    public string Name { get; }
    public Person(string name) => Name = name;

    public static Person New(in string item)
        => new Person(item);
}

上記のように IConstruct インターフェースを定義して、それを継承した型 DogPerson を作ってみました。 そしてこれを以下のように使います。

public static T Something<T>() where T : IConstruct<T, string>
{
    string arg = Console.ReadLine() ?? "";
    T obj = T.New(arg);
    return obj;
}

// -----------------------
// インスタンス生成を伴う処理をジェネリックに書けた
var dog = Something<Dog>();
var person = Something<Person>();

今までは実現できなかったジェネリックスによる引数ありのインスタンス生成が可能になりました。リフレクションも使っていません。

C# 10 以前ではリフレクションを使ったり DI (依存性注入) 的なことをしないとどう頑張ってもこれが書けなかったのですが、C# 11 ならできますね。

余談

上記の解説用のコード、ものすごく Rust っぽくないですか?(半分わざと似せてるのもあるんですが)

IConstruct<TSelf, T> はインターフェース内で自分自身の具象型にアクセスするために TSelf を必要としているのですが、これは Rust の trait における Self です。 先ほどの IConstruct<TSelf, T> は Rust の From<T> trait とやってることは完全に同じですね。

Rust で trait 境界から関数を呼んでもコンパイル時に解決されてゼロコストなのと同様、さきほどの C# のコードも動的なコストはありません。 むしろオーバーヘッドがあると、Generic Math な使い方で四則演算などの超軽量な処理を抽象化できません。遅すぎて使い物にならなくなってしまいます。