Mam metodę rozszerzenia ciągu C #, która powinna zwrócić IEnumerable<int>
wszystkie indeksy podciągu w ciągu. Działa idealnie zgodnie z przeznaczeniem, a oczekiwane wyniki są zwracane (co udowodnił jeden z moich testów, choć nie ten poniżej), ale inny test jednostkowy wykrył z nim problem: nie radzi sobie z argumentami zerowymi.
Oto metoda rozszerzenia, którą testuję:
public static IEnumerable<int> AllIndexesOf(this string str, string searchText)
{
if (searchText == null)
{
throw new ArgumentNullException("searchText");
}
for (int index = 0; ; index += searchText.Length)
{
index = str.IndexOf(searchText, index);
if (index == -1)
break;
yield return index;
}
}
Oto test, który zgłosił problem:
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Extensions_AllIndexesOf_HandlesNullArguments()
{
string test = "a.b.c.d.e";
test.AllIndexesOf(null);
}
Gdy test zostanie uruchomiony na mojej metodzie rozszerzającej, kończy się niepowodzeniem i wyświetlany jest standardowy komunikat o błędzie, że metoda „nie zgłosiła wyjątku”.
To jest mylące: wyraźnie przeszedłem null
do funkcji, ale z jakiegoś powodu porównanie null == null
wraca false
. W związku z tym nie jest zgłaszany żaden wyjątek, a kod jest kontynuowany.
Potwierdziłem, że to nie jest błąd testu: podczas uruchamiania metody w moim głównym projekcie z wywołaniem Console.WriteLine
w if
bloku porównania null nic nie jest wyświetlane na konsoli i żaden z catch
dodanych przeze mnie bloków nie wychwytuje żadnego wyjątku . Co więcej, używanie string.IsNullOrEmpty
zamiast == null
ma ten sam problem.
Dlaczego to rzekomo proste porównanie zawodzi?
źródło
Odpowiedzi:
Używasz
yield return
. Robiąc to, kompilator przepisuje twoją metodę do funkcji, która zwraca wygenerowaną klasę, która implementuje maszynę stanów.Mówiąc ogólnie, przepisuje lokalne zmienne do pól tej klasy i każda część algorytmu między
yield return
instrukcjami staje się stanem. Możesz sprawdzić za pomocą dekompilatora, czym ta metoda staje się po kompilacji (upewnij się, że wyłączono inteligentną dekompilację, która przyniosłaby efektyield return
).Ale najważniejsze jest to, że kod twojej metody nie zostanie wykonany, dopóki nie zaczniesz iteracji.
Zwykłym sposobem sprawdzenia warunków wstępnych jest podzielenie metody na dwie części:
To działa, ponieważ pierwsza metoda będzie zachowywać się tak, jak się spodziewasz (natychmiastowe wykonanie) i zwróci maszynę stanu zaimplementowaną przez drugą metodę.
Zauważ, że powinieneś również sprawdzić
str
parametr fornull
, ponieważ metody rozszerzeń mogą być wywoływane nanull
wartościach, ponieważ są one po prostu cukrem składniowym.Jeśli jesteś ciekawy, co kompilator robi z twoim kodem, oto twoja metoda, zdekompilowana za pomocą dotPeek przy użyciu opcji Pokaż kod wygenerowany przez kompilator .
Jest to nieprawidłowy kod C #, ponieważ kompilator może robić rzeczy, których język nie zezwala, ale które są legalne w IL - na przykład nazwanie zmiennych w sposób, w jaki nie można uniknąć kolizji nazw.
Ale jak widać,
AllIndexesOf
jedyny konstruuje i zwraca obiekt, którego konstruktor inicjuje tylko pewien stan.GetEnumerator
kopiuje tylko obiekt. Prawdziwa praca jest wykonywana, gdy zaczynasz wyliczać (przez wywołanieMoveNext
metody).źródło
str
parametr fornull
, ponieważ metody rozszerzeń mogą być wywoływane nanull
wartościach, ponieważ są one po prostu cukrem syntaktycznym.yield return
To w zasadzie fajny pomysł, ale ma wiele dziwnych problemów. Dzięki za wyciągnięcie tego na światło dzienne!MoveNext
jest nazywany pod maską przezforeach
konstrukcję. Napisałem wyjaśnienie, coforeach
w mojej odpowiedzi wyjaśnia semantykę zbioru, jeśli chcesz zobaczyć dokładny wzór.Masz blok iteratora. Żaden kod w tej metodzie nie jest nigdy uruchamiany poza wywołaniami
MoveNext
w zwróconym iteratorze. Wywołanie metody powoduje odnotowanie, ale utworzenie maszyny stanu, a to nigdy nie zawiedzie (poza skrajnościami, takimi jak błędy braku pamięci, przepełnienia stosu lub wyjątki przerywania wątków).Kiedy faktycznie spróbujesz powtórzyć sekwencję, otrzymasz wyjątki.
Dlatego metody LINQ faktycznie potrzebują dwóch metod, aby mieć żądaną semantykę obsługi błędów. Mają prywatną metodę, która jest blokiem iteratora, a następnie metodę blokową bez iteratora, która nie robi nic poza walidacją argumentów (aby można było to zrobić chętnie, a nie odkładać), jednocześnie odraczając wszystkie inne funkcje.
Więc to jest ogólny wzór:
źródło
Enumeratory, jak powiedzieli inni, nie są oceniane, dopóki nie zaczną się wyliczać (tj.
IEnumerable.GetNext
Metoda zostanie wywołana). Tak więc tonie jest oceniany, dopóki nie zaczniesz wyliczać, tj
źródło