【问题标题】:Can an Action/delegate change it's arguments value?动作/委托可以改变它的参数值吗?
【发布时间】:2012-01-19 15:19:03
【问题描述】:

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

【问题讨论】:

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


    【解决方案1】:

    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&lt;T&gt; 的扩展方法,那么如果您真的需要它,您应该认真考虑它(尤其是当您编写的目的是修改集合时)。你得到的不多,但得到的却很多(比如意想不到的副作用)。我确信有例外,但我坚持这条规则,它对我很有帮助。

    【讨论】:

    • 我同意我想要的是一个 for 循环,但是如果在调用操作时参数按值传递,我将如何使它在扩展方法中工作?看起来没有办法通过引用传递给 ForEach 函数的动作委托来传递值。我想每个人都知道,但我没有意识到 foreach(int num in numbers){ num = 0; } 行不通。为什么 num 是临时副本而不是参考?我有点假设 foreach 只是“for”的语法糖。我想我需要停止做这么多假设了。
    • 我想找出一些方法来编写 ForEach 扩展,以便它可以更改 int 的值。我不认为这是有道理的,但这是我想解决的难题。
    • ForEachWithIndex 的添加使得此后。我对事情的运作方式是错误的。现在我站得更正了,我想。谢谢。
    【解决方案2】:

    因为 num 是一个副本。 就好像你在这样做:

    int i = numbers[0];
    i = 0;
    

    你不会指望这会改变数字[0],对吗?

    【讨论】:

      【解决方案3】:

      因为 int 是一个value type,并作为值参数传递给您的扩展方法。因此,numbers 的副本将传递给您的 ForEach 方法。在BasicForEachTest 方法中初始化的numbers 数组中存储的值永远不会被修改。

      查看 Jon Skeet 的 article 以了解有关值类型和值参数的更多信息。

      【讨论】:

        【解决方案4】:

        我并不是说这个答案中的代码很有用,但是(它有效并且)我认为它说明了您需要什么才能使您的方法有效。参数必须标记为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&lt;&gt;,你必须这样做:

        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 ByRef(带有refout))。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2014-12-10
          • 2017-02-21
          • 2015-09-20
          • 1970-01-01
          • 1970-01-01
          • 2020-02-09
          • 2023-04-04
          • 2011-12-04
          相关资源
          最近更新 更多