Может ли действие / делегат изменить значение аргументов? - PullRequest
5 голосов
/ 13 декабря 2011

Я столкнулся с неожиданным результатом при тестировании простого метода расширения 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 ]

5 голосов
/ 13 декабря 2011

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 голос
/ 13 декабря 2011

Потому что num - это копия.Это как если бы вы делали это:

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

Вы не ожидаете, что это изменит числа [0], не так ли?

0 голосов
/ 28 ноября 2014

Я не утверждаю, что код в этом ответе полезен, но (он работает и), я думаю, он иллюстрирует то, что вам нужно для того, чтобы ваш подход работал. Аргумент должен быть помечен 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 ByRef (с ref или out)) внутри тела цикла.

0 голосов
/ 13 декабря 2011

Поскольку int является типом значения и передается вашему методу расширения в качестве параметра значения.Таким образом, копия numbers передается в ваш метод ForEach.Значения, хранящиеся в массиве numbers, который инициализируется в методе BasicForEachTest, никогда не изменяются.

Проверьте эту статью от Jon Skeet, чтобы узнать больше о типах значений и параметрах значений.

...