Является ли этот метод расширения C # нечистым и, если да, плохим кодом? - PullRequest
4 голосов
/ 29 марта 2009

Я немного изучаю программирование функций, и мне интересно:

1) Если мой метод расширения ForEach чистый? То, как я это называю, похоже, нарушает принцип "не связывайтесь с объектом, который передается", верно?

public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
  foreach ( var item in source )
     action(item);
}


static void Main(string[] args)
{
    List<Cat> cats = new List<Cat>()
    {
        new Cat{ Purring=true,Name="Marcus",Age=10},
        new Cat{ Purring=false, Name="Fuzzbucket",Age=25 },
        new Cat{ Purring=false, Name="Beanhead",Age=9 },
        new Cat{Purring=true,Name="Doofus",Age=3}
    };


    cats.Where(x=>x.Purring==true).ForEach(x =>
    {
        Console.WriteLine("{0} is a purring cat... purr!", x.Name);
    });

    // *************************************************
    //  Does this code make the extension method impure?
    // *************************************************
    cats.Where(x => x.Purring == false).ForEach(x =>
    {
        x.Purring = true; // purr,baby
    });

    // all the cats now purr
    cats.Where(x=>x.Purring==true).ForEach(x =>
    {
        Console.WriteLine("{0} is a purring cat... purr!", x.Name);
    });
}

public class Cat {
        public bool Purring;
        public string Name;
        public int Age;
}

2) Если это нечисто, это плохой код? Я лично думаю, что он делает код более чистым, чем старый foreach ( var item in items) { blah; }, но я беспокоюсь, что, поскольку он может быть нечистым, он может создать беспорядок.

3) Будет ли плохой код, если он вернет IEnumerable<T> вместо void? Я бы сказал, пока это нечисто, да, это был бы очень плохой код, так как он поощрял бы цепочку чего-то, что изменило бы цепочку. Например, это плохой код?

// possibly bad extension
public static IEnumerable<T> ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
    foreach ( var item in source )
        action(item);

    return source;

}

Ответы [ 4 ]

12 голосов
/ 29 марта 2009

Примесь не обязательно означает плохой код. Многим людям легко и полезно использовать побочные эффекты для решения проблемы. Сначала нужно знать, как сделать это чисто, чтобы вы знали, когда подходит примесь:).

.NET не имеет понятия чистоты в системе типов, поэтому «чистый» метод, который принимает произвольные делегаты, всегда может быть нечистым, в зависимости от того, как он вызывается. Например, «Где», или «фильтр», обычно считается чистой функцией, поскольку она не изменяет свои аргументы и не изменяет глобальное состояние.

Но ничто не мешает вам поместить такой код в аргумент Where. Например:

things.Where(x => { Console.WriteLine("um?"); 
                    return true; })
      .Count();

Так что это определенно нечистое использование Where. Перечислимые могут делать все, что они хотят, когда они повторяются.

Ваш код плох? Нет. Использование цикла foreach столь же «нечисто» - вы все еще модифицируете исходные объекты. Я пишу такой код все время. Соедините несколько элементов select, filter и т. Д., Затем выполните ForEach для вызова некоторой работы. Вы правы, это чище и проще.

Пример: ObservableCollection. У него нет метода AddRange по какой-то причине. Итак, если я хочу добавить к этому кучу вещей, что мне делать?

foreach(var x in things.Where(y => y.Foo > 0)) { collection.Add(x)); } 

или

things.Where(x => x.Foo > 0).ForEach(collection.Add);

Я предпочитаю второй. Как минимум, я не вижу, как это может быть истолковано как худшее, чем первый способ.

Когда это плохой код? Когда это делает побочный эффект кода в месте, которое не ожидается. Это случай для моего первого примера с использованием Где. И даже тогда бывают случаи, когда область действия очень ограничена и использование очевидно.

Цепочка ForEach

Я написал код, который делает подобные вещи. Чтобы избежать путаницы, я бы дал ему другое имя. Основная путаница заключается в том, «это сразу оценивают или лень?». ForEach подразумевает, что он сразу же выполнит цикл. Но что-то, возвращающее IEnumerable, подразумевает, что элементы будут обрабатываться по мере необходимости. Поэтому я бы предложил дать ему другое имя («Process», «ModifySeq», «OnEach» ... как-то так) и сделать его ленивым:

public static IEnumerable<T> OnEach(this IEnumerable<T> src, Action<T> f) {
  foreach(var x in src) {
    f(x);
    yield return x;
  }
}
6 голосов
/ 29 марта 2009

Он не чистый, так как может вызывать нечистые методы. Я думаю, что в соответствии с типичными определениями чистота является транзитивным замыканием - функция является чистой только в том случае, если все функции, которые она вызывает (прямо или косвенно), также являются чистыми, или если эффекты этих функций инкапсулированы (например, они изменяют только неэкранирующие). локальная переменная).

4 голосов
/ 29 марта 2009

Да, это не чисто, но это спорный вопрос, поскольку это даже не функция.

Поскольку метод ничего не возвращает, единственная возможность для него вообще что-либо сделать - это воздействовать на объекты, которые вы отправляете, или воздействовать на что-то не связанное (например, запись в окно консоли).

Edit:
Ответить на ваш третий вопрос; да, это плохой код, поскольку кажется, что он делает то, чего не делает. Метод возвращает коллекцию, поэтому он кажется чистым, но поскольку он просто возвращает отправленную коллекцию, он на самом деле не более чистый, чем первая версия. Чтобы иметь какой-либо смысл, метод должен использовать делегат Func<T,T> для использования в качестве преобразования и возвращать коллекцию преобразованных элементов:

public static IEnumerable<T> ForEach<T>(this IEnumerable<T> source, Func<T,T> converter) {
   foreach (T item in source) {
      yield return converter(item);
   }
}

Конечно, все еще зависит от функции конвертера, если вызов расширения является чистым. Если он не делает копию элемента ввода, а просто изменяет его и возвращает его, вызов все еще не является чистым.

3 голосов
/ 29 марта 2009

Действительно, поскольку ваше лямбда-выражение содержит присваивание, функция теперь по определению является нечистой. Независимо от того, связано ли присвоение с одним из аргументов или с другим объектом, определенным вне текущей функции ... Функция не должна иметь побочных эффектов, чтобы ее можно было вызвать pure . См. Википедия для более точного (хотя и довольно простого) определения, в котором подробно описываются два условия, которым должна удовлетворять функция, чтобы считаться pure (одним из них является отсутствие побочных эффектов). Я полагаю, что лямбда-выражения, как правило, предназначены для использования в качестве чистых функций (по крайней мере, я бы предположил, что они изначально изучались как таковые с математической точки зрения), хотя ясно, что в C # это не так строго, как в чисто функциональных языках. Так что это, вероятно, неплохая практика, хотя определенно стоит быть в стороне, потому что такая функция нечиста.

...