型がわからない処理から特定の型に対する処理を呼び出すためには、ディスパッチが不可欠です。
この記事では「ディスパッチ」を「型が一つに定まらない処理(A)から特定の型に対する処理(B)を呼び出す操作」として用います。
静的ディスパッチ
1. ポリシーパターン
void A<T>(T t) where T : I
{
t.B();
}
interface I
{
void B();
}
struct S : I
{
public void B() { ... };
}
インライン展開が強制的にされるので最速です。
2. STD (Static Type Dictionary)
void A<T>(T t)
{
STD<T>.b(t);
}
class C
{
public void B() { ... }
}
static class STD<T>
{
public Action<T> b;
static STD()
{
b = x => x.B();
}
}
デリゲートを挟みますが十分早いです。
動的ディスパッチ
インターフェース
void A(I i)
{
i.B();
}
class C : I
{
public void B();
}
interface I
{
void B();
}
シンプル。
インライン化が効くと静的ディスパッチになるので最速になりもします。
ポリシーパターン風(無意味)
virtual void A<T>(T t) where T : I
{
t.B();
}
interface I
{
void B();
}
struct S : I
{
public void B() { ... };
}
一見正しいポリシーパターンに見えますが、仮想メソッド(virtual
)には通用しません。ただのインターフェース経由の呼び出しになります。
自作vtable
// 呼び出すときは、`C c = ...; A(c.ToObjectWithTable());`
void A(ObjectWithTable objectWithTable)
{
objectWithTable.B();
}
class C
{
public void B() { ... };
public ToObjectWithTable() => new(this, x => Unsafe.As<C>(x).B());
}
struct ObjectWithTable
{
object o;
Action<object> b;
public void B() => b(o);
public ObjectWithTable(object o, Action<object> b) { ... };
}
自作vtable(unsafe)
// 呼び出すときは、`C c = ...; A(c.ToObjectWithTable());`
void A(ObjectWithTable objectWithTable)
{
objectWithTable.B();
}
class C
{
public void B() { ... };
unsafe ObjectWithTable ToObjectWithTable() => new(this, (delegate managed<object, void>*)&Extensions.B);
}
static Extentions
{
public static void B(this C @this) => @this.B();
}
unsafe struct ObjectWithTable
{
object o;
delegate managed<object, void>* b;
public void B() => b(o);
public ObjectWithTable(object o, delegate managed<object, void>* b) { ... };
}
インターフェースでやってることとあまり変わらない。
ハッシュテーブル
void A(object o)
{
table[o.GetType()](o);
}
class C
{
public void B() { ... }
}
static Dictionary<Type, Action<object>> table = new()
{
{ typeof(C), x => Unsafe.As<C>(x).B(); }
};
遅い。
STD
void A<T>(T t)
{
STD<T>.b();
}
class C
{
public void B() { ... }
static C()
{
STD<C>.b = x => x.B();
}
}
static class STD<T>
{
public Action<T> b;
}
既存の型(=使用者が新たにメソッドを追加できない型)の場合にも使えて、そこそこ高速(静的の4倍ぐらい)。
配列テーブル
void A<T>(T t)
{
Unsafe.As<Action<C>>(table[STD<C>.typeId])(t);
}
class C
{
public void B() { ... }
static C()
{
table[STD<C>.typeId] = (Action<C>)(x => x.B());
}
}
static object[] table = new object[];
static int typeId_next;
static class STD<T>
{
public static int typeId;
static STD()
{
typeId = Interlocked.Increment(ref typeId_next);
}
}
STD(動的)の仕組みは恐らくこんなものだろうと推測する。
is
void A(object o)
{
if (o is C c) c.B();
else if (o is D d) d.B();
else if ...
}
class C
{
public void B() { ... }
}
かなり遅い
dynamic
void A(dynamic d)
{
d.B();
}
class C
{
public void B() { ... }
}
これと等価。
リフレクション
void A(object o)
{
o.GetType().GetMethod("B").CreateDelegate<Action>(o).Invoke();
}
class C
{
public void B();
}
最強に遅い。