この記事は土屋つかさアドベントカレンダーの3日目です。嘘です。今年はACに一個も参加してなかいので言いたくなっただけです。
今回はC#でメソッドディスパッチをする方法の話。土屋がコード書いてる時に必要になったんですが、ググッても上手く見つからなかったので記事にしておきます。
値に応じて処理を分岐させる
指定された値に応じて異なる処理をしたいとします。数個ならswitch-caseで値ごとの処理を直接書いてしまってもいいかもしれません。
switch(index){ case 0: //処理A break; case 1: //処理B break; case 2: //処理C break; }
一見分かりやすいですが、個々の処理でのコードが増えたり、処理の数自体が増えた時、swith文の中が肥大化してしまいます。各処理を別の人が実装するような場合、一つのswitch文を皆で手を入れるのは避けたい所です。
またC#のswitch文は全体で一つのスコープになっていて、caseブロック内で同名のローカル変数が定義出来ません。ちょっとした落とし穴ですが、気を付けたいところです。
処理をメソッドに分ける
ではどうするのか。異なる処理を同一のスコープに置かないという原則*1に則り、個々の処理をメソッドに切り出しましょう(便宜上各メソッドの引数の数/型は同一とする)。
switch(index){ case 0: FuncA(arg1, arg2); //処理A break; case 1: FuncB(arg1, arg2); //処理B break; case 2: FuncB(arg1, arg2); //処理C break; }
さっきよりはスッキリしました。処理の数が数個であれば、これでもいいように思えます。しかし今度は、処理の数が十数個~数十個になった時に、このcase文をずらずら書き連ねるのかという問題に直面します。
少し脱線しますが、「そもそもそんな多数の処理分岐コードを書く事態に陥る事がおかしい」という指摘もあるでしょうし、それもある意味正しいとも思います。ゲームプログラミングは並列な(≒階層構造にならない)2桁~3桁の状態が存在しうる得意な分野なのかもしれないし、あるいは何か見落としがあるのかもしれません。
メソッドディスパッチ1:デリゲート型
閑話休題*2。今回は、この問題をメソッドディスパッチで対応してみます。(C#ではなく)C++言語では、こういう時に関数ポインタの配列に各関数への参照を格納しておいて、その配列に値でアクセスして関数を呼び出すという事をしていました。このような処理を「メソッドディスパッチ」と呼びます。
C#には関数ポインタが無いので*3、代わりにデリゲート型を使います。デリゲート型のインスタンスは、定義したインターフェイスを持つメソッドへの参照を保持出来ます。
サンプルコードは以下になります。説明が目的なので実用性はありません。
public class ClassA { delegate void Dispacher(int x, int y); void FuncA(int x, int y){} void FuncB(int x, int y){} void FuncC(int x, int y){} public ClassA() { Dispacher[] dispatcherA = { FuncA, FuncB, FuncC }; dispatcherA[0](0,0); //FuncA(0,0)呼び出し } }
まずデリゲート型を宣言します。これはクラス型の宣言と同じスコープで記述します。
//①デリゲート型を宣言 delegate void Dispacher(int x, int y);
ここでは、Dispacherというデリゲート型を宣言しています。Dispacherはintを二つ引数に取り、戻り値を持たない(=voidの)メソッドへの参照を格納出来る型です。宣言した型はインスタンス化して利用できます。
「デリゲート型を宣言し、そのインスタンスを使用する」というのが分かり難いかもしれませんが、クラスでの「クラス型を宣言し、そのインスタンスを使用する」と同じです。
次にDispacher型の配列dispatcherAをインスタンス化します。
//②デリゲート型インスタンスを定義 Dispacher[] dispatcherA = { FuncA, FuncB, FuncC };
配列に格納しているのはクラスメソッドFuncA/FuncB/FuncCです。いずれもインターフェイスが同じ(intを2個引数に取り、戻り値を持たない(=void))のでDispacher型に格納できます。
あとは、格納されているメソッドをインデックスで指定して呼び出せます。
//③デリゲートの呼び出し dispatcherA[0](0,0); //FuncA(0,0)呼び出し
先程と同じ事がswitch-case無しで、実現出来ている事が分かるかと思います。コードがとてもシンプルになりました。今後メソッドが増えた場合も、配列にメソッド名を追加するだけなのでメンテもしやすいです(case分を一個増やすのとどちらがよりメンテしやすいかは、人によって評価が分かれるかもしれませんが)。これがデリゲートによるメソッドディスパッチです。
補足
デリゲート型の変数が、new無しにインスタンス化されるのが奇妙に見えるかもしれません。これは、実際にはnewしているのですが、記述が煩雑なため省略記法が用意されているのです。
//delegate void Dispacher(int x, int y); Dispacher dispacherC = new Dispacher(FuncA); //デリゲート型のDispacherのインスタンスdispacherCを生成 Dispacher dispacherD = FuncA; //上記のシンタックスシュガー
元の記法も分かりやすいとは言えないので、省略記法を使うのが良いでしょう。
メソッドディスパッチ2:Action/Funcによる簡易表現
先程のデリゲート型宣言をもう一度見てみます。
delegate void Dispacher(int x, int y);
Dispacher型の宣言に対し、voidやintが"Dispacher"の左右に記述されるのがもやもやしませんか(土屋はする)。また、クラススコープで一度宣言する必要があるのが煩雑な印象があります。
そこで、C#ではそこで、C#では組み込みのAction型/Func型を使います。その場合、先の②③は以下の様になります。①のdelegateによる型宣言は必要ありません*4。
Action<int, int>[] dispatcherB = { FuncA, FuncB, FuncC }; dispatcherB[0](0, 0); //FuncA(0,0)呼び出し
Action<>は、戻り値の無いメソッドのデリゲート型を宣言できる物で、ジェネリクスで引数の型を指定します*5。戻り値のあるメソッドの場合はFunc<>を使用します。
ちなみにAction<>は単なるヘルパーで、内部で以下の様なデリゲート型の宣言をしてるだけです。
public delegate void Action<Tin1, Tin2>(Tin1 arg1, Tin2 arg2);
この宣言が既に行われているので、どこでもAction/Funcを書けるわけですね。
メソッドディスパッチ3:static対応
デリゲート型は内部で「クラスインスタンスへの参照」と「メソッドへのアドレス」の二つの情報を格納しています。そのため、クラスが生成した後で無いと、メソッドを格納する事ができません。
今回の様にディスパッチするメソッドが3個であれば構わないかも知れませんが、これが十数個~数十個になった場合、クラスを生成するたびにデリゲート型配列の初期化処理が発生するのは避けたい所です。
その場合、登録するメソッドをstaticのみにすると、それを格納するデリゲート配列も以下のようにクラスフィールドとして定義出来ます*6。
public class ClassB { readonly Action<int, int>[] dispatcherB = { FuncA, FuncB, FuncC }; static void FuncA(int x, int y) { } static void FuncB(int x, int y) { } static void FuncC(int x, int y) { } public ClassB() { dispatcherB[0](0, 0); //FuncA(0,0)呼び出し } }
終わりに
無事C#でメソッドディスパッチを実現できました。デリゲートが関数ポインタよりは処理が重いであろう事は想像に難くないですが*7、土屋の用途としてはこれで十分です。
C#でデリゲートと言うと、「OnClickイベントにメソッドを追加してコールバックさせて~」的な用途で解説される事が多いかと思います。それもあって、土屋はC++の関数ポインタのようには使えない物だと勝手に思い込んでいました。
実際、上記のように書けると気付いたのは、メソッドディスパッチを実装しようと相当試行錯誤した後でして、「まだまだC#全然わかってないんだなー俺は……」と反省し、記事に起こした次第でありました。
宣伝「Unityシェーダープログラミングの教科書シリーズ」
土屋は同人誌「Unityシェーダープログラミングの教科書」シリーズを書いています。現在以下の5冊が出ています。
- Unityシェーダープログラミングの教科書 ShaderLab言語解説編
- Unityシェーダープログラミングの教科書2【反射モデル&テクスチャマップ編】
- Unityシェーダープログラミングの教科書3 ライティング&GI(大域照明)解説編
- Unityシェーダープログラミングの教科書4 SRP[1]UniversalRP/Litシェーダー解説編
- Unityシェーダープログラミングの教科書5 SRP[2]UniversalRP URP拡張カメラ/HDR/ポストプロセス編
これらの同人誌は、現在BOOTHの下記ストアにてPDF版を有料頒布しています。
Unityのシェーダープログラミング関連について、世界で最も詳細な本だと自負しています。リンク先にサンプルページがあるので、是非御覧になってください。