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

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

Синтаксический сахар в C#

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

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

Здесь стоит отметить и другое мнение: существуют люди, которые считают, что синтаксический сахар только усложняет код, делая его менее читаемым.

Я придерживаюсь данного мной определения, хотя опыт показал, что не всё так однозначно. Разберем на примерах:

Примеры:

  • Конструкция new () {1, 2} заменяет new List() {1, 2}

  • Оператор += заменяет конструкцию

    • number += 1 =>

    • number = number + 1

  • А оператор ??= вообще имеет долгую историю

    • foo ??= "Строка была равна null" =>

    • foo = foo ?? "Строка была равна null" =>

    • foo = foo is null ? "Строка была равна null" : foo =>

    • if (foo is null)
      {
      foo = "Строка была равна null";
      }

Эти примеры лишь малая часть всех тех конструкций, которые иcпользуются в C#. С каждой новой версией языка появляется всё больше новых конструкций и нужно пристально следить, чтобы ничего не упустить.

Неожиданные Null Reference Exception

Итак, перейдем к истории. Всё началось с того момента, когда в нашем проекте начали "выстреливать" ошибки NRE. По стек-трейсу было ясно, что код "падает" при инициализации класса. Ниже представлен фрагмент кода, приближенный к тому, что был у нас в проекте.

...
var result = new ExampleClass
{
    ExString = "Тут не должно быть взрыва",
    ExList = { 0, 1 },
    ExString2 = "И тут",
    ExInt = 24
};
...

NRE выбрасывалось на строке var result = new ExampleClass, которая, в теории, не должна вызывать такую ошибку. Отсюда стало очевидно, что следует обратить внимание на то, как инициализируются поля объекта.

Вот как выглядел класс, который мы пытались инициализировать:

public class ExampleClass
{
    public string? ExString { get; set; }
    public List<int>? ExList { get; set; }
    public string? ExString2 { get; set; }
    public int ExInt { get; set; }
}

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

Пояснение

Можно обратить внимание на весьма странный синтаксис в коде.

...
var result = new ExampleClass
{
    ...
    ExList = { 0, 1 },
    ...
};
...

Выше я приводил пример того, как конструкция new() {0, 1} заменяет new List() {0, 1}. Можно заметить, что {0, 1} уж очень похоже на new() {0, 1}.
Первый раз, глядя на эту конструкцию, в голову совсем не приходит мысль о том, что тут что-то не так. Скорее возникает мысль: "Наверное, это новый синтаксис". После 10 минут интенсивного просмотра кода было принято решение заменить {0, 1} на new() {0, 1}. И, о чудо, ошибки исчезли.

Но причём тут NRE? А всё дело в том, что{0, 1} заменяет конструкцию .Add() И вот эти два блока кода оказываются идентичными:

// 1
var result1 = new ExampleClass
{
    ExList = { 1, 2 },
    ExString = "Привет",
    ExInt = 24
};

// 2
var result2 = new ExampleClass
{
    ExString = "Привет",
    ExInt = 24
};
result.ExList.Add(1);
result.ExList.Add(2);

Так как ExList при инициализации равен null, то при попытке вызывать null.Add() мы получаем NRE.

Почему такое произошло?

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

Как этого избежать?

Самое главное — быть внимательным и проверять код. Можно попробовать следить за актуальными конструкциями, но, по моему опыту, это сделать весьма проблематично. Как ни странно, хоть IDE в этом случае подвело, но оно все равно умеет замечать большинство таких проблем. Именно поэтому стоит прислушиваться к его предупреждениям.

Заключение

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

P.S.

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

Комментарии (51)


  1. Heggi
    30.09.2023 17:07
    +13

    У вас какая-то странная IDE. У меня подсвечивает (vscode)


    1. s207883
      30.09.2023 17:07
      +21

      Rider тоже такое не пропустил

      Hidden text

      Обмажутся своими блокнотами, а потом в ноги стреляют!


      1. chkaff Автор
        30.09.2023 17:07

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


        1. iBljad
          30.09.2023 17:07
          +3

          Недавно сталкивался с тем, что мне Idea 2023 ultimate (не знаю, что важнее из этих двух) подсвечивала ошибку, а коллеге в версии 2022 community — нет. Так что вполне можно быть, что и в новых версиях Райдера добавили проверок.


      1. amalinin
        30.09.2023 17:07
        +8

        Я думаю, что не IDE должна показывать такие ошибки, а компилятор должен запрещать компилировать подобный код.


        1. chkaff Автор
          30.09.2023 17:07
          -3

          Кажется, что в IDE должна быть настройка, позволяющая превращать определенные предупреждения в ошибки компиляции.


          1. zetroot
            30.09.2023 17:07
            +12

            TreatWarningsAsErrors

            Это директива компилятора, включается в файле проекта.


          1. Krawler
            30.09.2023 17:07

            Del


  1. ColdPhoenix
    30.09.2023 17:07
    +4

    VS2022 подствечивает там потенциальную ошибку.

    Может раньше не было правда.


    1. chkaff Автор
      30.09.2023 17:07

      Спасибо, что протестировали это в своем IDE. В данной ситуации использовался Rider. Подробнее написал тут: https://habr.com/ru/articles/764586/#comment_26015534
      Очень круто что сейчас ошибки подсвечиваются.


  1. impwx
    30.09.2023 17:07
    +2

    Вроде все логично: конструкция с фигурными скобками разворачивается в серию последовательных вызовов .Add() на коллекции, и нововведением это не назовешь - кажется, еще с версии C# 5.0 доступно. Иногда эта возможность действительно удобна, например когда поле является get-only:

    class Foo {
        public List<int> A { get; set; } = new List<int>();
        public List<int> B { get; } = new List<int>();
    }
    
    var foo = new Foo
    {
        A = new List<int> { 1, 2 }, // работает
        // B = new List<int> { 3, 4 } // не сработает - ошибка компиляции
        B = { 3, 4 } // сработает
    };
    

    А вот то, что ошибка показывается на первой строке выражения, а не на той где действительно указана вызывающая ошибку операция - это досадная недоработка.


    1. NeoCode
      30.09.2023 17:07
      +24

      Ну не знаю, в моем понимании ExList = { 1, 2 } это присваивание объекту некоего составного литерала, а никакие не Add. Так что создатели C# здесь сделали фигню. Добавление там new не изменяет семантики всего выражения - это по прежнему присваивание объекту другого объекта.


      1. impwx
        30.09.2023 17:07
        +2

        Если пытаться прочитать код на языке X, опираясь на интуицию и знание какого-то другого языка Y, то абсолютно любая фича языка X может показаться странной. Наверное, программисту на PHP покажется, что foo.bar - это конкатенация двух строк, а не обращение к методу, и так далее. Такой подход едва ли конструктивен.

        В сишарпе добавление new меняет-таки семантику, потому что new { ... } - это выражение, и new () { ... } - это выражение, а вот просто { ... } - нет. Его нельзя сохранить в переменную или передать в функцию. Соответственно, когда вы пишете справа от знака равно что-то, что не является выражением, это не может быть обычным присваиванием и должно быть чем-то еще - в данном случае, инициализацией коллекции через последовательные вызовы метода Add.

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


        1. nin-jin
          30.09.2023 17:07

          ExList ~= { 1, 2 }

          Ну или как там у вас конкатенация обозначается.


          1. boldape
            30.09.2023 17:07
            +8

            ExList += { 1, 2 } // add semantic

            ExList = { 1, 2 } // assign semantic

            Мне кажется как то получше чем то что есть. + это должно работать как то так для присваивания

            ExList = { 1, 2 } =>

            if ExList.IsNull () { ExList = new() { 1, 2 } }

            else { ExList.Clear(); ExList = { 1, 2 } }

            И как то так для добавления

            if ExList.IsNull() { ExList = new() {1, 2 } }

            else { ExList.Add(1); ExList(2); }

            Я не очень представляю в каком случае будет иметь смысл НЕ инициализировать объект заданными значениями если он нулл, а кидать исключение как сейчас делает такой синтаксис, ну и явный += как то заметней чем разное поведение в зависимости от отсутвия наличия new ()

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


            1. mayorovp
              30.09.2023 17:07
              +2

              Если сделать так, то снова получается неконсистентность — при инициализации оператор += есть, а отдельно его нету. А если сделать его отдельно, то снова беда: один и тот же оператор вызывает то operator +, то метод .Add() в зависимости от того что справа. А сделать всё через один метод нельзя — потому что {1, 2} не литерал объекта, а просто синтаксис для инициализации коллекции.


              1. boldape
                30.09.2023 17:07

                если сделать его отдельно, то снова беда: один и тот же оператор вызывает то operator +, то метод .Add() в зависимости от того что справа.

                А в чем собственно беда? Сахар он на то и сахар что бы вот такие неконсистености скыравать за простым синтаксисом, ну или я не понял о чем вы.


              1. NooneAtAll3
                30.09.2023 17:07

                снова беда: один и тот же оператор вызывает то operator +, то метод .Add() в зависимости от того что справа

                возможно тут написано что-то на джававском, но в моем понимании одно должно вызывать другое… неужели в шарпах не так?


                1. mayorovp
                  30.09.2023 17:07

                  А сделать всё через один метод нельзя — потому что {1, 2} не литерал объекта, а просто синтаксис для инициализации коллекции.


    1. chkaff Автор
      30.09.2023 17:07

      Спасибо за полезный пример применения, он добавляет ясности. Да, действительно, это нельзя назвать нововведением, но в тот момент людей знающих этот синтаксис не оказалось рядом и для меня это было чем то новым.


  1. Krawler
    30.09.2023 17:07
    +7

    Конструкция new() без указания типа вообще была введена как аналог var, но для R-value

    То есть можно написать либо:

    var l = new List<int>(){1, 2, 3};

    либо

    List<int> l = new(){1,2,3};

    Но причём тут NRE? А всё дело в том, что{0, 1} заменяет конструкцию .Add() И вот эти два блока кода оказываются идентичными:

    А это так вообще первый раз слышу. И IDE почему-то тоже не в курсе

    VS2022 17.7.4
    VS2022 17.7.4


    1. chkaff Автор
      30.09.2023 17:07
      +1

      Да, действительно, это не работает с обычным Add. Эта конструкция используется только при инициализации объекта. Выше подсказали хороший пример использования: https://habr.com/ru/articles/764586/#comment_26015170.


  1. NN1
    30.09.2023 17:07
    +2

  1. Nurked
    30.09.2023 17:07
    +2

    Вот именно из-за всего этого я ушёл с C#. Писал на нём с 2003 года. Помню как вышел .NET 1.1. Помню как было круто с дженериками в .NET 2.0.

    Я прилежно учился и следил за всеми новыми выпусками .NET. Я помню тот день, когда Скотт Хансельман показал гитхаб для ASP.NET.

    Я думал о том, как будет круто когда сам шарп превратился в по-настоящему открытый язык. Но нет. Это его просто убило в моих глазах.

    Я вообще не понимаю, зачем надо было смешивать строго типизированный язык с Яваскриптом и чисто функциональными языками. Что не так с MyClass c = new MyClass()? Учитывая с тем, что всё на ватокомплите и после набора первых трёх быкв вижуал студия [и любая другая ИДЕ] могла всё автозаполнить. Неочевидно, но наличие var эту фитчу отключило. В любом случае, что ты будешь делать с сэкономленными фемтосекундами?

    После того, как самый простой switch-case превратился в тьюнинг-полного монстра я ушёл в мир голанга.

    Все эти свистоперделки и рюшечки превратили отличный язык в перл. Сейчас код на шарпах выглядит одинаково до и после шифрования файлов исходника.


    1. insighter
      30.09.2023 17:07

      думаю вы ушли на голанг потому, что за него больше платят


      1. Nurked
        30.09.2023 17:07
        +3

        С таким подходом вы не заработаете денег. Сейчас я вообще пишу на ноде, потому что этого требует бизнес. Я - профессиональный программист. Это значит, что я не промываю мозги своему начальнику рассказами о том, как хорош или плох какой-то ЯП. Я просто беру проблему, которую нужно решить для бизнеса и решаю её без рассказов о том, на каком языке она пишется. Я просто решаю проблемы. За это платят намного больше, чем за рассказы о хороших языках.


        1. insighter
          30.09.2023 17:07
          +2

          Примечательно, что безобидное высказывание вызвало у кого-то негатив :)

          Работодатели всё-таки ищут не просто [профессиональных] разработчиков, а тех у кого есть опыт с определенным стеком. Нельзя написать в резюме "я решаю задачи бизнеса" и стать нарасхват.

          Перекосы в разных стеках есть - не в разы конечно, но всё же C#/.NET никогда не славился большими ЗП.

          С таким подходом вы не заработаете денег.

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


    1. aegoroff
      30.09.2023 17:07
      +2

      Поддерживаю - постоянное добавление новых возможностей потихоньку превращает его в C++ в котором можно сделать одно и тоже отстрелить себе ногу 1001 способом. Как мне кажется, на версии 8 вполне можно было остановиться - выразительности более чем достаточно, а дальнейшее добавление новых фич будет лишь во вред


    1. impwx
      30.09.2023 17:07
      +1

      зачем надо было смешивать строго типизированный язык с Яваскриптом и чисто функциональными языками

      Тут можно дать очень обширный ответ, но если в кратце - то потому, что так удобнее. Например, работа с коллекциями через LINQ гораздо лаконичнее, удобнее и нагляднее, чем если писать все то же самое на циклах. Кроме того, некоторые особенности функционального стиля (чистые функции, иммутабельные переменные, композиция функций) позволяют легче распараллелить и отлаживать код.

      Что не так с MyClass c = new MyClass()?

      Да всё в порядке. Если визуальный шум вам не мешает, то пишите на здоровье. В стайлгайдах Microsoft принято в большинстве случаев указывать тип, если только он не очевиден по выражению справа, как у вас. Но многим людям это кажется избыточным, поэтому они используют `var`.

      самый простой switch-case превратился в тьюнинг-полного монстра

      Опять же, обычный switch-case остался как есть. Вам никто не запрещает писать так, как вы писали во времена 15 лет назад во времена .NET 2.0. Лично мне новый switch expression тоже не нравится, я считаю его избыточным и плохо вписывающимся в язык, поэтому я им практически никогда не пользуюсь. Портит ли его наличие язык в целом? Едва ли, хотя разработчики инструментария (Resharper, Rider, статических анализаторов и т.д.), которые вынуждены это поддерживать, наверняка со мной не согласятся.

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


      1. Nurked
        30.09.2023 17:07

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


        1. impwx
          30.09.2023 17:07
          +3

          С технической точки зрения все решаемо: поставьте минимальный langversion в проекте, настройте codestyle на свое усмотрение с помощью Stylecop, Resharper, Roslyn Analyzers - на здоровье. Останется только найти людей, которые согласятся в таких условиях работать


      1. develmax
        30.09.2023 17:07
        +2

        Дело в том, что если вы должны писать тону кода и развивать огромный проект, то switch expression позволяет писать то же самое, но быстрее - это здорово экономит время и превращает сотню if в элегантную конструкцию, и становится проще анализировать код, который занимает много места. В то же время бывают спорные решения, но благо развитие языка открыто и можно предлагать свои или обсуждать предложенные другими функции на github.


        1. impwx
          30.09.2023 17:07

          Мне не нравится, что switch expression служит той же цели, но отличается от switch statement практически во всем: ключевое слово switch пишется после выражения а не до, вместо default используется _, вместо case - стрелка =>, и самое важное - не проваливается на следующий вариант при отсутствии break.

          Вместо этого можно было все унифицировать:

          • Писать switch всегда перед проверяемым выражением. Разделять на switch expression и switch statement по тому, располагается ли оно внутри выражения или нет.

          • Разрешить X => Y в switch statement как синоним case X: Y; break;, по аналогии с expression body в свойствах и функциях

          • Разрешить _ как синоним default

          • Не делать case > 1, а использовать более общий case var x when x > 1

          Самое досадное, что в Java это сделали как надо, а в C# не смогли.


      1. mvv-rus
        30.09.2023 17:07
        +1

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

        Это вы про C# или вообще?
        Если про C# то покажите мне способ получиь гарантию от компилятора, что делегат, который мне могут передать в мою функцию, не может не быть чистой функцией. Я такого способа, например, не знаю. А в отсутствии этой гарантии от компилятора функциональное программирование превращается в «функциональщину», которая ничего такого прекрасного не позволит.


        1. impwx
          30.09.2023 17:07

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


    1. hVostt
      30.09.2023 17:07
      +4

      Какие-то смузи-стайл аргументы, уж извините. Я ушёл из C# потому что, всё не так, я думал оно вот так, а оно вон оно как, не так и не эдак. Зачем так надо было-то? То то, то это, не то ни это, монстр какой-то.

      Сейчас код на шарпах у нас решает множество задач. При чём производительных, под высокими нагрузками с весьма скромными требованиями. Наблюдаемость у проектов на C# феноменальная. С чем только и как не интегрировались, работает как часы.

      Когда в Go внезапно что-нибудь добавят, и там найдётся свой процент ненавистников "свистоперделок", которые уйдут на что-нибудь очередное хайповое. Ох уж эти свистоперделки, никакого спасу от них нет :)))))


    1. Krawler
      30.09.2023 17:07

      Ничего оно не отключило. Если внимат


  1. dopusteam
    30.09.2023 17:07
    +2

    Имхо, во всех примерах проблема не с сахаром, а с кодом


    1. kchhay
      30.09.2023 17:07
      +2

      Есть теория, что "хороший интерфейс должно быть сложно использовать неправильно". То есть, даже если вы используете сахар, неправильный код должен выглядеть инородно, неправильно, вызывать подозрения. Я не шарпист, для меня то, как оно было написано - выглядело целиком нормально. Автор статьи - шарпист. И для него это тоже выглядело нормально. Если неправильное использование синтаксиса выглядит настолько органично, то, на мой вкус, проблема все же в синтаксисе, а не в использующем коде.


      1. ColdPhoenix
        30.09.2023 17:07

        Код абсолютно верный и рабочий когда коллекция существует.


      1. dopusteam
        30.09.2023 17:07

        var result = new ExampleClass
        {
            ExString = "Тут не должно быть взрыва",
            ExList = { 0, 1 },
            ExString2 = "И тут",
            ExInt = 24
        };

        Смотрите, автор указал такой код.

        Если я напишу без сахара, типа как

        var result = new ExampleClass();
        result.ExList.Add(0);
        result.ExList.Add(1);

        То он так же развалится, тут не в сахаре дело.

        Плюс эта ошибка всегда подсвечивается IDE, о чём автору сообщили в самых первых комментариях (не знаю, в чём он код пишет), а если ещё и настроить всё правильно, то можно даже ошибку компиляции получить.


        1. kchhay
          30.09.2023 17:07

          Признаться, я не очень понимаю, к чему вы ведете. В коде используется синтаксический сахар. Из-за использования сахара получается то, о чем я (да и вы, в общем-то) сказал - выглядящий совершенно нормально код содержит в себе проблему. Если убрать сахар - мы увидим код, который совершенно очевидно содержит в себе ошибку. То есть, проблема не в исходном синтаксисе языка, а в синтаксическом сахаре.
          Хотя, зная о подобных выкрутасах, я бы, наверное, допустимый на проекте сахар как-то ограничивал и тогда это можно считать проблемой кода, да.
          Само собой, комментарии об IDE совершенно логичны. Еще можно настроить синтаксический анализатор.


          1. dopusteam
            30.09.2023 17:07
            +1

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

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

            Признаться, я не очень понимаю, к чему вы ведете

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

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

            Всё началось с того момента, когда в нашем проекте начали "выстреливать" ошибки NRE.

            Очевидно, что код из статьи начал бы сыпаться при первом же запуске, а не неожиданно начал выстреливать где то. Либо автор чего то недоговаривает, либо намеренно упростил код и там вокруг ещё куча не очень хорошего кода. Такой код должны были обнаружить сначала при разработке, потом на ревью, а потом и при тестировании (я всё таки предполагаю, что 'в нашем проекте начали "выстреливать" ошибки NRE' - это где то на проде или на тестовом контуре, а не при локальной разработке).

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

            По поводу ограничения сахара - мне кажется тут всё таки решается всё культурой разработки в первую очередь.


  1. navrocky
    30.09.2023 17:07
    +1

    А разве включение nullable начиная с 8 версии не спасает?


    1. chkaff Автор
      30.09.2023 17:07

      Спасает, но в тот момент Rider не выкинул предупреждения. Сейчас Rider работает корректно, и, как оказалось, другие IDE тоже.


      1. develmax
        30.09.2023 17:07

        Да, нужно включить везде nullable и настроить правильно IDE и компилятор, так чтобы при возникновении null где-либо - такой код не компилировался.


  1. Bagir123
    30.09.2023 17:07
    +1

    Синтаксический сахар упрощает написание кода, но никто не мешает писать без него, если есть такая проблема.

    Единственное разбирать чужой код из сахара проблема.

    Допустим мне бы в голову не могло прийти что такое вообще возможно на c#: (a, b) = (b, a). Это обмен значений между переменными, кто не в курсе.


    1. ColdPhoenix
      30.09.2023 17:07
      +1

      Если точнее это создание кортежа и его декострукция.


  1. Mirual
    30.09.2023 17:07

    Забавно, я думал, что только в питоне идут споры про приемлемость применения list comprehension и подобных сокращений/улучшений/ухудшений кода, а оказывается нет)


  1. MWGuy
    30.09.2023 17:07
    +5

    Синтаксический диабет. Извините.


  1. konstunn
    30.09.2023 17:07

    Недостаточное покрытие тестами.