为什么一个 lambda 表达式作为一个纯委托参数提供时必须强制转换

采用 System.Windows.Forms.Control.Invoke (委托方法)方法

为什么会出现编译时错误:

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

然而,这种做法行之有效:

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

什么时候该方法需要一个普通的“委托”?

68763 次浏览

Lambda 表达式可以转换为委托类型或表达式树,但它必须知道 哪个委托类型。仅仅知道签名是不够的。例如,假设我有:

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


...


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

你希望 x引用的对象的具体类型是什么?是的,编译器 可以生成一个带有适当签名的新委托类型,但这很少有用,并且最终错误检查的机会较少。

如果你想使用 Action调用 Control.Invoke变得容易,最简单的方法就是给 Control 添加一个扩展方法:

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

厌倦了一遍又一遍地抛出小羊羔?

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

十分之九的情况下,人们得到这个结果是因为他们试图编组到 UI 线程上。这就是懒惰的方法:

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

现在它被输入了,问题就解决了(qv Skeet 的回答) ,我们有了这个非常简洁的语法:

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

为了加分,这里还有一个小贴士。对于 UI 内容,您不会这样做,但是在需要 Some Method 阻塞直到完成(例如请求/响应 I/O,等待响应)的情况下,可以使用 等待处理(qv msdn WaitAll,WaitAny,WaitOne)。

注意,AutoResetEvent 是 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
}

最后一个技巧,因为事情可能会变得混乱: WaitHandles 拖延线程。这是他们应该做的。如果您尝试编组到 UI 线程 而你却让它熄火了,您的应用程序将挂起。在这种情况下,(a)需要进行一些严肃的重构,(b)作为一种临时黑客,你可以这样等待:

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

Peter Wone 你是个男人。 把你的概念再深入一点,我想出了这两个函数。

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

我将这两个函数放入我的 Form 应用程序中,然后我就可以像这样从后台工作者那里进行调用

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

也许有点懒,但我不需要设置 worker done 函数, 在这种情况下非常方便

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

实际上,从 gui DataGridView 获取一些 ip 地址,ping 它们,将生成的图标设置为绿色或红色,并在表单上重新启用按钮。是的,它是后台工作者中的“并行 for”。是的,这是一个大量的调用开销,但它可以忽略的短列表,和更紧凑的代码。

我试图建立在 @ Andrey Naumov的答案之上。也许这是一个小小的改进。

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

其中类型参数 S是形式参数(输入参数,这是推断其余类型所需的最小值)。现在你可以这样称呼它:

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

您可以在同一个类中为 Action<S>Expression<Action<S>>进行类似的额外重载。对于内置在委托和表达式类型中的 其他,您必须编写单独的类,如 LambdaLambda<S, T>Lambda<S, T, U>等。

我认为这比最初的方法更有优势:

  1. 少了一个类型规范(只需要指定形式参数)。

  2. 这给了您对任何 Func<int, T>使用它的自由,而不仅仅是当 Tstring时,如示例所示。

  3. 直接支持表达式。在前面的方法中,您必须再次指定类型,如:

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

    表达方式。

  4. 为其他委托(和表达式)类型扩展类同样很麻烦,如上所示。

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

In my approach you have to declare types only once (that too one less for Funcs).


One another way to implement Andrey's answer is like not going fully generic

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

因此,事情简化为:

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

这样打字就更少了,但是会失去某种类型的安全性,以及 IMO 这不值得。

 this.Dispatcher.Invoke((Action)(() => { textBox1.Text = "Test 123"; }));

有点晚了,但你也可以这样演

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

使用 XUnit 和 流利的断言可以以一种我觉得非常酷的方式使用这种内联功能。

之前

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

之后

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

其他的答案在编写的时候是正确的,但是从 C # 10.0(从2021年开始)开始,编译器 可以在这种情况下推断出一个合适的委托类型(像一些 Func<...>Action<...>或生成的委托类型)。

参见 C # 10特性-Lambda 改进

string str = "woop";
Invoke(() => this.Text = str);   // OK in C# 10.0, in this case 'Func<string>' is picked
string str = "woop";
Invoke(() => { this.Text = str; });   // OK in C# 10.0, in this case 'Action' is picked

注释假定签名 Invoke(Delegate method)与您的问题一样。当然,如果方法需要一个特定的(非抽象的)委托类型,C # 将尝试将 lambda 转换为该委托类型,C # 10.0之前的情况也是如此。