24

例として、さまざまなタイプの要素、さまざまな要素タイプを評価する関数、および要素を格納して関数を実行するためのコンテキストを備えた電卓のようなものを使用してみましょう。インターフェイスは次のようなものです。

public interface IElement {
}
public interface IChildElement : IElement {
    double Score { get; }
}
public interface IGrandchildElement : IChildElement {
    int Rank { get; }
}

public interface IFunction<Tout, in Tin> where Tin : IElement {
    Tout Evaluate(Tin x, Tin y);
}

public interface IContext<Tin> where Tin : IElement {
    Tout Evaluate<Tout>(string x, string y, IFunction<Tout, Tin> eval);
}

関数は任意の型を返す場合があることに注意してください。ダミーの実装は次のとおりです。ここでは、 と の両方に使用できる関数が呼び出され、Foo両方の場合に戻ります。IChildElementIGrandchildElementdouble

public class ChildElement : IChildElement {
    public double Score { get; internal set; }
}
public class GrandchildElement : ChildElement, IGrandchildElement {
    public int Rank { get; internal set; }
}

public class Foo : IFunction<double, IChildElement>, IFunction<double, IGrandchildElement> {
    public double Evaluate(IChildElement x, IChildElement y) {
        return x.Score / y.Score;
    }
    public double Evaluate(IGrandchildElement x, IGrandchildElement y) {
        return x.Score * x.Rank / y.Score / y.Rank;
    }
}

public class Context<T> : IContext<T> where T : IElement {
    protected Dictionary<string, T> Results { get; set; }

    public Context() {
        this.Results = new Dictionary<string, T>();
    }

    public void AddElement(string key, T e) {
        this.Results[key] = e;
    }
    public Tout Evaluate<Tout>(string x, string y, IFunction<Tout, T> eval) {
        return eval.Evaluate(this.Results[x], this.Results[y]);
    }
}

いくつかのサンプル実行:

Context<IChildElement> cont = new Context<IChildElement>();
cont.AddElement("x", new ChildElement() { Score = 1.0 });
cont.AddElement("y", new ChildElement() { Score = 2.0 });
Foo f = new Foo();
double res1 = cont.Evaluate("x", "y", f); // This does not compile
double res2 = cont.Evaluate<double>("x", "y", f); // This does

ご覧のとおり、私の問題は、への呼び出しをハードタイプする必要があるように見えることContext.Evaluateです。そうしないと、コンパイラは引数の型を推測できないと言います。Fooどちらの場合も関数が を返すため、これは特に印象的doubleです。

Foo実装のみの場合IFunction<double, IChildElement>、またはIFunction<double, IGrandchildElement>この問題はありません。しかし、そうです。

わかりません。つまり、 を追加し<double>ても と を区別しませんIFunction<double, IGrandchildElement>IFunction<double, IChildElement>どちらも を返すからdoubleです。私が理解している限りでは、コンパイラに区別するための追加情報は提供されません。

いずれにせよ、へのすべての呼び出しをハードタイプする必要を避ける方法はありますTask.Evaluateか? 現実の世界では、私はいくつかの機能を持っているので、それを回避できるのは素晴らしいことです.

<double>追加がコンパイラに役立つ理由の適切な説明に対する報奨金これは、いわばコンパイラが怠惰すぎるという問題ですか?

古い更新: デリゲートの使用

IFunctionオプションは、 s の代わりにデリゲートを使用することIContext.Evaluateです。

public interface IContext<Tin> where Tin : IElement {
    Tout Evaluate<Tout>(string x, string y, Func<Tin, Tin, Tout> eval);
}
public class Context<T> : IContext<T> where T : IElement {
    // ...
    public Tout Evaluate<Tout>(string x, string y, Func<T, T, Tout> eval) {
        return eval(this.Results[x], this.Results[y]);
    }
}

そうすることで、<double>呼び出すときにハードタイプする必要がなくなりますIContext.Evaluate:

Foo f = new Foo();
double res1 = cont.Evaluate("x", "y", f.Evaluate); // This does compile now
double res2 = cont.Evaluate<double>("x", "y", f.Evaluate); // This still compiles

したがって、ここでコンパイラは期待どおりに動作します。ハードタイプする必要はありませんが、オブジェクト自体IFunction.Evaluateの代わりに使用するという事実は好きではありません。IFunction

4

3 に答える 3

35

(デリゲート版はまだ読んでいません。この回答はもう十分長いと思いました...)

コードをかなり単純化することから始めましょう。これは問題を示す短いが完全な例ですが、関係のないものはすべて削除しています。IFunctionまた、より通常の規則に一致させるために、型引数の順序も変更しました(例: Func<T, TResult>)。

// We could even simplify further to only have IElement and IChildElement...
public interface IElement {}
public interface IChildElement : IElement {}
public interface IGrandchildElement : IChildElement {}

public interface IFunction<in T, TResult> where T : IElement
{
    TResult Evaluate(T x);
}

public class Foo : IFunction<IChildElement, double>,
                   IFunction<IGrandchildElement, double>
{
    public double Evaluate(IChildElement x) { return 0; }
    public double Evaluate(IGrandchildElement x) { return 1; }
}

class Test
{
    static TResult Evaluate<TResult>(IFunction<IChildElement, TResult> function)
    {
        return function.Evaluate(null);
    }

    static void Main()
    {
        Foo f = new Foo();
        double res1 = Evaluate(f);
        double res2 = Evaluate<double>(f);
    }
}

これにはまだ同じ問題があります。

Test.cs(27,23): error CS0411: The type arguments for method
        'Test.Evaluate<TResult>(IFunction<IChildElement,TResult>)' cannot be
        inferred from the usage. Try specifying the type arguments explicitly.

さて、なぜそれが起こるのかについては...他の人が言ったように、問題は型推論です。C# の型推論メカニズム (C# 3 以降) は非常に優れていますが、それほど強力ではありません。

C# 5 言語仕様を参照して、メソッド呼び出し部分で何が起こるかを見てみましょう。

7.6.5.1 (メソッド呼び出し) が重要な部分です。最初のステップは次のとおりです。

メソッド呼び出しの候補メソッドのセットが構築されます。メソッド グループ M に関連付けられたメソッド F ごとに、次のようになります。

  • F が非ジェネリックの場合、F は次の場合に候補になります。
    • M には型引数リストがなく、
    • F は A に関して適用可能です (§7.5.3.1)。
  • F がジェネリックで、M に型引数リストがない場合、F は次の場合に候補になります。
    • 型推論 (§7.5.2) が成功し、呼び出しの型引数のリストが推論されます。
    • 推論された型引数が対応するメソッドの型パラメーターに置き換えられると、F のパラメーター リストで構築されたすべての型が制約を満たし (§4.4.4)、F のパラメーター リストは A に関して適用可能になります (§7.5.3.1)。 )。
  • F がジェネリックで、M に型引数リストが含まれる場合、F は次の場合に候補になります。
    • F には、型引数リストで指定されたのと同じ数のメソッド型パラメーターがあり、
    • 型引数が対応するメソッドの型パラメーターに置き換えられると、F のパラメーター リストで構築されたすべての型が制約を満たし (§4.4.4)、F のパラメーター リストは A に関して適用可能になります (§7.5.3.1)。 .

ここで、メソッド グループMは単一のメソッド ( ) を持つセットTest.Evaluateです。幸いなことに、セクション 7.4 (メンバー ルックアップ) は単純です。したがってF、考慮すべき方法は 1 つだけです。

これジェネリックであり、M には型引数リストがないため、セクション 7.5.2 - 型推論でまっすぐに終わります。引数リストがある場合、これが完全にスキップされ、上記の 3 番目の主要な箇条書きが満たされることに注意してください。これが、呼び出しEvaluate<double>(f)成功する理由です。

これで、問題が型推論にあることがかなりよくわかりました。それに飛び込みましょう。(ここがややこしいところです、恐れ入ります。)

7.5.2 自体は、型推論が段階的に発生するという事実を含め、ほとんど単なる説明です。

呼び出そうとしている汎用メソッドは、次のように記述されています。

Tr M<X1...Xn>(T1 x1 ... Tm xm)

メソッド呼び出しは次のように記述されます。

M(E1 ... Em)

したがって、私たちの場合、次のようになります。

  • T rはX 1TResultと同じです。
  • T1は_IFunction<IChildElement, TResult>
  • x 1function、値パラメータ
  • E 1f、タイプのFoo

それでは、残りの型推論にそれを適用してみましょう...

7.5.2.1 最初のフェーズ
各メソッド引数 E iについて:

  • E iが無名関数の場合、E iから T iへの明示的なパラメーター型の推論 (§7.5.2.7) が行われます。
  • それ以外の場合、E iの型が U で x iが値パラメーターの場合、U から T iへの下限の推論が行われます。
  • それ以外の場合、E iの型が U で x iが ref または out パラメーターの場合、U から T iへの正確な推論が行われます。
  • それ以外の場合、この引数の推論は行われません。

ここで 2 番目の箇条書きが関連します。E 1は無名関数ではなく、E 1は typeFooを持ち、x 1は値パラメーターです。Fooしたがって、から T 1までの下限の推論になります。その下限の推論は、7.5.2.9 で説明されています。ここで重要な部分は次のとおりです。

それ以外の場合、セット U 1 ...U kおよび V 1 ...V kは、次のケースのいずれかが適用されるかどうかを確認することによって決定されます。

  • [...]
  • V は、構築されたクラス、構造体、インターフェイス、またはデリゲート型 C<V 1 ...V k > であり、一意の型 C<U 1 ...U k > が存在し、U (または、U が型パラメーターの場合) 、その有効な基本クラスまたはその有効なインターフェイス セットの任意のメンバー) は、C<U 1 ...U k > と同一であるか、(直接的または間接的に) 継承するか、または (直接的または間接的に) C<U 1 ...U k > を実装します。(「一意性」の制限とは、インターフェイス C<T>{} クラス U: C<X>, C<Y>{} の場合、U から C<T> への推論時に推論が行われないことを意味します X または Y の可能性があります。)

このパートでUFoo、 は 、Vは ですIFunction<IChildElement, TResult>。ただし、と の両方をFoo実装します。したがって、どちらの場合U 2 asになりますが、この節は満たされていません。IFunction<IChildElement, double>IFunction<IGrandchildelement, double>double

これで私を驚かせることの 1 つは、これが反変であることに依存していないことです。パーツを削除すると、同じ問題が発生します。からへの変換がないため、その場合は機能すると予想していました。その部分はコンパイラのバグである可能性がありますが、仕様を読み違えている可能性が高くなります。しかし、それが実際に与えられた場合、それは無関係です - の反変性のために、そのような変換があるので、両方のインターフェースは本当に重要です。TIFunction<in T, TResult>inIFunction<IGrandchildElement, TResult>IFunction<IChildElement, TResult>T

とにかく、これは、実際にはこの引数から型を推論することにはならないことを意味します!

以上が第 1 フェーズの全体です。

第 2 フェーズは次のように記述されます。

7.5.2.2 第二段階

第 2 段階は次のように進行します。

  • どの Xj にも依存しない (§7.5.2.5) 固定されていない型変数 Xi はすべて固定されています (§7.5.2.10)。
  • そのような型変数が存在しない場合、次のすべてが成り立つすべての固定されていない型変数 Xi が固定されます。
    • Xi に依存する型変数 Xj が少なくとも 1 つある
    • Xi には空でない範囲のセットがあります
  • そのような型変数が存在せず、まだ固定されていない型変数がある場合、型の推論は失敗します。
  • それ以外の場合、固定されていない型変数がそれ以上存在しない場合、型の推論は成功します。
  • それ以外の場合、出力型 (§7.5.2.4) には固定されていない型変数 Xj が含まれるが、入力型 (§7.5.2.3) には含まれない、対応するパラメーター型 Ti を持つすべての引数 Ei について、出力型推論 (§7.5.2.6) は次のようになります。エイからティまで。その後、第 2 フェーズが繰り返されます。

すべてのサブ条項をコピーするつもりはありませんが、私たちの場合は...

  • 型変数 X 1は、他の型変数がないため、他の型変数に依存しません。したがって、X 1を修正する必要があります。(ここでのセクション参照は間違っています。実際には 7.5.2.11 のはずです。Mads に知らせます。)

X 1には境界がありません(以前の下限の推論が役に立たなかったため) ため、この時点で型の推論に失敗することになります。バン。それはすべて、7.5.2.9 の一意性の部分にかかっています。

もちろん、これは修正できます。仕様の型推論部分をより強力にすることができます - 問題は、それがより複雑になり、結果として次のようになることです:

  • 開発者が型推論について推論するのが難しくなっています (それだけでも十分に難しいのです!)
  • 隙間なく正確に指定するのが難しい
  • 正しく実装するのが難しい
  • おそらく、コンパイル時のパフォーマンスが低下する可能性があります (Visual Studio などの対話型エディターでは、Intellisense などを機能させるために同じ型推論を実行する必要があるため、問題になる可能性があります)。

それはすべてバランスをとる行為です。C# チームはかなりうまくやっていると思います。このようなまれなケースでは機能しないという事実は、それほど大きな問題ではありません。

于 2013-04-27T08:41:53.523 に答える
2

これが起こっている理由は、 がとの両方をFoo()実装しているためです。あなたの使用法はタイプであるため、いずれかを参照している可能性があり、呼び出しはあいまいです。とが原因で問題が発生していないことに注意してください。ただし、 2 つの潜在的な型が実装されているため、戻り値の型も考慮されていませんIFunctionIChildElementIGrandchildElementIChildElementIChildElementIGrandchildElementIFunction<double, IGrandchildElement>IFunction<double, IChildElement>IChildElementIGrandchildElementIFunctiondouble

// f is both an IFunction<double, IGrandchildElement>
// and an IFunction<double, IChildElement>
Foo f = new Foo();
double res1 = cont.Evaluate("x", "y", f); // This does not compile
double res2 = cont.Evaluate<double>("x", "y", f); // This does

したがって、どういうわけかより具体的にする必要があります。キャストを使用してこれを行うには2つの方法があります。

double res3 = cont.Evaluate<double>("x", "y", f);
double res4 = cont.Evaluate("x", "y", (IFunction<double, IChildElement>)f);

あなたが言ったように毎回これをやりたくないのですが、最後の行のキャスト方法はあなたの問題に対する潜在的な解決策を明らかにします; 目的のインターフェイスを変数にキャストFooし、呼び出し時にその変数を使用しますcont.Evaluate()

IFunction<double, IChildElement> iFunc = f;
double res5 = cont.Evaluate("x", "y", iFunc);
于 2013-03-29T04:42:30.413 に答える