У меня есть следующий метод расширения:
public static IEnumerable<T> Apply<T>(
[NotNull] this IEnumerable<T> source,
[NotNull] Action<T> action)
where T : class
{
source.CheckArgumentNull("source");
action.CheckArgumentNull("action");
return source.ApplyIterator(action);
}
private static IEnumerable<T> ApplyIterator<T>(this IEnumerable<T> source, Action<T> action)
where T : class
{
foreach (var item in source)
{
action(item);
yield return item;
}
}
Он просто применяет действие к каждому элементу последовательности перед его возвратом.
Мне было интересно, должен ли я применить Pure
атрибут (из аннотаций Resharper) к этому методу, и я могу видеть аргументы за и против него.
Плюсы:
- строго говоря, является чистым; просто вызов его в последовательности не изменяет последовательность (она возвращает новую последовательность) или вносит какие-либо наблюдаемые изменения состояния
- вызывать его без использования результата явно ошибка, поскольку он не имеет никакого эффекта, если последовательность не перечислена, поэтому я бы хотел, чтобы Решарпер предупредил меня, если я это сделаю.
Минусы:
- даже если
Apply
сам метод чист, перечисление полученной последовательности будет сделать наблюдаемые изменения состояния (которая является точкой методы). Например,items.Apply(i => i.Count++)
будет изменять значения элементов при каждом перечислении. Так что применение атрибута Pure, вероятно, вводит в заблуждение ...
Что вы думаете? Должен ли я применить атрибут или нет?
c#
pure-function
Томас Левеск
источник
источник
Ответы:
Нет, это не чисто, потому что имеет побочный эффект. Конкретно это зовет
action
на каждый предмет. Кроме того, это не потокобезопасно.Основное свойство чистых функций заключается в том, что он может вызываться любое количество раз и никогда не делает ничего, кроме как возвращать одно и то же значение. Что не ваш случай. Кроме того, быть чистым означает, что вы не используете ничего, кроме входных параметров. Это означает, что он может быть вызван из любого потока в любое время и не вызывает неожиданного поведения. Опять же, это не случай вашей функции.
Кроме того, вы можете ошибаться в одном: чистота функции - это не вопрос плюсов или минусов. Даже одного сомнения, что он может иметь побочный эффект, достаточно, чтобы сделать его не чистым.
Эрик Липперт поднимает хорошую мысль. Я собираюсь использовать http://msdn.microsoft.com/en-us/library/dd264808(v=vs.110).aspx как часть моего контраргумента. Особенно линия
Допустим, мы создаем метод следующим образом:
Во-первых, это предполагает, что
GetEnumerator
это тоже чисто (я не могу найти источник по этому поводу). Если это так, то в соответствии с вышеприведенным правилом мы можем аннотировать этот метод с помощью [Pure], поскольку он изменяет только тот экземпляр, который был создан внутри самого тела. После этого мы можем составить это и тоApplyIterator
, что должно привести к чистой функции, верно?Нет. Эта композиция не чиста, даже когда
Count
иApplyIterator
чиста. Но я, возможно, строю этот аргумент на неправильной предпосылке. Я думаю, что идея о том, что экземпляры, созданные в методе, освобождены от правила чистоты, либо неверна, либо, по крайней мере, недостаточно конкретна.источник
where T : class
, однако, если OP просто выразился,where T : strut
он был бы чистым.sequence.Apply(action)
не имеет побочного эффекта; если это так, укажите побочный эффект, который он имеет. Теперь у звонкаsequence.Apply(action).GetEnumerator().MoveNext()
есть побочный эффект, но мы уже знали это; это изменяет счетчик! Почему следуетsequence.Apply(action)
считать нечистым, потому что призваниеMoveNext
нечисто, ноsequence.Where(predicate)
следует считать чистым?sequence.Where(predicate).GetEnumerator().MoveNext()
это как нечистота.GetEnumerator
, помимо выделения перечислителя в его начальном состоянии?Я не согласен с ответами Эйфорика и Роберта Харви . Абсолютно это чистая функция; проблема в том, что
очень непонятно, что означает первое «это». Если «это» означает одну из этих функций, то это неправильно; ни одна из этих функций не делает этого;
MoveNext
из энумератора последовательности делает это, и она «возвращает» деталь черезCurrent
собственность, не возвращая его.Эти последовательности перечисляются лениво , а не с нетерпением, поэтому, безусловно, дело не в том, что действие применяется до того, как последовательность будет возвращена
Apply
. Действие применяется после того, как последовательность возвращена, еслиMoveNext
вызывается в перечислителе.Как вы заметили, эти функции выполняют действие и последовательность и возвращают последовательность; выход зависит от входа, и никаких побочных эффектов не возникает, так что это чистые функции.
Теперь, если вы создаете перечислитель полученной последовательности и затем вызываете MoveNext на этом итераторе, тогда метод MoveNext не является чистым, поскольку он вызывает действие и вызывает побочный эффект. Но мы уже знали, что MoveNext не был чистым, потому что он мутирует перечислитель!
Теперь, что касается вашего вопроса, вы должны применить атрибут: я бы не применил атрибут, потому что я бы не писал этот метод в первую очередь . Если я хочу применить действие к последовательности, я пишу
что хорошо понятно.
источник
ForEach
метод расширения, который намеренно не является частью Linq, потому что его цель - создавать побочные эффекты ...Any()
со временем подвергается нескольким вызовам ; действие будет выполняться снова и снова, но только по первому пункту! Последовательность должна быть последовательностью значений ; если вы хотите последовательность действий, сделайтеIEnumerable<Action>
.action
, поэтому чистота неaction
имеет значения. Я знаю, что это похоже на вызовыaction
, но этот метод является синтаксическим сахаром для двух методов, один из которых возвращает перечислитель, а другой -MoveNext
перечислителя. Первый явно чистый, а второй явно нет. Посмотрите на это так: вы бы сказали, чтоIEnumerable ApplyIterator(whatever) { return new MyIterator(whatever); }
это чисто? Потому что это функция, которая действительно есть.ApplyIterator
Метод возвращает немедленно . Никакой код в телеApplyIterator
не выполняется до первого вызоваMoveNext
перечислителя возвращаемого объекта. Теперь, когда вы это знаете, вы можете вывести ответ на эту загадку: blogs.msdn.com/b/ericlippert/archive/2007/09/05/… Ответ здесь: blogs.msdn.com/b/ericlippert/archive / 2007/09/06 /…Это не чистая функция, поэтому применение атрибута Pure вводит в заблуждение.
Чистые функции не изменяют исходную коллекцию, и не имеет значения, передаете ли вы действие, которое не имеет никакого эффекта или нет; это все еще нечистая функция, потому что ее цель - вызвать побочные эффекты.
Если вы хотите сделать функцию чистой, скопируйте коллекцию в новую коллекцию, примените изменения, которые Действие выполняет к новой коллекции, и верните новую коллекцию, оставив исходную коллекцию без изменений.
источник
item
это ссылочный тип, он изменяет исходную коллекцию, даже если вы возвращаетесьitem
в итератор. См. Stackoverflow.com/questions/1538301action
может иметь побочные эффекты, кроме изменения переданного ей предмета.()=>{}
конвертируется в Action, и это чистая функция. Его выходы зависят исключительно от его входов и не имеют видимых побочных эффектов.На мой взгляд, тот факт, что он получает действие (а не что-то вроде PureAction) делает его не чистым.
И я даже не согласен с Эриком Липпертом. Он написал это "() => {} можно преобразовать в Action, и это чистая функция. Его выходные данные зависят только от его входных данных и не имеют видимых побочных эффектов".
Хорошо, представьте, что вместо использования делегата ApplyIterator вызывал метод с именем Action.
Если Action является чистым, то ApplyIterator также является чистым. Если действие не является чистым, то ApplyIterator не может быть чистым.
Учитывая тип делегата (не фактическое заданное значение), у нас нет гарантии, что он будет чистым, поэтому метод будет вести себя как чистый метод только тогда, когда делегат чистый. Итак, чтобы сделать его действительно чистым, он должен получить чистый делегат (и он существует, мы можем объявить делегат как [Pure], чтобы мы могли иметь PureAction).
Объясняя это по-разному, метод Pure всегда должен давать один и тот же результат при одинаковых входных данных и не должен генерировать наблюдаемых изменений. ApplyIterator может быть предоставлен один и тот же источник и делегат дважды, но, если делегат меняет ссылочный тип, следующее выполнение даст другие результаты. Пример: делегат делает что-то вроде item.Content + = "Changed";
Итак, используя ApplyIterator над списком «строковых контейнеров» (объект со свойством Content типа string), мы можем получить следующие исходные значения:
После первого выполнения список будет иметь это:
И это в третий раз:
Таким образом, мы меняем содержимое списка, потому что делегат не является чистым, и никакая оптимизация не может быть выполнена, чтобы избежать выполнения вызова 3 раза, если вызов вызван 3 раза, поскольку каждое выполнение будет генерировать различный результат.
источник