¿Por qué se debe emitir una expresión lambda cuando se proporciona como un parámetro Delegate simple?

124

Tome el método System.Windows.Forms.Control.Invoke (método Delegate)

¿Por qué esto da un error de tiempo de compilación?

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

Sin embargo, esto funciona bien:

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

Cuando el método espera un delegado simple?

xyz
fuente

Respuestas:

125

Una expresión lambda puede convertirse a un tipo de delegado o un árbol de expresión, pero tiene que saber qué tipo de delegado. Solo saber que la firma no es suficiente. Por ejemplo, supongamos que tengo:

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

...

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

¿Cuál esperarías que sea el tipo concreto del objeto al que se refiere x? Sí, el compilador podría generar un nuevo tipo de delegado con una firma apropiada, pero eso rara vez es útil y terminas con menos oportunidades para la verificación de errores.

Si desea hacer más fácil a la llamada Control.Invokecon una Actionlo más fácil de hacer es añadir un método de extensión de control:

public static void Invoke(this Control control, Action action)
{
    control.Invoke((Delegate) action);
}
Jon Skeet
fuente
1
Gracias. Actualicé la pregunta porque creo que sin tipo era el término incorrecto para usar.
xyz
1
Esa es una solución muy elegante y madura. Probablemente lo llamaría "InvokeAction" para que el nombre sugiera lo que realmente estamos invocando (en lugar de un delegado genérico) pero ciertamente funciona para mí :)
Matthias Hryniszak
77
No estoy de acuerdo con que sea "rara vez útil y ...". En el caso de llamar a Begin / Invoke con una lambda, ciertamente no le importa si el tipo de delegado se genera automáticamente, solo queremos que se realice la llamada. ¿En qué situación le importaría a un método que acepta un Delegado (el tipo base) cuál es el tipo concreto? Además, ¿cuál es el propósito del método de extensión? No hace nada más fácil.
Tergiver
55
Ah! Agregué el método de extensión e intenté Invoke(()=>DoStuff)y aún recibí el error. El problema fue que usé el implícito 'esto'. Para conseguir que funcione desde el interior de un órgano de control que tiene que ser explícito: this.Invoke(()=>DoStuff).
Tergiver
2
Para cualquiera que lea esto, creo que la pregunta y las respuestas a C #: Automatización del patrón de código InvokeRequired son muy útiles.
Erik Philips
34

¿Cansado de lanzar lambdas una y otra vez?

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

fuente
3
Ese es un hermoso uso de los genéricos.
Peter Wone
2
Tengo que admitir que esto me llevó un tiempo descubrir por qué funcionó. Brillante. Lástima que no lo uso en este momento.
William
1
¿Puedes explicar el uso de esto? ¿Es difícil para mí comprender esto? Muchas gracias.
shahkalpesh
¡Es para leer esto y mucho menos decirlo, pero creo que prefiero esta respuesta a Jon Skeet!
Pogrindis
@shahkalpesh no es muy complejo. Véalo de esta manera, la Lambda<T>clase tiene un método de conversión de identidad llamado Cast, que devuelve todo lo que se pasa ( Func<T, T>). Ahora Lambda<T>se declara como lo Lambda<Func<int, string>>que significa que si pasa un método Func<int, string>to Cast, regresa Func<int, string>, ya que Ten este caso es Func<int, string>.
nawfal
12

Nueve décimas de las veces las personas obtienen esto porque están tratando de reunir el hilo de la interfaz de usuario. Aquí está la manera perezosa:

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

Ahora que está escrito, el problema desaparece (qv Skeet's anwer) y tenemos esta sintaxis muy sucinta:

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

Para puntos de bonificación aquí hay otro consejo. No haría esto para las cosas de la interfaz de usuario, pero en los casos en que necesite SomeMethod para bloquear hasta que se complete (por ejemplo, E / S de solicitud / respuesta, esperando la respuesta) use un WaitHandle (qv msdn WaitAll, WaitAny, WaitOne).

Tenga en cuenta que AutoResetEvent es un derivado 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
}

Y un consejo final porque las cosas pueden enredarse: WaitHandles detiene el hilo. Esto es lo que se supone que deben hacer. Si intentas reunir el hilo de la interfaz de usuario mientras lo tienes bloqueado , tu aplicación se bloqueará. En este caso (a) es necesaria una refactorización seria, y (b) como un truco temporal puede esperar así:

  bool wait = true;
  ThreadPool.QueueUserWorkItem ((state) =>
  {
    //do asynch stuff        
    wait = false;
  });
  while (wait) Thread.Sleep(100);
Peter Wone
fuente
3
Me parece fascinante que las personas tengan el descaro de rechazar una respuesta simplemente porque personalmente no la encuentran atractiva. Si está mal y sabes esto, entonces di lo que está mal. Si no puede hacerlo, entonces no tiene base para un voto negativo. Si está épicamente mal, entonces diga algo como "Baloney. Vea [respuesta correcta]" o tal vez "No es una solución recomendada, vea [cosas mejores]"
Peter Wone
1
Sí, soy la frankstreadstress; pero de todos modos no tengo idea de por qué fue rechazado; aunque no he usado el código real, pensé que esta era una introducción rápida y agradable a las invocaciones de hilos cruzados de la interfaz de usuario, y tiene algunas cosas que realmente no había pensado tanto, definitivamente +1 para ir más allá. :) Quiero decir, diste un buen método rápido para hacer invocaciones de delegado; Usted da una opción para las llamadas que se deben esperar; y lo sigues con una forma rápida y agradable para que alguien que está atrapado en UI Thread Hell recupere un poco de control. Buena respuesta, voy a decir + <3 también. :)
shelleybutterfly
System.Windows.Threading.Dispatcher.CurrentDispatcherdevolverá el despachador del subproceso ACTUAL, es decir, si llama a este método desde un subproceso que no es el subproceso de la interfaz de usuario, el código no se ejecutará en el subproceso de la interfaz de usuario.
BrainSlugs83
@ BrainSlugs83 buen punto, probablemente lo mejor es que una aplicación capture una referencia al despachador de subprocesos de la interfaz de usuario y la coloque en un lugar accesible a nivel mundial. ¡Me sorprende que a alguien le haya tomado tanto tiempo darse cuenta de eso!
Peter Wone
4

Peter Wone eres un hombre Llevando su concepto un poco más allá, se me ocurrieron estas dos funciones.

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

Coloco estas dos funciones en mi aplicación Form, y puedo hacer llamadas de trabajadores en segundo plano como este

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

Tal vez sea un poco vago, pero no tengo que configurar las funciones realizadas por el trabajador, lo cual es muy útil en casos como este

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

Esencialmente, obtenga algunas direcciones IP de un DataGridView gui, haga ping, configure los iconos resultantes en verde o rojo y vuelva a habilitar los botones en el formulario. Sí, es un "paralelo. Para" en un trabajador de fondo. Sí, es MUCHA invocación de gastos generales, pero es insignificante para listas cortas y código mucho más compacto.

cohetes
fuente
1

Traté de construir esto sobre la respuesta de @Andrey Naumov . Puede ser que esto sea una ligera mejora.

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

Donde parámetro de tipo Ses el parámetro formal (el parámetro de entrada, que es el mínimo requerido para inferir el resto de los tipos). Ahora puedes llamarlo así:

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

Puede tener sobrecargas adicionales para Action<S>y de Expression<Action<S>>manera similar en la misma clase. Por otra construida en delegado y expresión tipos, tendrá que escribir clases separadas como Lambda, Lambda<S, T>, Lambda<S, T, U>etc.

Ventaja de esto que veo sobre el enfoque original:

  1. Una especificación de tipo menos (solo se necesita especificar el parámetro formal).

  2. Lo que le da la libertad de usarlo contra cualquiera Func<int, T>, no solo cuando Tse dice string, como se muestra en los ejemplos.

  3. Admite expresiones de inmediato. En el enfoque anterior, tendrá que especificar tipos nuevamente, como:

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

    para expresiones

  4. Ampliar la clase para otros tipos de delegado (y expresión) es igualmente engorroso como el anterior.

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

En mi enfoque, debe declarar tipos solo una vez (eso también es menos para Funcs).


Otra forma de implementar la respuesta de Andrey es como no ser completamente genérico

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

Entonces las cosas se reducen a:

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

Eso es aún menos mecanografía, pero pierde cierta seguridad de tipografía, y eso no vale la pena.

nawfal
fuente
1

Un poco tarde para la fiesta, pero también puedes emitir así

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

Jugando con XUnit y Fluent Assertions fue posible usar esta capacidad en línea de una manera que me parece genial.

antes de

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

Después

[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
fuente