Warum muss ein Lambda-Ausdruck umgewandelt werden, wenn er als einfacher Delegate-Parameter angegeben wird?


124

Nehmen Sie die Methode System.Windows.Forms.Control.Invoke (Delegate-Methode)

Warum führt dies zu einem Fehler bei der Kompilierung:

string str = "woop";
Invoke(() => this.Text = str);
// Error: Cannot convert lambda expression to type 'System.Delegate'
// because it is not a delegate type

Dies funktioniert jedoch gut:

string str = "woop";
Invoke((Action)(() => this.Text = str));

Wann erwartet die Methode einen einfachen Delegaten?

Antworten:


125

Ein Lambda-Ausdruck kann entweder in einen Delegatentyp oder einen Ausdrucksbaum konvertiert werden - er muss jedoch wissen, welcher Delegattyp. Nur die Unterschrift zu kennen, reicht nicht aus. Angenommen, ich habe:

public delegate void Action1();
public delegate void Action2();

...

Delegate x = () => Console.WriteLine("hi");

Was würden Sie von dem konkreten Typ des Objekts erwarten, auf das Bezug genommen xwird? Ja, der Compiler könnte einen neuen Delegatentyp mit einer geeigneten Signatur generieren, aber das ist selten nützlich und Sie haben weniger Gelegenheit zur Fehlerprüfung.

Wenn Sie es einfach anrufen wollen Control.Invokemit einem Actionder einfachste Sache zu tun ist , eine Verlängerung Methode zur Steuerung hinzufügen:

public static void Invoke(this Control control, Action action)
{
    control.Invoke((Delegate) action);
}

1
Danke - Ich habe die Frage aktualisiert, weil ich denke, dass untypisiert der falsche Begriff war.
XYZ

1
Das ist eine sehr elegante und ausgereifte Lösung. Ich würde es wahrscheinlich "InvokeAction" nennen, damit der Name andeutet, was wir tatsächlich aufrufen (anstelle eines generischen Delegaten), aber es funktioniert auf jeden
Fall

7
Ich bin nicht der Meinung, dass es "selten nützlich und ..." ist. Wenn Sie Begin / Invoke mit einem Lambda aufrufen, ist es Ihnen sicherlich egal, ob der Delegatentyp automatisch generiert wird. Wir möchten nur, dass der Anruf getätigt wird. In welcher Situation würde sich eine Methode, die einen Delegaten (den Basistyp) akzeptiert, um den konkreten Typ kümmern? Was ist der Zweck der Erweiterungsmethode? Es macht nichts einfacher.
Tergiver

5
Ah! Ich habe die Erweiterungsmethode hinzugefügt und versucht, Invoke(()=>DoStuff)den Fehler zu erhalten. Das Problem war, dass ich das implizite 'dies' verwendet habe. Damit es in einem Control-Mitglied funktioniert, müssen Sie Folgendes angeben : this.Invoke(()=>DoStuff).
Tergiver

2
Für alle anderen, die dies lesen, sind die Fragen und Antworten zu C #: Automatisieren des InvokeRequired-Codemusters sehr hilfreich.
Erik Philips

34

Sind Sie es leid, immer wieder Lambdas zu werfen?

public sealed class Lambda<T>
{
    public static Func<T, T> Cast = x => x;
}

public class Example
{
    public void Run()
    {
        // Declare
        var c = Lambda<Func<int, string>>.Cast;
        // Use
        var f1 = c(x => x.ToString());
        var f2 = c(x => "Hello!");
        var f3 = c(x => (x + x).ToString());
    }
}

3
Das ist eine schöne Verwendung von Generika.
Peter Wone

2
Ich muss zugeben, ich habe eine Weile gebraucht, um herauszufinden, warum es funktioniert hat. Brillant. Schade, dass ich momentan keine Verwendung dafür habe.
William

1
Können Sie bitte die Verwendung erklären? Ist es schwierig für mich, das zu verstehen? Vielen Dank.
Shahkalpesh

Es ist jemals zu lesen, geschweige denn es zu sagen, aber ich glaube, ich ziehe diese Antwort Jon Skeets vor!
Pogrindis

@ Shahkalpesh ist nicht sehr komplex. Auf diese Weise verfügt die Lambda<T>Klasse über eine Identitätskonvertierungsmethode namens Cast, die alles zurückgibt, was übergeben wird ( Func<T, T>). Jetzt wird das Lambda<T>als deklariert. Lambda<Func<int, string>>Wenn Sie eine Func<int, string>to- CastMethode übergeben, wird es Func<int, string>zurückgegeben, da dies Tin diesem Fall der Fall ist Func<int, string>.
Nawfal

12

Neun Zehntel der Fälle erhalten die Benutzer dies, weil sie versuchen, auf dem UI-Thread zu marshallen. Hier ist der faule Weg:

static void UI(Action action) 
{ 
  System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke(action); 
}

Jetzt, da es getippt ist, verschwindet das Problem (siehe Skeets Antwort) und wir haben diese sehr prägnante Syntax:

int foo = 5;
public void SomeMethod()
{
  var bar = "a string";
  UI(() =>
  {
    //lifting is marvellous, anything in scope where the lambda
    //expression is defined is available to the asynch code
    someTextBlock.Text = string.Format("{0} = {1}", foo, bar);        
  });
}

Für Bonuspunkte hier ein weiterer Tipp. Sie würden dies nicht für UI-Inhalte tun, aber in Fällen, in denen SomeMethod bis zum Abschluss blockiert werden muss (z. B. Anforderungs- / Antwort-E / A, Warten auf die Antwort), verwenden Sie ein WaitHandle (siehe msdn WaitAll, WaitAny, WaitOne).

Beachten Sie, dass AutoResetEvent eine WaitHandle-Ableitung ist.

public void BlockingMethod()
{
  AutoResetEvent are = new AutoResetEvent(false);
  ThreadPool.QueueUserWorkItem ((state) =>
  {
    //do asynch stuff        
    are.Set();
  });      
  are.WaitOne(); //don't exit till asynch stuff finishes
}

Und ein letzter Tipp, weil sich die Dinge verheddern können: WaitHandles blockieren den Faden. Das sollen sie tun. Wenn Sie versuchen, auf dem UI-Thread zu marshallen , während Sie ihn blockiert haben , bleibt Ihre App hängen. In diesem Fall (a) ist ein ernsthaftes Refactoring angebracht, und (b) als vorübergehender Hack können Sie folgendermaßen warten:

  bool wait = true;
  ThreadPool.QueueUserWorkItem ((state) =>
  {
    //do asynch stuff        
    wait = false;
  });
  while (wait) Thread.Sleep(100);

3
Ich finde es faszinierend, dass die Leute die Wange haben, eine Antwort abzustimmen, nur weil sie es persönlich nicht attraktiv finden. Wenn es falsch ist und Sie das wissen, dann sagen Sie, was daran falsch ist. Wenn Sie dies nicht können, haben Sie keine Grundlage für eine Ablehnung. Wenn es episch falsch ist, dann sagen Sie etwas wie "Quatsch. Siehe [richtige Antwort]" oder vielleicht "Keine empfohlene Lösung, siehe [besseres Zeug]"
Peter Wone

1
Ja, ich bin die Frankenthreadstress; aber trotzdem habe ich keine Ahnung, warum es abgelehnt wurde; Obwohl ich den eigentlichen Code nicht verwendet habe, dachte ich, dies sei eine nette kurze Einführung in die Cross-Thread-Aufrufe der Benutzeroberfläche, und es gibt einige Dinge, an die ich nicht wirklich gedacht hatte, definitiv +1, um darüber hinauszugehen. :) Ich meine, Sie haben eine nette schnelle Methode angegeben, um Delegatenaufrufe zu machen. Sie geben eine Option für Anrufe an, auf die gewartet werden muss. und Sie folgen ihm mit einer schönen schnellen Möglichkeit für jemanden, der in der UI-Thread-Hölle steckt, ein bisschen Kontrolle zurückzugewinnen. Gute Antwort, ich werde auch + <3 sagen. :)
Shelleybutterfly

System.Windows.Threading.Dispatcher.CurrentDispatchergibt den Dispatcher des CURRENT-Threads zurück. Wenn Sie diese Methode von einem Thread aufrufen, der nicht der UI-Thread ist, wird der Code nicht auf dem UI-Thread ausgeführt.
BrainSlugs83

@ BrainSlugs83 guter Punkt, wahrscheinlich ist es das Beste für eine App, einen Verweis auf den UI-Thread-Dispatcher zu erfassen und ihn an einem global zugänglichen Ort abzulegen. Ich bin erstaunt, dass es so lange gedauert hat, bis jemand das bemerkt hat!
Peter Wone

4

Peter Wone. Du bist ein Mann. Ich habe Ihr Konzept etwas weiterentwickelt und mir diese beiden Funktionen ausgedacht.

private void UIA(Action action) {this.Invoke(action);}
private T UIF<T>(Func<T> func) {return (T)this.Invoke(func);}

Ich platziere diese beiden Funktionen in meiner Formular-App und kann Anrufe von solchen Hintergrundmitarbeitern tätigen

int row = 5;
string ip = UIF<string>(() => this.GetIp(row));
bool r = GoPingIt(ip);
UIA(() => this.SetPing(i, r));

Vielleicht ein bisschen faul, aber ich muss keine Worker-Doed-Funktionen einrichten, was in solchen Fällen sehr praktisch ist

private void Ping_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
  int count = this.dg.Rows.Count;
  System.Threading.Tasks.Parallel.For(0, count, i => 
  {
    string ip = UIF<string>(() => this.GetIp(i));
    bool r = GoPingIt(ip);
    UIA(() => this.SetPing(i, r));
  });
  UIA(() => SetAllControlsEnabled(true));
}

Rufen Sie im Wesentlichen einige IP-Adressen von einer GUI-DataGridView ab, pingen Sie sie an, setzen Sie die resultierenden Symbole auf Grün oder Rot und aktivieren Sie die Schaltflächen im Formular erneut. Ja, es ist ein "parallel.for" in einem Hintergrundarbeiter. Ja, es ist eine Menge Aufruf von Overhead, aber es ist vernachlässigbar für kurze Listen und viel kompakteren Code.


1

Ich habe versucht, dies auf der Antwort von @Andrey Naumov aufzubauen . Möglicherweise ist dies eine leichte Verbesserung.

public sealed class Lambda<S>
{
    public static Func<S, T> CreateFunc<T>(Func<S, T> func)
    {
        return func;
    }

    public static Expression<Func<S, T>> CreateExpression<T>(Expression<Func<S, T>> expression)
    {
        return expression;
    }

    public Func<S, T> Func<T>(Func<S, T> func)
    {
        return func;
    }

    public Expression<Func<S, T>> Expression<T>(Expression<Func<S, T>> expression)
    {
        return expression;
    }
}

Dabei Sist der Typparameter der formale Parameter (der Eingabeparameter, der mindestens erforderlich ist, um auf den Rest der Typen zu schließen). Jetzt können Sie es so nennen:

var l = new Lambda<int>();
var d1 = l.Func(x => x.ToString());
var e1 = l.Expression(x => "Hello!");
var d2 = l.Func(x => x + x);

//or if you have only one lambda, consider a static overload
var e2 = Lambda<int>.CreateExpression(x => "Hello!");

Sie können zusätzliche Überladungen für Action<S>und Expression<Action<S>>in derselben Klasse haben. Für andere in Delegaten und Ausdrucksarten gebaut, werden Sie separate Klassen gerne schreiben müssen Lambda, Lambda<S, T>,Lambda<S, T, U> usw.

Vorteil davon sehe ich gegenüber dem ursprünglichen Ansatz:

  1. Eine Typspezifikation weniger (nur der formale Parameter muss angegeben werden).

  2. Dies gibt Ihnen die Freiheit, es gegen jeden zu verwenden Func<int, T>, nicht nur wenn Tes gesagt wird string, wie in Beispielen gezeigt.

  3. Unterstützt Ausdrücke sofort. Bei dem früheren Ansatz müssen Sie erneut Typen angeben, z.

    var e = Lambda<Expression<Func<int, string>>>.Cast(x => "Hello!");
    
    //or in case 'Cast' is an instance member on non-generic 'Lambda' class:
    var e = lambda.Cast<Expression<Func<int, string>>>(x => "Hello!");

    für Ausdrücke.

  4. Das Erweitern der Klasse für andere Delegatentypen (und Ausdruckstypen) ist ähnlich umständlich wie oben.

    var e = Lambda<Action<int>>.Cast(x => x.ToString());
    
    //or for Expression<Action<T>> if 'Cast' is an instance member on non-generic 'Lambda' class:
    var e = lambda.Cast<Expression<Action<int>>>(x => x.ToString());

In meinem Ansatz müssen Sie Typen nur einmal deklarieren (das auch eins weniger für Funcs).


Eine andere Möglichkeit, Andreys Antwort umzusetzen, besteht darin, nicht vollständig generisch zu werden

public sealed class Lambda<T>
{
    public static Func<Func<T, object>, Func<T, object>> Func = x => x;
    public static Func<Expression<Func<T, object>>, Expression<Func<T, object>>> Expression = x => x;
}

Die Dinge reduzieren sich also auf:

var l = Lambda<int>.Expression;
var e1 = l(x => x.ToString());
var e2 = l(x => "Hello!");
var e3 = l(x => x + x);

Das ist noch weniger Tippen, aber Sie verlieren bestimmte Typensicherheit, und imo, das ist es nicht wert.


1

Etwas spät zur Party, aber du kannst auch so besetzen

this.BeginInvoke((Action)delegate {
    // do awesome stuff
});


0

Beim Spielen mit XUnit und Fluent Assertions war es möglich, diese Inline-Funktion auf eine Weise zu nutzen, die ich wirklich cool finde.

Vor

[Fact]
public void Pass_Open_Connection_Without_Provider()
{
    Action action = () => {
        using (var c = DbProviderFactories.GetFactory("MySql.Data.MySqlClient").CreateConnection())
        {
            c.ConnectionString = "<xxx>";
            c.Open();
        }
    };

    action.Should().Throw<Exception>().WithMessage("xxx");
}

Nach dem

[Fact]
public void Pass_Open_Connection_Without_Provider()
{
    ((Action)(() => {
        using (var c = DbProviderFactories.GetFactory("<provider>").CreateConnection())
        {
            c.ConnectionString = "<connection>";
            c.Open();
        }
    })).Should().Throw<Exception>().WithMessage("Unable to find the requested .Net Framework Data Provider.  It may not be installed.");
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.