tutorial metodo expressions expresiones expresion c# .net loops lambda language-features

c# - metodo - ¿Problema con la optimización de bucle o cierre de lambda?



linq lambda c# (4)

En el siguiente método, estoy enviando una enumeración de acciones y quiero una matriz de ICommands que llame Action<object> que envuelva esas acciones (necesarias para el comando relay).

El problema es que si hago esto dentro de cada ciclo (o incluso uno) obtengo comandos que siempre ejecutan la primera acción aprobada en los parámetros.

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

Parece que la lambda siempre se reutiliza dentro del ciclo pensando que hace lo mismo, pero no es así.

¿Cómo supero esta situación? ¿Cómo puedo forzar el ciclo a la amenaza o => { action(); } o => { action(); } siempre como uno nuevo?

¡Gracias!

Lo que probé según las sugerencias, pero no me ayudó:

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

Lo que parece funcionar para mí es:

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

Código de 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 }


Haga una referencia local a la action en el alcance del ciclo.

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


Acabo de hacer un ejemplo práctico de lo que estás intentando hacer: 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(); }

Salida:

A B B

¿Esto ayuda?


Solo está utilizando el primer elemento en la matriz actionArray.

es decir

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

Necesita iterar a través de la colección de acciones.

p.ej

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

El código es a mano alzada, por lo que podría tratarse de algunos errores tipográficos, pero debería indicarle la idea correcta.


Este problema se informa varias veces por semana en . El problema es que cada nueva lambda creada dentro del ciclo comparte la misma variable de "acción". Las lambdas no capturan el valor, capturan la variable. Es decir, cuando dices

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

eso por supuesto imprime "10" porque ese es el valor de x ahora . La acción es "escribir el valor actual de x", no "escribir el valor que x tenía cuando se creó el delegado".

Para evitar este problema, crea una nueva variable:

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

Como este problema es tan común, estamos considerando cambiar la próxima versión de C # para que se cree una nueva variable cada vez a través del ciclo foreach.

Consulte http://ericlippert.com/2009/11/12/closing-over-the-loop-variable-considered-harmful-part-one/ para obtener más detalles.

ACTUALIZACIÓN: De los comentarios:

Cada ICommand tiene el mismo methodinfo:

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

Sí, por supuesto que lo hace. El método es el mismo todo el tiempo. Creo que estás malinterpretando lo que es una creación delegada. Míralo de esta manera. Supongamos que dijiste:

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

OK, tenemos una lista de funciones que devuelven Ints. El primero devuelve 10, el segundo devuelve 20.

Esto es lo mismo que:

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

¿Tiene sentido hasta ahora? Ahora agregamos su ciclo foreach:

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

OK, ¿qué significa eso ? Eso significa exactamente lo mismo que:

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

Ahora está claro qué está pasando aquí? Cada vez que pasa el ciclo no crea un nuevo cierre y ciertamente no crea un nuevo método. El delegado que cree siempre apunta al mismo método y siempre al mismo cierre.

Ahora, si en su lugar escribiste

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

entonces el código que generaríamos sería

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

Ahora, cada nueva función en la lista tiene el mismo methodinfo , sigue siendo DoTheThing, pero un cierre diferente .

Ahora, ¿tiene sentido por qué estás viendo tu resultado?

Es posible que también desee leer:

¿Cuál es la vida útil de un delegado creado por un lambda en C #?

OTRA ACTUALIZACIÓN: De la pregunta editada:

Lo que probé según las sugerencias, pero no me ayudó:

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

Por supuesto que eso no ayudó. Eso tiene exactamente el mismo problema que antes. El problema es que la lambda se cierra sobre la variable única ''acción'' y no sobre cada valor de acción. Moverse por donde se crea el lambda obviamente no resuelve ese problema. Lo que quieres hacer es crear una nueva variable . Su segunda solución lo hace asignando una nueva variable haciendo un campo de tipo de referencia. No necesita hacer eso explícitamente; como mencioné anteriormente, el compilador lo hará por usted si crea un nuevo interior variable para el cuerpo del bucle.

La forma correcta y breve para solucionar el problema es

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

De esta forma, crea una nueva variable cada vez que pasa por el ciclo y, por lo tanto, cada lambda en el ciclo cierra una variable diferente .

Cada delegado tendrá la misma información de método pero un cierre diferente .

No estoy realmente seguro acerca de estos cierres y lambdas

Está realizando una programación funcional de orden superior en su programa. Será mejor que aprenda sobre "estos cierres y lambdas" si quiere tener la oportunidad de hacerlo correctamente. No hay tiempo como el presente.