同人エロゲーを書くというのは、(実行結果はともかく)要するにソースコードに一切責任を持つ必要がないことを意味します。そこで、それは新しいプログラミングテクニックの実験場になります。
というわけで、そこで割と良かったC# 2.0ならではの書き方を1つ紹介しましょう。
念のために言えば、これは「このような書き方ができる」という説明であって、それが正しいという主張ではありません。むしろ、正しいか否かは疑って掛かるべきでしょう。これが有効であった場面には遭遇していますが、他の場面でも有効であるという保証はありません。
題材 §
C# 2.0を使います。
抽象クラスと継承を使ったポリモーフィズムを、抽象クラスと継承抜きで、ほぼ等価の結果を得られるコードに書き換えてみます。
それによってクラスの階層構造が消失し、コード量が減り、プログラム全体の構造がフラットになります。
最初のお題の提示 §
まずは、抽象クラスと継承を使ったコードを書いてみましょう。
型AのMethod1の呼び出しが2回ありますが、それぞれ違うメッセージを出力させます。
これがここで実現すべき機能性です。この機能性を実現する異なるコードを求めて行きます。
リスト1 §
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApplication1
{
abstract class A
{
public abstract void Method1();
}
class B : A
{
public override void Method1()
{
Console.WriteLine("おいらはBだ!");
}
}
class C : A
{
public override void Method1()
{
Console.WriteLine("わたしはCよ!");
}
}
class Program
{
static void Main(string[] args)
{
A instance;
instance = new B();
instance.Method1();
instance = new C();
instance.Method1();
}
}
}
デリゲートとクローンで書き直す §
抽象メソッドをデリゲートに置き換えてみましょう。
デリゲート型フィールドの内容は、コンストラクタで引数として受け取って代入させます。
これで、Method1の振る舞いは外部からカスタマイズ可能になりました。
もう継承は必要ないので、クラスB/Cは消滅します。
ここで、クラスAのインスタンスの作成は、リスト1での「継承と実装」と同じ役割を持ちます。インスタンスの作成は新しくAsというクラスを作ってその中で行っていますが、これはどこに書いても構いません。
では、リスト1でのインスタンス化と同じ役割を持つのは何かと言えば、IClonableインターフェースを実装したCloneメソッドです。(プロトタイプベースOOPでのオブジェクト生成っぽい)
これでほぼ等価の結果が得られ、かつ、クラスの数が減り、継承や抽象メソッドは消滅しました。
リスト2 §
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApplication2
{
delegate void MyMethodInvoker();
class A : ICloneable
{
public readonly MyMethodInvoker Method1;
public A(MyMethodInvoker method1)
{
this.Method1 = method1;
}
public object Clone()
{
return new A(Method1);
}
}
static class As
{
public static A b = new A(
delegate()
{
Console.WriteLine("おいらはBだ!");
});
public static A c = new A(
delegate()
{
Console.WriteLine("わたしはCよ!");
});
}
class Program
{
static void Main(string[] args)
{
A instance;
instance = (A)As.b.Clone();
instance.Method1();
instance = (A)As.c.Clone();
instance.Method1();
}
}
}
このケースではクローンはいらない §
リスト2のケースに限って言えば、インスタンスを区別する意味がないので、クローンは取り去っても結果は変わりません。
実は、この程度の簡単なコードで済むケースが結構ありますので、クローン機能を取っ払ったシンプルなソースもお見せしましょう。
これで済めば、継承に準じる「インターフェースの実装」というコードも消失します。
リスト3 §
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApplication3
{
delegate void MyMethodInvoker();
class A
{
public readonly MyMethodInvoker Method1;
public A(MyMethodInvoker method1)
{
this.Method1 = method1;
}
}
static class As
{
public static A b = new A(
delegate()
{
Console.WriteLine("おいらはBだ!");
});
public static A c = new A(
delegate()
{
Console.WriteLine("わたしはCよ!");
});
}
class Program
{
static void Main(string[] args)
{
A instance;
instance = As.b;
instance.Method1();
instance = As.c;
instance.Method1();
}
}
}
インスタンスを区別する必要性を試そう §
ここまでは題材があまりに簡単すぎたので、少し意地悪しましょう。
リスト1まで戻って、これを拡張します。
具体的には、クラスCにのみフィールドを追加します。
リスト2/3が成立するのは、あくまでクラスB/Cに追加フィールドやメソッドが無いからであって、それが追加されれば破綻するはずです。どうしてもフィールドを追加したければクラスAに追加するしかありませんが、そうするとフィールド不要の「おいらはBだ!」までフィールドを持ってしまいます。
というわけで、継承を使った意地悪コードがリスト4です。
リスト4 §
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApplication4
{
abstract class A
{
public abstract void Method1();
public abstract void Method2();
}
class B : A
{
public override void Method1()
{
Console.WriteLine("おいらはBだ!");
}
public override void Method2()
{
Console.WriteLine("ほんとにBだぜ!");
}
}
class C : A
{
private int a;
public override void Method1()
{
Console.WriteLine("わたしはCよ!");
a = 17;
}
public override void Method2()
{
Console.WriteLine("おはだぴちぴち{0}歳よ!",a);
}
}
class Program
{
static void Main(string[] args)
{
A instance;
instance = new B();
instance.Method1();
instance.Method2();
instance = new C();
instance.Method1();
instance.Method2();
}
}
}
書けないコード §
いじわるを受けて、リスト4に相当するコードをリスト3のやり方で書き下ろしてみましょう。すると、以下のようになり、破綻して書けません。
リスト5 §
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApplication5
{
delegate void MyMethodInvoker();
class A
{
public readonly MyMethodInvoker Method1;
public readonly MyMethodInvoker Method2;
public A(MyMethodInvoker method1, MyMethodInvoker method2)
{
this.Method1 = method1;
this.Method2 = method2;
}
}
static class As
{
public static A b = new A(
delegate()
{
Console.WriteLine("おいらはBだ!");
},
delegate()
{
Console.WriteLine("ほんとにBだぜ!");
});
public static A c = new A(
// int a; どこにも書けない!
delegate()
{
Console.WriteLine("わたしはCよ!");
a = 17; // aは宣言されていないからエラーになる!
},
delegate()
{
// aは宣言されていないからエラーになる!
Console.WriteLine("おはだぴちぴち{0}歳よ!", a);
});
}
class Program
{
static void Main(string[] args)
{
A instance;
instance = As.b;
instance.Method1();
instance.Method2();
instance = As.c;
instance.Method1();
instance.Method2();
}
}
}
ファクトリメソッドによる解決 §
しかし、この程度なら容易に解決できます。
インスタンスを作成するメソッドを新規に書き下ろせばよいのです。これを、ファクトリメソッドと呼びましょう。
匿名メソッドは上位スコープに属するため、ファクトリメソッドの中に書かれた匿名メソッドは、ファクトリメソッドのローカル変数を参照できます。
更に、ファクトリメソッドの導入は、クローン機能不要という特徴ももたらします。つまり、継承に準じる存在であるインターフェースの実装も不要にします。
リスト6 §
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApplication6
{
delegate void MyMethodInvoker();
class A
{
public readonly MyMethodInvoker Method1;
public readonly MyMethodInvoker Method2;
public A(MyMethodInvoker method1, MyMethodInvoker method2)
{
this.Method1 = method1;
this.Method2 = method2;
}
}
static class As
{
public static A CreateB()
{
return new A(
delegate()
{
Console.WriteLine("おいらはBだ!");
},
delegate()
{
Console.WriteLine("ほんとにBだぜ!");
});
}
public static A CreateC()
{
int a = 0; // コンパイラに怒られるので明示的に初期化
return new A(
delegate()
{
Console.WriteLine("わたしはCよ!");
a = 17;
},
delegate()
{
Console.WriteLine("おはだぴちぴち{0}歳よ!", a);
});
}
}
class Program
{
static void Main(string[] args)
{
A instance;
instance = As.CreateB();
instance.Method1();
instance.Method2();
instance = As.CreateC();
instance.Method1();
instance.Method2();
}
}
}
リスト6の補足 §
リスト6では、変数aのスコープが極めて制限されているという特徴もあります。これは、ファクトリメソッドの中でしか有効ではありません。外部からこれを読み書き可能にするために、匿名メソッドでアクセサメソッドを書き足せば……と思うかもしれませんが、アクセサメソッドを書くためにはクラスの定義にそれを追加しなければなりません。それは本来の趣旨に反するのでできません。
つまり、クラスと継承を使って実現したケースと比較して、自由度が著しく制限されています。これをメリットと見るか、デメリットと見るかは微妙なところです。しかし、複雑で込み入ったコードを書くことが抑止される……という意味でのメリットはあるような気がします。
このようなコードの書き方は、「それでは書けない複雑な問題を解決するために継承を使う可能性」を留保しつつ、使える場面でどんどん使ってしまう……というのも悪くないかなという気がします。
気がするだけで、それが正しいか否かはまだ分かっていませんが。
リスト6はC# 2.0ならでは? §
もしかしたら、リスト6はクロージャをサポートした言語では等価のコードを書けないかもしれません。
C# 2.0の匿名メソッドはクロージャではなく単なる匿名メソッドだからです。
と言いつつ自信がありません。Ruby等で等価のコードを書いて確かめれば良いのですけどね。今のところ、C# 2.0を満足に使いこなす方が先で余裕がありません。
2007/09/24追記 この説明は嘘です。C# 2.0はクロージャをサポートした言語です。匿名メソッドがキャプチャする変数が同じか違うかの問題がごちゃごちゃになった誤認です。
フラットな構造になるメリットとは? §
定義が階層化せずフラットになると、コードに手を入れるのがずっと楽になるのですよね。把握しやすくなるし、定義の依存関係の修正が玉突きで発生しにくいので。