ネコのために鐘は鳴る

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

(C#) 型の外部から疑似的に interface を追加する

interface を後付けしたい

interface は C#ポリモーフィズムを実現するために、メソッド・プロパティの宣言だけを決めておき、実際の挙動は型の実装者に任せるというものです。今更説明するまでもないですね、普段みなさんが使ってるやつです。

普通は型定義の段階で interface を継承させて使うのですが、例えば外部ライブラリで定義されている型に自作の interface を継承させることはできません。

public class Apple  // 外部ライブラリのクラス
{
    public string Name => "apple";
}

public class Orange  // 外部ライブラリのクラス
{
    public string Name => "orange";
}

public interface IName  // 自作の interface
{
    string Name { get; }
}

上記のようなAppleOrangeクラスが外部ライブラリにあって、これらを統一的に扱うためにINameという自作の interface をつけたいとしても、当然外部ライブラリ内にあるクラスに勝手に継承を追加させることはできません。

 

今回は、これをローカル関数と関数ポインタ (と必要ならジェネリクス) を使って疑似的に実現させます。

public unsafe readonly struct IName
{
    private readonly object _obj;
    private readonly delegate*<object, string> _func;

    public string Name => _func(_obj);

    private IName(Apple apple)
    {
        _obj = apple;
        _func = &GetName;
        static string GetName(object obj) => Unsafe.As<Apple>(obj).Name;
    }
    
    private IName(Orange orange)
    {
        _obj = orange;
        _func = &GetName;
        static string GetName(object obj) => Unsafe.As<Orange>(obj).Name;
    }

    public static implicit operator IName(Apple apple) => new IName(apple);
    public static implicit operator IName(Orange orange) => new IName(orange);
    public static explicit operator Apple(in IName iName) => (Apple)iName._obj;
    public static explicit operator Orange(in IName iName) => (Orange)iName._obj;
    // サンプルなので Equals とか GetHashCode とかの override は省略
}

コンストラクタ内で、元になるオブジェクトと実装を呼ぶローカル関数の関数ポインタをフィールドに格納しておき、疑似インターフェースの定義したプロパティの呼び出し時に、関数ポインタ経由で処理を呼びます。ついでに、対象となる型とのキャストを作っておきます。

ローカル関数内で使っているUnsafe.As<Apple>(obj)(Apple)objと同義ですが、一切の安全チェックを省いた最速のキャストです。本来は危険なのですが、今回の場合はobjは絶対に対象の型であることが静的に保証できているため問題ありません。

一見意味不明ですが、短いですのでよーくコードを眺めてください。理解できるはずです。(というかこれ以上説明することがない)

 

原理としては、元になったオブジェクトの型によって関数ポインタ経由で処理を変えています。ここでよく考えてみてください、これって仮想関数テーブルから間接参照で処理に飛ぶというインターフェースの原理そのものですよね。

あたかも本物の interface のように振舞い、しかも原理まで同じなのでパフォーマンスもほぼ同等です。

Apple apple = new Apple();
IName iName = apple;    // 暗黙的キャスト

一応、INameという型名にしましたが、これは interface のプレフィックスである Iを冠していいのかは不明です。何しろ interface ではなく構造体ですので。というか、こんな実装方法はたぶん想定されていないため、命名方法の慣習も何もありません、無敵です。

ジェネリクス型にジェネリクス挙動をさせる

ここからは、先ほどまでとは別のことをします。記事のタイトルとして「型の外部から interface を追加する」というタイトルをつけましたが、本来この記事で言いたかった内容は「ローカル関数×関数ポインタ×ジェネリクスによるハックな実装」です。型の外部から interface を追加するのはその一例です。

では、先ほどと似た原理を使って、非ジェネリクス型にジェネリクス (のような) 挙動をさせてみましょう。

public unsafe class MyClass
{
    private readonly delegate*<void> _say;

    private MyClass(delegate*<void> say)
    {
        _say = say;
    }

    public void SayGenericsType() => _say();

    public static MyClass Create<T>()
    {
        if(typeof(T) == typeof(int))
            return new MyClass(&SayInt);
        else if(typeof(T) == typeof(double))
            return new MyClass(&SayDouble);
        else
            return new MyClass(&SayOtherTypes);

        static void SayInt() => Console.WriteLine("I am int");
        static void SayDouble() => Console.WriteLine("I am double");
        static void SayOtherTypes() => Console.WriteLine($"I am {typeof(T).FullName}");
    }
}

上記のMyClassジェネリクス型ではないですが、SayGenericsType()メソッドを呼ぶとジェネリクス挙動をします。

var obj1 = MyClass.Create<int>();
var obj2 = MyClass.Create<double>();
var obj3 = MyClass.Create<string>();

obj1.SayGenericsType();   // "I am int"
obj2.SayGenericsType();   // "I am double"
obj3.SayGenericsType();   // "I am System.String"

コンストラクタに型引数<T>を宣言することはできないため、Createという静的メソッドをファクトリーメソッドとしています。ファクトリーメソッド内で型によって関数ポインタを変えることで、挙動を操っています。そして、このたくさん並んだif分岐は JIT コンパイル時に分岐が消えます。これについては以前の記事 (3種類の定数と JIT コンパイル時最適化) で解説しているため、気になった方は読んでください。

これのすごい所は実行時型Typeを保存しているわけではなく、ジェネリクス<T>を静的型制約として復元できる点です。上記のobj3は生成時にstringを指定していますが、後でメソッドを呼ぶときにはどこにもstringと指定していません。にもかかわらず、SayGenericsType()メソッドの呼び出し時にジェネリクス型が復元されます。状態をインスタンスに保持するという点で、これはラムダ式の変数キャプチャに似ています。

実際、これはCreateメソッドの<T>がローカル関数のSayOtherTypesに入り込んでいる (これをキャプチャと呼んでいいのかどうかはわかりません) のですが、<T>のキャプチャがラムダ式の変数キャプチャと違う点は、パフォーマンス上のコストがないことです。変数キャプチャが動的状態 (値) をインスタンスに保存するのに対し、<T>のキャプチャは静的状態 (型制約) をインスタンスに保存するのです。

ローカル関数にジェネリクス<T>をキャプチャしつつ関数ポインタに保持し、オブジェクト生成時の<T>インスタンスに保存する、という C# の言語機能をパズルのように組み合わせた何とも珍妙な実装です。記事タイトルを「ローカル関数×関数ポインタ×ジェネリクスによるハックな実装」と題したかった意味が分かっていただけますでしょうか……?

ISpan<T>を作る

この記事の本命です。配列T[]やリストList<T>Span<T>を生成することができます。(公式にList<T>からSpan<T>を生成できるのは .NET5 以降からですが、ちょっとUnsafeを使うとそれ以前のバージョンでもできます。詳しくは過去記事を参照)

もし、C# 1.0 からSpan<T>が存在していたら、おそらく以下のようなISpan<T>というインターフェイスが存在したことでしょう。(少なくとも、私が C# の言語設計者なら作ります。)

public interface ISpan<T>
{
    Span<T> AsSpan();
}

ところが、Span<T>自体が割と最近登場した型で、残念ながらそんなものは存在しません。そこで、ここまで解説してきた外付け疑似インターフェースを作ります。

public unsafe readonly struct ISpan<T>
{
    private readonly object _obj;
    private readonly delegate*<object, Span<T>> _asSpan;

    public Span<T> AsSpan() => _asSpan(_obj);

    private ISpan(T[] array)
    {
        _obj = array;
        _asSpan = &AsSpan;
        static Span<T> AsSpan(object obj) => Unsafe.As<T[]>(obj).AsSpan();
    }

    private ISpan(List<T> list)
    {
        _obj = list;
        _asSpan = &AsSpan;
        static Span<T> AsSpan(object obj)
        {
            return CollectionsMarshal.AsSpan(Unsafe.As<List<T>>(obj));
        }
    }

    public static implicit operator ISpan<T>(T[] array) => new ISpan<T>(array);
    public static implicit operator ISpan<T>(List<T> list) => new ISpan<T>(list);
    public static explicit operator T[](in ISpan<T> iSpan) => (T[])iSpan._obj;
    public static explicit operator List<T>(in ISpan<T> iSpan) => (List<T>)iSpan._obj;
    // サンプルなので Equals とか GetHashCode とかの override は省略
}

原理は先ほどまでと同じなので分かるかと思います。これで、配列とリストを「Span<T>を生成することができる」という意味のインターフェースで共通に扱えますね。やったね。

 

そして、今回のオチです。このコード、本記事執筆現在の最新のコンパイラ (Visual Studio 2019 16.8.2) でコンパイルエラーになります。文法、実装内容ともに完全に正しい C# のコードです。まさかのコンパイラのバグを踏みました。

エラー内容は驚きの「CS8751 C# コンパイラで内部エラーが発生しました。」です。初めて見た。

おそらく、関数ポインタdelegate*の戻り値としてSpan<T>を指定しているのがまずいようです。(現状、言語仕様としては禁止されていません。) 関数ポインタもref structもどちらも最近 C# に入った新しめの機能なので枯れておらず、言語設計の未検討の仕様を突いてしまったと思われます。腐ってやがる。早すぎたんだ。

一応 roslyn のリポジトリに issue を出しました。

github.com

[2021/7/24 追記] バグは修正されて、最新のコンパイラでは正常にコンパイルできます。

以上、外付けできる疑似 interface の作り方と、ローカル関数×関数ポインタ×ジェネリクスなミラクル実装でISpan<T>を作ろうとしたらコンパイラのバグを踏んだ話でした。。。

[追記] Memory<T>について

ご存知かと思いますが「Span<T>を生成するもの」という意味において、似たような機能のものとしてSystem.Memory<T>が公式で存在しています。しかし、私はあれが嫌いです。オーバーヘッドが大きくて遅い。独自型をMemory<T>対応させるためにはMemoryManager<T>を使うのですが実装がイケてなさすぎる。

私のライブラリで UnmanagedArray というライブラリがあるのですが、あれがMemory<T>に対応していないのもそんな理由です。

なら、私ならどう実装するのか?と言われると、目的に応じてケースバイケースなので回答を1つには出せませんが、答えの一つが先程の疑似ISpan<T>です。アンマネージドメモリの生ポインタとかも考慮に入れて考えると、あれでは boxing を挟んでしまうのでやや不十分なのですが、そこはケースバイケースという便利な単語でお茶を濁します。