Можете ли вы уверенно сказать, что будет выведено на консоль в результате выполнения следующего кода?
Если вы ответили восемь, то, скорее всего, эта заметка не для вас, остальным же предлагаю отправиться вместе со мной в небольшое путешествие в волшебную страну .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, мы снова и снова запускаем энумератор, ведь множества как такового не существует и для того, чтобы предоставить результаты выборки нужно совершить ДЕЙСТВИЕ. В этом причина изменений счетчика в отладчике.
Данный аспект еще принято называть отложенным (или ленивым) выполнением (хоть с точки зрения реализации здесь все выполняется тогда, когда предписано кодом).
Комментарии (111)
SShtole
01.01.2022 19:29+9Познавательно, конечно, спасибо, но если бы меня такое спросили, я бы ответил, что лучше такой код переписать. Ведь что у нас по сути? Во-первых, предикат с побочным эффектом. Во-вторых, лямбда с императивным кодом. И всё это одна сущность!
qw1
01.01.2022 19:58+4Тут учебный пример, чтобы посчитать, сколько раз вызывается оператор сравнения.
Naf2000
01.01.2022 19:54+11Резюмируя:
Метод Where (и многие другие методы-расширения IEnumerable) - ленивый, он строит всего лишь объект, состоящий из пары: исходная последовательность и предикат. Больше ничего он не делает. Построенный объект при своем итерировании будет заставлять итерироваться внуренний и проверять предикат.
Метод First в силу своей специфики будет итерировать последовательность (а значит и исходную) до первого элемента - а для исходной до первого выполнения условия. это 3 итерации.
Метод Last - будет итерировать всю псоледовательность (ну и соответственно исходную тоже) - это 5 итераций.
-
Для каждого получения представления результата Where будет проитерирована вся исходная последовательсть - отсюда инкременты равные 5.
Ничего не упустил?
ARad
01.01.2022 19:58+2Зачем обсуждать в статьях какой именно будет побочный эффект неправильного кода?
qw1
01.01.2022 20:04+4Это просто формализация задачи.
Вопрос мог быть задан так: сколько раз выполнится проверка неравенства (x>0) в кодеvar array = new[] { 0, 0, 1, 0, 1 }; var bytes = array.Where(x => x > 0); bool t = bytes.First() == bytes.Last();
К такой постановке вопроса есть претензии?lair
01.01.2022 20:16+6Есть: "а зачем вы спрашиваете?".
Есть простая мнемоника: никогда не делайте multiple enumeration по enumerable. Она этот вопрос снимает полностью.
pankraty
01.01.2022 21:16Увы не полностью. С единственным вызовом Last() можно ожидать, что сравнение будет одно, т.к. обход будет идти с конца, но легко упустить, что после Where мы имеем дело не с массивом (как вы уже написали ниже), и обход будет полным. Так что пусть пример несколько синтетический, но полезная информация в нём определённо есть, чтобы не попадаться на таких ловушках.
lair
01.01.2022 21:24+5С единственным вызовом Last() можно ожидать, что сравнение будет одно, т.к. обход будет идти с конца
Нет, нельзя этого ожидать.
Enumerable
двигается только вперед.pankraty
01.01.2022 21:27+2Речь про беглый анализ кода, при котором легко ошибочно заключить что x.Last(predicate) и x.Where(predicate).Last() эквивалентны. И то, что несколько человек на этом попались, опровергает тезис "нельзя".
pankraty
01.01.2022 21:29-1Кроме того, это никак р-не противоречит моему доводы о том, что "простая мнемоника никогда не делайте multiple enumeration по enumerable" снимает не все проблемы.
lair
01.01.2022 21:58+3Я и не говорил, что она снимает все проблемы, я говорил, что она снимает вопрос в статье.
lair
01.01.2022 21:58+2Речь про беглый анализ кода, при котором легко ошибочно заключить что x.Last(predicate) и x.Where(predicate).Last() эквивалентны.
Почему ошибочно-то? Надо исходить из того, что они эквивалентны, а все остальное — необязательная оптимизация.
И то, что несколько человек на этом попались, опровергает тезис "нельзя".
Неа, не опровергает. Оно лишь доказывает, что люди имеют какие-то ожидания. Правильные ли это ожидания — вопрос отдельный.
pankraty
01.01.2022 21:59+2Собственно, данная статья и работает в пользу приведения ожиданий людей к реальности.
mayorovp
01.01.2022 21:27+1Учитывая, что там дофигилион оптимизаций...
mvv-rus
01.01.2022 21:54+2… которые по-любому зарубаются использованием побочного эффекта в коде из статьи.
PS А по жизни, согласен с многими предыдущими комментаторами, такой код писать не надо. Но, к сожалению, читать временами приходится и такой код.
sasha1024
02.01.2022 09:51-2Мир не ограничивается одним .NET, есть другие библиотеки с более эффективными аналогами
Enumerable
, поэтому ожидание «с единственным вызовом Last() можно ожидать, что сравнение будет одно, т.к. обход будет идти с конца» у бегло просматривающиего код вполне ожидаемо (извиняюсь за тавтологию).lair
02.01.2022 10:17+5Мир не ограничивается одним .NET
В статье про .NET разумно ожидать, что будет рассматриваться поведение .NET.
sasha1024
02.01.2022 10:50+2Безусловно.
Но:
Пиша на .NET, неразумно писать так, как будто каждый читающий код на 100% знает всю подноготную .NET. Чем интуитивнее код — тем лучше.
Читая статью про подноготную .NET, неразумно автоматически предполагать, что она написана для людей, на 100% знающих подноготную .NET. Таким людям эта статья просто не нужна.
lair
02.01.2022 10:59+3Пиша на .NET, неразумно писать так, как будто каждый читающий код на 100% знает всю подноготную .NET. Чем интуитивнее код — тем лучше.
… и как же вы предлагаете сделать "интуитивнее" в случае кода в статье?
dopusteam
02.01.2022 11:26+4Расскажите, как найти последний элемент перечисления, не перебирая его?
Не массива, не списка или коллекции, а именно перечисления
На всякий случай, вот Вам интерфейс ienumerator
MoveNext возвращает true, если есть следующий элемент и пишет его в Current.
Current возвращает текущий элемент
tyomitch
02.01.2022 12:03Расширив интерфейс, как в docs.oracle.com/javase/7/docs/api/java/util/ListIterator.html или en.cppreference.com/w/cpp/iterator/bidirectional_iterator
dopusteam
02.01.2022 12:30+1Ну я ж сказал, не лист и не массив, а перечисление
qw1
02.01.2022 12:44+1Зачем вы настаиваете на перечислении, если в примере конкретно массив. И библиотеки могут оптимизироваться под тип коллекции, а могут и не оптимизироваться.
dopusteam
02.01.2022 12:54+2Потому что изначально я отреагировал на вот это "есть другие библиотеки с более эффективными аналогами Enumerable"
Более того, сам .net имеет оптимизации для листов и не только для кейса Last, но и для других.
А тут, видимо, предлагается ввести целую иерархию новых интерфейсов
qw1
01.01.2022 22:22+7Есть: «а зачем вы спрашиваете?».
Вопрос на понимание, что происходит под капотом.Есть простая мнемоника: никогда не делайте multiple enumeration по enumerable. Она этот вопрос снимает полностью.
Вы слишком категоричны. Наверняка можно придумать сценарии, в которых повторные проходы по фильтрованному enumerable дешевле, чем например копирование его в List.lair
01.01.2022 22:24+5Вопрос на понимание, что происходит под капотом.
Понимаю: двойное перечисление, которого надо избегать. Сколько при этом будет вызовов лямбды — вопрос неочевидный, и зависит от имплементации.
Наверняка можно придумать сценарии, в которых повторные проходы по фильтрованному enumerable дешевле, чем например копирование его в List.
Это если вы знаете, что ваш Enumerable не упадет от повторного прохода.
(понятно, что из правил есть исключения, но мнемоника на то и мнемоника, чтобы помогать в большинстве случаев)
mvv-rus
02.01.2022 02:09Есть простая мнемоника: никогда не делайте multiple enumeration по enumerable.
А как вы применяете эту мнемонику к рассматриваемой задаче — уточнить можно?
Мне это непонятно. Потому как в этой задаче нет ситуации с созданием нескольких одновременно активных итераторов — той самой, которую описывает мнемоника и которая реально может вызвать проблемы, например, потому что повторный вызов GetEnumerable имеет право вернуть тот же самый итератор, что и при первом вызове.
В этой же задаче такого нет. Мы же все помним, что исполнение методов расширения First и Last для IEnumerable производится немедленно, отложенного исполнения для них не предусмотрено? А потому при вычислении первого из значений по сторонам равенства (не важно в каком порядке) итератор запрашивается, используется и освобождается, и только потом, при вычислении следующего значения, итератор запрашивается повторно, опять используется и освобождается. То есть — никакого multiple enumeration нет.
PS Безотносительно к вашему комментарию. Обращение к IEnumerable<T>.Current после возврата false из IEnumerable<T>.MoveNext мне не нравится: в документации по IEnumerable.Current были слова о том, что значение Current не определено, в документации по IEnumerable<T>.Current этих слов нет, но и обратного утверждения нет тоже.dopusteam
02.01.2022 07:52+2Потому как в этой задаче нет ситуации с созданием нескольких одновременно активных итераторов — той самой, которую описывает мнемоника и которая реально может вызвать проблемы, например, потому что повторный вызов GetEnumerable имеет право вернуть тот же самый итератор, что и при первом вывызове.М
Мнемоника совсем не об одновременно активных итераторах. Вообще, если GetEnumerator возвращает тот же самый итератор, а не каждый раз новый - это плохой итератор, именно для этого разделены интерфейсы IEnumerable и IEnumerator.
А мнемоника как раз о том, что enumerator может лезть в БД или отправлять запросы и делать кучу чего тяжёлого и лучше, без необходимости, этого не делать. Тут ни слова о том, что Вы описали с одновременным использованием enumerator
lair
02.01.2022 10:16+2Потому как в этой задаче нет ситуации с созданием нескольких одновременно активных итераторов — той самой, которую описывает мнемоника
Мнемоника описывает любую ситуацию, когда итератор получается больше одного раза, а не только одновременные.
То есть — никакого multiple enumeration нет.
Как только вы вызвали GetEnumerator больше одного раза, случился multiple enumeration.
sasha1024
02.01.2022 09:46+3никогда не делайте multiple enumeration по enumerable
Копирование в общем случае может обходиться как дешевле, так и дороже повторного использования enumerable, так что Ваше «никогда» слишком категорично.
lair
02.01.2022 10:18+3Задача мнемоники — покрывать большую часть случаев, а не все.
В общем случае повторное использование может быть просто невозможно.
sasha1024
02.01.2022 10:46+1Задача мнемоники — покрывать большую часть случаев, а не все.
А где об этом написано? Насколько я знаю:
Задача мнемоники — упростить запоминание какого-то утверждения. При этом наличие у утверждения мнемоники совершенно не подразумевает каких-либо поблажек к точности утверждения. Т.е. иными словами мнемоника — лишь средство запоминание, а не характеристика точности/правильности утверждения.
Разве Ваша фраза вообще мнемоника?
В общем случае повторное использование может быть просто невозможно.
Ок, уточню: в общем случае использования интерфейса, который позволяет повторное использование. Да, технически в .NET нету отдельных IEnumerableOnce и IRobustEnumerable, но логически-то это всё равно разные вещи. И во многих случаях мы знаем, какой именно «подвид» IEnumerable тут гарантирован.
lair
02.01.2022 10:56+1А где об этом написано?
Это мнемоника, которую я привел, и я использую. Я не говорил, что это общепринятое правило (хотя многие средства статического анализа подсвечивают такое использование как ошибочное).
в общем случае использования интерфейса, который позволяет повторное использование
Если передо мной неизвестный IEnumerable, для меня безопаснее считать, что он не позволяет повторное использование, чем наоброт.
И во многих случаях мы знаем, какой именно «подвид» IEnumerable тут гарантирован.
Как вам, однако, просто жить. Я вот как раз во многих случаях этого не знаю.
qw1
01.01.2022 20:00-1Вообще я думал, что ответ будет 4, т.к. Last должен идти от конца массива, пока не встретит подходящий элемент. Но оказывается, текущая реализация в .NET не такая умная.
Хотя вот здесь получается именно 4:bool t = array.First(x => { linqCounter++; return x > 0; }) == array.Last(x => { linqCounter++; return x > 0; });
lair
01.01.2022 20:17+12Вообще я думал, что ответ будет 4, т.к. Last должен идти от конца массива, пока не встретит подходящий элемент.
После
Where
LINQ уже не знает, массив там или нет.qw1
01.01.2022 22:25+1Это сейчас не знает, а завтра сделают оптимизацию, и Where вернёт не IEnumerable, а IQueryable, и тогда Last сможет искать с конца.
lair
01.01.2022 22:26+5Проблема "завтра сделают оптимизацию" в том, что могут сделать, а могут не сделать, а могут сделать не так, как ожидается. Держать это все в голове обычно не выгодно, проще профилировать конкретные узкие места.
qw1
01.01.2022 22:32Значит, ваше утверждение в общем случае неверно
После Where LINQ уже не знает, массив там или нет.
lair
01.01.2022 22:34+5У нас с вами, видимо, разное понимание "общего случая".
qw1
01.01.2022 22:40Я это понимаю так, что в текущей версии .NET (предположительно) «После Where LINQ уже не знает, массив там или нет», а будущих версиях можно сделать, чтобы знал и оптимизировал (я ниже написал, как).
То есть, общий случай — это некая произвольная версия .NETlair
01.01.2022 22:43+3Ну вот мне как раз не очень интересно обсуждать некую произвольную версию .NET, в которой что-нибудь может быть решили поменять и переделать.
qw1
01.01.2022 22:49Чуть выше вы ссылаетесь на некие общие соображения («зависит от реализации»), то есть вам было лень разбирать конкретную версию. А сейчас вы отстаиваете точку зрения, что в неопределённых спецификацией моментах надо ориентироваться на конкретную версию. Как удобно…
lair
01.01.2022 22:53+4А сейчас вы отстаиваете точку зрения, что в неопределённых спецификацией моментах надо ориентироваться на конкретную версию.
Эээ, я не отстаиваю эту точку зрения. Я как раз считаю, что нужно ориентироваться на описанное поведение (т.е. обычный для enumerable перебор), которое не специфично для версии.
qw1
01.01.2022 22:38Даже можно не уходить на IQueryable, а остаться на IEnumerable. Сделать специализацию для Where для IList, которая вернёт ListEnumerable: IEnumerable, и сделать специализацию для Last, которая для ListEnumerable будет перекрывать реализацию произвольного IEnumerable.
lair
01.01.2022 22:44+1Мммм, вы представляете себе количество труда для такой "специализации"?
(иными словами, как вы думаете, почему оптимизация для
Count
делает проверки наICollection
внутри?)qw1
01.01.2022 22:52Это не снимает моего возражения против ваших утверждений что «После Where LINQ уже не знает». Можно так же генерировать ListEnumerable для источника IList, но проверять на него не в compile-time, а в run-rime.
mayorovp
02.01.2022 12:47На самом деле, не очень много. Все нужные оптимизации уже есть в методе
Last(predicate)
, осталось только научиться автоматически сокращатьWhere(predicate).Last()
до более оптимальной формы.lair
02.01.2022 12:58+1осталось только научиться автоматически сокращать Where(predicate).Last() до более оптимальной формы.
… всего ничего, да.
Впрочем, дело не в этом даже. Дело — для меня — в том, что никогда не знаешь, где уже пора остановиться. Вот мы оптимизировали
Where().Last()
. АSelect().Last()
надо оптимизировать? АWhere().Select().Last()
? АSkip().Select().Last()
?
marshinov
02.01.2022 11:26-3Не сделают, посмотрите интерфейсы IEnunerable и IEnumerator. First и Ladt объявлены именно для IEnumerable. Разная реализация через апкасты нарушит LSP. Не сказать, что IQueryable не нарушает LSP, но это был общий, а не частный случай и осознанный выбор, который оказался верным. Не говоря уже о бесконечных последовательностях.
mayorovp
02.01.2022 11:37+4Но там внутрях уже разная реализация через апкасты! Например, array.Count() не будет перечислять массив, а просто вернёт Length.
Кстати, реализация First и Last нарушать LSP не может в принципе, потому что LSP — требование к иерархии типов, а не к внешним операциям над ними.
marshinov
02.01.2022 12:27-2Но там внутрях уже разная реализация через апкасты! Например, array.Count() не будет перечислять массив, а просто вернёт Length.
Пока вы не добавили предикат
Кстати, реализация First и Last нарушать LSP не может в принципе, потому что LSP — требование к иерархии типов, а не к внешним операциям над ними.
Это терминологическая демагогия. Каким термином вы предлагаете кратко описывать ситуацию, когда метод работает с одним специализированным типом из иерархии, но не работает с другим?
mayorovp
02.01.2022 12:44+3А почему, собственно, он не работает с другим? Работает, но по-другому.
Вы бы вместо теории посмотрели как там на самом деле всё реализовано:
https://github.com/dotnet/runtime/blob/main/src/libraries/System.Linq/src/System/Linq/Last.cs#L64
Как видно, там уже есть отдельные ветки для списков и для внутреннего интерфейса
IPartition<T>
. И вполне возможна ситуация, что семейство классовWhereFooIterator<…>
однажды тоже попадёт в этот список.Собственно, вызов
Last(predicate)
, который вроде как "эквивалентен" обсуждаемомуWhere(predicate).Last()
, для массивов и прочих списков уже работает "с конца"!lair
02.01.2022 13:06+2Что меня в этом коде расстраивает — так это то, что можно внезапно больно удариться.
Вот был у нас класс, не знаю,
Recordset
, и реализовывал онIEnumerable<Record>
. Все было хорошо, ходил в БД, отдавал данные, блаблабла.А потом один программист взял и добавил к этому интерфейсу
IList<Record>
. И все бы ничего, но… начали течь ресурсы.Как так?
mayorovp
02.01.2022 13:22Именно в этом коде вот так удариться можно? Что-то не верится.
Больше походе, что проблема в самом добавлении
IList<Record>
, как-то не вяжется этот интерфейс с кодом, который непосредственно ходит в базу.lair
02.01.2022 13:24+1Именно в этом коде вот так удариться можно? Что-то не верится.
Можно. Имплементаторы
IPartition
иIList
не диспозятся, а стандартный энумератор — диспозится.Я, в принципе, понимаю, почему так сделано, и скорее с этим согласен, чем нет. Но для внешнего имплементатора это может оказаться неожиданностью.
mayorovp
02.01.2022 13:30Но ведь они в этом методе и не создаются, в отличии от энумератора.
Общее правило — кто создавал, тот и диспозит, если не оговорено обратное — соблюдается. Так откуда неожиданности?
lair
02.01.2022 13:31Я же говорю, я понимаю, по какой логике там действовали.
Однако люди, которые запихнули закрытие коннекшна к БД в
Dispose
от итератора, тоже вполне себе руководствовались понятной (и существующей) логикой. И оно вполне себе работало… а теперь сломалось.
mayorovp
02.01.2022 13:39+2Э-э-э, а что бы эти люди делали при множественных итерациях, которые вроде и должны избегаться — но всё ещё разрешены?
Если соединение закрывается в Dispose — значит, оно должно открываться в GetEnumerator и только там. Все остальные способы дают на выходе хрупкий код, и нечего тут на библиотеку пенять когда сами ерунду по-написали.
lair
02.01.2022 13:41+2Э-э-э, а что бы эти люди делали при множественных итерациях, которые вроде и должны избегаться — но всё ещё разрешены?
А ничего. Не разрешены у пользователей (этого кода) множественные итерации.
(это, в отличие от добавления IList, не вымышленный дизайн)
marshinov
02.01.2022 16:48Да, про Last без Where загнался, вы правы. Я имел в виду именно вариант, когда предикат записан отдельным Where, чтобы на выходе IEnumerable был
.Where(predicate).Last();
Я по этому поводу согласен с @lair, что можно словить нежданчик. Может имело смысл сделать отдельную перегрузку для IList, чтобы такие вещи были более явными.
Naf2000
01.01.2022 20:17+4Оно не может идти от конца массива, т.к. оперирует IEnumerable, а для него чтобы узнать, где конец - надо попробовать все. Иногда конца даже может не быть ((
korsetlr473
01.01.2022 21:05+11"Можете ли вы уверенно сказать, что будет выведено на консоль в результате выполнения следующего кода?"
да
marshinov
01.01.2022 21:13Ну вот зачем захватывать что-то в лябмду? Прям иногда это бывает нужно, но очень редко.
mvv-rus
01.01.2022 21:33Ну вот зачем захватывать что-то в лябмду?
По жизни — чтобы снизить число параметров. С точки зрения теории — а как без захвата реализовать на C#, к примеру, каррирование?
Насчет очень редко… Не знаю, как вам, а команде, которая ASP.NET Core разрабатывала, это требовалось отнюдь не редко — в коде инициализации ASP.NET Core таких лямбд полно.
Что реально плохо в C# (с точки зрения использования функционального стиля, настоящим программистам это, наоборот, хорошо) — это то, что переменная захватывается по ссылке и это проиисходит AFAIK молча (не знаю, может есть опция включить на это предупреждение компилятора, но я такой не видел). В результате можно получить очень интересные и неожиданные изменения.marshinov
02.01.2022 11:14На сколько я помню, в asp.net все лямбды захватывают только immutable-переменные, поэтому поведения из статьи в asp.net не наблюдается. Поправьте, если не прав.
lair
02.01.2022 11:20+4На сколько я помню, в asp.net все лямбды захватывают только immutable-переменные
Давайте сначала разберемся только: вы под immutable-переменной понимаете что? Скажем, вот такое — это захват mutable или immutable?
Action Foo(HttpContext ctx) { return () => Bar(ctx); }
pankraty
01.01.2022 21:40+1Так это просто для иллюстрации того, сколько раз выполняется сравнение. Когда коллекция большая, а сравнение медленное, могут возникнуть проблемы, там где их не ждёшь. А захват тут не при чем.
marshinov
02.01.2022 11:19Я не понимаю почему вы говорите про "медленное" и "быстрое" сравнение, если там ещё и multiple enumeration. Вообще пример очень неудачный, потому что в реальности его попросят переписать и "неожиданности" из статьи не будут актуальны
VolodjaT
02.01.2022 17:32+2Чтобы попросить переписать надо понимать чем такое чревато. И по опыту, не всем дотнетчикам это очевидно
lair
01.01.2022 21:59+5Я не знаю, мне это прям регулярно нужно.
marshinov
02.01.2022 11:30Не точно выразился. Вы такие побочные эффекты захватываете, чтобы потом по последовательности несколько раз проитерироваться или вы один раз что-то захватываете и ничего не мутируете / проходите строго один раз по IEnumerable?
lair
02.01.2022 11:31+4Ни то, ни другое. Я захватываю что-то в лямбду, потому что это удобный способ передать что-то в лямбду без кучи лишнего кода. Иногда то, что в нее захвачено, изменяется — потому что, опять же, так удобнее.
Revertis
01.01.2022 21:38-8Можете ли вы уверенно сказать, что будет выведено на консоль
А когда предлог в заменили на на? Ведь всегда было "вывести в консоль" или "вывести на экран". Или это отголоски всяких "погуглить за программирование" и "вспомнил за канал"?
mvv-rus
01.01.2022 21:50+5А когда предлог в заменили на на?
IMHO использование любого из этих предлогов никак не влияет на легкость восприятия статьи.
alex1t
01.01.2022 23:39+2После прочтения статьи можно сделать вывод, что автор просто изначально не знал как работает LINQ и его "отложенное" исполнение и что реальное перечисление будет только после вызова определённых операторов. Из этого также следует и то, что перечисление может быть вызвано в отладчике сколько угодно раз - сколько раз я мышкой наведу на переменную. В общем поздравляю автора с открытием - всё таки он сам докопался до истины. Но всё таки как говорится RTFM - всё это давно расписано и разжёвано.
P.S. Можно было сделать
array.Where(...).ToArray()
и тогда ответ бы был всегда 5, что в выводе, что в отладчике, но может в конкретном случае перечисление заранее не нужно.Kolonist
02.01.2022 00:19+3А почему Вы решили, что автор не знал?
Статья же не для автора, а для читателей. А многие из читателей могли этого не знать (и ряд каментов это доказывает). Особенно новички, которые еще не углублялись в особенности отложенного исполнения при использовании LINQ.
Не все же родились с глубоким знанием всех аспектов .NET. Кто-то только изучает, а кто-то перешел с других языков и действует по аналогии, котороая не всегда работает.
marshinov
02.01.2022 11:31+1Плохо, что новички не знают. Все есть в спеке. На крайняк это очень легко дебажится.
Kolonist
02.01.2022 11:42+1А Вы, когда язык изучали, за день всю спеку прочитали и запомнили? И писали сразу без ошибок? Или, все же, постепенно на собственном опыте и статьям из Интернета постигали все тонкости?
marshinov
02.01.2022 12:16+3Мне было проще: когда я учил C# LINQ'а не было:) Поэтому, когда он появился, то прочитал release notes. Кажется, там про ленивую природу было много написано.
lair
02.01.2022 13:03+2Или, все же, постепенно на собственном опыте и статьям из Интернета постигали все тонкости?
… забавляет меня отсутствие в этом списке, знаете, книг. Которые, кстати, сильно помогают постигать всякие тонкости.
Впрочем, тут дело какое: пока сам десяток раз не врежешься в то, что какие-то вещи работают неинтуитивно (например, вернешь
IEnumerable
изнутриusing
, или, из более свежего, вернешьTask
оттуда же), все равно не запомнишь. И "старшим товарищам", которые говорят, что не надо так делать, все равно не будешь верить. "Ну, у меня ж работает".Kolonist
02.01.2022 13:12Разумеется! В том, что как-то нельзя делать, главное же не то, что нельзя делать, главное - почему это нельзя делать.
Данная статья это вполне объясняет, так что я вообще не понимаю десятки комментаторов, которые пишут: "Такой код заставят переписать", "Так писать нельзя" и т.п.
lair
02.01.2022 13:13А статья где-то объясняет, чего и почему нельзя делать?
Мне показалось, статья только констатирует "у меня тут вот такой код".
Kolonist
02.01.2022 13:15-3Дочитайте до конца.
lair
02.01.2022 13:17-2Дочитал. Ничего кроме
Данный аспект еще принято называть отложенным (или ленивым) выполнением (хоть с точки зрения реализации здесь все выполняется тогда, когда предписано кодом).
не вижу.
Kolonist
02.01.2022 13:22+1Я же не сказал, прочитайте последний абзац, там и другие есть.
А впрочем, бессмысленно кого-то в чем-то убеждать. Количество положительных оценок говорит о том, что такие статьи гораздо более востребованы, чем академические и "сильные технические", хоть в них и нет негативных комментариев.
lair
02.01.2022 13:23-1Я же не сказал, прочитайте последний абзац, там и другие есть.
… ну так может вы цитату приведете?
Kolonist
02.01.2022 13:35+1А, всё ясно, вы и мои комментарии тоже невнимательно читаете. Я же выше написал, что не столько важно, как нельзя, сколько важно, почему нельзя.
И вот на вопрос почему, статья вполне развернуто отвечает, объясняя откуда берется 8, в какой момент происходит вычисление, и почему в отладчике по-другому. Понимая это, читатель уже разберётся, где и как ему использовать LINQ.
lair
02.01.2022 13:37+1На всякий случай повторю свой вопрос выше по треду:
А статья где-то объясняет, чего и почему нельзя делать?
Есть разница между объяснением, что происходит, и объяснением, почему нельзя так делать. Статья второго не делает, и это ее недостаток.
Понимая это, читатель уже разберётся, где и как ему использовать LINQ.
… или не разберется. В этом и проблема.
Kolonist
02.01.2022 13:47-1Статья второго не делает, и это ее недостаток
Вот мы и дошли до сути. Вместо того, чтобы просто написать это автору, у нас тут пара десятков абсолютно неконструктивных комментариев о том, что такой код заставят переписать, потому что потому.
lair
02.01.2022 13:50-1Вместо того, чтобы просто написать это автору
После чтения ответов автора в этой дискуссии я не вижу смысла ему что-то писать.
А читателю полезнее сразу видеть реакцию "не надо так делать" (почему — тоже объяснено в комментах).
Kolonist
02.01.2022 13:58Так вы определитесь, после чтения его ответов или потому что он ещё ничего не ответил?
lair
02.01.2022 13:59-2Похоже, иронию надо было сразу выделять, иначе не понятно.
Окей, прямым текстом: я не вижу смысла ему что-то писать, потому что я не видел никакой его реакции на комментарии в статье.
Kolonist
02.01.2022 14:02+3Так ещё даже суток не прошло, он статью вчера вечером написал.
А теперь он и не напишет, просто чтобы не отхватить минусов, которых ему накидают при любом ответе.
marshinov
02.01.2022 11:44+2Предлагаю автору ещё заменить linq на plinq и в следующей статье раскрыть тайну, почему без interlocked там то 7, то 8, то 9. Если уж стрелять себе в ноги, то до конца:)
dopusteam
Linq тут, в общем то, ни при чём, это специфика IEnumerable\IEnumerator.
Осмелюсь предположить, что заминусуют за низкий технический уровень
WhiteBlackGoose
Честно говоря, превью статьи, встречающая меня с мутирующей лямбдой в linq, довольно оттвергает от чтения. Надеюсь, кому-то все-таки будет полезно...
VolodjaT
Этот мутирующий код для наглядной демонстрации сколько раз делаем одну и ту же работу. Ох и сколько раз видел код типа .Select(x=>new Entity(x.id))
А потом последующие вызовы несколько раз First и удивление девелопера почему два результата first не равны по ссылке