在下面的方法中,我发送一个动作的枚举,并希望返回一个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(); }
总是作为一个新的?
谢谢!
我根据建议尝试了什么,但没有帮助:
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
}
答案 0 :(得分:27)
在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 - 但是不同的闭包。
现在看看你的结果是否有意义?
您可能还想阅读:
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都会关闭不同的变量。
每个代表都有相同的methodinfo ,但是不同的闭包。
我不确定这些闭包和lambdas
您正在程序中执行高阶函数式编程。 如果你想有机会这样做,你最好先了解一下这些“关闭和lambdas”。没有时间像现在这样。
答案 1 :(得分:1)
我刚刚提供了一个确实正在尝试做的工作示例: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
这对你有帮助吗?
答案 2 :(得分:0)
在循环范围内对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(); }));
}
答案 3 :(得分:0)
您只使用actionArray数组中的第一项。
即
commands.Add(new RelayCommand(o => { actionArray[0](); }));
您需要遍历一系列操作。
例如
public static ICommand[] CreateCommands(IEnumerable<Action> actions)
{
commands = actions.Select(o => new RelayCommand(o)).ToArray();
}
代码是徒手的,所以可能是一些错别字,但应该指出正确的想法。