web-dev-qa-db-fra.com

Pourquoi une expression lambda doit-elle être convertie lorsqu'elle est fournie en tant que paramètre délégué simple

Prenez la méthode System.Windows.Forms.Control.Invoke (méthode déléguée)

Pourquoi cela donne-t-il une erreur de temps de compilation:

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

Pourtant, cela fonctionne bien:

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

Lorsque la méthode attend un simple délégué?

120
xyz

Une expression lambda peut être convertie en un type délégué ou une arborescence d'expressions - mais elle doit savoir quel type délégué. Le simple fait de connaître la signature ne suffit pas. Par exemple, supposons que j'ai:

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

...

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

Quel serait le type concret de l'objet auquel se réfère x? Oui, le compilateur pourrait générer un nouveau type de délégué avec une signature appropriée, mais c'est rarement utile et vous vous retrouvez avec moins de possibilités de vérification d'erreur.

Si vous souhaitez faciliter l’appel Control.Invoke avec un Action la chose la plus simple à faire est d'ajouter une méthode d'extension à Control:

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

Fatigué de lancer des lambdas encore et encore?

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

Neuf dixièmes du temps, les gens obtiennent cela parce qu'ils essaient de marshaler sur le thread d'interface utilisateur. Voici la manière paresseuse:

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

Maintenant qu'il est tapé, le problème disparaît (réponse de qv Skeet) et nous avons cette syntaxe très succincte:

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

Pour les points bonus, voici une autre astuce. Vous ne feriez pas cela pour les choses de l'interface utilisateur, mais dans les cas où vous avez besoin que SomeMethod se bloque jusqu'à ce qu'il se termine (par exemple, les E/S de demande/réponse, en attente de la réponse), utilisez un WaitHandle (qv msdn WaitAll, WaitAny, WaitOne).

Notez que AutoResetEvent est un dérivé 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
}

Et un dernier conseil parce que les choses peuvent s'emmêler: les WaitHandles bloquent le fil. C'est ce qu'ils sont censés faire. Si vous essayez de marshaler sur le thread d'interface utilisateur alors que vous l'avez bloqué , votre application se bloquera. Dans ce cas (a) une refactorisation sérieuse s'impose, et (b) en tant que hack temporaire, vous pouvez attendre comme ceci:

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

Peter Wone. tu es un homme. En poussant votre concept un peu plus loin, j'ai trouvé ces deux fonctions.

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

Je place ces deux fonctions dans mon application Form et je peux passer des appels avec des travailleurs en arrière-plan comme celui-ci

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

Peut-être un peu paresseux, mais je n'ai pas à configurer les fonctions de travail des travailleurs, ce qui est très pratique dans des cas comme celui-ci

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

Essentiellement, obtenez des adresses IP à partir d'une interface graphique DataGridView, envoyez-leur une requête ping, définissez les icônes résultantes sur vert ou rouge et réactivez les boutons du formulaire. Oui, c'est un "parallel.for" dans un backgroundworker. Oui, c'est BEAUCOUP d'invoquer les frais généraux, mais c'est négligeable pour les listes courtes et le code beaucoup plus compact.

4
rocketsarefast

J'ai essayé de construire ceci sur @ Andrey Naumov 's answer. C'est peut-être une légère amélioration.

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

Où type paramètre S est le paramètre formel (le paramètre d'entrée, qui est minimum requis pour déduire le reste des types). Maintenant, vous pouvez l'appeler comme:

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

Vous pouvez avoir des surcharges supplémentaires pour Action<S> et Expression<Action<S>> de même dans la même classe. Pour autre intégré aux types délégué et expression, vous devrez écrire des classes distinctes comme Lambda, Lambda<S, T>, Lambda<S, T, U> etc.

Avantage de cela, je vois sur l'approche originale:

  1. Une spécification de type en moins (seul le paramètre formel doit être spécifié).

  2. Ce qui vous donne la liberté de l'utiliser contre n'importe quel Func<int, T>, pas seulement lorsque T signifie, string, comme illustré dans les exemples.

  3. Prend en charge les expressions immédiatement. Dans l'approche précédente, vous devrez à nouveau spécifier des types, comme:

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

    pour les expressions.

  4. L'extension de la classe pour d'autres types de délégués (et d'expressions) est tout aussi lourde que ci-dessus.

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

Dans mon approche, vous devez déclarer les types une seule fois (celui-là aussi un de moins pour Funcs).


Une autre façon de mettre en œuvre la réponse d'Andrey est comme ne pas devenir entièrement générique

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

Donc, les choses se réduisent à:

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

C'est encore moins de taper, mais vous perdez une certaine sécurité de type, et imo, cela ne vaut pas la peine.

1
nawfal

Un peu tard pour la fête mais vous pouvez aussi lancer comme ça

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

Jouer avec XUnit et Fluent Assertions il était possible d'utiliser cette capacité en ligne d'une manière que je trouve vraiment cool.

Avant

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

Aprè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.");
}
0
 this.Dispatcher.Invoke((Action)(() => { textBox1.Text = "Test 123"; }));
0
Narottam Goyal