c# - 循环优化或 lambda 闭合有问题?

标签 c# .net loops lambda language-features

在下面的方法中,我发送了一个 Action 枚举,并希望返回一个调用 Action<object> 的 ICommand 数组。包装那些 Action (relayCommand 需要)。

问题是,如果我在 for each(甚至是 for 循环)中执行此操作,我得到的命令总是执行参数中传递的第一个操作。

    public static ICommand[] CreateCommands(IEnumerable<Action> actions)
    {
        List<ICommand> commands = new List<ICommand>();

        Action[] actionArray = actions.ToArray();

        // works
        //commands.Add(new RelayCommand(o => { actionArray[0](); }));  // (_execute = {Method = {Void <CreateCommands>b__0(System.Object)}})
        //commands.Add(new RelayCommand(o => { actionArray[1](); }));  // (_execute = {Method = {Void <CreateCommands>b__1(System.Object)}})

        foreach (var action in actionArray)
        {
            // always add the same _execute member for each RelayCommand (_execute = {Method = {Void <CreateCommands>b__0(System.Object)}})
            commands.Add(new RelayCommand(o => { action(); }));
        }

        return commands.ToArray();
    }

似乎 lambda 总是在循环内重用,认为它做的是一样的,但事实并非如此。

我该如何克服这种情况? 我如何强制循环威胁 o => { action(); }总是像新的一样?

谢谢!

我按照建议尝试但没有帮助:

        foreach (var action in actionArray)
        {
            Action<object> executeHandler = o => { action(); };
            commands.Add(new RelayCommand(executeHandler));
        }

似乎对我有用的是:

    class RelayExecuteWrapper
    {
        Action _action;

        public RelayExecuteWrapper(Action action)
        {
            _action = action;
        }

        public void Execute(object o) 
        {
            _action();
        }
    }

/// ...
    foreach (var action in actionArray)
    {
        RelayExecuteWrapper rxw = new RelayExecuteWrapper(action);
        commands.Add(new RelayCommand(rxw.Execute));
    }

RelayCommand 代码:

/// <summary>
/// A command whose sole purpose is to 
/// relay its functionality to other
/// objects by invoking delegates. The
/// default return value for the CanExecute
/// method is 'true'.
/// </summary>
public class RelayCommand : ICommand
{
    #region Fields

    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;        

    #endregion // Fields

    #region Constructors

    /// <summary>
    /// Creates a new command that can always execute.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    public RelayCommand(Action<object> execute)
        : this(execute, null)
    {
    }

    /// <summary>
    /// Creates a new command.
    /// </summary>
    /// <param name="execute">The execution logic.</param>
    /// <param name="canExecute">The execution status logic.</param>
    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        _execute = execute;
        _canExecute = canExecute;           
    }

    #endregion // Constructors

    #region ICommand Members

    [DebuggerStepThrough]
    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }

    #endregion // ICommand Members
}

最佳答案

这个问题每周都会在 StackOverflow 上报告几次。问题是在循环内创建的每个新 lambda 共享相同“ Action ”变量。 lambda 不捕获值,它们捕获变量。也就是说,当你说

List<Action> list = new List<Action>();
foreach(int x in Range(0, 10))
    list.Add( ()=>{Console.WriteLine(x);} );
list[0]();

那当然会打印“10”,因为这是 x now 的值。该操作是“写入 x 的当前值”,而不是“写入创建委托(delegate)时 x 返回的值”。

为了解决这个问题,创建一个新变量:

List<Action> list = new List<Action>();
foreach(int x in Range(0, 10))
{
    int y = x;
    list.Add( ()=>{Console.WriteLine(y);} );
}
list[0]();

由于这个问题很常见,我们正在考虑更改 C# 的下一个版本,以便每次通过 foreach 循环都会创建一个新变量。

参见 http://ericlippert.com/2009/11/12/closing-over-the-loop-variable-considered-harmful-part-one/了解更多详情。

更新:来自评论:

Every ICommand has the same methodinfo:

{ Method = {Void <CreateCommands>b__0(System.Object)}}

是的,当然可以。方法每次都一样。我认为您误解了委托(delegate)创建是什么。这样看。假设你说:

var firstList = new List<Func<int>>() 
{ 
  ()=>10, ()=>20 
};

好的,我们有一个返回整数的函数列表。第一个返回 10,第二个返回 20。

这与:

static int ReturnTen() { return 10; }
static int ReturnTwenty() { return 20; }
...
var firstList = new List<Func<int>>() 
{ ReturnTen, ReturnTwenty };

到目前为止有意义吗?现在我们添加您的 foreach 循环:

var secondList = new List<Func<int>>();
foreach(var func in firstList)
    secondList.Add(()=>func());

好的,是什么意思?这意味着完全相同的事情:

class Closure
{
    public Func<int> func;
    public int DoTheThing() { return this.func(); }
}
...
var secondList = new List<Func<int>>();
Closure closure = new Closure();
foreach(var func in firstList)
{
    closure.func = func;
    secondList.Add(closure.DoTheThing);
}

现在清楚这里发生了什么了吗?每次循环你都不会创建一个新的闭包,你当然不会创建一个新的方法。您创建的委托(delegate)始终指向相同的方法,并且始终指向相同的闭包。

现在,如果你写成

foreach(var loopFunc in firstList)
{
    var func = loopFunc;
    secondList.Add(func);
}

那么我们生成的代码将是

foreach(var loopFunc in firstList)
{
    var closure = new Closure();
    closure.func = loopFunc;
    secondList.Add(closure.DoTheThing);
}

现在列表中的每个新函数都有相同的方法信息——它仍然是 DoTheThing——但有一个不同的闭包

现在您看到结果的原因有意义吗?

您可能还想阅读:

What is the lifetime of a delegate created by a lambda in C#?

另一个更新:来自已编辑的问题:

What I tried as per suggestions, but did not help:

    foreach (var action in actionArray)         
    {
         Action<object> executeHandler = o => { action(); };
         commands.Add(new RelayCommand(executeHandler));         } 
    }

当然这没有帮助。这与以前有完全相同的问题。 问题是 lambda 在单个变量“action”上封闭,而不是在每个 action 值上封闭。在创建 lambda 的地方四处移动显然不能解决这个问题。您要做的是创建一个新变量。您的第二个解决方案通过创建一个引用类型的字段来分配一个新变量。您不需要明确地这样做;正如我上面提到的,如果您在循环体内部创建一个新变量,编译器会为您这样做。

解决这个问题的正确而简便的方法是

    foreach (var action in actionArray)         
    {
         Action<object> copy = action;
         commands.Add(new RelayCommand(x=>{copy();}));
    }

这样您每次通过循环时都会创建一个新变量,因此循环中的每个 lambda 都会关闭一个不同的变量

每个委托(delegate)都有相同的方法信息不同的闭包

I'm not really sure about these closure and lambdas

您正在您的程序中进行高阶函数式编程。 如果您想有任何机会正确地这样做,您最好了解“这些闭包和 lambda”。现在是时候了。

关于c# - 循环优化或 lambda 闭合有问题?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/6360637/

相关文章:

c# - Windows 7 64 位中的递归

c# - 集合<T>RemoveAt 与RemoveItem

c# - 即使在函数中将 returnValue 设置为 False 后, Controller 操作也会执行

c# - 如何从 'Guid' 返回 'Nullable<Guid>' ?

.net - 如何使用 WinDbg 在托管核心转储中找到 N 个最大的字符串?

javascript - 在 for 循环中使用可变编号 "then"进行 Promise

c# - 避免多次嵌套 For Each 循环

c# - 在 Entity Framework 中创建递归实体的正确方法是什么?

MySQL 触发插入文本进入无限循环

java - 我的递归方法无法正确返回 char 出现的总数