Perché è necessario eseguire il cast di un'espressione lambda quando viene fornita come un semplice parametro Delegate


124

Prendi il metodo System.Windows.Forms.Control.Invoke (metodo Delegate)

Perché questo dà un errore in fase di compilazione:

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

Eppure funziona bene:

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

Quando il metodo si aspetta un semplice delegato?

Risposte:


125

Un'espressione lambda può essere convertita in un tipo delegato o in un albero delle espressioni, ma deve sapere quale tipo di delegato. Solo conoscere la firma non è sufficiente. Ad esempio, supponiamo che io abbia:

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

...

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

Che cosa vi aspettate il tipo concreto dell'oggetto a cui si riferisce xdi essere? Sì, il compilatore potrebbe generare un nuovo tipo di delegato con una firma appropriata, ma raramente è utile e si hanno meno possibilità di controllo degli errori.

Se vuoi rendere facile chiamare Control.Invokecon una Actioncosa più semplice da fare è aggiungere un metodo di estensione a Control:

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

1
Grazie - ho aggiornato la domanda perché penso che untyped fosse il termine sbagliato da usare.
xyz

1
Questa è una soluzione molto elegante e matura. Probabilmente lo chiamerei "InvokeAction" in modo che il nome suggerisca ciò che stiamo effettivamente invocando (invece di un delegato generico) ma sicuramente funziona per me :)
Matthias Hryniszak

7
Non sono d'accordo sul fatto che sia "raramente utile e ...". Nel caso in cui si chiami Begin / Invoke con un lambda, di certo non ti interessa se il tipo di delegato è generato automaticamente, vogliamo solo che venga effettuata la chiamata. In quale situazione un metodo che accetta un Delegato (il tipo di base) si preoccuperebbe di quale sia il tipo concreto? Inoltre, qual è lo scopo del metodo di estensione? Non rende niente più facile.
Tergiver

5
Ah! Ho aggiunto il metodo di estensione Invoke(()=>DoStuff)e ho provato e ho ancora ricevuto l'errore. Il problema era che ho usato l'implicito "questo". Per farlo funzionare dall'interno di un organo di comando devi essere esplicito: this.Invoke(()=>DoStuff).
Tergiver

2
Per chiunque altro legga questo, penso che la domanda e le risposte a C #: Automating the InvokeRequired code pattern siano molto utili.
Erik Philips

34

Stanco di lanciare lambda più e più volte?

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
È un bell'uso dei generici.
Peter ha vinto il

2
Devo ammetterlo, mi ci è voluto un po 'per capire perché ha funzionato. Brillante. Peccato che non ne abbia alcuna utilità in questo momento.
William

1
Puoi per favore spiegare l'uso di questo? È difficile per me comprenderlo? Molte grazie.
shahkalpesh

È da leggere mai questo, figuriamoci dirlo, ma penso di preferire questa risposta a quella di Jon Skeet!
Pogrindis

@shahkalpesh non è molto complesso. Visto in questo modo, la Lambda<T>classe ha un metodo di conversione dell'identità chiamato Cast, che restituisce tutto ciò che viene passato ( Func<T, T>). Ora l' Lambda<T>è dichiarato come Lambda<Func<int, string>>che significa che se si passa una Func<int, string>per Castmetodo, torna Func<int, string>indietro, dal momento che Tin questo caso è Func<int, string>.
nawfal

12

Nove decimi delle volte le persone ottengono questo perché stanno tentando di eseguire il marshalling nel thread dell'interfaccia utente. Ecco il modo pigro:

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

Ora che è stato digitato, il problema scompare (ad esempio la risposta di Skeet) e abbiamo questa sintassi molto succinta:

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

Per i punti bonus ecco un altro suggerimento. Non lo faresti per le cose dell'interfaccia utente, ma nei casi in cui è necessario che SomeMethod si blocchi fino al completamento (ad esempio richiesta / risposta I / O, in attesa della risposta) usa un WaitHandle (qv msdn WaitAll, WaitAny, WaitOne).

Notare che AutoResetEvent è un derivato di 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
}

E un ultimo consiglio perché le cose possono aggrovigliarsi: WaitHandles blocca il filo. Questo è quello che dovrebbero fare. Se provi a eseguire il marshalling sul thread dell'interfaccia utente mentre è bloccato , l'app si bloccherà. In questo caso (a) è necessario un serio refactoring e (b) come hack temporaneo puoi aspettare in questo modo:

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

3
Trovo affascinante che le persone abbiano la sfacciataggine di votare una risposta solo perché personalmente non la trovano attraente. Se è sbagliato e lo sai, allora dì cosa c'è che non va. Se non puoi farlo, non hai basi per un voto negativo. Se è epicamente sbagliato, dì qualcosa come "Baloney. Vedi [risposta corretta]" o forse "Non una soluzione consigliata, vedi [cose migliori]"
Peter Wone

1
Sì, sono la frankenthreadstress; ma comunque non ho idea del perché sia ​​stato bocciato; anche se non ho usato il codice effettivo, ho pensato che questa fosse una bella introduzione veloce alle invocazioni cross-thread dell'interfaccia utente, e ha alcune cose a cui non avevo davvero pensato così complimenti, sicuramente +1 per andare oltre. :) Voglio dire, hai fornito un bel metodo veloce per fare invocazioni ai delegati; date un'opzione per le chiamate che devono essere aspettate; e lo segui con un bel modo veloce per qualcuno che è bloccato in UI Thread Hell per riprendere un po 'di controllo. Bella risposta, dico anche + <3. :)
shelleybutterfly

System.Windows.Threading.Dispatcher.CurrentDispatcherrestituirà il dispatcher del thread ATTUALE, ovvero se chiami questo metodo da un thread che non è il thread dell'interfaccia utente, il codice non verrà eseguito sul thread dell'interfaccia utente.
BrainSlugs83

@ BrainSlugs83 buon punto, probabilmente la cosa migliore è che un'app acquisisca un riferimento al dispatcher del thread dell'interfaccia utente e lo metta da qualche parte accessibile a livello globale. Sono stupito che ci sia voluto così tanto tempo prima che qualcuno se ne accorgesse!
Peter ha vinto

4

Peter Wone. sei un uomo. Portando il tuo concetto un po 'oltre, ho ideato queste due funzioni.

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

Inserisco queste due funzioni nella mia app Form e posso effettuare chiamate da operatori in background in questo modo

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

Forse un po 'pigro, ma non devo impostare le funzioni eseguite dal lavoratore, il che è molto utile in casi come questo

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

In sostanza, prendi alcuni indirizzi IP da una GUI DataGridView, esegui il ping, imposta le icone risultanti su verde o rosso e riattiva i pulsanti sul modulo. Sì, è un "parallel.for" in un backgroundworker. Sì, è un MOLTO sovraccarico di invocazione, ma è trascurabile per elenchi brevi e codice molto più compatto.


1

Ho provato a costruire questo sulla risposta di @Andrey Naumov . Potrebbe essere un leggero miglioramento.

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

Dove tipo parametro Sè il parametro formale (il parametro di input, che è il minimo richiesto per dedurre il resto dei tipi). Ora puoi chiamarlo come:

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

Puoi avere sovraccarichi aggiuntivi per Action<S>e in Expression<Action<S>>modo simile nella stessa classe. Per altro costruito in tipi delegato e di espressione, si dovrà scrivere classi separate piace Lambda, Lambda<S, T>,Lambda<S, T, U> etc.

Vantaggio di questo che vedo rispetto all'approccio originale:

  1. Una specifica di tipo in meno (è necessario specificare solo il parametro formale).

  2. Il che ti dà la libertà di usarlo contro chiunque Func<int, T>, non solo quando Tsi dice string, come mostrato negli esempi.

  3. Supporta immediatamente le espressioni. Nell'approccio precedente dovrai specificare di nuovo i tipi, come:

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

    per le espressioni.

  4. L'estensione della classe per altri tipi di delegati (ed espressioni) è altrettanto complicata come sopra.

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

Nel mio approccio devi dichiarare i tipi solo una volta (anche quella in meno per Funcs).


Un altro modo per implementare la risposta di Andrey è come non essere completamente generici

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

Quindi le cose si riducono a:

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

È ancora meno digitando, ma perdi una certa sicurezza di tipo, e imo, non ne vale la pena.


1

Un po 'in ritardo per la festa, ma puoi anche lanciare in questo modo

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


0

Giocare con XUnit e Fluent Assertions è stato possibile utilizzare questa funzionalità in linea in un modo che trovo davvero interessante.

Prima

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

Dopo

[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.");
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.