Mam następującą metodę rozszerzenia:
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;
}
}
Po prostu stosuje akcję do każdego elementu sekwencji przed jej zwróceniem.
Zastanawiałem się, czy powinienem zastosować Pure
atrybut (z adnotacji Resharper) do tej metody i widzę argumenty za i przeciw.
Plusy:
- ściśle mówiąc, jest czysty; samo wywołanie go w sekwencji nie zmienia sekwencji (zwraca nową sekwencję) ani nie powoduje zmiany obserwowalnego stanu
- wywołanie go bez użycia wyniku jest oczywiście błędem, ponieważ nie ma żadnego efektu, chyba że sekwencja jest wymieniona, dlatego chciałbym, aby Resharper mnie ostrzegł, jeśli to zrobię.
Cons:
- nawet jeśli
Apply
sama metoda jest czysta, wyliczenie wynikowej sekwencji spowoduje obserwowalne zmiany stanu (co jest celem metody). Na przykładitems.Apply(i => i.Count++)
zmieni wartości elementów za każdym razem, gdy są wyliczane. Zatem zastosowanie atrybutu Pure prawdopodobnie wprowadza w błąd ...
Co myślisz? Czy powinienem zastosować ten atrybut, czy nie?
c#
pure-function
Thomas Levesque
źródło
źródło
Odpowiedzi:
Nie, to nie jest czyste, ponieważ ma efekt uboczny. Konkretnie, wzywa
action
do każdego przedmiotu. Ponadto nie jest wątkowo bezpieczny.Główną właściwością funkcji czystych jest to, że można ją wywoływać dowolną liczbę razy i nigdy nie robi nic poza zwracaniem tej samej wartości. Co nie jest twoją sprawą. Ponadto bycie czystym oznacza, że nie używasz niczego poza parametrami wejściowymi. Oznacza to, że można go wywołać z dowolnego wątku w dowolnym momencie i nie powodować żadnych nieoczekiwanych zachowań. Ponownie, to nie jest przypadek twojej funkcji.
Być może mylisz się co do jednej rzeczy: czystość funkcji nie jest kwestią zalet ani wad. Nawet jedna wątpliwość, że może mieć efekt uboczny, wystarczy, aby nie był czysty.
Eric Lippert podnosi dobry punkt. Będę używał http://msdn.microsoft.com/en-us/library/dd264808(v=vs.110).aspx jako części mojego kontrargumentu. Zwłaszcza linia
Powiedzmy, że tworzymy taką metodę:
Po pierwsze, zakłada się, że
GetEnumerator
to też jest czyste (tak naprawdę nie mogę znaleźć na to żadnego źródła). Jeśli tak, to zgodnie z powyższą regułą możemy opisać tę metodę za pomocą [Pure], ponieważ modyfikuje ona tylko instancję utworzoną w samym ciele. Następnie możemy skomponować to i toApplyIterator
, co powinno dać czystą funkcję, prawda?Ilość ta kompozycja nie jest czysty, nawet gdy oba
Count
iApplyIterator
są czyste. Ale może buduję ten argument na złych przesłankach. Myślę, że idea, że instancje utworzone w ramach tej metody są zwolnione z zasady czystości, jest albo błędna, albo przynajmniej nie dość szczegółowa.źródło
where T : class
, ale jeśli OP po prostuwhere T : strut
to powiedzi , BYŁoby czyste.sequence.Apply(action)
nie ma skutków ubocznych; jeśli tak, podaj efekt uboczny, który ma. Teraz dzwonieniesequence.Apply(action).GetEnumerator().MoveNext()
ma efekt uboczny, ale już to wiedzieliśmy; mutuje moduł wyliczający! Dlaczego należysequence.Apply(action)
uważać go za nieczystego, ponieważ dzwonienieMoveNext
jest nieczyste, asequence.Where(predicate)
za czyste?sequence.Where(predicate).GetEnumerator().MoveNext()
jest równie nieczyste.GetEnumerator
wywoływanie, poza przydzieleniem licznika w jego początkowym stanie?Nie zgadzam się zarówno z odpowiedziami Euforii, jak i Roberta Harveya . Oczywiście jest to czysta funkcja; problemem jest
jest bardzo niejasne, co oznacza pierwsze „to”. Jeśli „to” oznacza jedną z tych funkcji, to nie jest właściwe; żadna z tych funkcji tego nie robi;
MoveNext
z wyliczający sekwencji robi, i it „zwraca” pozycja za pośrednictwemCurrent
nieruchomości, a nie poprzez jego zwrot.Sekwencje te są wyliczane leniwie , nie chętnie, więc z pewnością nie jest tak, że akcja jest wykonywana przed zwróceniem sekwencji
Apply
. Akcja jest stosowana po zwróceniu sekwencji, jeśliMoveNext
zostanie wywołana w module wyliczającym.Jak zauważasz, funkcje te podejmują akcję i sekwencję i zwracają sekwencję; wynik zależy od danych wejściowych i nie występują żadne skutki uboczne, więc są to wyłącznie funkcje.
Teraz, jeśli utworzysz moduł wyliczający wynikową sekwencję, a następnie wywołasz MoveNext na tym iteratorze, wówczas metoda MoveNext nie będzie czysta, ponieważ wywołuje akcję i wywołuje efekt uboczny. Ale już wiedzieliśmy, że MoveNext nie był czysty, ponieważ mutuje moduł wyliczający!
Teraz, jeśli chodzi o twoje pytanie, czy powinieneś zastosować atrybut: Nie zastosowałbym tego atrybutu, ponieważ nie napisałbym tej metody w pierwszej kolejności . Jeśli chcę zastosować akcję do sekwencji, piszę
co jest całkiem jasne.
źródło
ForEach
metoda przedłużania, która celowo nie jest częścią Linq, ponieważ jej celem jest wywoływanie efektów ubocznych ...Any()
z czasem poddane wielokrotnym wywołaniom ; akcja będzie wykonywana raz po raz, ale tylko na pierwszym elemencie! Sekwencja powinna być sekwencją wartości ; jeśli chcesz sekwencji działań, wykonajIEnumerable<Action>
.action
, więc czystość nieaction
ma znaczenia. Wiem, że wygląda na toaction
, że wywołuje , ale ta metoda jest cukrem składniowym dla dwóch metod, jednej, która zwraca moduł wyliczający, i drugiej, która jestMoveNext
mechanizmem wyliczającym. Pierwsza jest wyraźnie czysta, a druga wyraźnie nie jest. Spójrz na to w ten sposób: czy powiedziałbyś, żeIEnumerable ApplyIterator(whatever) { return new MyIterator(whatever); }
to czyste? Ponieważ tak naprawdę jest to funkcja.ApplyIterator
Metoda zwraca natychmiast . Żaden kod w treści nieApplyIterator
jest uruchamiany do czasu pierwszego wywołaniaMoveNext
w module wyliczającym zwróconego obiektu. Teraz, gdy już o tym wiesz, możesz wydedukować odpowiedź na tę zagadkę: blogs.msdn.com/b/ericlippert/archive/2007/09/05/… Odpowiedź znajduje się tutaj: blogs.msdn.com/b/ericlippert/archive / 2007/09/06 /…To nie jest czysta funkcja, więc stosowanie atrybutu Pure wprowadza w błąd.
Czyste funkcje nie modyfikują oryginalnej kolekcji i nie ma znaczenia, czy przekazujesz akcję, która nie ma efektu, czy nie; wciąż jest nieczystą funkcją, ponieważ jej celem jest wywoływanie skutków ubocznych.
Jeśli chcesz, aby funkcja była czysta, skopiuj kolekcję do nowej kolekcji, zastosuj zmiany wprowadzone w Akcji do nowej kolekcji i zwróć nową kolekcję, pozostawiając oryginalną kolekcję bez zmian.
źródło
item
jest typem odniesienia, modyfikuje oryginalną kolekcję, nawet jeśli powracaszitem
w iteratorze. Zobacz stackoverflow.com/questions/1538301action
może mieć skutki uboczne inne niż modyfikacja przekazywanego do niej przedmiotu.()=>{}
jest konwertowany na Action i jest czystą funkcją. Jego wyniki zależą wyłącznie od jego nakładów i nie ma zauważalnych efektów ubocznych.Moim zdaniem fakt otrzymania akcji (a nie czegoś takiego jak PureAction) sprawia, że nie jest ona czysta.
I nawet nie zgadzam się z Erikiem Lippertem. Napisał on: „() => {} można przekształcić w Action i jest to czysta funkcja. Jego wyniki zależą wyłącznie od danych wejściowych i nie ma żadnych zauważalnych efektów ubocznych”.
Wyobraź sobie, że zamiast użyć delegata, ApplyIterator wywoływał metodę o nazwie Action.
Jeśli akcja jest czysta, wówczas ApplyIterator jest również czysty. Jeśli Action nie jest czyste, wówczas ApplyIterator nie może być czysty.
Biorąc pod uwagę typ delegowanego (a nie faktyczną podaną wartość), nie mamy gwarancji, że będzie czysty, więc metoda będzie zachowywać się jak czysta metoda tylko wtedy, gdy delegowany jest czysty. Tak więc, aby było naprawdę czyste, powinien otrzymać czystego delegata (i że istnieje, możemy zadeklarować delegata jako [Pure], abyśmy mogli mieć PureAction).
Wyjaśniając to inaczej, metoda Pure powinna zawsze dawać ten sam wynik przy tych samych danych wejściowych i nie powinna generować obserwowalnych zmian. ApplyIterator może otrzymać dwa razy to samo źródło i delegować, ale jeśli delegat zmienia typ odwołania, następne wykonanie da inne wyniki. Przykład: Delegat robi coś takiego jak item.Content + = "Changed";
Tak więc, stosując ApplyIterator na liście „kontenerów ciągów” (obiekt z właściwością Content typu ciąg znaków), możemy mieć te oryginalne wartości:
Po pierwszym wykonaniu lista będzie miała:
A to trzeci raz:
Tak więc zmieniamy zawartość listy, ponieważ delegat nie jest czysty i nie można przeprowadzić optymalizacji, aby uniknąć wykonania połączenia 3 razy, jeśli zostanie wywołany 3 razy, ponieważ każde wykonanie wygeneruje inny wynik.
źródło