2010-10-25 4 views
43

Betrachten wir zwei Erweiterungsmethoden:Mehrdeutige Anruf zwischen zwei C# Erweiterung generische Methoden ein, wo T: Klasse und andere, wo T: struct

public static T MyExtension<T>(this T o) where T:class 
public static T MyExtension<T>(this T o) where T:struct 

Und eine Klasse:

class MyClass() { ... } 

Nun ist die Erweiterung Methode aufrufen auf einer Instanz der oben Klasse:

var o = new MyClass(...); 
o.MyExtension(); //compiler error here.. 
o.MyExtension<MyClass>(); //tried this as well - still compiler error.. 

der Compiler sagt, dass die Methode ruft eine mehrdeutige cal l wenn ich es in einer Klasse anrufe. Ich hätte gedacht, dass es bestimmen könnte, welche Erweiterungsmethode aufgerufen werden soll, da MyClass eine Klasse und keine Struktur ist?

+0

Ordentlich finden! Aber was ist deine Frage? Eine Problemumgehung? –

+2

Gute Frage. Ich dachte, ich hätte eine einfache Antwort darauf, aber es stellte sich heraus, dass ich es nicht tat. Ich hoffe, es macht Ihnen nichts aus, dass meine "Antwort" eher eine Erkundung dessen ist, was vor sich geht, als eine Antwort an sich. –

+0

Danke für Ihre Kommentare. Eamon - Entschuldigung, meine Frage ist nicht so klar - das ist der Grund, warum der Compiler nicht die beste Methode bestimmen kann. Nach dem Lesen der Kommentare und Fragen und des von LukeH bereitgestellten Links liegt das daran, dass der Compiler die Typenzwänge bei der Bestimmung der besten zu verwendenden Methode nicht berücksichtigt. –

Antwort

34

EDIT: Ich habe jetzt blogged about this im Detail.


Meine ursprüngliche (und ich glaube, jetzt falsch) Gedanke: generic Einschränkungen nicht berücksichtigt bei der Überladungsauflösung und Typinferenz Phasen genommen - sie sind nur das Ergebnis der Überladungsauflösung zu validieren.

EDIT: Okay, nach einem Los davon zu gehen auf diesem, ich denke, ich bin da. Grundsätzlich war mein erster Gedanke fast korrekt.

Generische Typ-Constraints wirken nur, um Methoden aus einem Kandidatensatz in einem sehr begrenzten Satz von Umständen zu entfernen ... insbesondere, nur wenn der Typ eines Parameters selbst generisch ist; nicht nur ein Typparameter, sondern ein generischer Typ, der einen generischen Typparameter verwendet. An diesem Punkt werden die Einschränkungen für die Typparameter des generischen Typs überprüft, nicht die Einschränkungen für die Typparameter der generischen Methode, die Sie aufrufen.

Zum Beispiel:

// Constraint won't be considered when building the candidate set 
void Foo<T>(T value) where T : struct 

// The constraint *we express* won't be considered when building the candidate 
// set, but then constraint on Nullable<T> will 
void Foo<T>(Nullable<T> value) where T : struct 

Also, wenn Sie versuchen, Foo<object>(null) das obige Verfahren zu nennen nicht Teil des Kandidatensatzes, weil Nullable<object> value die Zwänge des Nullable<T> nicht erfüllt. Wenn es andere anwendbare Methoden gibt, könnte der Aufruf dennoch erfolgreich sein.

Jetzt im obigen Fall sind die Einschränkungen genau gleich ... aber sie müssen nicht sein. Betrachten wir zum Beispiel:

class Factory<TItem> where TItem : new() 

void Foo<T>(Factory<T> factory) where T : struct 

Wenn Sie versuchen, Foo<object>(null) zu nennen, werden nach wie vor das Verfahren Teil des Kandidatensatzes sein - denn wenn TItemobject, ausgedrückt die Einschränkung in Factory<TItem> hält nach wie vor, und dass ist, was überprüft hat beim Aufbau des Kandidatensets. Wenn dies die beste Methode erweist, wird es dann nicht Validierung später, in der Nähe des Ende von 7.6.5.1:

Wenn die beste Methode, eine generische Methode ist, die Typargumente (geliefert oder abgeleitet) werden gegen die Einschränkungen (§4.4.4), die für die generische Methode deklariert sind, geprüft. Wenn ein Typargument die entsprechenden Integritätsbedingungen für den Typparameter nicht erfüllt, tritt ein Bindungszeitfehler auf.

Eric's blog post enthält weitere Details zu diesem Thema.

+0

+1, und Paging Eric Lippert ... – Richard

+0

@ Jon: Ich bin mir ziemlich sicher, dass Ihre ursprüngliche Antwort korrekt ist, obwohl ich zustimmen, dass es schwierig ist, genau zu sehen, wo/wie dies in der Spezifikation codiert ist. Abschnitt 7.6.5.1 scheint dabei mehrdeutig zu sein, aber die Idee, dass Constraints nicht Teil der Signatur sind, ist gut etabliert (und wird von denen, die diese Dinge kennen, sicher behauptet, zum Beispiel http://blogs.msdn.com/b /ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx). – LukeH

+1

Meine Spekulationen darüber, warum Ihr Beispiel nicht kompiliert ... Eine der Regeln in 7.5.3.2 sagt * "... wenn alle Parameter von Mp ein entsprechendes Argument haben, während Standardargumente für mindestens einen optionalen Parameter ersetzt werden müssen in Mq ist dann Mp besser als Mq "*. Gemäß dieser Regel - und unter der Annahme, dass Einschränkungen * nicht berücksichtigt werden - stellt sich heraus, dass Ihre zweite Methode besser zu "Foo ()" passt, obwohl dies später zu einem Fehler bei den Einschränkungen führt sind validiert. (Es ist besser, weil es keine Ersetzungen erfordert, während die erste Methode tut.) – LukeH

10

Eric Lippert erklärt besser als ich jemals könnte, here.

Ich bin auf diese selbst gestoßen. Meine Lösung war

public void DoSomthing<T> (T theThing){ 
    if (typeof (T).IsValueType) 
     DoSomthingWithStruct (theThing); 
    else 
     DoSomthingWithClass (theThing); 
} 

// edit - seems I just lived with boxing 

public void DoSomthingWithStruct (object theThing) 
public void DoSomthingWithClass(object theThing) 
+0

Courtney, ich konnte das nicht zum Kompilieren bringen - innerhalb der DoSomething-Methode heißt es, dass erwartet wird, dass ein Werttyp DoSomthingWithStruct aufruft und ein Referenztyp, um DoSomthingWithClass aufzurufen. –

+0

Ah, ich werde genau überprüfen müssen, was ich morgen gemacht habe –

+0

Um Boxen zu vermeiden, sollte man eine statische Klasse 'SomethingDoer ' mit einer schreibgeschützten Feldeigenschaft vom Typ 'Action ' namens 'DoSomething' definieren ; die Klasse Konstruktor sollte Reflection verwenden einen Delegierten zu konstruieren 'DoSomethingWithStruct (T param), wobei T zu nennen: struct',' DoSomethingWithClass (T param) where T: CLASS' oder 'DoSomethingWithNullable (Nullable param)' und speichern es in diesem Bereich. Die Reflexion müsste nur einmal für jeden gegebenen Typparameter verwendet werden; danach würde der Delegierte die entsprechende Methode direkt aufrufen. – supercat

4

ich diese „interessant“ seltsame Art und Weise festgestellt, dass zu tun, in Werten .NET 4.5 unter Verwendung von Standard-Parametern :) Vielleicht ist nützlicher für Bildungs ​​\ spekulative Zwecke als für wirkliche Verwendung, aber ich mag zeigen, es:

/// <summary>Special magic class that can be used to differentiate generic extension methods.</summary> 
public class MagicValueType<TBase> 
    where TBase : struct 
{ 
} 

/// <summary>Special magic class that can be used to differentiate generic extension methods.</summary> 
public class MagicRefType<TBase> 
    where TBase : class 
{ 
} 

struct MyClass1 
{ 
} 

class MyClass2 
{ 
} 

// Extensions 
public static class Extensions 
{ 
    // Rainbows and pink unicorns happens here. 
    public static T Test<T>(this T t, MagicRefType<T> x = null) 
     where T : class 
    { 
     Console.Write("1:" + t.ToString() + " "); 
     return t; 
    } 

    // More magic, other pink unicorns and rainbows. 
    public static T Test<T>(this T t, MagicValueType<T> x = null) 
     where T : struct 
    { 
     Console.Write("2:" + t.ToString() + " "); 
     return t; 
    } 
} 

class Program 
{ 
    static void Main(string[] args) 
    { 

     MyClass1 t1 = new MyClass1(); 
     MyClass2 t2 = new MyClass2(); 

     MyClass1 t1result = t1.Test(); 
     Console.WriteLine(t1result.ToString()); 

     MyClass2 t2result = t2.Test(); 
     Console.WriteLine(t2result.ToString()); 

     Console.ReadLine(); 
    } 
} 
+0

Warum wird das Attribut "[Serializable]" benötigt? – Juan

+0

Ist nicht. Mein Fehler :) –

Verwandte Themen