読者です 読者をやめる 読者になる 読者になる

戻り値の型の推論

.NET

お.これは懐かしい話題.

前から書いているが

Dictionary<List<Abrakadabra>, IEnumerable<Abrakadabra>> dic = new Dictionary<List<Abrakadabra>, IEnumerable<Abrakadabra>>();

Dictionary<List<Abrakadabra>, IEnumerable<Abrakadabra>> dic = new();

で書きたいんだよな。って、C#3.0ではvarキーワードと型推論が使えるから短く書けるのかな? (まだあまり使ってないのがばればれ)

仰るとおり,短さで言えば以下のように書けますね.

var dic = new Dictionary<List<Abrakadabra>, IEnumerable<Abrakadabra>>();

さて,話を戻して「左辺に合うように右辺の new を推論して欲しい」ですが,確かに同様の要望は以前からちらほら見かけました.

ただこれ,単なるマクロ (シンタックスシュガー) と見る人と,新しい推論ルール (逆順の推論; ターゲットタイプを固定して,ソースタイプをそれに近づけるもの; いわゆる暗黙の型変換的な推論) の追加と見る人でだいぶ印象が違う気がします.私は後者なので,この推論ルールを認めるなら,以下のような記法も可能ということになりますけど本当にいいんですか? とつい考えちゃうと.

static StringBuilder ReadAll(FileStream fs, StringBuilder sb) { ... }

// メソッドシグネチャから引数の型は定まるので,型名を省略可能 ???
StringBuilder result = ReadAll(new ("foo.txt", FileMode.OpenOrCreate), new ());

マクロ派の人は,「同じ型のインスタンスを宣言と同時に生成する時」の専用構文と考えていて,こういうケースは想定外かもしれませんけどね.
ちなみに C++ では,ユーザ定義の暗黙の型変換で template を許すため,実際次のようなことが可能です.

namespace ImplicitNew
{
    class ImplicitNewT {
        int dummy;
    public:
        template <typename T>
        operator T()
        {
            return T();
        }
    } _;
}
void foo(int x, long y, char c, void* ptr) { }

int main()
{
    using namespace ImplicitNew;

    // 順に,int, long, char, void* を生成したことになる
    foo(_, _, _, _);
    return 0;
}

これなんかはまさに,ターゲットタイプに合わせるように推論を行っているわけです.得られる動作自体は,最初に話題に上った new での型の省略に近いんではなかろーかと.
この手の逆順の推論ですが,C# ではどんなものがあるかというと,私にすぐに思い付くのはこれぐらいです.

  • 暗黙の型変換 (ユーザ定義可能.Generic Method にはできない)
  • Method Group からデリゲート型への変換
  • Anonymous Methods からデリゲート型への変換
  • ラムダ式からデリゲート型への変換
  • ラムダ式から Expression 型への変換

ここに型総称性と型推論が加わるややこしさ,C# 2.0 から 3.0 の間にあったような推論ルールの変化などは,案外と知られていないように思います.
ところで Java Generics では,

  • Generic Method の戻り値の型の推論

も可能なように見えます.これは C++ 的おもしろさがあります.

以下,例に挙げられていた Java コード.

public static <E> ArrayList<E> newArrayList() {
    return new ArrayList<E>();
}
List<String> listOfStrings = CollectionsUtil.newArrayList();

ここで,CollectionsUtil.newArrayList の戻り値の型は ArrayList<E> ですが,左辺を固定して右辺を動かす単一化により,E は String と決定されているように見えます.見えますが,Java Generics はあまり詳しくないので,詳しくは仕様書をあたって下さいませ.Eclipse 付属コンパイラの拡張機能*1? とか,実は Type Erasure 故の技だったりするのかも? と不安になる程度に分かってないです.
さて,C# Generics で,同様の推論が可能かどうかについてですが,まず Visual C# 2008 に付属する C# コンパイラでは不可能です.じゃあ仕様的に不可能かというと,実はこのあたり結構ごたごたしていてよく分からないんですよね.

上に挙げたリンクで問題にされているのは,主に,(左辺の) デリゲートの戻り値型から (右辺の) ジェネリックメソッドの戻り値の型を推論するというケースです.このケースの中で,言語仕様では出来ると書かれているものが,Visual C# 2008 の C# コンパイラでは出来ないことがあるけどなんで?というお話です.ちなみに結論は,仕様も実装も Errata があったとのこと.
いずれ出版済みの言語仕様も修正するそうです.結局この辺の事情がどなっているのか・今後どうなるかは私にもよく分かりません.新しい実装は C# 4.0 で入りそうなので,もしかしたら上記 Java のような型推論が可能になっているんじゃないかと密かに期待しています.

追記

上に紹介した Java型推論について,こういう記事を発見.うーむ,実装上の抜け穴っすか.

しかし、当座はGoogle Collections Libraryの別の機能を利用すれば、この冗長性を回避できます。

Map<String, List<String>> mapOfLists =
   Maps.newHashMap();

Maps.newHashMap()は、Google Collections Library内のMapsというユーティリティクラスで定義されている静的メソッドです。これはJavaにおけるジェネリックスの実装上の抜け穴を突いたもので、補助クリエータnewHashMapに型シグニチャを渡すことは実際には必要ないのです。Javaのすべての標準コレクションとGoogleの新しいコレクションのすべてについて、対応する同様の補助クリエータが用意されています。この単純なアイデアだけでコードの可読性がこれほど向上するとは驚きです。

追記2

だいたい把握.詳しくはコメント欄参照のこと.