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


124

Prenez la méthode System.Windows.Forms.Control.Invoke (méthode Delegate)

Pourquoi cela donne-t-il une erreur 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));

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

Réponses:


125

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

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

...

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

À quoi vous attendriez-vous que le type concret de l'objet auquel se réfère xsoit? Oui, le compilateur peut 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 des erreurs.

Si vous voulez le rendre facile d'appeler Control.Invokeavec Actionla meilleure chose à faire est d' ajouter une méthode d'extension de contrôle:

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

1
Merci - J'ai mis à jour la question car je pense que le terme non tapé était le mauvais terme à utiliser.
xyz le

1
C'est une solution très élégante et mature. Je l'appellerais probablement "InvokeAction" pour que le nom suggère ce que nous invoquons réellement (au lieu d'un délégué générique) mais cela fonctionne certainement pour moi :)
Matthias Hryniszak

7
Je ne suis pas d'accord pour dire que c'est "rarement utile et ...". Dans le cas de l'appel de Begin / Invoke avec un lambda, vous ne vous souciez certainement pas de savoir si le type de délégué est généré automatiquement, nous voulons simplement que l'appel soit effectué. Dans quelle situation une méthode qui accepte un délégué (le type de base) se soucierait-elle du type concret? Aussi, quel est le but de la méthode d'extension? Cela ne facilite rien.
Tergiver

5
Ah! J'ai ajouté la méthode d'extension et essayé Invoke(()=>DoStuff)et j'ai toujours eu l'erreur. Le problème était que j'utilisais le «ceci» implicite. Pour le faire fonctionner à partir d'un membre de contrôle , vous devez être explicite: this.Invoke(()=>DoStuff).
Tergiver

2
Pour tous ceux qui lisent ceci, je pense que la question et les réponses à C #: Automatiser le modèle de code InvokeRequired sont très utiles.
Erik Philips

34

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

3
C'est une belle utilisation des génériques.
Peter Wone

2
Je dois admettre que cela m'a pris un certain temps pour comprendre pourquoi cela fonctionnait. Brillant. Dommage que je n'en ai pas besoin en ce moment.
William

1
Pouvez-vous s'il vous plaît expliquer l'utilisation de ceci? C'est difficile pour moi de comprendre cela? Merci beaucoup.
shahkalpesh

C'est de jamais lire ceci et encore moins de le dire, mais je pense que je préfère cette réponse à celle de Jon Skeet!
Pogrindis

@shahkalpesh ce n'est pas très complexe. Voir les choses de cette façon, la Lambda<T>classe a une méthode de conversion d'identité appelée Cast, qui renvoie tout ce qui est passé ( Func<T, T>). Maintenant , la Lambda<T>est déclarée comme ce Lambda<Func<int, string>>qui signifie que si vous passez un Func<int, string>à la Castméthode, elle retourne en Func<int, string>arrière, car Tdans ce cas Func<int, string>.
nawfal

12

Neuf dixièmes du temps, les gens obtiennent cela parce qu'ils essaient de se rassembler 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 le feriez pas pour les éléments de l'interface utilisateur, mais dans les cas où vous avez besoin de SomeMethod pour bloquer jusqu'à ce qu'il se termine (par exemple, 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é 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 car les choses peuvent s'emmêler: WaitHandles bloque le fil. C'est ce qu'ils sont censés faire. Si vous essayez de marshaler sur le thread d'interface utilisateur pendant qu'il est bloqué , votre application se bloque . Dans ce cas (a) une refactorisation sérieuse est en ordre, 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);

3
Je trouve fascinant que les gens aient le courage de voter contre une réponse simplement parce qu'ils ne la trouvent pas personnellement attrayante. Si c'est faux et que vous le savez, dites ce qui ne va pas. Si vous ne pouvez pas le faire, vous n'avez aucune base pour un vote défavorable. Si c'est épiquement faux, dites quelque chose comme "Baloney. Voir [réponse correcte]" ou peut-être "Pas une solution recommandée, voir [mieux]"
Peter Wone

1
Oui, je suis la frankenthreadstress; mais de toute façon, je ne sais pas pourquoi il a été rejeté; bien que je n'ai pas utilisé le code réel, je pensais que c'était une belle introduction rapide aux invocations croisées de l'interface utilisateur, et il y a des choses auxquelles je n'avais pas vraiment pensé, alors bravo, certainement +1 pour aller au-delà. :) Je veux dire, vous avez donné une bonne méthode rapide pour faire des invocations de délégués; vous donnez une option pour les appels qui doivent être attendus; et vous le suivez avec un bon moyen rapide pour quelqu'un qui est coincé dans UI Thread Hell de récupérer un peu de contrôle. Bonne réponse, je vais dire + <3 aussi. :)
shelleybutterfly

System.Windows.Threading.Dispatcher.CurrentDispatcherretournera le répartiteur du thread ACTUEL - c'est-à-dire que si vous appelez cette méthode à partir d'un thread qui n'est pas le thread d'interface utilisateur, le code ne sera pas exécuté sur le thread d'interface utilisateur.
BrainSlugs83

@ BrainSlugs83 Bon point, la meilleure chose est probablement pour une application de capturer une référence au répartiteur de threads de l'interface utilisateur et de la placer dans un endroit accessible à l'échelle mondiale. Je suis étonné que quelqu'un ait mis autant de temps à le remarquer!
Peter Wone

4

Peter Wone. tu es un homme. En poussant un peu plus loin votre concept, j'ai proposé 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 à partir de 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 besoin de configurer les fonctions 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 un 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 de surcharge, mais c'est négligeable pour les listes courtes et le code beaucoup plus compact.


1

J'ai essayé de me baser sur la réponse de @Andrey Naumov . 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ù le paramètre type Sest le paramètre formel (le paramètre d'entrée, qui est au 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 la même manière dans la même classe. Pour les autres types intégrés de délégués et d' expression, vous devrez écrire des classes séparées 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 quand Tc'est-à-dire string, comme le montrent les exemples.

  3. Prend en charge les expressions immédiatement. Dans l'approche précédente, vous devrez spécifier à nouveau les 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 également fastidieuse, comme 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 ne devez déclarer les types qu'une seule fois (cela aussi un de moins pour Funcs).


Une autre façon de mettre en œuvre la réponse d'Andrey est de ne pas devenir complètement 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;
}

Les choses se réduisent donc à:

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 certains types de sécurité, et à mon avis, cela n'en vaut pas la peine.


1

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

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


0

Jouer avec XUnit et les assertions Fluent 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.");
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.