戻り値型推論は認められない

varキーワードによる型推論は、C#に追加された当初こそ否定的な意見も多かったですが、今となってはそれほど警戒すべきものでもないでしょう。 C#の型推論はローカル変数に対してしか使えず、型が見えにくくて困るとしても、せいぜいローカルな狭い範囲だけの話です。

むしろ、ローカル変数にしかvarを使えないことに不満がる声すらあります。 関数の戻り値や複合型のメンバーの型を推論してほしいという要望もよく出ます。 C#以外の言語だと、こういう、より多くの場面で型推論を認めているプログラミング言語もあります。 要するに、以下のような書き方を許してほしいという話です。

class Program
{
    static void Main()
    {
        var x = Add(M, N); // (int, int) に推論される
    }

    static var M = 50;  // int に推論される
    static var N = 100; // 同上
    static var Add<T>(T x, T y) => (x, y);
}

これに関しては、C#で認められることは今後もないでしょう。 推論が多段になることが原因です。 ローカル変数のvarと違って、以下のように、書いた人も書かれた場所も違うコードを多段に追いかける必要があります。

// 開発者 X がソースコード A.cs に書いたコード
class A
{
    public static var a = 1;
}

// 開発者 Y がソースコード B.cs に書いたコード
class B
{
    public static var b = A.a + 1;
}

// 開発者 Z がソースコード C.cs に書いたコード
class C
{
    public static var c = B.b * B.b;
}

多段になることで、いくつかの観点での問題を起こします。

  • 性能上の問題: 型推論に掛かる時間が読めなくなる
  • 影響範囲の問題:
    • 深くまでソースコードを追わないと実際の型がわからくなる
    • 自分の変更が他人に与える影響が読めなくなる。
    • コンパイル時のエラーの発生場所と、実際のエラーの原因になっている場所が遠く離れて直しにくくなる

性能上の問題

多段の型推論は、先ほど示したようなシンプルな例でも、書いた行数に比例した時間がかかる可能性があります。 さらに問題になるのは、書いた行数に対して指数的な時間がかかる場合すらあることです。 以下のようなコードでも、4行に対して、下手な実装だと2の4乗で16倍の時間がかかりかねません。

using System;

class A
{
    public static var a = 1;
    public static var b = Tuple.Create(a, a);
    public static var c = Tuple.Create(b, b);
    public static var d = Tuple.Create(c, c);
}

もっと複雑なコードであれば、ちゃんとした実装であっても指数時間を避けれない場合があり得ます。

実際、大幅に型推論を許しているプログラミング言語の中には、「型推論に時間が掛かりすぎているので推論を打ち切ります。コンパイルできませんでした」というエラー メッセージを出すものもあったりします。

C#はコンパイル時間にもかなり気を使っているプログラミング言語なので、この時間の増大はあまり許容できません。

影響範囲の問題

先ほどの3つの型A, B, Cの例で、Aの作者が軽い気持ちでちょこっと値を書き換えたとします。 例えば以下のような感じで、計算過程でint(左)の代わりにdouble(右)を使いたくなったとしましょう。

class A
{
    public static var a = 1;
}
class B
{
    public static var b = A.a + 1;
}
class C
{
    public static var c = B.b * B.b;
}
class A
{
    public static var a = 1.0;
}
class B
{
    public static var b = A.a + 1;
}
class C
{
    public static var c = B.b * B.b;
}

以下のようなことが起こっています。

  • Aの作者としてはメンバーaの型まで変えるつもりはなかったかもしれませんが、推論によって勝手にdoubleに変化しました
  • 芋づる式に、B.bC.cの型もdoubleに変わりました
  • Cを利用していた人が、intを前提としたコードを書いていて、コンパイル エラーを起こします
  • よくわからないけども、C.cの型が変わったらしいです。でもC.cのコード的には型が見えず、何のせいでこうなったのかわかりません
  • Cの作者に問い合わせます。Cの作者は身に覚えがありません。たぶんBのせい?
  • Bの作者(以下略
  • Aの作者が初めて問題に気づきます

高々3段、高々3人ですらそこそこ面倒な問題になるでしょう。 まして、実際のプログラムはもっと複雑です。 前節の性能上の問題と同様、段数に対して指数的に関係者が増える可能性もあります。

もう1例見てみましょう。 今度は、戻り値の型推論と匿名型が組み合わさっています。

using System;

class A
{
    public static var F(int x, int y)
        => new { x, y };
}
class B
{
    public static var G(int x) = A.F(x, x);
}

class Program
{
    static void Main()
    {
        var p = B.G(1);
        Console.WriteLine(p.x);
    }
}
using System;

class A
{
    public static var F(int x, int y)
        => new { X = x + y, Y = x - y };
    // ↑ new { x, y } だったのが new { X, Y } に変わった
}
class B
{
    public static var G(int x) => A.F(x, x);
}

class Program
{
    static void Main()
    {
        var p = B.G(1);
        Console.WriteLine(p.x); // ここでエラー
    }
}

こちらは、メンバー名の変更です。小文字のxyだったものが、大文字のXYに変わっています。 結果的に、利用側の、Mainの中のp.xの部分でコンパイル エラーになるでしょう。 このとき、エラー メッセージとしては、「出所はよくわからないけども、匿名型にメンバーxがありません」というようなメッセージしか出せなかったりします。

大幅に型推論を許しているプログラミング言語では、コードが複雑になると、ちょっとしたエラーで、読めた代物じゃない上に膨大な量のエラー メッセージが出ることがあります。

C#のように、大規模プロジェクトでも使われるプログラミング言語では、このような問題は、型推論で得られるメリットよりも大幅なデメリットになります。

対案: 左辺から右辺の型推論

この通り、C#では、ローカル変数以外のvarによる型推論は、おそらくずっと認められることはないでしょう。 代わりと言ってはなんですが、「逆向きの型推論」が入る可能性はあります。 すなわち、以下のような書き方です。

class A
{
    public A() { }
    public A(int n) { }
}

class Program
{
    A n = default; // defautl(A)
    A x = new();   // new A()
    A y = new(1);  // new A(1)
    A F(int n) => new(n); // new A(n)
}

new演算子の後ろや、default演算子の()を省略可能で、型は、フィールドの型や戻り値の型から推論されます。

型推論をしたいという要望の大部分が、「同じ型を2度ずつ書くのが無駄」という面倒さに対する嫌悪感なので、 この構文が入ればvarによる(右辺から左辺の)型推論の必要性は下がるはずです。 かつ、この向き(左辺から右辺)の型推論であれば、これまで問題視してきた多段になるような事態は避けられます。

この構文は、早ければC# 8あたり(2017~2018年頃?)で入りそうです。

(追記: A n = default; は C# 7.1 (default 式)で、 A x = new(); は C# 9.0 (ターゲットからの new 型推論)で入りました。)

更新履歴

ブログ