Por que uma expressão lambda deve ser convertida quando fornecida como um parâmetro Delegate simples

124

Pegue o método System.Windows.Forms.Control.Invoke (método Delegate)

Por que isso gera um erro de tempo de compilação:

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

No entanto, isso funciona bem:

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

Quando o método espera um delegado simples?

xyz
fonte

Respostas:

125

Uma expressão lambda pode ser convertida em um tipo de delegado ou em uma árvore de expressão - mas é necessário saber qual tipo de delegado. Apenas conhecer a assinatura não é suficiente. Por exemplo, suponha que eu tenha:

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

...

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

O que você esperaria que fosse o tipo concreto do objeto referido x? Sim, o compilador pode gerar um novo tipo de delegado com uma assinatura apropriada, mas isso raramente é útil e você acaba com menos oportunidade de verificação de erros.

Se você deseja facilitar a chamada Control.Invokecom Actiona coisa mais fácil, adicione um método de extensão ao Control:

public static void Invoke(this Control control, Action action)
{
    control.Invoke((Delegate) action);
}
Jon Skeet
fonte
1
Obrigado - atualizei a pergunta porque acho que não digitado era o termo errado a ser usado.
xyz
1
Essa é uma solução muito elegante e madura. Eu provavelmente diria que é "InvokeAction" para que o nome sugere o que estamos realmente invocando (em vez de um delegado genérico) mas certamente funciona para mim :)
Matthias Hryniszak
7
Não concordo que seja "raramente útil e ...". No caso de chamar Begin / Invoke com um lambda, você certamente não se importa se o tipo de delegado é gerado automaticamente, apenas queremos que a chamada seja feita. Em que situação um método que aceita um Delegado (o tipo base) se importa com o tipo concreto? Além disso, qual é o objetivo do método de extensão? Não facilita nada.
Tergiver
5
Ah! Eu adicionei o método de extensão e tentei Invoke(()=>DoStuff)e ainda recebi o erro. O problema era que eu usei o implícito 'this'. Para fazê-lo funcionar a partir de um membro de controle que você tem que ser explícito: this.Invoke(()=>DoStuff).
Tergiver
2
Para qualquer pessoa que esteja lendo isso, acho que as perguntas e respostas para C #: Automatizar o padrão de código InvokeRequired são muito úteis.
precisa saber é o seguinte
34

Cansado de lançar lambdas repetidamente?

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

fonte
3
Esse é um belo uso de genéricos.
quer
2
Vou ter que admitir, isso me levou um tempo para descobrir por que funcionou. Brilhante. Pena que não tenho utilidade para isso agora.
William
1
Você pode explicar o uso disso? É difícil para mim entender isso? Muito obrigado.
precisa saber é o seguinte
É sempre ler isso e muito menos dizê-lo, mas acho que prefiro esta resposta a Jon Skeet's!
Pogrindis
@shahkalpesh não é muito complexo. Veja desta maneira, a Lambda<T>classe tem um método de conversão de identidade chamado Cast, que retorna o que foi passado ( Func<T, T>). Agora o Lambda<T>é declarado, o Lambda<Func<int, string>>que significa que se você passar um método Func<int, string>para Cast, ele retornará Func<int, string>, pois Tnesse caso é Func<int, string>.
Nawfal
12

Nove décimos das vezes as pessoas conseguem isso porque estão tentando empacotar no thread da interface do usuário. Aqui está o caminho preguiçoso:

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

Agora que foi digitado, o problema desaparece (resposta de qv Skeet) e temos esta sintaxe muito 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 pontos de bônus, aqui está outra dica. Você não faria isso para as coisas da interface do usuário, mas nos casos em que você precisa que o SomeMethod bloqueie até que seja concluído (por exemplo, E / S de solicitação / resposta, aguardando a resposta), use um WaitHandle (qv msdn WaitAll, WaitAny, WaitOne).

Observe que AutoResetEvent é um derivado de 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 uma dica final, porque as coisas podem ficar confusas: WaitHandles interrompe o processo. Isto é o que eles deveriam fazer. Se você tentar empacotar no encadeamento da interface do usuário enquanto o interromper , seu aplicativo será interrompido . Nesse caso (a) alguma refatoração séria está em ordem e (b) como um corte temporário, você pode esperar assim:

  bool wait = true;
  ThreadPool.QueueUserWorkItem ((state) =>
  {
    //do asynch stuff        
    wait = false;
  });
  while (wait) Thread.Sleep(100);
Peter Wone
fonte
3
Acho fascinante que as pessoas tenham coragem de votar em uma resposta simplesmente porque pessoalmente não a acham atraente. Se estiver errado e você souber disso, diga o que há de errado. Se você não puder fazer isso, não terá base para um voto negativo. Se estiver epicamente errado, diga algo como "Bobagem. Veja [resposta correta]" ou talvez "Não é uma solução recomendada, veja [coisas melhores]"
Peter Wone
1
Sim, eu sou o frankenthreadstress; de qualquer maneira, não tenho idéia do motivo pelo qual foi rejeitado; embora eu não tenha usado o código real, achei que essa era uma introdução rápida e agradável à interface de linha cruzada da interface do usuário e tem algumas coisas que eu realmente não tinha pensado tão bem, definitivamente +1 por ir além. :) Quero dizer, você deu um método rápido e agradável para fazer chamadas de delegado; você oferece uma opção para chamadas que devem ser aguardadas; e você o segue de uma maneira rápida e agradável para alguém que está preso no UI Thread Hell para recuperar um pouco de controle. Ótima resposta, eu vou dizer + <3 também. :)
shelleybutterfly
System.Windows.Threading.Dispatcher.CurrentDispatcherretornará o distribuidor do encadeamento CURRENT - ou seja, se você chamar esse método a partir de um encadeamento que não seja o encadeamento da interface do usuário, o código não será executado no encadeamento da interface do usuário.
BrainSlugs83
@ BrainSlugs83 bom ponto, provavelmente a melhor coisa é que um aplicativo capture uma referência ao despachante de threads da interface do usuário e coloque-a em algum lugar acessível globalmente. Estou surpreso que demorou tanto tempo para alguém perceber isso!
Peter Wone
4

Peter Wone. você é homem. Levando um pouco mais o seu conceito, criei essas duas funções.

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

Coloco essas duas funções no meu aplicativo Formulário e posso fazer chamadas de trabalhadores em segundo plano como este

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

Talvez um pouco preguiçoso, mas não preciso configurar funções executadas pelo trabalhador, o que é super útil em 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));
}

Essencialmente, obtenha alguns endereços IP de um gui DataGridView, faça ping neles, defina os ícones resultantes para verde ou vermelho e reative os botões no formulário. Sim, é um "paralelo.para" em um trabalhador em segundo plano. Sim, é muita sobrecarga de chamada, mas é insignificante para listas curtas e código muito mais compacto.

rocketsarefast
fonte
1

Tentei construir isso com base na resposta de @Andrey Naumov . Pode ser que isso seja uma ligeira melhora.

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

Onde type parameter Sé o parâmetro formal (o parâmetro de entrada, que é mínimo necessário para inferir o restante dos tipos). Agora você pode chamar assim:

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

Você pode ter sobrecargas adicionais para Action<S>e da Expression<Action<S>>mesma forma na mesma classe. Por outro construído em tipos de delegado e de expressão, você terá que escrever classes separadas como Lambda, Lambda<S, T>, Lambda<S, T, U>etc.

Vantagem disso eu vejo sobre a abordagem original:

  1. Uma especificação de tipo a menos (somente o parâmetro formal precisa ser especificado).

  2. O que lhe dá a liberdade de usá-lo contra qualquer um Func<int, T>, não apenas quando Té dito string, como mostra os exemplos.

  3. Suporta expressões imediatamente. Na abordagem anterior, você precisará especificar tipos novamente, 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 expressões.

  4. Estender a classe para outros tipos de delegados (e expressões) é igualmente complicado como acima.

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

Na minha abordagem, você deve declarar tipos apenas uma vez (menos um para Funcs).


Outra maneira de implementar a resposta de Andrey é como não ser totalmente 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;
}

Então, as coisas se reduzem a:

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

Isso é ainda menos digitado, mas você perde certo tipo de segurança e , imo, isso não vale a pena.

nawfal
fonte
1

Um pouco atrasado para a festa, mas você também pode transmitir assim

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

Jogando com XUnit e Fluent Assertions , foi possível usar esse recurso embutido de uma maneira que eu acho muito legal.

Antes

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

Depois de

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