Комментарии 119
Linq тут, в общем то, ни при чём, это специфика IEnumerable\IEnumerator.
Осмелюсь предположить, что заминусуют за низкий технический уровень
Честно говоря, превью статьи, встречающая меня с мутирующей лямбдой в linq, довольно оттвергает от чтения. Надеюсь, кому-то все-таки будет полезно...
Резюмируя:
Метод Where (и многие другие методы-расширения IEnumerable) - ленивый, он строит всего лишь объект, состоящий из пары: исходная последовательность и предикат. Больше ничего он не делает. Построенный объект при своем итерировании будет заставлять итерироваться внуренний и проверять предикат.
Метод First в силу своей специфики будет итерировать последовательность (а значит и исходную) до первого элемента - а для исходной до первого выполнения условия. это 3 итерации.
Метод Last - будет итерировать всю псоледовательность (ну и соответственно исходную тоже) - это 5 итераций.
Для каждого получения представления результата Where будет проитерирована вся исходная последовательсть - отсюда инкременты равные 5.
Ничего не упустил?
Зачем обсуждать в статьях какой именно будет побочный эффект неправильного кода?
Вопрос мог быть задан так: сколько раз выполнится проверка неравенства (x>0) в коде
var array = new[] { 0, 0, 1, 0, 1 };
var bytes = array.Where(x => x > 0);
bool t = bytes.First() == bytes.Last();
К такой постановке вопроса есть претензии?
Есть: "а зачем вы спрашиваете?".
Есть простая мнемоника: никогда не делайте multiple enumeration по enumerable. Она этот вопрос снимает полностью.
Увы не полностью. С единственным вызовом Last() можно ожидать, что сравнение будет одно, т.к. обход будет идти с конца, но легко упустить, что после Where мы имеем дело не с массивом (как вы уже написали ниже), и обход будет полным. Так что пусть пример несколько синтетический, но полезная информация в нём определённо есть, чтобы не попадаться на таких ловушках.
С единственным вызовом Last() можно ожидать, что сравнение будет одно, т.к. обход будет идти с конца
Нет, нельзя этого ожидать. Enumerable
двигается только вперед.
Речь про беглый анализ кода, при котором легко ошибочно заключить что x.Last(predicate) и x.Where(predicate).Last() эквивалентны. И то, что несколько человек на этом попались, опровергает тезис "нельзя".
Кроме того, это никак р-не противоречит моему доводы о том, что "простая мнемоника никогда не делайте multiple enumeration по enumerable" снимает не все проблемы.
Речь про беглый анализ кода, при котором легко ошибочно заключить что x.Last(predicate) и x.Where(predicate).Last() эквивалентны.
Почему ошибочно-то? Надо исходить из того, что они эквивалентны, а все остальное — необязательная оптимизация.
И то, что несколько человек на этом попались, опровергает тезис "нельзя".
Неа, не опровергает. Оно лишь доказывает, что люди имеют какие-то ожидания. Правильные ли это ожидания — вопрос отдельный.
Учитывая, что там дофигилион оптимизаций...
PS А по жизни, согласен с многими предыдущими комментаторами, такой код писать не надо. Но, к сожалению, читать временами приходится и такой код.
… про которые никогда не знаешь, какие у тебя будут в текущей версии.
Не туда написал
Мир не ограничивается одним .NET, есть другие библиотеки с более эффективными аналогами Enumerable
, поэтому ожидание «с единственным вызовом Last() можно ожидать, что сравнение будет одно, т.к. обход будет идти с конца» у бегло просматривающиего код вполне ожидаемо (извиняюсь за тавтологию).
Мир не ограничивается одним .NET
В статье про .NET разумно ожидать, что будет рассматриваться поведение .NET.
Безусловно.
Но:
Пиша на .NET, неразумно писать так, как будто каждый читающий код на 100% знает всю подноготную .NET. Чем интуитивнее код — тем лучше.
Читая статью про подноготную .NET, неразумно автоматически предполагать, что она написана для людей, на 100% знающих подноготную .NET. Таким людям эта статья просто не нужна.
Расскажите, как найти последний элемент перечисления, не перебирая его?
Не массива, не списка или коллекции, а именно перечисления
На всякий случай, вот Вам интерфейс ienumerator
MoveNext возвращает true, если есть следующий элемент и пишет его в Current.
Current возвращает текущий элемент
Ну я ж сказал, не лист и не массив, а перечисление
Потому что изначально я отреагировал на вот это "есть другие библиотеки с более эффективными аналогами Enumerable"
Более того, сам .net имеет оптимизации для листов и не только для кейса Last, но и для других.
А тут, видимо, предлагается ввести целую иерархию новых интерфейсов
Есть: «а зачем вы спрашиваете?».Вопрос на понимание, что происходит под капотом.
Есть простая мнемоника: никогда не делайте multiple enumeration по enumerable. Она этот вопрос снимает полностью.Вы слишком категоричны. Наверняка можно придумать сценарии, в которых повторные проходы по фильтрованному enumerable дешевле, чем например копирование его в List.
Вопрос на понимание, что происходит под капотом.
Понимаю: двойное перечисление, которого надо избегать. Сколько при этом будет вызовов лямбды — вопрос неочевидный, и зависит от имплементации.
Наверняка можно придумать сценарии, в которых повторные проходы по фильтрованному enumerable дешевле, чем например копирование его в List.
Это если вы знаете, что ваш Enumerable не упадет от повторного прохода.
(понятно, что из правил есть исключения, но мнемоника на то и мнемоника, чтобы помогать в большинстве случаев)
Есть простая мнемоника: никогда не делайте multiple enumeration по enumerable.
А как вы применяете эту мнемонику к рассматриваемой задаче — уточнить можно?
Мне это непонятно. Потому как в этой задаче нет ситуации с созданием нескольких одновременно активных итераторов — той самой, которую описывает мнемоника и которая реально может вызвать проблемы, например, потому что повторный вызов GetEnumerable имеет право вернуть тот же самый итератор, что и при первом вызове.
В этой же задаче такого нет. Мы же все помним, что исполнение методов расширения First и Last для IEnumerable производится немедленно, отложенного исполнения для них не предусмотрено? А потому при вычислении первого из значений по сторонам равенства (не важно в каком порядке) итератор запрашивается, используется и освобождается, и только потом, при вычислении следующего значения, итератор запрашивается повторно, опять используется и освобождается. То есть — никакого multiple enumeration нет.
PS Безотносительно к вашему комментарию. Обращение к IEnumerable<T>.Current после возврата false из IEnumerable<T>.MoveNext мне не нравится: в документации по IEnumerable.Current были слова о том, что значение Current не определено, в документации по IEnumerable<T>.Current этих слов нет, но и обратного утверждения нет тоже.
Потому как в этой задаче нет ситуации с созданием нескольких одновременно активных итераторов — той самой, которую описывает мнемоника и которая реально может вызвать проблемы, например, потому что повторный вызов GetEnumerable имеет право вернуть тот же самый итератор, что и при первом вывызове.М
Мнемоника совсем не об одновременно активных итераторах. Вообще, если GetEnumerator возвращает тот же самый итератор, а не каждый раз новый - это плохой итератор, именно для этого разделены интерфейсы IEnumerable и IEnumerator.
А мнемоника как раз о том, что enumerator может лезть в БД или отправлять запросы и делать кучу чего тяжёлого и лучше, без необходимости, этого не делать. Тут ни слова о том, что Вы описали с одновременным использованием enumerator
Потому как в этой задаче нет ситуации с созданием нескольких одновременно активных итераторов — той самой, которую описывает мнемоника
Мнемоника описывает любую ситуацию, когда итератор получается больше одного раза, а не только одновременные.
То есть — никакого multiple enumeration нет.
Как только вы вызвали GetEnumerator больше одного раза, случился multiple enumeration.
никогда не делайте multiple enumeration по enumerable
Копирование в общем случае может обходиться как дешевле, так и дороже повторного использования enumerable, так что Ваше «никогда» слишком категорично.
Задача мнемоники — покрывать большую часть случаев, а не все.
В общем случае повторное использование может быть просто невозможно.
Задача мнемоники — покрывать большую часть случаев, а не все.
А где об этом написано? Насколько я знаю:
Задача мнемоники — упростить запоминание какого-то утверждения. При этом наличие у утверждения мнемоники совершенно не подразумевает каких-либо поблажек к точности утверждения. Т.е. иными словами мнемоника — лишь средство запоминание, а не характеристика точности/правильности утверждения.
Разве Ваша фраза вообще мнемоника?
В общем случае повторное использование может быть просто невозможно.
Ок, уточню: в общем случае использования интерфейса, который позволяет повторное использование. Да, технически в .NET нету отдельных IEnumerableOnce и IRobustEnumerable, но логически-то это всё равно разные вещи. И во многих случаях мы знаем, какой именно «подвид» IEnumerable тут гарантирован.
А где об этом написано?
Это мнемоника, которую я привел, и я использую. Я не говорил, что это общепринятое правило (хотя многие средства статического анализа подсвечивают такое использование как ошибочное).
в общем случае использования интерфейса, который позволяет повторное использование
Если передо мной неизвестный IEnumerable, для меня безопаснее считать, что он не позволяет повторное использование, чем наоброт.
И во многих случаях мы знаем, какой именно «подвид» IEnumerable тут гарантирован.
Как вам, однако, просто жить. Я вот как раз во многих случаях этого не знаю.
Хотя вот здесь получается именно 4:
bool t =
array.First(x => { linqCounter++; return x > 0; })
== array.Last(x => { linqCounter++; return x > 0; });
Вообще я думал, что ответ будет 4, т.к. Last должен идти от конца массива, пока не встретит подходящий элемент.
После Where
LINQ уже не знает, массив там или нет.
Проблема "завтра сделают оптимизацию" в том, что могут сделать, а могут не сделать, а могут сделать не так, как ожидается. Держать это все в голове обычно не выгодно, проще профилировать конкретные узкие места.
После Where LINQ уже не знает, массив там или нет.
У нас с вами, видимо, разное понимание "общего случая".
То есть, общий случай — это некая произвольная версия .NET
Ну вот мне как раз не очень интересно обсуждать некую произвольную версию .NET, в которой что-нибудь может быть решили поменять и переделать.
А сейчас вы отстаиваете точку зрения, что в неопределённых спецификацией моментах надо ориентироваться на конкретную версию.
Эээ, я не отстаиваю эту точку зрения. Я как раз считаю, что нужно ориентироваться на описанное поведение (т.е. обычный для enumerable перебор), которое не специфично для версии.
Мммм, вы представляете себе количество труда для такой "специализации"?
(иными словами, как вы думаете, почему оптимизация для Count
делает проверки на ICollection
внутри?)
На самом деле, не очень много. Все нужные оптимизации уже есть в методе Last(predicate)
, осталось только научиться автоматически сокращать Where(predicate).Last()
до более оптимальной формы.
осталось только научиться автоматически сокращать Where(predicate).Last() до более оптимальной формы.
… всего ничего, да.
Впрочем, дело не в этом даже. Дело — для меня — в том, что никогда не знаешь, где уже пора остановиться. Вот мы оптимизировали Where().Last()
. А Select().Last()
надо оптимизировать? А Where().Select().Last()
? А Skip().Select().Last()
?
Не сделают, посмотрите интерфейсы IEnunerable и IEnumerator. First и Ladt объявлены именно для IEnumerable. Разная реализация через апкасты нарушит LSP. Не сказать, что IQueryable не нарушает LSP, но это был общий, а не частный случай и осознанный выбор, который оказался верным. Не говоря уже о бесконечных последовательностях.
Но там внутрях уже разная реализация через апкасты! Например, array.Count() не будет перечислять массив, а просто вернёт Length.
Кстати, реализация First и Last нарушать LSP не может в принципе, потому что LSP — требование к иерархии типов, а не к внешним операциям над ними.
Но там внутрях уже разная реализация через апкасты! Например, array.Count() не будет перечислять массив, а просто вернёт Length.
Пока вы не добавили предикат
Кстати, реализация First и Last нарушать LSP не может в принципе, потому что LSP — требование к иерархии типов, а не к внешним операциям над ними.
Это терминологическая демагогия. Каким термином вы предлагаете кратко описывать ситуацию, когда метод работает с одним специализированным типом из иерархии, но не работает с другим?
А почему, собственно, он не работает с другим? Работает, но по-другому.
Вы бы вместо теории посмотрели как там на самом деле всё реализовано:
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()
, для массивов и прочих списков уже работает "с конца"!
Что меня в этом коде расстраивает — так это то, что можно внезапно больно удариться.
Вот был у нас класс, не знаю, Recordset
, и реализовывал он IEnumerable<Record>
. Все было хорошо, ходил в БД, отдавал данные, блаблабла.
А потом один программист взял и добавил к этому интерфейсу IList<Record>
. И все бы ничего, но… начали течь ресурсы.
Как так?
Именно в этом коде вот так удариться можно? Что-то не верится.
Больше походе, что проблема в самом добавлении IList<Record>
, как-то не вяжется этот интерфейс с кодом, который непосредственно ходит в базу.
Именно в этом коде вот так удариться можно? Что-то не верится.
Можно. Имплементаторы IPartition
и IList
не диспозятся, а стандартный энумератор — диспозится.
Я, в принципе, понимаю, почему так сделано, и скорее с этим согласен, чем нет. Но для внешнего имплементатора это может оказаться неожиданностью.
Но ведь они в этом методе и не создаются, в отличии от энумератора.
Общее правило — кто создавал, тот и диспозит, если не оговорено обратное — соблюдается. Так откуда неожиданности?
Я же говорю, я понимаю, по какой логике там действовали.
Однако люди, которые запихнули закрытие коннекшна к БД в Dispose
от итератора, тоже вполне себе руководствовались понятной (и существующей) логикой. И оно вполне себе работало… а теперь сломалось.
Э-э-э, а что бы эти люди делали при множественных итерациях, которые вроде и должны избегаться — но всё ещё разрешены?
Если соединение закрывается в Dispose — значит, оно должно открываться в GetEnumerator и только там. Все остальные способы дают на выходе хрупкий код, и нечего тут на библиотеку пенять когда сами ерунду по-написали.
Да, про Last без Where загнался, вы правы. Я имел в виду именно вариант, когда предикат записан отдельным Where, чтобы на выходе IEnumerable был
.Where(predicate).Last();
Я по этому поводу согласен с @lair, что можно словить нежданчик. Может имело смысл сделать отдельную перегрузку для IList, чтобы такие вещи были более явными.
Оно не может идти от конца массива, т.к. оперирует IEnumerable, а для него чтобы узнать, где конец - надо попробовать все. Иногда конца даже может не быть ((
Замените var на точный тип, и узнаете много нового :)
"Можете ли вы уверенно сказать, что будет выведено на консоль в результате выполнения следующего кода?"
да
Ну вот зачем захватывать что-то в лябмду? Прям иногда это бывает нужно, но очень редко.
Ну вот зачем захватывать что-то в лябмду?
По жизни — чтобы снизить число параметров. С точки зрения теории — а как без захвата реализовать на C#, к примеру, каррирование?
Насчет очень редко… Не знаю, как вам, а команде, которая ASP.NET Core разрабатывала, это требовалось отнюдь не редко — в коде инициализации ASP.NET Core таких лямбд полно.
Что реально плохо в C# (с точки зрения использования функционального стиля, настоящим программистам это, наоборот, хорошо) — это то, что переменная захватывается по ссылке и это проиисходит AFAIK молча (не знаю, может есть опция включить на это предупреждение компилятора, но я такой не видел). В результате можно получить очень интересные и неожиданные изменения.
На сколько я помню, в asp.net все лямбды захватывают только immutable-переменные, поэтому поведения из статьи в asp.net не наблюдается. Поправьте, если не прав.
Так это просто для иллюстрации того, сколько раз выполняется сравнение. Когда коллекция большая, а сравнение медленное, могут возникнуть проблемы, там где их не ждёшь. А захват тут не при чем.
Я не знаю, мне это прям регулярно нужно.
Не точно выразился. Вы такие побочные эффекты захватываете, чтобы потом по последовательности несколько раз проитерироваться или вы один раз что-то захватываете и ничего не мутируете / проходите строго один раз по IEnumerable?
Можете ли вы уверенно сказать, что будет выведено на консоль
А когда предлог в заменили на на? Ведь всегда было "вывести в консоль" или "вывести на экран". Или это отголоски всяких "погуглить за программирование" и "вспомнил за канал"?
После прочтения статьи можно сделать вывод, что автор просто изначально не знал как работает LINQ и его "отложенное" исполнение и что реальное перечисление будет только после вызова определённых операторов. Из этого также следует и то, что перечисление может быть вызвано в отладчике сколько угодно раз - сколько раз я мышкой наведу на переменную. В общем поздравляю автора с открытием - всё таки он сам докопался до истины. Но всё таки как говорится RTFM - всё это давно расписано и разжёвано.
P.S. Можно было сделать array.Where(...).ToArray()
и тогда ответ бы был всегда 5, что в выводе, что в отладчике, но может в конкретном случае перечисление заранее не нужно.
А почему Вы решили, что автор не знал?
Статья же не для автора, а для читателей. А многие из читателей могли этого не знать (и ряд каментов это доказывает). Особенно новички, которые еще не углублялись в особенности отложенного исполнения при использовании LINQ.
Не все же родились с глубоким знанием всех аспектов .NET. Кто-то только изучает, а кто-то перешел с других языков и действует по аналогии, котороая не всегда работает.
Плохо, что новички не знают. Все есть в спеке. На крайняк это очень легко дебажится.
А Вы, когда язык изучали, за день всю спеку прочитали и запомнили? И писали сразу без ошибок? Или, все же, постепенно на собственном опыте и статьям из Интернета постигали все тонкости?
Мне было проще: когда я учил C# LINQ'а не было:) Поэтому, когда он появился, то прочитал release notes. Кажется, там про ленивую природу было много написано.
Или, все же, постепенно на собственном опыте и статьям из Интернета постигали все тонкости?
… забавляет меня отсутствие в этом списке, знаете, книг. Которые, кстати, сильно помогают постигать всякие тонкости.
Впрочем, тут дело какое: пока сам десяток раз не врежешься в то, что какие-то вещи работают неинтуитивно (например, вернешь IEnumerable
изнутри using
, или, из более свежего, вернешь Task
оттуда же), все равно не запомнишь. И "старшим товарищам", которые говорят, что не надо так делать, все равно не будешь верить. "Ну, у меня ж работает".
Разумеется! В том, что как-то нельзя делать, главное же не то, что нельзя делать, главное - почему это нельзя делать.
Данная статья это вполне объясняет, так что я вообще не понимаю десятки комментаторов, которые пишут: "Такой код заставят переписать", "Так писать нельзя" и т.п.
А статья где-то объясняет, чего и почему нельзя делать?
Мне показалось, статья только констатирует "у меня тут вот такой код".
Дочитайте до конца.
Дочитал. Ничего кроме
Данный аспект еще принято называть отложенным (или ленивым) выполнением (хоть с точки зрения реализации здесь все выполняется тогда, когда предписано кодом).
не вижу.
Я же не сказал, прочитайте последний абзац, там и другие есть.
А впрочем, бессмысленно кого-то в чем-то убеждать. Количество положительных оценок говорит о том, что такие статьи гораздо более востребованы, чем академические и "сильные технические", хоть в них и нет негативных комментариев.
Я же не сказал, прочитайте последний абзац, там и другие есть.
… ну так может вы цитату приведете?
Мне пол статьи в комментарий засунуть?
Нет, конкретное место, где написано, как делать нельзя.
А, всё ясно, вы и мои комментарии тоже невнимательно читаете. Я же выше написал, что не столько важно, как нельзя, сколько важно, почему нельзя.
И вот на вопрос почему, статья вполне развернуто отвечает, объясняя откуда берется 8, в какой момент происходит вычисление, и почему в отладчике по-другому. Понимая это, читатель уже разберётся, где и как ему использовать LINQ.
На всякий случай повторю свой вопрос выше по треду:
А статья где-то объясняет, чего и почему нельзя делать?
Есть разница между объяснением, что происходит, и объяснением, почему нельзя так делать. Статья второго не делает, и это ее недостаток.
Понимая это, читатель уже разберётся, где и как ему использовать LINQ.
… или не разберется. В этом и проблема.
Статья второго не делает, и это ее недостаток
Вот мы и дошли до сути. Вместо того, чтобы просто написать это автору, у нас тут пара десятков абсолютно неконструктивных комментариев о том, что такой код заставят переписать, потому что потому.
Вместо того, чтобы просто написать это автору
После чтения ответов автора в этой дискуссии я не вижу смысла ему что-то писать.
А читателю полезнее сразу видеть реакцию "не надо так делать" (почему — тоже объяснено в комментах).
А где вы видите хоть один ответ автора в этой дискуссии?
Вот именно поэтому и не вижу смысла что-то ему писать.
Так вы определитесь, после чтения его ответов или потому что он ещё ничего не ответил?
Похоже, иронию надо было сразу выделять, иначе не понятно.
Окей, прямым текстом: я не вижу смысла ему что-то писать, потому что я не видел никакой его реакции на комментарии в статье.
Отличная статья, столько холивара на пустом месте я давно не видел)))
Предлагаю автору ещё заменить linq на plinq и в следующей статье раскрыть тайну, почему без interlocked там то 7, то 8, то 9. Если уж стрелять себе в ноги, то до конца:)
У Вас там в массиве сначала две 1, потом 1 и 2, но продолжаете писать о двух единицах.
Вся статья - лишь Lazy Evaluation IEnumerable. И больше не нужно ничего изобретать, это уже джун должен знать. Всегда на собесах подобное спрашивал, иначе человек в принципе не понимает, что такое IEnumerable.
https://github.com/Mr0N/ExampleCod/blob/master/ExampleCod/Program.cs
Если использовать буфер то можно избежать этой проблемы
П.С. Разница между ToList в том, что код остается Lazy
Linq в замочную скважину…