web-dev-qa-db-ja.com

なぜC#がジェネリック型を推測しないのですか?

汎用的な方法でFuncyの楽しみがたくさんあります(楽しいことを意図しています)。ほとんどの場合、C#型の推論は、ジェネリックメソッドで使用するジェネリック引数を見つけるのに十分スマートですが、C#コンパイラが成功しないデザインを取得しましたが、正しいタイプ。

この場合、コンパイラが少し愚かであるか、それとも私の汎用引数を推測できない非常に明確な理由がありますか?

コードは次のとおりです。

クラスとインターフェースの定義:

interface IQuery<TResult> { }

interface IQueryProcessor
{
    TResult Process<TQuery, TResult>(TQuery query)
        where TQuery : IQuery<TResult>;
}

class SomeQuery : IQuery<string>
{
}

コンパイルされないコード:

class Test
{
    void Test(IQueryProcessor p)
    {
        var query = new SomeQuery();

        // Does not compile :-(
        p.Process(query);

        // Must explicitly write all arguments
        p.Process<SomeQuery, string>(query);
    }
}

どうしてこれなの?ここで何が欠けていますか?

コンパイラのエラーメッセージは次のとおりです(想像力にはあまり影響しません)。

メソッドIQueryProcessor.Process(TQuery)の型引数は、使用法から推測できません。型引数を明示的に指定してみてください。

C#が推論できるはずだと思う理由は、次のとおりです。

  1. IQuery<TResult>を実装するオブジェクトを提供します。
  2. 型が実装するIQuery<TResult>バージョンのみがIQuery<string>であるため、TResultはstringでなければなりません。
  3. この情報により、コンパイラにはTResultとTQueryがあります。

[〜#〜] solution [〜#〜]

私にとって最良の解決策は、IQueryProcessorインターフェイスを変更し、実装で動的型付けを使用することでした。

public interface IQueryProcessor
{
    TResult Process<TResult>(IQuery<TResult> query);
}

// Implementation
sealed class QueryProcessor : IQueryProcessor {
    private readonly Container container;

    public QueryProcessor(Container container) {
        this.container = container;
    }

    public TResult Process<TResult>(IQuery<TResult> query) {
        var handlerType =
            typeof(IQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResult));
        dynamic handler = container.GetInstance(handlerType);
        return handler.Handle((dynamic)query);
    }
}

IQueryProcessorインターフェースはIQuery<TResult>パラメーターを受け取るようになりました。この方法でTResultを返すことができ、これにより消費者の観点から問題が解決されます。実際の実装を取得するには、実装でリフレクションを使用する必要があります(具体的なクエリタイプが必要なため)。しかし、ここでは、動的な型指定が助けとなり、それが私たちのために反映されます。これについての詳細は、この 記事 で読むことができます。

63
Steven

多くの人が、C#は制約に基づいて推論を行わないことを指摘しています。それは正しく、質問に関連しています。推論は、引数およびそれらに対応するフォーマルパラメータタイプおよび推論情報の唯一のソースです。

その後、多くの人々がこの記事にリンクしています:

http://blogs.msdn.com/b/ericlippert/archive/2007/11/05/c-3-0-return-type-in​​ference-does-not-work-on-member-groups。 aspx

その記事は古く、質問とは無関係です。 C#3.0で行った設計上の決定を記述しているため、C#4.0でそれを元に戻しました。これは主にその記事への応答に基づいています。その効果の更新を記事に追加しました。

この記事はメソッドグループ引数からジェネリックデリゲートフォーマルパラメーターへの型推論を返すためのものであるため、無関係です。それは、元のポスターが尋ねる状況ではありません。

読むべき私の関連記事はむしろこれです。

http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx

更新:C#7.3が制約が適用されるタイミングの規則をわずかに変更し、上記の10年前の記事がもはや正確ではなくなったというニュースを聞きました。時間があれば、以前の同僚が加えた変更を確認し、新しいブログに修正を投稿する価値があるかどうかを確認します。それまでは、注意して、C#7.3が実際に行うことを確認してください。

53
Eric Lippert

C#は、ジェネリックメソッドの戻り値の型に基づいてジェネリック型を推測せず、メソッドの引数のみを推測します。

また、型推論の一部として制約を使用しないため、ジェネリック制約が型を提供しなくなります。

詳細については、 Eric Lippertのこの件に関する投稿 を参照してください。

15
Reed Copsey

型を推測するために制約を使用しません。むしろ、型を推測し(可能な場合)、制約をチェックします。

したがって、TResultパラメーターで使用できる唯一のSomeQueryは、表示されません。

また、SomeQueryIQuery<int>を実装することも完全に可能であることに注意してください。これは、コンパイラの制限が悪い考えではない理由の1つです。

11
Jon Hanna

仕様はこれをかなり明確に示しています。

セクション7.4.2型推論

指定された引数の数がメソッドのパラメーターの数と異なる場合、推論はすぐに失敗します。それ以外の場合、ジェネリックメソッドには次のシグネチャがあると仮定します。

Tr M(T1 x1…Tm xm)

M(E1…Em)という形式のメソッド呼び出しでは、型推論のタスクは型パラメーターX1…Xnごとに一意の型引数S1…Snを見つけて、呼び出しM(E1…Em)有効になります。

ご覧のとおり、戻り型は型推論には使用されません。メソッド呼び出しが型引数に直接マップしない場合、推論はすぐに失敗します。

コンパイラは、stringTResult引数として必要であると仮定するだけでなく、そうすることもできません。文字列から派生したTResultを想像してください。両方とも有効であるため、どちらを選択しますか?明示的であることが望ましい。

4
Ed S.

whyは十分に回答されていますが、別の解決策があります。私は定期的に同じ問題に直面しますが、dynamicまたはリフレクションを使用するかデータを割り当てるソリューションは私の場合は問題ではありません(ビデオゲームの喜び...)

代わりに、戻り値をoutパラメーターとして渡します。これは、その後正しく推測されます。

interface IQueryProcessor
{
     void Process<TQuery, TResult>(TQuery query, out TResult result)
         where TQuery : IQuery<TResult>;
}

class Test
{
    void Test(IQueryProcessor p)
    {
        var query = new SomeQuery();

        // Instead of
        // string result = p.Process<SomeQuery, string>(query);

        // You write
        string result;
        p.Process(query, out result);
    }
}

私が考えることができる唯一の欠点は、「var」の使用を禁止していることです。

2
Baptiste Dupy

私は再び理由に行きません、私はエリック・リッパートより良い説明をすることができるという幻想を持っていません。

ただし、メソッド呼び出しに遅延バインディングまたは追加のパラメーターを必要としないソリューションがあります。しかし、それは非常に直感的ではないので、改善するかどうかは読者に任せます。

まず、IQueryを変更して自己参照型にします。

public interface IQuery<TQuery, TResult> where TQuery: IQuery<TQuery, TResult>
{
}

IQueryProcessorは次のようになります。

public interface IQueryProcessor
{
    Task<TResult> ProcessAsync<TQuery, TResult>(IQuery<TQuery, TResult> query)
        where TQuery: IQuery<TQuery, TResult>;
}

実際のクエリタイプ:

public class MyQuery: IQuery<MyQuery, MyResult>
{
    // Neccessary query parameters
}

プロセッサの実装は次のようになります。

public Task<TResult> ProcessAsync<TQuery, TResult>(IQuery<TQuery, TResult> query)
    where TQuery: IQuery<TQuery, TResult>
{
    var handler = serviceProvider.Resolve<QueryHandler<TQuery, TResult>>();
    // etc.
}
1
Thorarin

この問題の別の回避策は、型解決のパラメーターを追加することです。たとえば、次の拡張子を追加できます。

static class QueryProcessorExtension
{
    public static TResult Process<TQuery, TResult>(
        this IQueryProcessor processor, TQuery query,
        //Additional parameter for TQuery -> IQuery<TResult> type resolution:
        Func<TQuery, IQuery<TResult>> typeResolver)
        where TQuery : IQuery<TResult>
    {
        return processor.Process<TQuery, TResult>(query);
    }
}

これで、この拡張機能を次のように使用できます。

void Test(IQueryProcessor p)
{
    var query = new SomeQuery();

    //You can now call it like this:
    p.Process(query, x => x);
    //Instead of
    p.Process<SomeQuery, string>(query);
}

これは理想からはほど遠いですが、明示的に型を提供するよりもはるかに優れています。

追伸ドットネットリポジトリのこの問題に関連するリンク:

https://github.com/dotnet/csharplang/issues/997

https://github.com/dotnet/roslyn/pull/785

0
Roman Artiukhin