Action / delegate可以改变它的参数值吗?

时间:2011-12-13 18:48:18

标签: c# linq foreach extension-methods anonymous-function

在测试简单的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?

4 个答案:

答案 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}}))传递到循环内体。