在测试简单的ForEach
扩展方法时,我遇到了意想不到的结果。
ForEach
方法
public static void ForEach<T>(this IEnumerable<T> list, Action<T> action)
{
if (action == null) throw new ArgumentNullException("action");
foreach (T element in list)
{
action(element);
}
}
Test
方法
[TestMethod]
public void BasicForEachTest()
{
int[] numbers = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
numbers.ForEach(num =>
{
num = 0;
});
Assert.AreEqual(0, numbers.Sum());
}
为什么numbers.Sum()
等于55而不是0?
答案 0 :(得分:5)
num
是您要迭代的当前元素的值的副本。所以你只是在改变副本。
你所做的基本上是这样的:
foreach(int num in numbers)
{
num = 0;
}
当然你不希望这会改变数组的内容吗?
修改:您想要的是:
for (int i in numbers.Length)
{
numbers[i] = 0;
}
在您的特定情况下,您可以在ForEach
扩展方法中维护一个索引,并将其作为第二个参数传递给该操作,然后像这样使用它:
numbers.ForEachWithIndex((num, index) => numbers[index] = 0);
但是一般情况下:创建修改应用它们的集合的Linq样式扩展方法是坏样式(IMO)。如果你编写了一个无法应用于IEnumerable<T>
的扩展方法,那么如果你真的需要它,你应该认真思考它(特别是当你打算修改集合时)。你没有太大的收获,但很多东西松散(像意想不到的副作用)。我确信有例外,但我坚持这条规则并且它对我有利。
答案 1 :(得分:1)
因为num是副本。 就好像你这样做:
int i = numbers[0];
i = 0;
你不会期望改变数字[0],不是吗?
答案 2 :(得分:0)
因为 int 是value type并且作为值参数传递给您的扩展方法。因此,numbers
的副本会传递给您的ForEach
方法。存储在numbers
方法中初始化的BasicForEachTest
数组中的值永远不会被修改。
由Jon Skeet检查此article以阅读有关值类型和值参数的更多信息。
答案 3 :(得分:0)
我并不是说这个答案中的代码是有用的,但是(它有效)我认为它说明了为了使你的方法有效所需要的。参数必须标记为ref
。 BCL没有ref
的委托类型,所以只需编写自己的(不在任何类中):
public delegate void MyActionRef<T>(ref T arg);
这样,你的方法就变成了:
public static void ForEach2<T>(this T[] list, MyActionRef<T> actionRef)
{
if (actionRef == null)
throw new ArgumentNullException("actionRef");
for (int idx = 0; idx < list.Length; idx++)
{
actionRef(ref list[idx]);
}
}
现在,请记住在测试方法中使用ref
关键字:
numbers.ForEach2((ref int num) =>
{
num = 0;
});
这是有效的,因为可以传递数组条目ByRef(ref
)。
如果你想扩展IList<>
,你必须这样做:
public static void ForEach3<T>(this IList<T> list, MyActionRef<T> actionRef)
{
if (actionRef == null)
throw new ArgumentNullException("actionRef");
for (int idx = 0; idx < list.Count; idx++)
{
var temp = list[idx];
actionRef(ref temp);
list[idx] = temp;
}
}
希望这有助于您理解。
注意:我必须使用for
循环。在C#中,在foreach (var x in Yyyy) { /* ... */ }
中,不允许将x
分配给x
(其中包括将ref
ByRef(带out
或{{1}}))传递到循环内体。