2017-03-16 8 views
16

dieses MCVE Bedenken Sie:Delegate Rückgabetyp unterschiedlich mit Lambda-Funktion

using System; 

public interface IThing { } 

public class Foo : IThing 
{ 
    public static Foo Create() => new Foo(); 
} 

public class Bar : IThing 
{ 
    public static Bar Create() => new Bar(); 
} 

public delegate IThing ThingCreator(); 

class Program 
{ 
    static void Test(ThingCreator creator) 
    { 
     Console.WriteLine(creator.Method.ReturnType); 
    } 

    static void Main() 
    { 
     Test(Foo.Create);  // Prints: Foo 
     Test(Bar.Create);  // Prints: Bar 

     Test(() => new Foo()); // Prints: IThing 
     Test(() => new Bar()); // Prints: IThing 
    } 
} 

Warum spiegelt den Rückgabetyp für die statische Factory-Methode, um die konkrete Art geben, während der Konstruktor Inline-Aufruf gibt die Schnittstelle? Ich würde erwarten, dass sie beide gleich sind.

Gibt es auch eine Möglichkeit, in der Lambda-Version anzugeben, dass der Rückgabewert der konkrete Typ sein soll? Oder ruft eine statische Methode den einzigen Weg auf?

+0

Eine __static__ Methode ist nicht der einzige Weg. Sie können auch eine nicht statische verwenden. Zum Beispiel 'Func f =() => neu Foo(); Test (f.Invoke); 'Aber die wirkliche Frage ist, warum checkst du' .Method.ReturnType' auf einer (möglicherweise Multicast) Delegierteninstanz? Was nützt das? Warum kümmert dich das? –

+1

@Jeppe - Eine Bibliothek eines Drittanbieters verfügt über eine öffentliche API mit einer ähnlichen Methode wie "Test". Ich habe den Nebeneffekt nicht erwartet, indem ich ein Lambda anstelle eines Verweises auf eine tatsächliche Funktion übergeben habe. Musste graben um zu finden, dass sie den Rückgabetyp der Methode überprüften. Wenn es mein eigener Code wäre, würde ich das nicht schaffen. –

Antwort

12

Der Rückgabetyp eines Lambda-Ausdrucks ergibt sich nicht aus dem, was das Lambda tatsächlich zurückgibt, sondern aus dem Typ, dem es zugeordnet wird. Das heißt, man kann nicht eine Lambda wie folgt aus (außer wenn generische Typparameter involed werden, siehe Kommentare von Eric Lippert) zuordnen:

// This generates the compiler error: 
// "Cannot assign lambda expression to an implicitly-typed variable". 
var lambda =() => new Foo(); 

Sie müssen immer etwas tun (Lambdas sind immer einen Delegattyp zugewiesen) :

Func<MyType> lambda =() => new Foo(); 

Deshalb ist der Rückgabetyp des Lambdas in Test(() => new Foo()); wird aus dem Typ des Parameters es (IThing, der Rückgabetyp der ThingCreator) zugeordnet ist, bestimmt.

In Test(Foo.Create); haben Sie überhaupt kein Lambda, sondern eine als public static Foo Create() ... deklarierte Methode. Hier wird der Typ explizit angegeben und lautet Foo (es macht keinen Unterschied, ob es sich um eine statische oder eine Instanzmethode handelt).

+5

Abgesehen von der spezifischen Frage des ursprünglichen Posters bemerke ich, dass Ihre Behauptung, der Typ eines Lambda-Ausdrucks sei * nie * aus dem, was das Lambda zurückgibt, abgeleitet ist, falsch. Betrachten wir zum Beispiel 'static void M (Func f)' genannt als 'M (() => 1)'.In diesem Fall geben Inferenzgründe an, dass die formalen Parametertypen - alle Null von ihnen - bekannt sind, und daher kann T aus dem Rückgabetyp des Lambda abgeleitet werden, der int ist. –

+0

@EricLippert: Tritt dies nur auf, wenn generische Typparameter beteiligt sind? –

+1

Korrekt; der Fall, den ich erwähnt habe, ist IIRC, das einzige Mal, dass C# * vom Rückgabetyp eines Lambda ableitet. In allen anderen Fällen wird der Rückgabetyp auf Kompatibilität mit dem Rückgabetyp des Delegierten überprüft, der bereits bekannt sein muss. –

0

Meine persönliche Vermutung ist der Ort der Invokation. Wenn Sie () => new Foo() an die Funktion übergeben, wird es als ThingCreator abgerufen und aufgerufen, um IThing zu erhalten. Aber, wenn Sie die Factory-Methode des Betontyp der Test Methode, wenn Testmethode ruft sie senden, geht es an die konkrete Art der Create() was wiederum die konkrete Objekt zurückgibt, die durchaus akzeptabel ist, als auch es ist IThing

I Ich schätze, du brauchst mehr als ich denke. Entschuldigung, wenn ich falsch liege!

9

Oliviers Antwort ist grundsätzlich korrekt, könnte aber zusätzliche Erklärungen enthalten.

Warum gibt die Angabe des Rückgabetyps für die statische Factory-Methode den konkreten Typ an, während der Aufruf des Konstruktors inline die Schnittstelle gibt? Ich würde sie beide erwarten die gleiche sein

Ihre Programmklasse auf die folgende Klasse entspricht:

class Program 
{ 
    static void Test(ThingCreator creator) 
    { 
    Console.WriteLine(creator.Method.ReturnType); 
    } 
    static IThing Anon1() 
    { 
    return new Foo(); 
    } 
    static IThing Anon2() 
    { 
    return new Bar(); 
    } 
    static void Main() 
    { 
    Test(new ThingCreator(Foo.Create)); 
    Test(new ThingCreator(Bar.Create)); 
    Test(new ThingCreator(Program.Anon1)); 
    Test(new ThingCreator(Program.Anon2)); 
    } 
} 

Und jetzt sollte klar sein, warum das Programm druckt, was es tut.

Die Moral der Geschichte ist hier, dass, wenn wir die verborgene Methode für die Lambda-Ausdrücke erzeugen, diese verborgenen Methoden zurückgeben, was von den Delegierten erforderlich war, und nicht unabhängig von der Lambda- zurückgibt.

Warum ist das?

Ein Germane Beispiel würde zeigen, dass:

static void Blah(Func<object> f) 
{ 
    Console.WriteLine(f().ToString()); 
} 
static void Main() 
{ 
    Blah(() => 123); 
} 

Ich hoffe, Sie stimmen zu, dass dies als

static object Anon() { return (object)123; } 

und nicht

static int Anon() { return 123; } 

erzeugt werden muss, weil dieser nicht sein kann konvertiert in Func<object>! Es gibt keinen Platz für den Boxbefehl, aber der Aufruf an ToString erwartet, dass ein Referenztyp von f() zurückgegeben wird.

Die allgemeine Regel lautet also, dass das Lambda, wenn es als versteckte Methode verdinglicht wird, den Rückgabetyp haben muss, der durch die Konvertierung in den Delegattyp gewährt wird.

Gibt es auch eine Möglichkeit, in der Lambda-Version anzugeben, dass der Rückgabewert der konkrete Typ sein soll? Oder ruft eine statische Methode den einzigen Weg auf?

Sicher.

interface IThing {} 
class Foo : IThing {} 
delegate T ThingCreator<T>() where T : IThing; 
public class Program 
{ 
    static void Test<T>(ThingCreator<T> tc) where T : IThing 
    { 
    Console.WriteLine(tc.Method.ReturnType); 
    } 
    public static void Main() 
    { 
    Test(() => new Foo());  
    } 
} 

Warum ist das anders?

Da Inferenz Typ folgert, dass TFoo ist und deshalb konvertieren wir die Lambda ThingCreator<Foo>, welcher Typ Foo zurückkehren muss. Daher gibt die generierte Methode Foo zurück, wie der Delegattyp erwartet.

Aber warten Sie, sagen Sie ... ich nicht die Unterschrift des Tests oder von ThingCreator

keine Sorgen ändern können! Sie können immer noch diese Arbeit machen:

delegate T ThingCreator<T>() where T : IThing; 
delegate IThing ThingCreator();  
public class Program 
{ 
    static void Test(ThingCreator tc) 
    { 
     Console.WriteLine(tc.Method.ReturnType); 
    } 
    static ThingCreator DoIt<T>(ThingCreator<T> tc) where T : class, IThing 
    { 
     return tc.Invoke; 
    } 
    public static void Main() 
    { 
     Test(DoIt(() => new Foo())); 
    } 
} 

Die Kehrseite ist, dass jetzt jeder Ihrer Nicht-generic ThingCreator Delegierten ein Delegierter ist, die einen ThingCreator<T> Delegierten aufruft, die ein bisschen eine Verschwendung von Zeit und Speicher ist. Aber Sie erhalten Typinferenz, Methodengruppenumwandlung und Lambda-Konvertierung, um Sie zu einer Methode des gewünschten Rückgabetyps und zu einem Delegaten für diese Methode zu machen.

Beachten Sie die class Einschränkung. Sehen Sie, warum diese Einschränkung da sein muss? Dies ist eine Übung für den Leser.

+0

Sehr schöne Erklärung. Vielen Dank! –