繁体   English   中英

循环优化或lambda闭包的问题?

[英]Problem with loop optimization or closure of lambda?

在下面的方法中,我发送一个动作的枚举,并希望返回一个ICommands数组,调用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 => { action(); } 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));
    }

继电器代码:

/// <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的值 操作是“写入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/

更新:从评论:

每个ICommand都有相同的methodinfo:

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

是的,当然可以。 每次方法都是一样的。 我认为你误解了代表创作是什么。 以这种方式看待它。 假设你说:

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

好的,我们有一个返回int的函数列表。 第一个返回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 - 但是一个不同的闭包

现在,为什么你看到你的结果是有道理的?

您可能还想阅读:

在C#中由lambda创建的委托的生命周期是多少?

另一个更新:从编辑过的问题:

我根据建议尝试了什么,但没有帮助:

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

每个委托将具有相同的methodinfo但具有不同的闭包

我不太确定这些封闭和lambdas

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

我刚刚做了一个确实正在尝试做的工作的例子: 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

这对你有帮助吗?

在循环范围内对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(); }));
}

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

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

您需要遍历操作集合。

例如

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

代码是徒手的,所以可能是一些错别字,但应该指出正确的想法。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM