it-swarm.com.de

Warum schließt C # meine generischen Typen nicht?

Ich habe viel Spaß mit generischen Methoden. In den meisten Fällen ist die Inferenz vom Typ C # klug genug, um herauszufinden, welche generischen Argumente für meine generischen Methoden verwendet werden müssen, aber jetzt habe ich ein Design, bei dem der C # -Compiler nicht erfolgreich ist richtige typen.

Kann mir jemand sagen, ob der Compiler in diesem Fall etwas dumm ist, oder gibt es einen klaren Grund, warum er meine generischen Argumente nicht herleiten kann?

Hier ist der Code:

Klassen und Schnittstellendefinitionen:

interface IQuery<TResult> { }

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

class SomeQuery : IQuery<string>
{
}

Ein Code, der nicht kompiliert werden kann:

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);
    }
}

Warum ist das? Was fehlt mir hier?

Hier ist die Fehlermeldung des Compilers (sie lässt unserer Phantasie nicht viel zu):

Die Typargumente für die Methode IQueryProcessor.Process (TQuery) können aus der Verwendung nicht abgeleitet werden. Versuchen Sie, das .__ anzugeben. Geben Sie Argumente explizit ein.

Der Grund, warum ich glaube, dass C # daraus schließen kann, ist folgender:

  1. Ich gebe ein Objekt an, das IQuery<TResult> implementiert.
  2. Die einzige IQuery<TResult>-Version, die vom Typ implementiert wird, ist IQuery<string> und daher muss TResult string sein.
  3. Mit diesen Informationen besitzt der Compiler TResult und TQuery.

L&OUML;SUNG

Für mich war die beste Lösung, die IQueryProcessor-Schnittstelle zu ändern und die dynamische Typisierung in der Implementierung zu verwenden:

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);
    }
}

Die IQueryProcessor-Schnittstelle nimmt jetzt einen IQuery<TResult>-Parameter auf. Auf diese Weise kann eine TResult zurückgegeben werden, wodurch die Probleme aus Sicht des Verbrauchers gelöst werden. Wir müssen Reflektion in der Implementierung verwenden, um die tatsächliche Implementierung zu erhalten, da die konkreten Abfragetypen benötigt werden (in meinem Fall). Aber hier kommt dynamisches Tippen zur Rettung, das die Reflexion für uns tun wird. Mehr dazu lesen Sie in diesem Artikel .

58
Steven

Ein paar Leute haben darauf hingewiesen, dass C # nicht auf Einschränkungen basiert. Das ist richtig und relevant für die Frage. Inferenzen werden durch Untersuchen von arguments und ihren entsprechenden formalen Parametertypen gemacht, und dies ist die einzige Quelle für Inferenzinformationen.

Eine Reihe von Personen haben dann zu diesem Artikel verlinkt:

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

Dieser Artikel ist veraltet und für die Frage irrelevant. Es ist veraltet, weil es eine Entwurfsentscheidung beschreibt, die wir in C # 3.0 getroffen haben, die wir dann in C # 4.0 rückgängig gemacht haben, meist basierend auf der Antwort auf diesen Artikel. Ich habe dem Artikel gerade ein Update hinzugefügt.

Dies ist irrelevant, da es sich bei dem Artikel um die Rückschlussart return-Typ von Argumenten der Methodengruppe auf generische Delegierungsformalparameter handelt. Dies ist nicht die Situation, nach der das ursprüngliche Poster fragt.

Der relevante Artikel von mir zu lesen ist eher dieser:

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

UPDATE: Ich habe gehört, dass in C # 7.3 die Regeln für das Anwenden von Einschränkungen geringfügig geändert wurden, so dass der oben genannte zehnjährige Artikel nicht mehr genau ist. Wenn ich Zeit habe, werde ich die Änderungen meiner früheren Kollegen überprüfen und sehen, ob es sich lohnt, eine Korrektur in meinem neuen Blog zu veröffentlichen. Bis dahin sollten Sie vorsichtig sein und sehen, was C # 7.3 in der Praxis macht.

46
Eric Lippert

C # zieht keine generischen Typen basierend auf dem Rückgabetyp einer generischen Methode, sondern nur die Argumente für die Methode.

Die Einschränkungen werden auch nicht als Teil der Typeninferenz verwendet, wodurch die generische Einschränkung vom Typ für Sie ausgeschlossen wird. 

Für Details siehe Eric Lipperts Beitrag zum Thema .

15
Reed Copsey

Es verwendet keine Einschränkungen, um Typen abzuleiten. Sie führt vielmehr Typen (wenn möglich) heran und prüft dann Einschränkungen.

Obwohl dies die einzig mögliche TResult ist, die mit einem SomeQuery-Parameter verwendet werden kann, wird dies nicht angezeigt.

Beachten Sie auch, dass es durchaus möglich ist, dass SomeQuery auch IQuery<int> implementiert. Dies ist ein Grund, warum diese Einschränkung des Compilers keine schlechte Idee ist.

11
Jon Hanna

Die Spezifikation legt dies ziemlich klar fest:

Abschnitt 7.4.2 Typeninferenz

Wenn sich die angegebene Anzahl von Argumenten von der Anzahl der Parameter in der Methode unterscheidet, schlägt die Inferenz sofort fehl. Ansonsten nehmen Sie an, dass die generische Methode die folgende Signatur hat:

Tr M (T1 x1… Tm xm)

Bei einem Methodenaufruf der Form M (E1… Em) besteht die Aufgabe der Typeninferenz darin, eindeutige Typargumente S1… Sn für jeden der Typparameter X1… Xn zu finden, so dass der Aufruf M (E1… Em) gültig wird .

Wie Sie sehen, wird der Rückgabetyp nicht für die Typinferenz verwendet. Wenn der Methodenaufruf nicht direkt auf die Typargumente abgebildet wird, schlägt die Inferenz sofort fehl.

Der Compiler geht nicht nur davon aus, dass Sie string als TResult-Argument wollten, und kann es auch nicht. Stellen Sie sich eine TResult vor, die von einem String abgeleitet ist. Beide wären gültig, also was soll man wählen? Besser explizit sein.

4
Ed S.

Das warum wurde gut beantwortet, aber es gibt eine alternative Lösung. Ich bin regelmäßig mit den gleichen Problemen konfrontiert, aber dynamic oder jede Lösung, die Reflektion oder das Zuordnen von Daten verwendet, steht in meinem Fall außer Frage (Freude an Videospielen ...)

Stattdessen übergebe ich die Rückgabe als out-Parameter, die dann korrekt abgeleitet wird.

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);
    }
}

Der einzige Nachteil, den ich mir vorstellen kann, ist, dass es die Verwendung von 'var' verbietet.

2
Baptiste Dupy

Ich werde nicht noch einmal auf das Warum eingehen, ich habe keine Illusionen, eine bessere Erklärung als Eric Lippert machen zu können.

Es gibt jedoch eine Lösung, die keine späte Bindung oder zusätzliche Parameter für Ihren Methodenaufruf erfordert. Es ist jedoch nicht sehr intuitiv, also überlasse ich es dem Leser, zu entscheiden, ob es eine Verbesserung ist.

Ändern Sie zunächst IQuery, damit es selbst referenziert wird:

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

Ihre IQueryProcessor würde so aussehen:

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

Ein tatsächlicher Abfragetyp:

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

Eine Implementierung des Prozessors könnte folgendermaßen aussehen:

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

Eine weitere Problemumgehung für dieses Problem ist das Hinzufügen zusätzlicher Parameter für die Typauflösung . Beispielsweise können wir die folgende Erweiterung hinzufügen:

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);
    }
}

Jetzt können wir diese Erweiterung wie folgt verwenden:

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);
}

Das ist alles andere als ideal, aber viel besser als das explizite Angeben von Typen.

P.S. Verwandte Links zu diesem Problem im Dotnet-Repository:

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

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

0
Roman Artiukhin