Dlaczego wyrażenie lambda musi być rzutowane, gdy jest dostarczane jako zwykły parametr delegata

124

Wybierz metodę System.Windows.Forms.Control.Invoke (metoda delegata)

Dlaczego powoduje to błąd czasu kompilacji:

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

Ale to działa dobrze:

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

Kiedy metoda oczekuje zwykłego delegata?

xyz
źródło

Odpowiedzi:

125

Wyrażenie lambda można przekonwertować na typ delegata lub drzewo wyrażenia - ale musi wiedzieć, który typ delegata. Sama znajomość podpisu nie wystarczy. Załóżmy na przykład, że mam:

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

...

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

Jaki będzie konkretny typ obiektu, o którym mowa x? Tak, kompilator może wygenerować nowy typ delegata z odpowiednią sygnaturą, ale rzadko jest to przydatne i masz mniej okazji do sprawdzania błędów.

Jeśli chcesz ułatwiają rozmowy Control.Invokeze związkiem Actionnajłatwiej zrobić to dodać metodę rozszerzenia do sterowania:

public static void Invoke(this Control control, Action action)
{
    control.Invoke((Delegate) action);
}
Jon Skeet
źródło
1
Dzięki - zaktualizowałem pytanie, ponieważ uważam, że brak typu to niewłaściwy termin.
xyz
1
To bardzo eleganckie i dojrzałe rozwiązanie. Prawdopodobnie nazwałbym to „InvokeAction”, aby nazwa sugerowała to, co faktycznie wywołujemy (zamiast ogólnego delegata), ale na pewno działa dla mnie :)
Matthias Hryniszak
7
Nie zgadzam się, że jest to „rzadko przydatne i…”. W przypadku wywołania Begin / Invoke z lambdą na pewno nie obchodzi Cię, czy typ delegata jest generowany automatycznie, po prostu chcemy uzyskać wywołanie wykonane. W jakiej sytuacji metoda akceptująca delegata (typ podstawowy) obchodziłaby, jaki jest konkretny typ? Jaki jest cel metody rozszerzenia? To nic nie ułatwia.
Tergiver,
5
Ach! Dodałem metodę rozszerzenia i spróbowałem Invoke(()=>DoStuff)i nadal otrzymuję błąd. Problem polegał na tym, że użyłem domyślnego „tego”. Aby zmusić go do pracy z wnętrza elementu sterującego trzeba być jednoznaczne: this.Invoke(()=>DoStuff).
Tergiver,
2
Dla każdego, kto to czyta, myślę, że pytanie i odpowiedzi do C #: Automatyzacja wzorca kodu InvokeRequired są bardzo pomocne.
Erik Philips,
34

Masz dość ciągłego rzucania lambd?

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

źródło
3
To piękne zastosowanie leków generycznych.
Peter wygrał
2
Muszę przyznać, że zajęło mi to trochę czasu, zanim zrozumiałem, dlaczego to zadziałało. Znakomity. Szkoda, że ​​nie mam z tego teraz pożytku.
William,
1
Czy możesz wyjaśnić zastosowanie tego? Trudno mi to pojąć? Wielkie dzięki.
shahkalpesh
Warto to przeczytać, nie mówiąc już o tym, ale myślę, że wolę tę odpowiedź od Jona Skeeta!
Pogrindis
@shahkalpesh to niezbyt skomplikowane. Zobacz to w ten sposób, Lambda<T>klasa ma wywołaną metodę konwersji tożsamości Cast, która zwraca wszystko, co zostało przekazane ( Func<T, T>). Teraz Lambda<T>jest zadeklarowane jako, Lambda<Func<int, string>>co oznacza, że ​​jeśli przekażesz metodę Func<int, string>do Cast, powróci ona z Func<int, string>powrotem, ponieważ Tw tym przypadku jest Func<int, string>.
nawfal
12

Dziewięć dziesiątych przypadków ludzie to rozumieją, ponieważ próbują skierować się do wątku interfejsu użytkownika. Oto leniwy sposób:

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

Teraz, gdy jest wpisany, problem znika (odpowiedź Skeeta) i mamy bardzo zwięzłą składnię:

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

Jeśli chodzi o punkty bonusowe, oto kolejna wskazówka. Nie zrobiłbyś tego dla elementów interfejsu użytkownika, ale w przypadkach, w których potrzebujesz SomeMethod do blokowania aż do zakończenia (np. Żądanie / odpowiedź I / O, oczekiwanie na odpowiedź) użyj WaitHandle (qv msdn WaitAll, WaitAny, WaitOne).

Należy zauważyć, że AutoResetEvent jest pochodną WaitHandle.

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
}

I ostatnia wskazówka, ponieważ rzeczy mogą się zaplątać: WaitHandles blokuje nić. To właśnie powinni zrobić. Jeśli spróbujesz skierować do wątku interfejsu użytkownika, gdy jest on zablokowany , aplikacja się zawiesi. W takim przypadku (a) konieczna jest poważna refaktoryzacja i (b) jako tymczasowy hack możesz poczekać w ten sposób:

  bool wait = true;
  ThreadPool.QueueUserWorkItem ((state) =>
  {
    //do asynch stuff        
    wait = false;
  });
  while (wait) Thread.Sleep(100);
Peter Wone
źródło
3
To fascynujące, że ludzie mają odwagę głosować na odpowiedź tylko dlatego, że osobiście nie uważają jej za atrakcyjną. Jeśli to jest złe i wiesz o tym, powiedz, co w tym złego. Jeśli nie możesz tego zrobić, nie masz podstaw do głosowania przeciw. Jeśli jest niesamowicie źle, powiedz coś w stylu „Baloney. Zobacz [poprawna odpowiedź]” lub „Niezalecane rozwiązanie, zobacz [lepsze rzeczy]”
Peter Wone
1
Tak, jestem szczerą prowokacją; ale w każdym razie nie mam pojęcia, dlaczego został odrzucony; chociaż nie użyłem rzeczywistego kodu, pomyślałem, że to fajne, szybkie wprowadzenie do wywołań między wątkami interfejsu użytkownika i ma kilka rzeczy, o których tak naprawdę nie myślałem, więc chwała, zdecydowanie +1 za wykraczanie poza granice. :) Chodzi mi o to, że podałeś dobrą, szybką metodę wywoływania delegatów; dajesz opcję połączeń, na które trzeba czekać; i podążasz za tym w szybki sposób dla kogoś, kto utknął w Piekle Wątków UI, aby odzyskać trochę kontroli. Dobra odpowiedź, powiem też + <3. :)
shelleybutterfly
System.Windows.Threading.Dispatcher.CurrentDispatcherzwróci dyspozytor wątku CURRENT - tzn. jeśli wywołasz tę metodę z wątku, który nie jest wątkiem interfejsu użytkownika, kod nie zostanie uruchomiony w wątku interfejsu użytkownika.
BrainSlugs83
@ BrainSlugs83 dobra uwaga, prawdopodobnie najlepszą rzeczą jest, aby aplikacja przechwyciła odniesienie do programu rozsyłającego wątki interfejsu użytkownika i umieściła je w miejscu globalnie dostępnym. Jestem zdumiony, że ktoś tak długo to zauważył!
Peter wygrał
4

Peter Wone. jesteś człowiekiem. Idąc nieco dalej, wymyśliłem te dwie funkcje.

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

Umieszczam te dwie funkcje w mojej aplikacji Form i mogę nawiązywać połączenia z takimi pracownikami w tle

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

Może trochę leniwy, ale nie muszę konfigurować funkcji wykonanych przez pracownika, co jest bardzo przydatne w takich przypadkach

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

Zasadniczo pobierz adresy IP z gui DataGridView, wyślij do nich ping, ustaw wynikowe ikony na zielone lub czerwone i ponownie włącz przyciski w formularzu. Tak, jest to „parallel.for” w pracującym w tle. Tak, to DUŻO wywoływania narzutu, ale jest to pomijalne w przypadku krótkich list i znacznie bardziej zwartego kodu.

rakiety są szybkie
źródło
1

Próbowałem to zbudować na odpowiedzi @Andrey Naumov . Może to jest niewielka poprawa.

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

Gdzie parametr typu Sjest parametrem formalnym (parametrem wejściowym, który jest minimalny wymagany do wywnioskowania pozostałych typów). Teraz możesz to nazwać tak:

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!");

Możesz mieć dodatkowe przeciążenia dla Action<S>i Expression<Action<S>>podobnie w tej samej klasie. Na drugi zbudowany w typów delegatów i wypowiedzi, trzeba będzie napisać osobne zajęcia, takie jak Lambda, Lambda<S, T>, Lambda<S, T, U>itd.

Zaleta tego, którą widzę w porównaniu z oryginalnym podejściem:

  1. Jedna specyfikacja typu mniej (należy podać tylko parametr formalny).

  2. Co daje ci swobodę użycia go przeciwko każdemu Func<int, T>, nie tylko wtedy, gdy Tjest powiedz string, jak pokazano na przykładach.

  3. Obsługuje wyrażenia od razu. We wcześniejszym podejściu będziesz musiał ponownie określić typy, takie jak:

    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!");
    

    dla wyrażeń.

  4. Rozszerzanie klasy dla innych typów delegatów (i wyrażeń) jest podobnie nieporęczne, jak powyżej.

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

W moim podejściu musisz zadeklarować typy tylko raz (to za jeden mniej na Funcs).


Innym sposobem na wdrożenie odpowiedzi Andreya jest nie przejście w pełni na ogólny

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

Więc wszystko sprowadza się do:

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

To jeszcze mniej pisania, ale tracisz pewne bezpieczeństwo typów, a imo, to nie jest tego warte.

nawfal
źródło
1

Trochę za późno na imprezę, ale możesz też tak przesyłać

this.BeginInvoke((Action)delegate {
    // do awesome stuff
});
Tien Dinh
źródło
0
 this.Dispatcher.Invoke((Action)(() => { textBox1.Text = "Test 123"; }));
Narottam Goyal
źródło
0

Grając z XUnit i Fluent Assertions , udało mi się wykorzystać tę wbudowaną możliwość w sposób, który uważam za naprawdę fajny.

Przed

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

Po

[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.");
}
Fábio Augusto Pandolfo
źródło