Проблема с оптимизацией цикла или закрытием лямбда?

В следующем методе я отправляю перечисление действий и хочу, чтобы массив ICommands назад вызывал Action<object>, которые переносят эти действия (необходимые для relayCommand).

Проблема в том, что если я делаю это внутри каждого (или даже цикла 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();
    }

Кажется, что лямбда всегда используется внутри цикла, думая, что она делает то же самое, но это не так.

Как мне преодолеть эту ситуацию? Как я могу заставить цикл к угрозе 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
}

Ответ 1

Эта проблема сообщается несколько раз в неделю в StackOverflow. Проблема в том, что каждая новая лямбда, созданная внутри цикла, имеет одну и ту же переменную "действие". Лямбды не фиксируют значение, они захватывают переменную. То есть, когда вы говорите

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

Так как эта проблема так распространена, мы рассматриваем возможность изменения следующей версии С#, чтобы каждая переменная создавалась каждый раз через цикл foreach.

Подробнее см. http://ericlippert.com/2009/11/12/closing-over-the-loop-variable-considered-harmful-part-one/.

ОБНОВЛЕНИЕ: Из комментариев:

Каждая ICommand имеет тот же методinfo:

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

Теперь каждая новая функция в списке имеет тот же методinfo - это все еще DoTheThing - но другое закрытие.

Теперь понятно, почему вы видите результат?

Вы также можете прочитать:

Каково время жизни делегата, созданного лямбдой в С#?

ДРУГОЕ ОБНОВЛЕНИЕ: Из отредактированного вопроса:

То, что я пробовал в соответствии с предложениями, но не помогло:

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

Конечно, это не помогло. Это имеет ту же проблему, что и раньше. Проблема заключается в том, что лямбда закрыта по единственной переменной "действие", а не по каждому значению действия. Перемещение туда, где создается лямбда, явно не решает эту проблему. Вы хотите создать новую переменную. Ваше второе решение делает это, выделяя новую переменную, создавая поле ссылочного типа. Вам не нужно делать это явно; как я уже упоминал выше, компилятор сделает это для вас, если вы создадите новую переменную внутри тела цикла.

Правильный и короткий способ устранения проблемы -

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

Таким образом, вы каждый раз создаете новую переменную через цикл, и каждая лямбда в цикле поэтому закрывается по другой переменной.

Каждый делегат будет иметь тот же методinfo, но другое закрытие.

Я не уверен в этом закрытии и лямбдах

В вашей программе вы выполняете функциональное программирование более высокого порядка. Вам лучше узнать о "этих закрытиях и лямбдах", если вы хотите иметь все шансы сделать это правильно. Нет времени, как настоящее.

Ответ 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(); }));
}

Ответ 4

Вы используете только первый элемент массива actionArray.

т

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

Вам нужно выполнить итерацию по набору действий.

например,

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

Код является бесплатным, поэтому могут быть некоторые опечатки, но следует указать вам правильную идею.