В конце января группа разработчиков .NET Core выпустила новую версию фреймворка .NET Core 3 preview 2. В нем были реализованы некоторые новые возможности языка С#. Что мне кажется довольно интересным, это switch expressions. Хотя это добавление кажется простым, я думаю, что потенциал его довольно большой. Используя новую постфиксную конструкцию switch можно, при желании, заменить все остальные конструкции ветвления: if, switch, и тернарный оператор. И, что особенно интересно, сделать это в функциональном стиле. Как именно, вы узнаете прочитав статью до конца.




switch expression кажется довольно простым и интуитивно понятным. Думаю, что небольшого примера может быть вполне достаточно для ознакомления:

    // Этот код помещает в переменную result вычисленное значение.
    // В зависимости от значения переменной operation, будет
    // выполнено либо сложение, либо вычитание, либо деление.

    var result = operation switch
    {
        "+" => a + b,
        "-" => a - b,
        "/" => a / b
    };

Первое отличие со switch statement это, конечно, отсутствие ключевых слов case и break (или return). Второе, не столь очевидное, это то, что выражение после стрелки (=>) должно быть вычисляемым выражением. Как правая часть оператора присваивания.

Если вы хорошо знаете структуру switch, то вы можете спросить, как определить default? Тут надо заметить, что начиная с C#7 в switch можно использовать pattern matching (сопоставление с шаблоном). Поэтому вместо специального default блока используется шаблон, сопоставление с которым всегда вычисляется в true для любого значения. Это просто знак подчеркивания: _.

    // Этот код помещает в переменную result вычисленное значение.
    // В зависимости от значения переменной operation, будет
    // выполнено либо сложение, либо вычитание, либо деление.
    // В случае неподдерживаемой операции будет брошено исключение.

    var result = operation switch
    {
        "+" => a + b,
        "-" => a - b,
        "/" => a / b,
        _ => throw new NotSupportedException()
    };

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

Но есть одно но
На самом деле компилятор не даст вам это сделать. Ошибка будет примерно такая: error CS8510: The pattern has already been handled by a previous arm of the switch expression.

Другая неочевидная особенность switch expression — все выражения должны возвращать одинаковый тип. Например, только int. Или только string. В этом switch expression похож на тернарный оператор.

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

    // Этот код помещает в переменную result вычисленное значение.
    // В зависимости от значения переменной operation, будет
    // выполнено либо сложение, либо вычитание, либо деление.
    // В случае неподдерживаемой операции будет брошено исключение.
    // Все поддерживаемые операции логируются.
    var result = operation switch
    {
        "+" => ((Func<int>)(() => {
            Log("сложение");
            return a + b;
        }))(),
        "-" => ((Func<int>)(() => {
            Log("вычитание");
            return a - b;
        }))(),
        "/" => ((Func<int>)(() => {
            Log("деление");
            return a / b;
        }))(),
        _ => throw new NotSupportedException()
    };

Почему же я считаю это изменение потенциально значительным?

Давайте посмотрим, как switch expression заменяет другие конструкции языка. Пусть есть следующий if:

var error = "";
if (fileSize > 1000) {
    error = "file is too large"
} else {
    processFile();
}

Аналогичный код с использованием switch expression:

var error = (fileSize > 1000) switch {
    true => "file is too large",
    _ => ((Func<string>)(() => {
        processFile();
        return "";
    }))()
};

Поскольку switch относительно легко переводится в набор if-ов, заменить любой switch также не представляется сложным. Также тривиально заменяется и тернарный оператор.

Естественно, решение с явным использованием Func<> не назвать красивым. Но если эта техническая проблема будет решена, то switch expression будет использовать намного удобнее и, во многих случаях, предпочтительнее.

Если вам интересны возможности C#8, то вас, возможно заинтересуют скринкасты по nullable reference types и using declaration, которые можно найти на моем youtube канале.

Об авторе: Александр Неткачёв закончил Таврический Национальный Университет по специальности Информатика. Работает и проживает в Великобритании. На протяжении многих лет занимается профессиональной разработкой ПО, ведет персональный сайт [тут была ссылка на сайт автора, но поскольку TM считают, что наличие платного udemy курса и упоминание его на сайте автора — это платная услуга, то она убрана].

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


  1. smind
    14.02.2019 08:02
    +4

    Аналогичный код с использованием switch expression:


    var error = (fileSize > 1000) switch…
    ...


    Вы должно быть шутите?!
    Был хорошо читаемый if а вы его заменяете менее понятным куском кода.
    Уверен что можно найти более подходящие примеры использования.


    1. alexr64
      14.02.2019 08:59

      Да они не шутят — откровенно глумятся. Почему нельзя выводить типы во время компиляции и сократить это непотребство до такой формы?..

      var result = operation switch
          {
              "+" => {
                  Log("сложение");
                  return a + b;
              },
              "-" => {
                  Log("вычитание");
                  return a - b;
              },
              "/" => {
                  Log("деление");
                  return a / b;
              },
              _ => throw new NotSupportedException()
          };


      1. amberovsky
        15.02.2019 11:17

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


        1. TheShock
          15.02.2019 12:09
          +1

          А я бы не отказался от вменяемых иммутабельных структур в C#.


          1. PsyHaSTe
            16.02.2019 09:50

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


            1. TheShock
              16.02.2019 11:55

              А как вы решите парой атрибутов изменение в глубине дерева? Вот представьте:

              class Bar {
                int x = 1;
              }
              class Foo {
                Bar bar = new Bar();
              }
              
              var foo1 = new Foo();
              var foo2 = foo1 with { bar.x = 2; }


              Как вы себе это представляете на атрибутах? Кстати, в рекордах глубокое изменение как-то так будет делаться?

              edit: я посмотрел Proposal. Наверное оно будет как-то так:

              var foo2 = foo1 with { bar = foo1.bar with { x = 2; } }


              Не особо удобно. При чем, что еще хуже, метод with придется писать в каждом рекорде вручную. И в чем смысл?


              1. PsyHaSTe
                16.02.2019 12:40

                Мне больше нравится неизменяемость как модификатор переменной, а не самого типа. Беда что нельзя выразить наследование неизменяемости в C#


                1. TheShock
                  16.02.2019 12:44

                  А это как? Вот у вас есть

                  class Foo {
                    int bar = 1;
                  
                    bool Qux () {
                      bar = 2;
                      return false;
                    }
                  }
                  
                  immutable foo = new Foo();
                  var result = foo.Qux(); // <== что должен сделать язык, чтобы избежать изменения?


                  1. PsyHaSTe
                    16.02.2019 12:55

                    Ошибка компиляции


                    1. TheShock
                      16.02.2019 13:12

                      И что — оно должно пробегать по всем наследникам? А на какой строчке валится — та, которая foo.Qux(); или та, которая bar = 2;?


                      1. PsyHaSTe
                        16.02.2019 14:33

                        1. TheShock
                          16.02.2019 16:26

                          А как на Расте будет выглядеть этот пример?

                          var foo2 = foo1 with { bar = foo1.bar with { x = 2; } }


                          1. PsyHaSTe
                            16.02.2019 16:44

                            как-то так полагаю:


                            let foo = foo1.with(|w| w.bar = foo1.bar.with(|b| b.x = foo1.bar.x))


                            Но полагаю, более идеоматичным будет


                            let mut foo2 = foo1.clone();
                            foo2.bar.x = 2;

                            Т.к. в отличие от тех же шарпов почти все объекты предоставляют метод клонирования себя.


                            1. TheShock
                              16.02.2019 16:50

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

                              let mut foo2 = foo1.clone();
                              foo2.bar.x = 2;
                              foo1.test == foo2.test; // если это тоже объект


                              let foo = foo1.with(|w| w.bar = foo1.bar.with(|b| b.x = foo1.bar.x))

                              Вот черт, тоже не то. Как такое делается в исконно функциональных языках? Есть язык, где это делается как-то так?
                              let foo = foo1.with(|w| w.bar.x = 123)


                              1. PsyHaSTe
                                16.02.2019 19:49

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

                                Странная «предъява» :) С чего бы другому полю меняться? Это ж не С++ где за присваиванием может всевозможная магия происходить.

                                Вот черт, тоже не то. Как такое делается в исконно функциональных языках? Есть язык, где это делается как-то так?

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


                                1. TheShock
                                  16.02.2019 19:57

                                  Странная «предъява» :) С чего бы другому полю меняться? Это ж не С++ где за присваиванием может всевозможная магия происходить
                                  Если я правильно понял, то вы использовали глубокое клонирование (а иначе оно смысла не имеет), а значит все поля поменяются на свои клоны. Хотя те поля, которые реально не меняются могли бы и не быть клонами.

                                  Это будет нарушать закон деметры
                                  Ну это не догма, а скорее рекомендация. Я не очень знаю, как это делается во взрослых языках. Вот у меня есть игра. У игры есть мир, у мира есть дом, в доме есть комнаты, в комнате есть телевизор:
                                  {
                                    world: {
                                      houses: [
                                        { 
                                          rooms: [
                                            {
                                              objects: [
                                               { type: 'tv', status: 'off' }
                                              ]
                                            },
                                            {/* ... */},
                                            {/* ... */},
                                          ]
                                        }
                                      ]
                                    }
                                  }


                                  В какой-нибудь игрушке на ФП будет такое дерево стейта?
                                  И вот у меня есть задача — включить телевизор. Мне нужно получить новый мир, в котором этот телевизор включен? Как это сделать?


                                  1. PsyHaSTe
                                    16.02.2019 21:34

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

                                    Ну в том же расте все зависит от владения. При желании — делаете все поля CoW и тогда оверхед будет только у тех полей которые реально перезаписались.

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


    1. Alex_At_Net Автор
      14.02.2019 13:41
      -1

      А вы читать умеете? Я же сказал, что не считаю это красивым на данный момент. Но думаю, что в дальнейшем это улучшится.


      1. amberovsky
        15.02.2019 11:10

        Вы написали

        Почему же я считаю это изменение потенциально значительным?

        и привели далее этот пример. Логично предполагать, что вы приводите этот пример как потенциально значимое улучшение.


    1. vlx
      14.02.2019 20:21

      Для if else любителей однострочек вообще есть var error = (filesize > 1000)? Blah(): blahblah();


      1. Vahman
        14.02.2019 22:09

        Так тернарный оператор автор тоже хочет заменить на switch expression)))


  1. AkshinM
    14.02.2019 08:10
    +1

    Аналогичный код с использованием switch expression:

    бред
    image


    1. Alex_At_Net Автор
      14.02.2019 13:44
      -1

      Так никто же не против. Вас же никто не заставляет использовать такой синтаксис, чего вы?


      1. i8008
        15.02.2019 00:46

        Вас же никто не заставляет использовать такой синтаксис, чего вы?

        Увы, но на самом деле заставляют. Лично вы можете не использовать данный синтаксис, но другие будут его использовать, а вам придётся работать с чужим кодом.
        Я так про С++ думал – я не буду злоупотреблять и упарываться метапрограммированием на шаблонах. Я и не «злоупотреблял». Но очень часто сталкивался с чужим кодом, автор которого хотел получить ачивку «гуру метапрограммирования: потому, что могу»

        З.Ы. На всякий случай: это не упрек в ваш адрес, скорее негодование по поводу развития синтаксиса С#. ИМХО, в последних релизах они местами перебарщивают с синтаксическим сахаром


  1. a-tk
    14.02.2019 08:25
    -1

    Ждём постфиксные формы if, while и новый оператор unless. Как в Perl.


    1. Sioln
      15.02.2019 11:33

      Ага, после "_" в switch подумалось об переменных по умолчанию "$_" и подобных в перле.


      1. PsyHaSTe
        16.02.2019 09:51

        _ в матчинге это максимально стандартная штука во всех языках.


  1. mayorovp
    14.02.2019 08:59
    +1

    Если вы почему-то считаете использование IIFE нормальным — то для вас switch expression в языке существовал давным давно:


    var result = ((Func<int>)(() => {
        switch(operation)
        {
            case "+":
                 Log("сложение");
                 return a + b;
            case "-":
                 Log("вычитание");
                 return a - b;
            default:
                 throw new NotSupportedException();
        }
    }))();


    1. Alex_At_Net Автор
      14.02.2019 13:45

      Я не считаю такое использование нормальным. Но я думаю, что у C# есть потенциал сделать подобное использование проще. Спасибо за пример.


    1. TheShock
      14.02.2019 19:17

      Если вы почему-то считаете использование IIFE нормальным — то для вас switch expression в языке существовал давным давно:
      Я вот не понимаю, люди словно специально ищут способ сделать методы больше, усложнить их, когда это прекрасно разбивается на два метода и не нужно никаких IIFE.


      1. mayorovp
        14.02.2019 19:28

        Если делать отдельный метод — для него нужно придумывать имя. А именование, как известно, одна из двух основных проблем программирования… :-)


        1. TheShock
          14.02.2019 19:29

          Но ведь нельзя вообще отказаться от сложностей! А одной инвалидацией кеша сыт не будешь.


  1. ApeCoder
    14.02.2019 09:00
    +1

    Остается только вопрос, как выполнить несколько операций в выражении

    Оно для того и выражение, чтобы это была одна операция :).


  1. kxl
    14.02.2019 10:12

    В Scala, например, результат вычисления блока = результату последнего выражения в блоке.
    Поэтому там пишется как:

    val result = operation match {
      case "+" => 
         Log("сложение");
         a + b;
      case "-" => 
        Log("вычитание");
        a - b;    
      case "/" =>
        Log("деление");
        a / b;
      case _ =>
         throw new NotSupportedException()
    }

    case не нужен, ок… Так проще.
    Но, почему бы не сделать блочные конструкции подобно этому, пусть и с return, как тут предложено чуть выше…


    1. Alex_At_Net Автор
      14.02.2019 13:48

      Слишком изменится концепция языка, я думаю. Но в F# так и есть. Как я вижу, решения понемногу переходят из F# в C#, так что может и дождемся такого подхода. Ну или таки сделают возможность миксировать F# и С# в одном проекте.


  1. yarosroman
    14.02.2019 10:58

    А что такое тринарный оператор? Может всё-таки тернарный?


    1. Alex_At_Net Автор
      14.02.2019 13:49

      Огромное спасибо. Приятно все таки увидеть конструктивный комментарий с предложением улучшения.


  1. Eldhenn
    14.02.2019 11:35

    Заменять набор логических условий на выражение? Вы вообще в своём уме?!


    1. yarosroman
      14.02.2019 12:14

      Например в kotlin if это вполне себе выражение.


    1. Alex_At_Net Автор
      14.02.2019 13:50

      Что не так то?


    1. yarosroman
      14.02.2019 15:58

      А причем тут автор то?


  1. Sinatr
    14.02.2019 11:42

    В целом интересно. Непонятно, это только для core или будет и в обычном фреймворке?

    Примеры, если честно, не очень. Не обьяснено преимущество по сравнению со switch/case c pattern matching, зачем нужна новая конструкция, если уже давно добавленная может делать это и даже больше?

    Поскольку switch относительно легко переводится в набор if-ов, заменить любой switch также не представляется сложным.
    Непонятно к чему это фраза? Это ли не очевидно? Обе конструкции используют ключевое слово switch и выполняют одинаковую функцию — управление потоком выполнения программы. Новый возвращает результат, работая по сути как inline local function, но внутри — все тот же старый switch, с немного странным синтаксисом.

    В принципе чем-то похоже на попытку продолжить добавление expression-bodied members, что в случае методов и property getter/setter было довольно неплохой идеей. В новом switch выглядит немного странно, с одной стороны не нужно писать case/break/default, а с другой теряются некоторые интересные возможности.

    Как записать несколько условий для одного тела? Скажем, при значение operation "+" и «add» должен выполниться один и тот же код, в старом switch это элементарно, а в новом как?
    Об авторе: Александр Неткачёв закончил Таврический Национальный Университет по специальности Информатика. Работает и проживает в Великобритании. На протяжении многих лет занимается профессиональной разработкой ПО, ведет персональный сайт alexatnet.com.
    Слишком пафосно. Где вы живете — неважно. «На протяжении многих лет» — лучше убрать, скорее всего, вы хотели написать так, чтобы не пришлось через 5 лет редактировать статью, меняя 20 лет на 25 лет или сколько там по вашему «многих лет», но получилось странно, будто вы боитесь признаться, сколько именно лет. Если есть сайт — то можно было не писать об университете, а добавить эту инфу там и ограничиться «Заходите на мой сайт» в конце статьи. На хабре любой может кликнуть на пользователе и глянуть профиль, если интересует автор или его background.

    Плюс писать о себе в третьем лице немного странно.

    «Автор этого комментария на протяжении многих лет работет и живет в Германии, в уважаемой фирме, весьма известной в узких кругах группы. Он окончил университет, любит свою маму и заботиться о своих домашних животных, чтобы еще написать такое написать не по теме...»


    1. mayorovp
      14.02.2019 12:31

      Непонятно, это только для core или будет и в обычном фреймворке?

      А как эта фича вообще может зависеть от рантайма, хотя бы в теории?


      1. PsyHaSTe
        14.02.2019 12:52

        Генерики требовали изменения рантайма, динамики тоже. async/await тоже не сразу стал доступен, были бекпорты на 3.5.Но там прям понятно было, что от рантайма много чего требуется, а тут по сути портянку if else нагенерить нужно.


        1. mayorovp
          14.02.2019 12:55

          Я спрашивал про конкретную фичу, а не про все возможные.


    1. PsyHaSTe
      14.02.2019 12:51

      В целом интересно. Непонятно, это только для core или будет и в обычном фреймворке?

      Это фича C# 8.0. К фреймворку версия языка никак не относится. CLR не менялся со времен 2.0


      1. AnarchyMob
        14.02.2019 13:41

        Так как .NET Framework 4.8 не будет поддерживать .NET Standard 2.1, то некоторые фичи C# 8 в нем работать (из коробки) не будут, к примеру, асинхронные потоки и диапазоны, но при установке необходимой библиотеки из NuGet, с реализацией нужных типов, C# 8 полностью заведется на .NET Framework 4.8.


        Источник


        1. PsyHaSTe
          14.02.2019 14:52

          Ну это понятно. Ровно та же история была с таплами, а до этого с async/await в .net 3.5.


          1. yarosroman
            14.02.2019 15:55
            -1

            Ну async/await не требуют поддержки со стороны CIL. фактически это синтаксический сахар.


            1. i8008
              14.02.2019 23:55
              +1

              Не понимаю, за что поставили минус комментарию выше.
              Коллега прав, async/await не потребовало изменений CIL и даже CLR. Эта фича реализуется компилятором, который для async генерирует код, который реализует машину состояний. Аналогично как для yield return компилятор генерирует код реализации энумератора и т. д.
              Или я что-то упустил?


              1. PsyHaSTe
                16.02.2019 09:53

                Тоже не понимаю, но по факту из коробки не работало в .Net < 4.5. Не потому что рантам нужен был другой, а потому что Task/… типы были только в стандартной библиотеке 4.5. Через какое-то время появился пакет, который таски поставлял в 4.0 (те же типы в нугете).

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


        1. yarosroman
          14.02.2019 15:53

          А причем тут .Net Standart? Стандарт гарантирует лишь совместимость библиотек (идентичность классов) на различных рантаймах и к версии C# не имеет отношения.


          1. mayorovp
            14.02.2019 15:56

            Часть фишек C# требуют поддержки от рантайма, а значит и совместимости с определённой версией .Net Standart.


            1. yarosroman
              15.02.2019 01:25

              docs.microsoft.com/ru-ru/dotnet/standard/net-standard тут ни слова нет про рантайм.


      1. Alex_At_Net Автор
        14.02.2019 14:19

        А как же CLR 4.0?


      1. mayorovp
        14.02.2019 15:59

        Ну нет, менялся. Тем же ref struct требуется особая поддержка со стороны рантайма.


    1. BkmzSpb
      14.02.2019 12:53

      switch expressions больше всего подходят именно для pattern-matching (отсюда).
      Мне кажется, что с операциями это могло бы выглядеть как-то так


      double Compute(double a, double b, object op)
          => op switch 
              {
                      string s when (s == "add" || s == "+") => a + b,
                      string s when (s == "subtract" || s == "-") => a - b,
                      {}   => throw new ArgumentException("Unsupported type"),
                      null => throw new ArgumentNullException(nameof(op))
              }


      1. PsyHaSTe
        14.02.2019 13:02

        Проблема в том, что в шарпах statements != expressions, из-за чего и приходится воротить такие ужасные вещи как эти Func. Проще фичей не пользоваться в таких случаях вообще, что крайне снижает её полезность.


        1. BkmzSpb
          14.02.2019 13:16

          Простите, я не совсем понял, а к чему здесь Func? Я утверждаю что switch expressions годятся преимущественно для pattern-matching. Для множетсвенных действий на один case есть стандартный switch.
          Кроме pattern-matching, лично мне бы такой синтаксис пригодился, если нужно сопоставить некоторый enum и некоторый набор значений. Теперь это можно сделать лаконичнее.
          Либо так, либо я упускаю что-то важное.


          1. PsyHaSTe
            14.02.2019 14:56

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


            Ну вот например, в моем боте сейчас написано


            let result = from_slice::<ApiResult<T>>(chunk.as_ref());
            match result {
                Ok(api_result) => {
                    if api_result.ok {
                        Ok(api_result.result)
                    } else {
                        let text: String = String::from_utf8_lossy(chunk.as_ref()).into_owned();
                        Err(TelegramClientError::ConnectionError(text))
                    }
                }
                Err(e) => Err(TelegramClientError::SerdeError(e)),
            }

            Как это на C# написать с этой фичей? А никак, если только не извращаться как в примерах с Func.


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


            1. BkmzSpb
              14.02.2019 16:50

              Ну я плохо знаком с таким синтаксисом. Но такое количество операций я бы точно написал с помощью if-else. Например, как-то так.


              if(your_query(chunk) is var result
               && result is ApiResult api_result 
               && Ok(api_result))
              {
                  if(api_result.ok)
                      Ok(api_result.result);
                  else 
                       throw new ConnectionError(new string(GetFrom(chunk)));
              }
              else if(result is Error e)
                  throw new SerdeError(e);

              В силу проф. деформации это для меня гораздо читабельнее и работает в приближении когда Ok не возвращает значения. Если же возвращает, тогда так


              return your_query(chunk) switch 
              {
                  ApiResult api_result when Ok(api_result) => 
                      api_result.ok 
                          ? Ok(api_result.result) 
                          : throw new ConnectionError(new string(GetFrom(chunk))),
              
                  Error e => throw new SerdeError(e),
              
                  _ => throw new ThisShouldNeverHappenError()
              };

              Разумеется, я что-то мог не учесть, но мне кажется для относительно простого матчинга это работает неплохо. Если же у вас прямо целые блоки кода на каждый case, то лучше использовать либо традиционный switch, либо if-else. Но это, естественно, ИМХО.


              1. PsyHaSTe
                14.02.2019 17:04

                Ну я плохо знаком с таким синтаксисом.

                result — это просто энум, по которому мы делаем свитч (матч?)


                pub enum Result<T, E> {
                    Ok(T),
                    Err(E),
                }

                Разница только в том, что в отличие от шарповых энумов кроме собственно тэга разные варианты могут иметь данные какого-либо тип. Соответственно у нас либо Ok(ApiResult), либо Err(SomeError) Соответственно ваш второй вариант.


                Но такое количество операций я бы точно написал с помощью if-else. Например, как-то так.

                Так ведь в этом и смысл :) Вы упаковали создание переменной text и её использование в одно значение. А я как раз и показывал, что в реальных случаях нам нужно сделать что-то больше, чем просто вернуть результат. Ну допустим мы перепишем это как


                let result = from_slice::<ApiResult<T>>(chunk.as_ref());
                match result {
                    Ok(api_result) => {
                        if api_result.ok {
                            Ok(api_result.result)
                        } else {
                            let text: String = String::from_utf8_lossy(chunk.as_ref()).into_owned();
                            debug!("text value = {}", text);
                            Err(TelegramClientError::ConnectionError(text))
                        }
                    }
                    Err(e) => Err(TelegramClientError::SerdeError(e)),
                }

                В силу проф. деформации это для меня гораздо читабельнее и работает в приближении когда Ok не возвращает значения.

                Все ветки возвращают значение, в этом и смысл. И они тоже возвращают либо Ok(...), либо Err(TelegramClientError)


                Если же у вас прямо целые блоки кода на каждый case, то лучше использовать либо традиционный switch, либо if-else.

                Традиционный switch это statement, он не умеет возвращать значение.


                По сути претензия в том, что в шарпе нельзя написать


                let a = if condition {
                   10
                } else {
                   println!("Assigning a to 20");
                   20
                }

                И это иногда мешает.


                В силу проф. деформации это для меня гораздо читабельнее

                На самом деле вариант на матчах намного читаемее, просто непривычно + там много написано бойлерплейта который нужен для работы. Если его сократить до рафинированных вариантов как у вас то будет все сильно симпатичнее:


                match from_slice(chunk) {
                    Ok(api_result) => {
                        if api_result.ok {
                            Ok(api_result.result)
                        } else {
                            let text = String::from(chunk);
                            Err(text)
                        }
                    }
                    Err(e) => Err(e),
                }


                1. BkmzSpb
                  14.02.2019 17:58
                  +1

                  Теперь стало немного понятнее. Мне кажется, то, что вы предлагаете, просто не соответствует парадигме C#. Например возврат ошибок вместо throw.
                  Условно, вот так


                  if(from_slice(chunk) is var api_result && api_result is null)
                      throw new NullReferenceException(nameof(api_result));
                  
                  return api_result.result ?? throw new ResultException(chunk);

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


                  Если взять логику отличного от C# языка и попытаться ее натянуть на него, получится мягко говоря не очень. Верно и обратное.


                  1. PsyHaSTe
                    14.02.2019 18:05

                    Теперь стало немного понятнее. Мне кажется, то, что вы предлагаете, просто не соответствует парадигме C#.

                    Я просто привел пример на ошибках, но энумы это ведь не только они. Ну допустим будет:


                    match operator {
                        BinaryOperator(type, a, b) => {
                            if type == Operator::Addition {
                                a + b
                            } else {
                                pritnln!("Doing subtraction");
                                a - b
                            }
                        }
                        UnaryOperator(a) =>  {
                           println!("The only unary operator in the language - negation. Doint it...");
                           -a
                        },
                    }

                    Суть осталась ровно та же, но делать тут эксепшны наверное не стоит :)


                    Если взять логику отличного от C# языка и попытаться ее натянуть на него, получится мягко говоря не очень

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


            1. Taraflex
              14.02.2019 17:06

              Как это на C# написать с этой фичей? А никак,

              Оператор запятая. Хотя все равно бред конечно.


    1. Alex_At_Net Автор
      14.02.2019 14:13

      Спасибо за развернутый комментарий.

      В целом интересно. Непонятно, это только для core или будет и в обычном фреймворке?

      Я не нашел где можно точно посмотреть. Надеюсь, что со временем и в обычный фреймворк добавят.

      Примеры, если честно, не очень. Не обьяснено преимущество по сравнению со switch/case c pattern matching, зачем нужна новая конструкция, если уже давно добавленная может делать это и даже больше?

      Преимущество новой конструкции только в том, что это expression и, соответственно, его можно использовать там, разрешен expression — в инициализации переменных например.

      Поскольку switch относительно легко переводится в набор if-ов, заменить любой switch также не представляется сложным.

      Непонятно к чему это фраза? Это ли не очевидно? Обе конструкции используют ключевое слово switch и выполняют одинаковую функцию — управление потоком выполнения программы. Новый возвращает результат, работая по сути как inline local function, но внутри — все тот же старый switch, с немного странным синтаксисом.

      В принципе чем-то похоже на попытку продолжить добавление expression-bodied members, что в случае методов и property getter/setter было довольно неплохой идеей. В новом switch выглядит немного странно, с одной стороны не нужно писать case/break/default, а с другой теряются некоторые интересные возможности.

      Как записать несколько условий для одного тела? Скажем, при значение operation "+" и «add» должен выполниться один и тот же код, в старом switch это элементарно, а в новом как?

      Я, честно говоря, тоже задал себе этот вопрос, но пока не разобрался, как это сделать.

      Об авторе: Александр Неткачёв закончил Таврический Национальный Университет по специальности Информатика. Работает и проживает в Великобритании. На протяжении многих лет занимается профессиональной разработкой ПО, ведет персональный сайт alexatnet.com.

      Слишком пафосно. Где вы живете — неважно. «На протяжении многих лет» — лучше убрать, скорее всего, вы хотели написать так, чтобы не пришлось через 5 лет редактировать статью, меняя 20 лет на 25 лет или сколько там по вашему «многих лет», но получилось странно, будто вы боитесь признаться, сколько именно лет. Если есть сайт — то можно было не писать об университете, а добавить эту инфу там и ограничиться «Заходите на мой сайт» в конце статьи. На хабре любой может кликнуть на пользователе и глянуть профиль, если интересует автор или его background.

      Плюс писать о себе в третьем лице немного странно.

      «Автор этого комментария на протяжении многих лет работет и живет в Германии, в уважаемой фирме, весьма известной в узких кругах группы. Он окончил университет, любит свою маму и заботиться о своих домашних животных, чтобы еще написать такое написать не по теме...»


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


    1. yarosroman
      14.02.2019 15:34

      Ну и там и там Roslyn. Как вы думаете?


    1. Tangeman
      14.02.2019 19:40

      … зачем нужна новая конструкция, если уже давно добавленная может делать это и даже больше?

      Вместо многоветочного if или пусть даже обычного switch (где придётся добавлять case и break на каждый вариант) — очень компактная конструкция, которую легко писать (и читать). Впрочем, when-expression было бы ещё лучше (пример чисто для иллюстрации синтаксиса):

      var result = when {
          (a > b) => a - b,
          (b > a) => b - a,
          _ => 0,
      }
      

      По сравнению с многометровыми if-else или switch/when/break это гораздо приятней, и в реальных проектах достаточно ситуаций когда это было бы удобно.


  1. EngineerArt
    14.02.2019 14:21

    Синтаксис потихоньку превращается в JavaScript

    if else -> switch
    var error = (fileSize > 1000) switch {
        true => "file is too large",
        _ => ((Func<string>)(() => {
            processFile();
            return "";
        }))()
    };


    1. Alex_At_Net Автор
      14.02.2019 14:22

      Это ли не прекрасно? JS во многом более лаконичен.


  1. megasuperlexa
    14.02.2019 14:22

    Примеры, конечно, мега-неудачные, но как и отмечает автор красоты тут нет. И не будет, пока нет автоматического вывода типов, и пока весь язык построен вокруг иперативного стиля — а тут декларативные экспрешены. Что поделать, это c#


    1. Alex_At_Net Автор
      14.02.2019 14:26

      Ну все-таки движется в правильном направлении? Надеюсь, лет за 20 дождемся и вывода типов.


      1. UnclShura
        14.02.2019 21:06

        Какого вывода типов? Того, который уже есть или утиной типизации? С типами в C# все прекрасно.


        1. Alex_At_Net Автор
          15.02.2019 02:45

          type inference, как в F# — когда не надо устанавливать тип int, если функция возвращает 10.


  1. MarrySun
    15.02.2019 02:08

    Мне кажется, это тот самый случай когда лучше было подсмотреть у других и лучше у Kotlin.


    1. PsyHaSTe
      16.02.2019 09:55
      +1

      Они могли подсмотреть у самих себя в F#, проблема ведь не в том, что они «не знали». Это взвешенной решение, только фиговенькое.


  1. nomit
    15.02.2019 06:52

    Ну это абзац… Когда обычный pattern marching пытаются запихать во все щели. А потом с таким приходиться работать и рефакторить. И вроде сложный на вид код легко трансформируется в набор if else или switch case.