【问题标题】:Problem with loop optimization or closure of lambda?循环优化或关闭 lambda 的问题?
【发布时间】:2011-09-15 16:20:57
【问题描述】:

在下面的方法中,我发送了一个动作枚举,并希望返回一个 ICommand 数组,调用 Action<object> 来包装这些动作(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 =&gt; { 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
}

【问题讨论】:

  • 你为什么不直接“foreach”操作?
  • 你打电话给.ToArray()有什么原因吗?看来您可以只遍历 IEnumberable&lt;Action&gt; 并节省一些时间。
  • 它不会改变任何东西,这里有代码通过给出显式索引来测试它是否有效,并且它在这种情况下有效......
  • 你为什么不返回 List 或将命令设为 ICommand[]
  • 我对这些问题有点厌烦了,请尽量保持专注,问题不在于 .toArray() 或 ToList(),循环内的东西是......我会的如果我让它工作,请进行重构

标签: c# .net loops lambda language-features


【解决方案1】:

这个问题每周都会在 StackOverflow 上报告数次。问题是在循环内创建的每个新 lambda 共享 same “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 的当前值”,而不是“写入 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/

更新:来自 cmets:

每个 ICommand 都有相同的方法信息:

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

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

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

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

现在,如果你写了

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

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

现在你为什么看到你的结果有意义吗?

您可能还想阅读:

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

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

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

    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都会关闭一个不同的变量

每个委托都有相同的方法信息,但不同的闭包

我不太确定这些闭包和 lambdas

您正在您的程序中进行高阶函数式编程。 如果你想有机会正确地学习,最好了解“这些闭包和 lambdas”。 没有像现在这样的时间。

【讨论】:

  • 感谢 Eric 的建议,但它似乎不起作用。每个 ICommand 都有 _execute {Method = {Void &lt;CreateCommands&gt;b__0(System.Object)}}。这是我做 foreach 的方式:foreach (var action in actionArray) { Action&lt;object&gt; executeHandler = o =&gt; { action(); }; commands.Add(new RelayCommand(executeHandler)); }
  • 请注意,我没有存储我用执行它的lamba包装它的动作。
  • @Blindy:当你在循环块中声明一个变量时,编译器会神奇地为每次迭代重复使用相同的存储只有当这样做被推断为不可观察时。显然,如果优化可以改变程序的动作,你就不能做这样的优化!当一个变量被关闭时,我们停止尝试优化存储的重用,因为我们不能再保证一个变量的生命周期在下一个变量开始之前结束。
  • @Marino:您编辑过的帖子显示您的第一次修复尝试是错误的,您的第二次尝试是正确的,但比必要的复杂度大约十倍。 这个问题有一个简单的解决方案,就是我一开始说的那个。 如果你打算做高级高阶,你需要很好地理解这些东西是如何工作的函数式编程。
  • @Marino:methodinfo 无关。您似乎有这样的想法,即 methodinfo 与委托身份有关。 没有。您可以拥有一百万个在引用上不相等且具有不同接收者但共享相同底层方法的不同委托。
【解决方案2】:

我刚刚做了一个工作示例来说明您正在尝试做的事情:http://ideone.com/hNcGx

    interface ICommand
    {
        void Print();
    }

    class CommandA : ICommand
    {
        public void Print() { Console.WriteLine("A"); }
    }

    class CommandB : ICommand
    {
        public void Print() { Console.WriteLine("B"); }
    }

    public static void Main()
    {
        var actions = new List<Action>();
        foreach (var command in new ICommand[]{
                    new CommandA(), new CommandB(), new CommandB()})
        {
            var commandcopy = command;
            actions.Add(() => commandcopy.Print());
        }

        foreach (var action in actions)
            action();
    }

输出:

A
B
B

这有帮助吗?

【讨论】:

    【解决方案3】:

    在循环范围内对action进行本地引用。

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

    【讨论】:

    • 你认为o =&gt; { myAction(); } 对吗?但不幸的是它没有改变:/仍然得到调用相同lambda的两个命令......
    【解决方案4】:

    您只使用过 actionArray 数组中的第一项。

    commands.Add(new RelayCommand(o => { actionArray[0](); }));
    

    您需要遍历操作集合。

    例如

    public static ICommand[] CreateCommands(IEnumerable<Action> actions)
    {
      commands = actions.Select(o => new RelayCommand(o)).ToArray();
    }
    

    代码是写意的,所以可能有一些拼写错误,但应该指出正确的想法。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-26
      • 2016-06-29
      • 2011-07-04
      • 1970-01-01
      • 2016-10-09
      • 2021-12-12
      相关资源
      最近更新 更多