2016-07-25 22 views
4

Ich habe Probleme, den Compiler die richtige Überladung für eine Erweiterungsmethode aufzulösen. Der beste Weg für mich zu erklären ist mit ein wenig Code. Hier ist ein LINQPad Skript, das das Problem veranschaulicht. Dies wird nicht kompiliert, da das Problem ich habe:Weird-Extension-Methode Überladungsauflösung

void Main(){ 
    new Container<A>().Foo(a=>false); 
} 

interface IMarker{} 
class A : IMarker{ 
    public int AProp{get;set;} 
} 
class B : IMarker{ 
    public int BProp{get;set;} 
} 
class Container<T>{} 

static class Extensions{ 
    public static void Foo<T>(this T t, Func<T, bool> func) 
     where T : IMarker{ 
     string.Format("Foo({0}:IMarker)", typeof(T).Name).Dump(); 
    } 
    public static void Foo<T>(this Container<T> t, Func<T, bool> func){ 
     string.Format("Foo(Container<{0}>)", typeof(T).Name).Dump(); 
    } 
} 

irrtümlicher ich erhalte, ist:

Der Anruf zwischen den folgenden Methoden oder Eigenschaften nicht eindeutig ist: ‚Extensions.Foo<Container<A>>(Container<A>, System.Func<Container<A>,bool>)‘ und "Extensions.Foo<A>(Container<A>, System.Func<A,bool>) '

Es scheint mir, dass es überhaupt nicht mehrdeutig ist. Die erste Methode akzeptiert keine Container<T>, nur eine IMarker. Es scheint, wie die allgemeinen Einschränkungen nicht in der Überladungsauflösung zu unterstützen, aber in dieser Version des Codes, haben sie zu sein scheinen:

void Main(){ 
    new A().Bar(); 
    new A().Foo(a=>a.AProp == 0); 
    new A().Foo(a=>false); // even this works 
    new A().Foo(a=>{ 
     var x = a.AProp + 1; 
     return false; 
    }); 

    new Container<A>().Bar(); 
    new Container<A>().Foo(a=>a.AProp == 0); 
    new Container<A>().Foo(a=>{ 
     var x = a.AProp + 1; 
     return false; 
    }); 
} 

interface IMarker{} 
class A : IMarker{ 
    public int AProp{get;set;} 
} 
class B : IMarker{ 
    public int BProp{get;set;} 
} 
class Container<T>{} 

static class Extensions{ 
    public static void Foo<T>(this T t, Func<T, bool> func) 
     where T : IMarker{ 
     string.Format("Foo({0}:IMarker)", typeof(T).Name).Dump(); 
    } 
    public static void Foo<T>(this Container<T> t, Func<T, bool> func){ 
     string.Format("Foo(Container<{0}>)", typeof(T).Name).Dump(); 
    } 

    public static void Bar<T>(this T t) where T : IMarker{ 
     string.Format("Bar({0}:IMarker)", typeof(T).Name).Dump(); 
    } 
    public static void Bar<T>(this Container<T> t){ 
     string.Format("Bar(Container<{0}>)", typeof(T).Name).Dump(); 
    } 
} 

Dies kompiliert und erzeugt die erwarteten Ergebnisse:

Bar (A: IMarker)
Foo (A: IMarker)
Foo (A: IMarker)
Foo (A: IMarker)
Bar (Container <A>)
Foo (Container <A>)
Foo (Container <A>)

Es scheint nur ein Problem zu haben, wenn ich nicht den Lambda-Parameter in der Lambda-Ausdruck verweisen und dann nur mit der Container<T> Klasse. Beim Aufruf Bar gibt es kein Lambda, und es funktioniert gut. Beim Aufruf von Foo mit dem Rückgabewert basierend auf dem Lambda-Parameter funktioniert es gut. Auch wenn der Rückgabewert des Lambda derselbe ist wie der im Beispiel, der nicht kompiliert wird, aber der Lambda-Parameter durch eine Dummy-Zuweisung referenziert wird, funktioniert er.

Warum funktioniert es in diesen Fällen, aber nicht in der ersten? Mache ich etwas falsch oder habe ich einen Compiler Bug gefunden? Ich habe das Verhalten in C# 4 und C# 6 bestätigt.

+0

@ManoDestra: Bearbeitungen, weil Sie nicht mögen, wo ich meine geschweiften Klammern platzieren, sind nicht willkommen. –

+0

Dies ist C#. Daher die Bearbeitung. – ManoDestra

+0

Ich verstehe Ihren Standpunkt nicht. –

Antwort

5

Oh, ich habe es nach dem Re-Lesen meiner eigenen Antwort! Nice question =) Die Überladung funktioniert nicht, weil die Einschränkung where T:IMaker bei der Auflösung der Überladung nicht berücksichtigt wird (Einschränkung ist kein Teil der Methodensignatur). Wenn Sie einen Parameter in Lambda-Referenz Sie (können) einen Hinweis an den Compiler hinzuzufügen:

  1. Dies funktioniert:

    new Container<A>().Foo(a => a.AProp == 0); 
    

    weil hier wir andeuten, dass ein: A;

  2. funktioniert das nicht einmal mit einem Verweis auf Parameter:

    new Container<A>().Foo(a => a != null); 
    

    , weil es noch nicht genügend Informationen ist, die Art zu schließen.

Soweit ich die Spezifikation, in „Foo-Szenario“ verstehen kann die Schlussfolgerung nicht auf die zweite (Func) Argument der Anruf nicht eindeutig so zu machen.

Hier ist, was SPEC (25.6.4) sagt:

Typ-Inferenz als Teil der Verarbeitung eines Methodenaufruf Compile-Zeit auftritt (§14.5.5.1) und findet vor der Überladungsauflösung Schritt die Invokation. Wenn eine bestimmte Methodengruppe in einem Methodenaufruf angegeben wird und keine Typargumente als Teil des Methodenaufrufs angegeben werden, wird die Typinferenz auf jede generische Methode in der Methodengruppe angewendet. Wenn die Typinferenz erfolgreich ist, werden die Argumente vom abgeleiteten Typ verwendet, um die Arten von Argumenten für die nachfolgende Überladungsauflösung zu bestimmen.

Wenn die Überladungsauflösung eine generische Methode als die aufzurufende Methode auswählt, werden die Argumente des abgeleiteten Typs als Argumente des Laufzeittyps für den Aufruf verwendet. Wenn die Typrückschlüsse für eine bestimmte Methode fehlschlagen, nimmt diese Methode nicht an der Überladungsauflösung teil. Der Fehler der Typinferenz führt an sich nicht zu einem Fehler bei der Kompilierung. Es führt jedoch oft zu einem Kompilierungsfehler, wenn die Überladungsauflösung keine geeigneten Methoden findet.

Jetzt lassen Sie uns zu ziemlich einfach "Bar-Szenario". Nach Typinferenz werden wir nur eine Methode bekommen, da nur eine ist anwendbar:

  1. Bar(Container<A>) für new Container<A>() (nicht implementiert IMaker)
  2. Bar(A) für new A() (kein Container)

Und hier ist die ECMA-334 specification, nur für den Fall. Ps.s. Ich bin nicht 100% sicher, dass ich es richtig verstanden habe, aber ich denke lieber, dass ich den wesentlichen Teil verstanden habe.

+0

Okay, ich bin so weit mit dir, dass die Constraints nicht Teil der Methodensignatur sind (was auch der Grund dafür ist, dass du eine Methode nicht allein durch Constraint überladen kannst), und dass die Verwendung des Lambda-Parameters dem Compiler eindeutig einen Hinweis gibt wähle eine Überladung. Ich habe das vermutet. Was ich aber immer noch nicht verstehe, ist, warum die Aufrufe von "Bar" in meinem zweiten Ausschnitt funktionieren. Ihnen fehlt das Lambda vollständig. –

+0

Ich denke, unsere Antwort ist irgendwo in 25.6.4 und 14.4.2 der Sprachspezifikation. Aber es ist 7 Uhr morgens für mich, so dass ich bis morgen keine Ideen haben werde. –

+0

Würdest du gerne etwas mehr in deiner Antwort hinzufügen, vielleicht mit ein paar Zitaten aus der Spezifikation? –

1

Sergey herausgefunden, warum, was ich versuchte zu tun, funktioniert nicht, denke ich. Dies ist, was ich beschlossen habe, statt:

void Main(){ 
    new A().Bar(); 
    new A().Foo(a=>a.AProp == 0); 
    new A().Foo(a=>false); 

    new Container<A>().Bar(); 
    new Container<A>().Foo(a=>a.AProp == 0); 
    new Container<A>().Foo(a=>false); // yay, works now! 
} 

interface IMarker<T>{ 
    T Source{get;} 
} 

class A : IMarker<A>{ 
    public int AProp {get;set;} 
    public A Source{get{return this;}} 
} 
class B : IMarker<B>{ 
    public int BProp {get;set;} 
    public B Source{get{return this;}} 
} 

class Container<T> : IMarker<T>{ 
    public T Source{get;set;} 
} 

static class Extensions{ 
    public static void Foo<T>(this IMarker<T> t, Func<T, bool> func){} 
    public static void Bar<T>(this IMarker<T> t){} 
} 

Leider für mich ist dies eine große Veränderung für meine Anwendung. Aber zumindest wird der Erweiterungs-Layer einfacher sein, und am Ende wird es sowohl für den Compiler als auch für den Menschen weniger zweideutig sein, und das ist eine gute Sache.

+0

Wenn Container wirklich ein aussagekräftiger IMaker ist, dann sieht das nach einer netten Refactoring-Entscheidung aus. –