Можете ли вы уверенно сказать, что будет выведено на консоль в результате выполнения следующего кода?
Если вы ответили восемь, то, скорее всего, эта заметка не для вас, остальным же предлагаю отправиться вместе со мной в небольшое путешествие в волшебную страну .Net и поближе взглянуть на ее "магию".
Как же получилось 8? Основное удивление может нас постигнуть, если мы попробуем приоткрыть завесу тайны в отладчике:
Подробно изучая наш bytes в отладчике можно получить и 13 и 23 и т.д.? Что это за чертовщина? :(
Отладчик нам не сильно помог. Быть может все дело в волшебном Where()? Что будет, если мы (страшно представить), рискнем написать свой собственный Where с дженериками и предикатом и ... заменить линковский своим?
Это настолько непосильная задача, что укладывается всего в несколько строк кода:
И всё? А разговоров то было...(с).
Проверили, работает так же прекрасно, т.е. чертовщина как была так и осталась:
И где же спрятался наш волшебный гномик? Получается он где-то в нашем собственном Where.... давайте присмотримся.... Public, static...foreach... Так стоп, а что мы знаем про foreach? Как минимум то, что этот оператор цикла особенный...
Чтобы не тянуть кита за хвост посмотрим на foreach без макияжа сразу на простом примере, развернув С# код с помощью sharplab:
То есть эта штука преобразуется здесь в блок try-finaly и беззастенчиво юзает внутри себя "некий IEnumerator<T>"...
Ну а чем мы хуже?
Форич ликвидирован, код стал более понятным и приятным, наш Where работает так же прекрасно.
И раз уж мы начали заменять методы linq своими поделками, то давайте по-быстренькому заменим методы First() и Last() используемые в примере, кустарщиной:
Ать:
Два:
Проверяем:
Давайте разбираться дальше. Итак, foreach - это синтаксический сахар, использующий то, что делает IEnumerable<T> собой:
Конечно же с этого нужно было начинать... И о чем я раньше думал! Where возвращает IEnumerable<T>... а значит нечто, предоставляющее IEnumerator! Вот он проказник!
И это всё, что предоставляет IEnumerable<T>? Никаих структур данных, никаких множеств, списков или массиво-образных структур...
Неужто в чистом виде у IEnumerable<T> не существует состояния, а есть лишь поведение, выраженное в методе, предоставляющем некий энумератор?
Для самого энумератора состоянием является единственное поле - сurrent! То есть при обращении к энумератору, даже когда нам кажется, что мы имеем дело с неким множеством, в каждый отдельный момент времени это какой-то один элемент (или ни одного).
На абстрактном примере: если мы вытягиваем шарики из мешка по одному, то IEnumerable<T> - это не мешок и не шарики, а процесс (или подход), при котором в руке единовременно оказывается лишь один шарик. Рука в данном случае - энумератор.
Короче говоря, ошибкой является считать, что IEnumerable<T> - это некое статичное множество. И все становится на свои места, если представить, что IEnumerable<T> - это ДЕЙСТВИЕ (запрос).
И всякий раз когда мы к нему обращаемся, мы это действие запускаем. А теперь на нашем примере:
1 - формируем способ выполнения действия (запрос). Это еще не само действие, а только его определение.
2 - метод MyFirst() вызывает действие (обращается к нашему IEnumerable<T>) , которое выполняется ровно до момента, пока это действие методу необходимо, то есть до нахождения единицы. Здесь работает два энумератора. Энумератор метода MyFirst() ожидает предоставления элемента от энумератора IEnumerable<T> bytes. Данный энумератор делает MoveNext() 3 раза, находит первый элемент (1) и отдает его энумератору метода MyFirst(), после чего метод MyFirst() возвращает значение, завершается и потребности во втором энумераторе далее не испытывает. С этого момента действие IEnumerable<T> bytes с точки зрения его инициатора (MyFirst()) прекращается и второй энумератор получает свой Dispose(). Cчетчик на данном шаге инкрементируется до 3.
3 - метод MyLast() вызывает действие (обращается к нашему IEnumerable<T>) , которое выполняется ровно до момента, пока это действие методу необходимо... (что-то подобное выше мы уже проходили)... то есть до нахождения двойки. Здесь также работает два энумератора. Энумератор метода MyLast() вызывает свой MoveNext() два раза (так как всего два элемента соответствуют предикату). В первый раз это вынудит второй энумератор совершить MoveNext() 3 раза до нахождения единицы. Cчетчик инкрементируется с 3 до 6.
По второму запросу первого энумератора второму энумератору придется совершить еще два MoveNext() до того момента пока он не дойдет с 3 элемента массива до 5 (до конца). Здесь счетчик инкрементируется с 6 до 8.
Волшебство в отладчике объясняется тем, что всякий раз, когда мы пытаемся увидеть результирующие значения IEnumerable<T> bytes щелкая мышкой по ResultsView, мы снова и снова запускаем энумератор, ведь множества как такового не существует и для того, чтобы предоставить результаты выборки нужно совершить ДЕЙСТВИЕ. В этом причина изменений счетчика в отладчике.
Данный аспект еще принято называть отложенным (или ленивым) выполнением (хоть с точки зрения реализации здесь все выполняется тогда, когда предписано кодом).