У меня есть метод расширения строки C #, который должен возвращать IEnumerable<int>
все индексы подстроки в строке. Он отлично работает по своему назначению, и ожидаемые результаты возвращаются (что доказано одним из моих тестов, хотя и не приведенным ниже), но другой модульный тест обнаружил проблему с ним: он не может обрабатывать нулевые аргументы.
Вот метод расширения, который я тестирую:
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;
}
}
Вот тест, который выявил проблему:
[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Extensions_AllIndexesOf_HandlesNullArguments()
{
string test = "a.b.c.d.e";
test.AllIndexesOf(null);
}
Когда тест запускается с моим методом расширения, он терпит неудачу со стандартным сообщением об ошибке, что метод «не создал исключение».
Это сбивает с толку: я явно перешел null
в функцию, но по какой-то причине сравнение null == null
возвращается false
. Таким образом, исключение не создается, и код продолжается.
Я подтвердил, что это не ошибка теста: при запуске метода в моем основном проекте с вызовом Console.WriteLine
в if
блоке сравнения с нулевым значением ничего не отображается на консоли, и ни один из catch
добавляемых блоков не перехватывает исключение . Кроме того, использование string.IsNullOrEmpty
вместо == null
имеет ту же проблему.
Почему это якобы простое сравнение терпит неудачу?
источник
Ответы:
Вы используете
yield return
. При этом компилятор перепишет ваш метод в функцию, которая возвращает сгенерированный класс, реализующий конечный автомат.Вообще говоря, он переписывает локальные переменные в поля этого класса, и каждая часть вашего алгоритма между
yield return
инструкциями становится состоянием. Вы можете проверить с помощью декомпилятора, чем становится этот метод после компиляции (обязательно отключите интеллектуальную декомпиляцию, которая будет производитьyield return
).Но суть в следующем: код вашего метода не будет выполняться, пока вы не начнете итерацию.
Обычный способ проверки предварительных условий - разделить ваш метод на две части:
Это работает, потому что первый метод будет вести себя так, как вы ожидаете (немедленное выполнение), и вернет конечный автомат, реализованный вторым методом.
Обратите внимание, что вам также следует проверить
str
параметр дляnull
, потому что методы расширений могут вызываться дляnull
значений, поскольку они просто синтаксический сахар.Если вам интересно, что компилятор делает с вашим кодом, вот ваш метод, декомпилированный с помощью dotPeek с использованием параметра Показать код, созданный компилятором .
Это недопустимый код C #, потому что компилятору разрешено делать то, что не разрешено языком, но разрешено в IL - например, именовать переменные таким образом, чтобы избежать конфликтов имен.
Но, как видите,
AllIndexesOf
only создает и возвращает объект, конструктор которого только инициализирует некоторое состояние.GetEnumerator
только копирует объект. Настоящая работа выполняется, когда вы начинаете перечисление (путем вызоваMoveNext
метода).источник
str
параметрnull
, потому что методы расширений могут вызываться дляnull
значений, поскольку они просто синтаксический сахар.yield return
в принципе хорошая идея, но в ней есть так много странных ошибок. Спасибо, что осветили это!MoveNext
вызывается конструкцией под капотомforeach
. Я написал объяснение того, чтоforeach
в моем ответе объясняет семантику коллекции, если вы хотите увидеть точный шаблон.У вас есть блок итератора. Ни один из кодов этого метода никогда не запускается вне вызовов
MoveNext
возвращаемого итератора. При вызове метода конечный автомат регистрируется, но создается, и он никогда не выйдет из строя (за исключением крайних случаев, таких как ошибки нехватки памяти, переполнение стека или исключения прерывания потока).Когда вы действительно попытаетесь повторить последовательность, вы получите исключения.
Вот почему методам LINQ на самом деле нужны два метода, чтобы иметь желаемую семантику обработки ошибок. У них есть частный метод, который является блоком итератора, а затем метод блока без итератора, который ничего не делает, кроме проверки аргумента (так что это можно сделать с нетерпением, а не откладывать), при этом все остальные функции откладываются.
Итак, это общая картина:
источник
Перечислители, как говорили другие, не оцениваются до тех пор, пока они не начнут перечисляться (т. Е. Вызывается
IEnumerable.GetNext
метод). Таким образом, этоне оценивается, пока вы не начнете перечислять, т.е.
источник