Linq в замочную скважину…

в 14:56, , рубрики: .net, enumerators, foreach, ienumerable, linq, Занимательные задачки

Можете ли вы уверенно сказать, что будет выведено на консоль в результате выполнения следующего кода?

Linq в замочную скважину… - 1

Если вы ответили восемь, то, скорее всего, эта заметка не для вас, остальным же предлагаю отправиться вместе со мной в небольшое путешествие в волшебную страну .Net и поближе взглянуть на ее "магию".

Как же получилось 8? Основное удивление может нас постигнуть, если мы попробуем приоткрыть завесу тайны в отладчике:

Linq в замочную скважину… - 2
Linq в замочную скважину… - 3

Подробно изучая наш bytes в отладчике можно получить и 13 и 23 и т.д.? Что это за чертовщина? :(

Отладчик нам не сильно помог. Быть может все дело в волшебном Where()? Что будет, если мы (страшно представить), рискнем написать свой собственный Where с дженериками и предикатом и ... заменить линковский своим?

Это настолько непосильная задача, что укладывается всего в несколько строк кода:

Linq в замочную скважину… - 4

И всё? А разговоров то было...(с).

Проверили, работает так же прекрасно, т.е. чертовщина как была так и осталась:

Linq в замочную скважину… - 5
Linq в замочную скважину… - 6

И где же спрятался наш волшебный гномик? Получается он где-то в нашем собственном Where.... давайте присмотримся.... Public, static...foreach... Так стоп, а что мы знаем про foreach? Как минимум то, что этот оператор цикла особенный...

Чтобы не тянуть кита за хвост посмотрим на foreach без макияжа сразу на простом примере, развернув С# код с помощью sharplab:

Linq в замочную скважину… - 7

То есть эта штука преобразуется здесь в блок try-finaly и беззастенчиво юзает внутри себя "некий IEnumerator<T>"...

Ну а чем мы хуже?

Linq в замочную скважину… - 8

Форич ликвидирован, код стал более понятным и приятным, наш Where работает так же прекрасно.

И раз уж мы начали заменять методы linq своими поделками, то давайте по-быстренькому заменим методы First() и Last() используемые в примере, кустарщиной:

Ать:

Linq в замочную скважину… - 9

Два:

Linq в замочную скважину… - 10

Проверяем:

Linq в замочную скважину… - 11

Давайте разбираться дальше. Итак, foreach - это синтаксический сахар, использующий то, что делает IEnumerable<T> собой:

Linq в замочную скважину… - 12

Конечно же с этого нужно было начинать... И о чем я раньше думал! Where возвращает IEnumerable<T>... а значит нечто, предоставляющее IEnumerator! Вот он проказник!

Linq в замочную скважину… - 13

И это всё, что предоставляет IEnumerable<T>? Никаих структур данных, никаких множеств, списков или массиво-образных структур...

Неужто в чистом виде у IEnumerable<T> не существует состояния, а есть лишь поведение, выраженное в методе, предоставляющем некий энумератор?

Для самого энумератора состоянием является единственное поле - сurrent! То есть при обращении к энумератору, даже когда нам кажется, что мы имеем дело с неким множеством, в каждый отдельный момент времени это какой-то один элемент (или ни одного).

На абстрактном примере: если мы вытягиваем шарики из мешка по одному, то IEnumerable<T> - это не мешок и не шарики, а процесс (или подход), при котором в руке единовременно оказывается лишь один шарик. Рука в данном случае - энумератор.

Короче говоря, ошибкой является считать, что IEnumerable<T> - это некое статичное множество. И все становится на свои места, если представить, что IEnumerable<T> - это ДЕЙСТВИЕ (запрос).

И всякий раз когда мы к нему обращаемся, мы это действие запускаем. А теперь на нашем примере:

Linq в замочную скважину… - 14

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, мы снова и снова запускаем энумератор, ведь множества как такового не существует и для того, чтобы предоставить результаты выборки нужно совершить ДЕЙСТВИЕ. В этом причина изменений счетчика в отладчике.

Данный аспект еще принято называть отложенным (или ленивым) выполнением (хоть с точки зрения реализации здесь все выполняется тогда, когда предписано кодом).

Автор:
DmitryKublashvili

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js