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

Если вы ответили восемь, то, скорее всего, эта заметка не для вас, остальным же предлагаю отправиться вместе со мной в небольшое путешествие в волшебную страну .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)


  1. dopusteam
    01.01.2022 19:08
    +8

    Linq тут, в общем то, ни при чём, это специфика IEnumerable\IEnumerator.
    Осмелюсь предположить, что заминусуют за низкий технический уровень


    1. WhiteBlackGoose
      01.01.2022 19:47
      +11

      Честно говоря, превью статьи, встречающая меня с мутирующей лямбдой в linq, довольно оттвергает от чтения. Надеюсь, кому-то все-таки будет полезно...


      1. VolodjaT
        02.01.2022 15:33
        +2

        Этот мутирующий код для наглядной демонстрации сколько раз делаем одну и ту же работу. Ох и сколько раз видел код типа .Select(x=>new Entity(x.id))

        А потом последующие вызовы несколько раз First и удивление девелопера почему два результата first не равны по ссылке


  1. SShtole
    01.01.2022 19:29
    +9

    Познавательно, конечно, спасибо, но если бы меня такое спросили, я бы ответил, что лучше такой код переписать. Ведь что у нас по сути? Во-первых, предикат с побочным эффектом. Во-вторых, лямбда с императивным кодом. И всё это одна сущность!


    1. qw1
      01.01.2022 19:58
      +4

      Тут учебный пример, чтобы посчитать, сколько раз вызывается оператор сравнения.


  1. Naf2000
    01.01.2022 19:54
    +11

    Резюмируя:

    1. Метод Where (и многие другие методы-расширения IEnumerable) - ленивый, он строит всего лишь объект, состоящий из пары: исходная последовательность и предикат. Больше ничего он не делает. Построенный объект при своем итерировании будет заставлять итерироваться внуренний и проверять предикат.

    2. Метод First в силу своей специфики будет итерировать последовательность (а значит и исходную) до первого элемента - а для исходной до первого выполнения условия. это 3 итерации.

    3. Метод Last - будет итерировать всю псоледовательность (ну и соответственно исходную тоже) - это 5 итераций.

    4. Для каждого получения представления результата Where будет проитерирована вся исходная последовательсть - отсюда инкременты равные 5.

      Ничего не упустил?


  1. ARad
    01.01.2022 19:58
    +2

    Зачем обсуждать в статьях какой именно будет побочный эффект неправильного кода?


    1. 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();

      К такой постановке вопроса есть претензии?


      1. lair
        01.01.2022 20:16
        +6

        Есть: "а зачем вы спрашиваете?".


        Есть простая мнемоника: никогда не делайте multiple enumeration по enumerable. Она этот вопрос снимает полностью.


        1. pankraty
          01.01.2022 21:16

          Увы не полностью. С единственным вызовом Last() можно ожидать, что сравнение будет одно, т.к. обход будет идти с конца, но легко упустить, что после Where мы имеем дело не с массивом (как вы уже написали ниже), и обход будет полным. Так что пусть пример несколько синтетический, но полезная информация в нём определённо есть, чтобы не попадаться на таких ловушках.


          1. lair
            01.01.2022 21:24
            +5

            С единственным вызовом Last() можно ожидать, что сравнение будет одно, т.к. обход будет идти с конца

            Нет, нельзя этого ожидать. Enumerable двигается только вперед.


            1. pankraty
              01.01.2022 21:27
              +2

              Речь про беглый анализ кода, при котором легко ошибочно заключить что x.Last(predicate) и x.Where(predicate).Last() эквивалентны. И то, что несколько человек на этом попались, опровергает тезис "нельзя".


              1. pankraty
                01.01.2022 21:29
                -1

                Кроме того, это никак р-не противоречит моему доводы о том, что "простая мнемоника никогда не делайте multiple enumeration по enumerable" снимает не все проблемы.


                1. lair
                  01.01.2022 21:58
                  +3

                  Я и не говорил, что она снимает все проблемы, я говорил, что она снимает вопрос в статье.


              1. lair
                01.01.2022 21:58
                +2

                Речь про беглый анализ кода, при котором легко ошибочно заключить что x.Last(predicate) и x.Where(predicate).Last() эквивалентны.

                Почему ошибочно-то? Надо исходить из того, что они эквивалентны, а все остальное — необязательная оптимизация.


                И то, что несколько человек на этом попались, опровергает тезис "нельзя".

                Неа, не опровергает. Оно лишь доказывает, что люди имеют какие-то ожидания. Правильные ли это ожидания — вопрос отдельный.


                1. pankraty
                  01.01.2022 21:59
                  +2

                  Собственно, данная статья и работает в пользу приведения ожиданий людей к реальности.


            1. mayorovp
              01.01.2022 21:27
              +1

              Учитывая, что там дофигилион оптимизаций...


              1. mvv-rus
                01.01.2022 21:54
                +2

                … которые по-любому зарубаются использованием побочного эффекта в коде из статьи.

                PS А по жизни, согласен с многими предыдущими комментаторами, такой код писать не надо. Но, к сожалению, читать временами приходится и такой код.


              1. lair
                01.01.2022 21:59
                +1

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


              1. mvv-rus
                02.01.2022 01:04

                Del.
                Не туда написал


            1. sasha1024
              02.01.2022 09:51
              -2

              Мир не ограничивается одним .NET, есть другие библиотеки с более эффективными аналогами Enumerable, поэтому ожидание «с единственным вызовом Last() можно ожидать, что сравнение будет одно, т.к. обход будет идти с конца» у бегло просматривающиего код вполне ожидаемо (извиняюсь за тавтологию).


              1. lair
                02.01.2022 10:17
                +5

                Мир не ограничивается одним .NET

                В статье про .NET разумно ожидать, что будет рассматриваться поведение .NET.


                1. sasha1024
                  02.01.2022 10:50
                  +2

                  Безусловно.

                  Но:

                  1. Пиша на .NET, неразумно писать так, как будто каждый читающий код на 100% знает всю подноготную .NET. Чем интуитивнее код — тем лучше.

                  2. Читая статью про подноготную .NET, неразумно автоматически предполагать, что она написана для людей, на 100% знающих подноготную .NET. Таким людям эта статья просто не нужна.


                  1. lair
                    02.01.2022 10:59
                    +3

                    Пиша на .NET, неразумно писать так, как будто каждый читающий код на 100% знает всю подноготную .NET. Чем интуитивнее код — тем лучше.

                    … и как же вы предлагаете сделать "интуитивнее" в случае кода в статье?


              1. dopusteam
                02.01.2022 11:26
                +4

                Расскажите, как найти последний элемент перечисления, не перебирая его?

                Не массива, не списка или коллекции, а именно перечисления

                На всякий случай, вот Вам интерфейс ienumerator

                MoveNext возвращает true, если есть следующий элемент и пишет его в Current.

                Current возвращает текущий элемент


                1. tyomitch
                  02.01.2022 12:03

                  1. dopusteam
                    02.01.2022 12:30
                    +1

                    Ну я ж сказал, не лист и не массив, а перечисление


                    1. qw1
                      02.01.2022 12:44
                      +1

                      Зачем вы настаиваете на перечислении, если в примере конкретно массив. И библиотеки могут оптимизироваться под тип коллекции, а могут и не оптимизироваться.


                      1. dopusteam
                        02.01.2022 12:54
                        +2

                        Потому что изначально я отреагировал на вот это "есть другие библиотеки с более эффективными аналогами Enumerable"

                        Более того, сам .net имеет оптимизации для листов и не только для кейса Last, но и для других.
                        А тут, видимо, предлагается ввести целую иерархию новых интерфейсов


        1. qw1
          01.01.2022 22:22
          +7

          Есть: «а зачем вы спрашиваете?».
          Вопрос на понимание, что происходит под капотом.
          Есть простая мнемоника: никогда не делайте multiple enumeration по enumerable. Она этот вопрос снимает полностью.
          Вы слишком категоричны. Наверняка можно придумать сценарии, в которых повторные проходы по фильтрованному enumerable дешевле, чем например копирование его в List.


          1. lair
            01.01.2022 22:24
            +5

            Вопрос на понимание, что происходит под капотом.

            Понимаю: двойное перечисление, которого надо избегать. Сколько при этом будет вызовов лямбды — вопрос неочевидный, и зависит от имплементации.


            Наверняка можно придумать сценарии, в которых повторные проходы по фильтрованному enumerable дешевле, чем например копирование его в List.

            Это если вы знаете, что ваш Enumerable не упадет от повторного прохода.


            (понятно, что из правил есть исключения, но мнемоника на то и мнемоника, чтобы помогать в большинстве случаев)


        1. 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 этих слов нет, но и обратного утверждения нет тоже.


          1. dopusteam
            02.01.2022 07:52
            +2

            Потому как в этой задаче нет ситуации с созданием нескольких одновременно активных итераторов — той самой, которую описывает мнемоника и которая реально может вызвать проблемы, например, потому что повторный вызов GetEnumerable имеет право вернуть тот же самый итератор, что и при первом вывызове.М

            Мнемоника совсем не об одновременно активных итераторах. Вообще, если GetEnumerator возвращает тот же самый итератор, а не каждый раз новый - это плохой итератор, именно для этого разделены интерфейсы IEnumerable и IEnumerator.

            А мнемоника как раз о том, что enumerator может лезть в БД или отправлять запросы и делать кучу чего тяжёлого и лучше, без необходимости, этого не делать. Тут ни слова о том, что Вы описали с одновременным использованием enumerator


          1. lair
            02.01.2022 10:16
            +2

            Потому как в этой задаче нет ситуации с созданием нескольких одновременно активных итераторов — той самой, которую описывает мнемоника

            Мнемоника описывает любую ситуацию, когда итератор получается больше одного раза, а не только одновременные.


            То есть — никакого multiple enumeration нет.

            Как только вы вызвали GetEnumerator больше одного раза, случился multiple enumeration.


            1. mvv-rus
              02.01.2022 15:45

              Я понял вашу позицию.


        1. sasha1024
          02.01.2022 09:46
          +3

          никогда не делайте multiple enumeration по enumerable

          Копирование в общем случае может обходиться как дешевле, так и дороже повторного использования enumerable, так что Ваше «никогда» слишком категорично.


          1. lair
            02.01.2022 10:18
            +3

            Задача мнемоники — покрывать большую часть случаев, а не все.


            В общем случае повторное использование может быть просто невозможно.


            1. sasha1024
              02.01.2022 10:46
              +1

              Задача мнемоники — покрывать большую часть случаев, а не все.

              А где об этом написано? Насколько я знаю:

              1. Задача мнемоники — упростить запоминание какого-то утверждения. При этом наличие у утверждения мнемоники совершенно не подразумевает каких-либо поблажек к точности утверждения. Т.е. иными словами мнемоника — лишь средство запоминание, а не характеристика точности/правильности утверждения.

              2. Разве Ваша фраза вообще мнемоника?

              В общем случае повторное использование может быть просто невозможно.

              Ок, уточню: в общем случае использования интерфейса, который позволяет повторное использование. Да, технически в .NET нету отдельных IEnumerableOnce и IRobustEnumerable, но логически-то это всё равно разные вещи. И во многих случаях мы знаем, какой именно «подвид» IEnumerable тут гарантирован.


              1. lair
                02.01.2022 10:56
                +1

                А где об этом написано?

                Это мнемоника, которую я привел, и я использую. Я не говорил, что это общепринятое правило (хотя многие средства статического анализа подсвечивают такое использование как ошибочное).


                в общем случае использования интерфейса, который позволяет повторное использование

                Если передо мной неизвестный IEnumerable, для меня безопаснее считать, что он не позволяет повторное использование, чем наоброт.


                И во многих случаях мы знаем, какой именно «подвид» IEnumerable тут гарантирован.

                Как вам, однако, просто жить. Я вот как раз во многих случаях этого не знаю.


  1. 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; });


    1. lair
      01.01.2022 20:17
      +12

      Вообще я думал, что ответ будет 4, т.к. Last должен идти от конца массива, пока не встретит подходящий элемент.

      После Where LINQ уже не знает, массив там или нет.


      1. qw1
        01.01.2022 22:25
        +1

        Это сейчас не знает, а завтра сделают оптимизацию, и Where вернёт не IEnumerable, а IQueryable, и тогда Last сможет искать с конца.


        1. lair
          01.01.2022 22:26
          +5

          Проблема "завтра сделают оптимизацию" в том, что могут сделать, а могут не сделать, а могут сделать не так, как ожидается. Держать это все в голове обычно не выгодно, проще профилировать конкретные узкие места.


          1. qw1
            01.01.2022 22:32

            Значит, ваше утверждение в общем случае неверно

            После Where LINQ уже не знает, массив там или нет.


            1. lair
              01.01.2022 22:34
              +5

              У нас с вами, видимо, разное понимание "общего случая".


              1. qw1
                01.01.2022 22:40

                Я это понимаю так, что в текущей версии .NET (предположительно) «После Where LINQ уже не знает, массив там или нет», а будущих версиях можно сделать, чтобы знал и оптимизировал (я ниже написал, как).

                То есть, общий случай — это некая произвольная версия .NET


                1. lair
                  01.01.2022 22:43
                  +3

                  Ну вот мне как раз не очень интересно обсуждать некую произвольную версию .NET, в которой что-нибудь может быть решили поменять и переделать.


                  1. qw1
                    01.01.2022 22:49

                    Чуть выше вы ссылаетесь на некие общие соображения («зависит от реализации»), то есть вам было лень разбирать конкретную версию. А сейчас вы отстаиваете точку зрения, что в неопределённых спецификацией моментах надо ориентироваться на конкретную версию. Как удобно…


                    1. lair
                      01.01.2022 22:53
                      +4

                      А сейчас вы отстаиваете точку зрения, что в неопределённых спецификацией моментах надо ориентироваться на конкретную версию.

                      Эээ, я не отстаиваю эту точку зрения. Я как раз считаю, что нужно ориентироваться на описанное поведение (т.е. обычный для enumerable перебор), которое не специфично для версии.


            1. qw1
              01.01.2022 22:38

              Даже можно не уходить на IQueryable, а остаться на IEnumerable. Сделать специализацию для Where для IList, которая вернёт ListEnumerable: IEnumerable, и сделать специализацию для Last, которая для ListEnumerable будет перекрывать реализацию произвольного IEnumerable.


              1. lair
                01.01.2022 22:44
                +1

                Мммм, вы представляете себе количество труда для такой "специализации"?


                (иными словами, как вы думаете, почему оптимизация для Count делает проверки на ICollection внутри?)


                1. qw1
                  01.01.2022 22:52

                  Это не снимает моего возражения против ваших утверждений что «После Where LINQ уже не знает». Можно так же генерировать ListEnumerable для источника IList, но проверять на него не в compile-time, а в run-rime.


                1. mayorovp
                  02.01.2022 12:47

                  На самом деле, не очень много. Все нужные оптимизации уже есть в методе Last(predicate), осталось только научиться автоматически сокращать Where(predicate).Last() до более оптимальной формы.


                  1. lair
                    02.01.2022 12:58
                    +1

                    осталось только научиться автоматически сокращать Where(predicate).Last() до более оптимальной формы.

                    … всего ничего, да.


                    Впрочем, дело не в этом даже. Дело — для меня — в том, что никогда не знаешь, где уже пора остановиться. Вот мы оптимизировали Where().Last(). А Select().Last() надо оптимизировать? А Where().Select().Last()? А Skip().Select().Last()?


        1. marshinov
          02.01.2022 11:26
          -3

          Не сделают, посмотрите интерфейсы IEnunerable и IEnumerator. First и Ladt объявлены именно для IEnumerable. Разная реализация через апкасты нарушит LSP. Не сказать, что IQueryable не нарушает LSP, но это был общий, а не частный случай и осознанный выбор, который оказался верным. Не говоря уже о бесконечных последовательностях.


          1. mayorovp
            02.01.2022 11:37
            +4

            Но там внутрях уже разная реализация через апкасты! Например, array.Count() не будет перечислять массив, а просто вернёт Length.


            Кстати, реализация First и Last нарушать LSP не может в принципе, потому что LSP — требование к иерархии типов, а не к внешним операциям над ними.


            1. marshinov
              02.01.2022 12:27
              -2

              Но там внутрях уже разная реализация через апкасты! Например, array.Count() не будет перечислять массив, а просто вернёт Length.

              Пока вы не добавили предикат

              Кстати, реализация First и Last нарушать LSP не может в принципе, потому что LSP — требование к иерархии типов, а не к внешним операциям над ними.

              Это терминологическая демагогия. Каким термином вы предлагаете кратко описывать ситуацию, когда метод работает с одним специализированным типом из иерархии, но не работает с другим?


              1. 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(), для массивов и прочих списков уже работает "с конца"!


                1. lair
                  02.01.2022 13:06
                  +2

                  Что меня в этом коде расстраивает — так это то, что можно внезапно больно удариться.


                  Вот был у нас класс, не знаю, Recordset, и реализовывал он IEnumerable<Record>. Все было хорошо, ходил в БД, отдавал данные, блаблабла.


                  А потом один программист взял и добавил к этому интерфейсу IList<Record>. И все бы ничего, но… начали течь ресурсы.


                  Как так?


                  1. mayorovp
                    02.01.2022 13:22

                    Именно в этом коде вот так удариться можно? Что-то не верится.


                    Больше походе, что проблема в самом добавлении IList<Record>, как-то не вяжется этот интерфейс с кодом, который непосредственно ходит в базу.


                    1. lair
                      02.01.2022 13:24
                      +1

                      Именно в этом коде вот так удариться можно? Что-то не верится.

                      Можно. Имплементаторы IPartition и IList не диспозятся, а стандартный энумератор — диспозится.


                      Я, в принципе, понимаю, почему так сделано, и скорее с этим согласен, чем нет. Но для внешнего имплементатора это может оказаться неожиданностью.


                      1. mayorovp
                        02.01.2022 13:30

                        Но ведь они в этом методе и не создаются, в отличии от энумератора.


                        Общее правило — кто создавал, тот и диспозит, если не оговорено обратное — соблюдается. Так откуда неожиданности?


                      1. lair
                        02.01.2022 13:31

                        Я же говорю, я понимаю, по какой логике там действовали.


                        Однако люди, которые запихнули закрытие коннекшна к БД в Dispose от итератора, тоже вполне себе руководствовались понятной (и существующей) логикой. И оно вполне себе работало… а теперь сломалось.


                      1. mayorovp
                        02.01.2022 13:39
                        +2

                        Э-э-э, а что бы эти люди делали при множественных итерациях, которые вроде и должны избегаться — но всё ещё разрешены?


                        Если соединение закрывается в Dispose — значит, оно должно открываться в GetEnumerator и только там. Все остальные способы дают на выходе хрупкий код, и нечего тут на библиотеку пенять когда сами ерунду по-написали.


                      1. lair
                        02.01.2022 13:41
                        +2

                        Э-э-э, а что бы эти люди делали при множественных итерациях, которые вроде и должны избегаться — но всё ещё разрешены?

                        А ничего. Не разрешены у пользователей (этого кода) множественные итерации.


                        (это, в отличие от добавления IList, не вымышленный дизайн)


                1. marshinov
                  02.01.2022 16:48

                  Да, про Last без Where загнался, вы правы. Я имел в виду именно вариант, когда предикат записан отдельным Where, чтобы на выходе IEnumerable был

                  .Where(predicate).Last();

                  Я по этому поводу согласен с @lair, что можно словить нежданчик. Может имело смысл сделать отдельную перегрузку для IList, чтобы такие вещи были более явными.


    1. Naf2000
      01.01.2022 20:17
      +4

      Оно не может идти от конца массива, т.к. оперирует IEnumerable, а для него чтобы узнать, где конец - надо попробовать все. Иногда конца даже может не быть ((


  1. VISTALL
    01.01.2022 20:21
    +1

    Замените var на точный тип, и узнаете много нового :)


    1. KislyFan
      01.01.2022 21:04

      В подавляющем большинстве случаев, надо знать куда смотреть. Если не знаешь, то явное указание типов не поможет.


  1. korsetlr473
    01.01.2022 21:05
    +11

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

    да


  1. marshinov
    01.01.2022 21:13

    Ну вот зачем захватывать что-то в лябмду? Прям иногда это бывает нужно, но очень редко.


    1. mvv-rus
      01.01.2022 21:33

      Ну вот зачем захватывать что-то в лябмду?

      По жизни — чтобы снизить число параметров. С точки зрения теории — а как без захвата реализовать на C#, к примеру, каррирование?
      Насчет очень редко… Не знаю, как вам, а команде, которая ASP.NET Core разрабатывала, это требовалось отнюдь не редко — в коде инициализации ASP.NET Core таких лямбд полно.

      Что реально плохо в C# (с точки зрения использования функционального стиля, настоящим программистам это, наоборот, хорошо) — это то, что переменная захватывается по ссылке и это проиисходит AFAIK молча (не знаю, может есть опция включить на это предупреждение компилятора, но я такой не видел). В результате можно получить очень интересные и неожиданные изменения.


      1. marshinov
        02.01.2022 11:14

        На сколько я помню, в asp.net все лямбды захватывают только immutable-переменные, поэтому поведения из статьи в asp.net не наблюдается. Поправьте, если не прав.


        1. lair
          02.01.2022 11:20
          +4

          На сколько я помню, в asp.net все лямбды захватывают только immutable-переменные

          Давайте сначала разберемся только: вы под immutable-переменной понимаете что? Скажем, вот такое — это захват mutable или immutable?


          Action Foo(HttpContext ctx)
          {
            return () => Bar(ctx);
          }


          1. marshinov
            02.01.2022 12:12
            +1

            Убедили, был не прав)


    1. pankraty
      01.01.2022 21:40
      +1

      Так это просто для иллюстрации того, сколько раз выполняется сравнение. Когда коллекция большая, а сравнение медленное, могут возникнуть проблемы, там где их не ждёшь. А захват тут не при чем.


      1. marshinov
        02.01.2022 11:19

        Я не понимаю почему вы говорите про "медленное" и "быстрое" сравнение, если там ещё и multiple enumeration. Вообще пример очень неудачный, потому что в реальности его попросят переписать и "неожиданности" из статьи не будут актуальны


        1. VolodjaT
          02.01.2022 17:32
          +2

          Чтобы попросить переписать надо понимать чем такое чревато. И по опыту, не всем дотнетчикам это очевидно


    1. lair
      01.01.2022 21:59
      +5

      Я не знаю, мне это прям регулярно нужно.


      1. marshinov
        02.01.2022 11:30

        Не точно выразился. Вы такие побочные эффекты захватываете, чтобы потом по последовательности несколько раз проитерироваться или вы один раз что-то захватываете и ничего не мутируете / проходите строго один раз по IEnumerable?


        1. lair
          02.01.2022 11:31
          +4

          Ни то, ни другое. Я захватываю что-то в лямбду, потому что это удобный способ передать что-то в лямбду без кучи лишнего кода. Иногда то, что в нее захвачено, изменяется — потому что, опять же, так удобнее.


  1. Revertis
    01.01.2022 21:38
    -8

    Можете ли вы уверенно сказать, что будет выведено на консоль

    А когда предлог в заменили на на? Ведь всегда было "вывести в консоль" или "вывести на экран". Или это отголоски всяких "погуглить за программирование" и "вспомнил за канал"?


    1. mvv-rus
      01.01.2022 21:50
      +5

      А когда предлог в заменили на на?

      IMHO использование любого из этих предлогов никак не влияет на легкость восприятия статьи.


    1. Kolonist
      02.01.2022 00:15

      По правилам хорошего тона, о грамматических ошибках в статье принято писать автору в личку.


      1. Revertis
        02.01.2022 00:17
        -1

        Это я делаю десятки раз в день. Но не о таких массовых ошибках.


    1. tyomitch
      02.01.2022 12:21
      +8

      А когда предлог в заменили на на?

      Очень давно. Вот журнал «Физика» за декабрь 1982:



      Вот журнал «Вычислительная техника социалистических стран» того же года:



      Наоборот, «вывод в консоль» начался только в нашем веке.


  1. alex1t
    01.01.2022 23:39
    +2

    После прочтения статьи можно сделать вывод, что автор просто изначально не знал как работает LINQ и его "отложенное" исполнение и что реальное перечисление будет только после вызова определённых операторов. Из этого также следует и то, что перечисление может быть вызвано в отладчике сколько угодно раз - сколько раз я мышкой наведу на переменную. В общем поздравляю автора с открытием - всё таки он сам докопался до истины. Но всё таки как говорится RTFM - всё это давно расписано и разжёвано.

    P.S. Можно было сделать array.Where(...).ToArray() и тогда ответ бы был всегда 5, что в выводе, что в отладчике, но может в конкретном случае перечисление заранее не нужно.


    1. Kolonist
      02.01.2022 00:19
      +3

      А почему Вы решили, что автор не знал?

      Статья же не для автора, а для читателей. А многие из читателей могли этого не знать (и ряд каментов это доказывает). Особенно новички, которые еще не углублялись в особенности отложенного исполнения при использовании LINQ.

      Не все же родились с глубоким знанием всех аспектов .NET. Кто-то только изучает, а кто-то перешел с других языков и действует по аналогии, котороая не всегда работает.


      1. marshinov
        02.01.2022 11:31
        +1

        Плохо, что новички не знают. Все есть в спеке. На крайняк это очень легко дебажится.


        1. Kolonist
          02.01.2022 11:42
          +1

          А Вы, когда язык изучали, за день всю спеку прочитали и запомнили? И писали сразу без ошибок? Или, все же, постепенно на собственном опыте и статьям из Интернета постигали все тонкости?


          1. marshinov
            02.01.2022 12:16
            +3

            Мне было проще: когда я учил C# LINQ'а не было:) Поэтому, когда он появился, то прочитал release notes. Кажется, там про ленивую природу было много написано.


          1. lair
            02.01.2022 13:03
            +2

            Или, все же, постепенно на собственном опыте и статьям из Интернета постигали все тонкости?

            … забавляет меня отсутствие в этом списке, знаете, книг. Которые, кстати, сильно помогают постигать всякие тонкости.


            Впрочем, тут дело какое: пока сам десяток раз не врежешься в то, что какие-то вещи работают неинтуитивно (например, вернешь IEnumerable изнутри using, или, из более свежего, вернешь Task оттуда же), все равно не запомнишь. И "старшим товарищам", которые говорят, что не надо так делать, все равно не будешь верить. "Ну, у меня ж работает".


            1. Kolonist
              02.01.2022 13:12

              Разумеется! В том, что как-то нельзя делать, главное же не то, что нельзя делать, главное - почему это нельзя делать.

              Данная статья это вполне объясняет, так что я вообще не понимаю десятки комментаторов, которые пишут: "Такой код заставят переписать", "Так писать нельзя" и т.п.


              1. lair
                02.01.2022 13:13

                А статья где-то объясняет, чего и почему нельзя делать?


                Мне показалось, статья только констатирует "у меня тут вот такой код".


                1. Kolonist
                  02.01.2022 13:15
                  -3

                  Дочитайте до конца.


                  1. lair
                    02.01.2022 13:17
                    -2

                    Дочитал. Ничего кроме


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

                    не вижу.


                    1. Kolonist
                      02.01.2022 13:22
                      +1

                      Я же не сказал, прочитайте последний абзац, там и другие есть.

                      А впрочем, бессмысленно кого-то в чем-то убеждать. Количество положительных оценок говорит о том, что такие статьи гораздо более востребованы, чем академические и "сильные технические", хоть в них и нет негативных комментариев.


                      1. lair
                        02.01.2022 13:23
                        -1

                        Я же не сказал, прочитайте последний абзац, там и другие есть.

                        … ну так может вы цитату приведете?


                      1. Kolonist
                        02.01.2022 13:26
                        -1

                        Мне пол статьи в комментарий засунуть?


                      1. lair
                        02.01.2022 13:28
                        -1

                        Нет, конкретное место, где написано, как делать нельзя.


                      1. Kolonist
                        02.01.2022 13:35
                        +1

                        А, всё ясно, вы и мои комментарии тоже невнимательно читаете. Я же выше написал, что не столько важно, как нельзя, сколько важно, почему нельзя.

                        И вот на вопрос почему, статья вполне развернуто отвечает, объясняя откуда берется 8, в какой момент происходит вычисление, и почему в отладчике по-другому. Понимая это, читатель уже разберётся, где и как ему использовать LINQ.


                      1. lair
                        02.01.2022 13:37
                        +1

                        На всякий случай повторю свой вопрос выше по треду:


                        А статья где-то объясняет, чего и почему нельзя делать?

                        Есть разница между объяснением, что происходит, и объяснением, почему нельзя так делать. Статья второго не делает, и это ее недостаток.


                        Понимая это, читатель уже разберётся, где и как ему использовать LINQ.

                        … или не разберется. В этом и проблема.


                      1. Kolonist
                        02.01.2022 13:47
                        -1

                        Статья второго не делает, и это ее недостаток

                        Вот мы и дошли до сути. Вместо того, чтобы просто написать это автору, у нас тут пара десятков абсолютно неконструктивных комментариев о том, что такой код заставят переписать, потому что потому.


                      1. lair
                        02.01.2022 13:50
                        -1

                        Вместо того, чтобы просто написать это автору

                        После чтения ответов автора в этой дискуссии я не вижу смысла ему что-то писать.


                        А читателю полезнее сразу видеть реакцию "не надо так делать" (почему — тоже объяснено в комментах).


                      1. Kolonist
                        02.01.2022 13:55

                        А где вы видите хоть один ответ автора в этой дискуссии?


                      1. lair
                        02.01.2022 13:56
                        -2

                        Вот именно поэтому и не вижу смысла что-то ему писать.


                      1. Kolonist
                        02.01.2022 13:58

                        Так вы определитесь, после чтения его ответов или потому что он ещё ничего не ответил?


                      1. lair
                        02.01.2022 13:59
                        -2

                        Похоже, иронию надо было сразу выделять, иначе не понятно.


                        Окей, прямым текстом: я не вижу смысла ему что-то писать, потому что я не видел никакой его реакции на комментарии в статье.


                      1. Kolonist
                        02.01.2022 14:02
                        +3

                        Так ещё даже суток не прошло, он статью вчера вечером написал.

                        А теперь он и не напишет, просто чтобы не отхватить минусов, которых ему накидают при любом ответе.


  1. OkunevPY
    02.01.2022 00:15
    +11

    Отличная статья, столько холивара на пустом месте я давно не видел)))


  1. marshinov
    02.01.2022 11:44
    +2

    Предлагаю автору ещё заменить linq на plinq и в следующей статье раскрыть тайну, почему без interlocked там то 7, то 8, то 9. Если уж стрелять себе в ноги, то до конца:)