C# で 劣化 Variant を書いてみた
VB2005でのお話。そして現在調査中なのですが、できればいいなーというレベルで関数の戻り値のみが異なるようなオーバーロードをやってみました。
Private Function GetConfig(ByVal keyword As String) As String '(省略) End Function Private Function GetConfig(ByVal keyword As String) As Date '(省略) End Functionこのように関数の戻り値を異なる値にしてオーバーロードしようとすると「戻り値の型のみが異なるため、お互いをオーバーロードすることはできません」というエラーが発生します。
Private Function GetConfig(ByVal keyword As String) As String '(省略) End Function Private Function GetConfig(ByVal keyword As Date) As Date '(省略) End Functionのように引数の数か型を変えてあげれば、オーバーロードできます。引数を同一にした状態で戻り値のみ異なる型にしてオーバーロードを行う方法はあるのでしょうか。
他の言語だったらOKって訳じゃないですよね。
CLS Rule 37 と CLS Rule 38 絡みの話はあちらのコメント欄に書いたので置いておくとして,C# Generics と implicit operator を利用して劣化 Variant みたいなのを作ってみました.
public class LazyVariant<T1, T2> { private readonly Func<T1> _func1; private readonly Func<T2> _func2; private T1 _memoedValue1; private T2 _memoedValue2; private bool _memoed1 = false; private bool _memoed2 = false; public LazyVariant(Func<T1> func1, Func<T2> func2) { this._func1 = func1; this._func2 = func2; } public static implicit operator T1(LazyVariant<T1, T2> variant) { //TODO: use Interlocked API if (!variant._memoed1) { variant._memoedValue1 = variant._func1(); variant._memoed1 = true; } return variant._memoedValue1; } public static implicit operator T2(LazyVariant<T1, T2> variant) { //TODO: use Interlocked API if (!variant._memoed2) { variant._memoedValue2 = variant._func2(); variant._memoed2 = true; } return variant._memoedValue2; } }
これはおおざっぱに言えば,「T1 または T2 型」みたいな型で,F# や Nemerle の variant を意識しています.Visual Basic や COM の variant とは大違いなので注意.詳しくは id:akiramei:20050323:p2 や id:akiramei:20050324:p1,F# 本などを参照してくださいませ.(といいつつ,variant は「どれかひとつが入っている箱」なイメージなので,別の名前の方がよいかもしれず.LazyChoice とかかなぁ.Achiral に収録される日が来るとしたら,多分名前変えます.)
LazyVariant は,コンストラクタでデリデートを受け取ります.T1,T2 それぞれへの暗黙の変換が定義されていて,デリゲートの評価は implicit conversion の呼び出しをトリガーとしています.つまり遅延評価されます.また,評価結果はメモ化されます.
例えば以下のように使います.メソッドをオーバーロードする代わりに,戻り値の型を「string または DateTime 型」という感じに多重化しています.型の方を多重化することで,メソッド自体はひとつで済むわけです.
public class MyDB { private string GetConfigAsString(string keyword) { return DateTime.Now.ToString() + ", " + keyword; } private DateTime GetConfigAsDateTime(string keyword) { return DateTime.Now; } public LazyVariant<string, DateTime> GetConfig(string keyword) { return new LazyVariant<string, DateTime> ( () => GetConfigAsString(keyword), () => GetConfigAsDateTime(keyword) ); } }
得られた結果は string を受け取るメソッドにも,DateTime を受け取るメソッドにも同じように使えます.
static class Program { static void Foo(string str){} static void Bar(DateTime datetime){} static void Main(string[] args) { Test1(); Test2(); } static void Test1() { var mydb = new MyDB(); Foo(mydb.GetConfig("hello")); // string を受け取る場所に使える Bar(mydb.GetConfig("hello")); // DateTime を受け取る場所にも使える } static void Test2() { var mydb = new MyDB(); var config = mydb.GetConfig("hello"); Foo(config); // string 版が遅延評価される Bar(config); // DateTime 版が遅延評価される Bar(config); // メモ化された値が使われる } }
C++ や Java 由来の言語/クラスライブラリは,メソッドオーバーロードを多用することである種の「シンタックス的分かりやすさ」を実現しています.では (C++ や Java,C# 的な意味での) メソッドオーバーロードを完全に廃止してしまったら本当に困るでしょうか? 実際は,似たようなシンタックスを実現するだけなら別のアプローチでも十分に可能ということはないでしょうか? その辺の意識しながら,F# や Haskell の文法やプログラミングスタイルを見るようになってから,私はへーと思う機会が増えました.