Как я провел лето с C# 8

17lsojd9tbdr_t74_pp51miv7wu.png?v=1

В недавнем выпуске подкаста DotNet & More Blazor, NetCore 3.0 Preview, C#8 и не только мы лишь вскользь упомянули такую животрепещущую тему, как C#8. Рассказ об опыте работы с C# 8 был недостаточно большим, что-бы посвящать ему отдельный выпуск, так что было решено поделиться им средствами эпистолярного жанра.

В данной статье я бы хотел рассказать о своем опыте использования C#8 на продакшене в течение 4 месяцев. Ниже Вы сможете найти ответы на следующие вопросы:


  • Как «пишется» на новом C#
  • Какие фитчи оказались действительно полезными
  • Что разочаровало

Полный список фитчей C#8 можно найти в официальной документации от Microsoft. В данной статье я опущу те фитчи, которые не смог опробовать по тем или иным причинам, а именно:


  • Readonly members
  • Default interface members
  • Disposable ref structs
  • Asynchronous streams
  • Indices and ranges

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


Switch expressions

В наших мечтах мы представляем эту функцию достаточно радужно:

        int Exec(Operation operation, int x, int y) =>
            operation switch
            {
                Operation.Summ => x + y,
                Operation.Diff => x - y,
                Operation.Mult => x * y,
                Operation.Div => x / y,
                _ => throw new NotSupportedException()
            };

Но, к сожалению, реальность вносит свои коррективы.
Во-первых, отсутствует возможность объединения условий:

        string TrafficLights(Signal signal)
        {
            switch (signal)
            {
                case Signal.Red:                    
                case Signal.Yellow:
                    return "stop";
                case Signal.Green:
                    return "go";
                default:
                    throw new NotSupportedException();
            }
        }

На практике это означает что в половине случаев switch expression придется превращать в обычный switch, дабы избежать copy-paste.

Во-вторых, новый синтаксис не поддерживает statements, т.е. код, не возвращающий значения. Казалось бы, ну и не надо, но я был сам удивлен, когда понял, на сколько часто используется switch (в связке с pattern matching) для такой вещи как assertion в тестах.

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

        int ExecFull(Operation operation, int x, int y)
        {
            switch (operation)
            {
                case Operation.Summ:
                    logger.LogTrace("{x} + {y}", x, y);
                    return x + y;
                case Operation.Diff:
                    logger.LogTrace("{x} - {y}", x, y);
                    return x - y;
                case Operation.Mult:
                    logger.LogTrace("{x} * {y}", x, y);
                    return x * y;
                case Operation.Div:
                    logger.LogTrace("{x} / {y}", x, y);
                    return x / y;
                default:
                    throw new NotSupportedException();
            }
        }

Я не хочу сказать, что новый switch плох. Нет, он хорош, просто недостаточно хорош.


Property & Positional patterns

Год назад они мне казались главными кандидатами на звание «фитча, изменившая разработку». И, как и ожидалось, что-бы использовать всю мощь positional и property patterns, необходимо поменять свой подход к разработке. А именно, приходится имитировать алгебраические типы данных.
Казалось бы, в чем проблема: берешь маркер-интерфейс и вперед. К сожалению, в большом проекте у этого способа есть серьезный недостаток: никто не гарантирует отслеживание в design time расширения Ваших алгебраических типов. А значит, велика вероятность того, что со временем внесение изменений в код будет приводить к массе «проваливаний в default» в самых неожиданных местах.


Tuple patterns

А вот «младший брат» новых возможностей сопоставления с образцом показал себя настоящим молодцом. Все дело в том, что tuple pattern не требует каких либо изменений в привычной архитектуре нашего кода, он просто упрощает некоторые кейсы:

        Player? Play(Gesture left, Gesture right)
        {
            switch (left, right)
            {
                case (Gesture.Rock, Gesture.Rock):
                case (Gesture.Paper, Gesture.Paper):
                case (Gesture.Scissors, Gesture.Scissors):
                    return null;
                case (Gesture.Rock, Gesture.Scissors):
                case (Gesture.Scissors, Gesture.Paper):
                case (Gesture.Paper, Gesture.Rock):
                    return Player.Left;
                case (Gesture.Paper, Gesture.Scissors):
                case (Gesture.Rock, Gesture.Paper):
                case (Gesture.Scissors, Gesture.Rock):
                    return Player.Right;
                default:
                    throw new NotSupportedException();
            }
        }

Но самое прекрасное, данная фитча, что достаточно предсказуемо, замечательно работает с методом Deconstruct. Достаточно просто передать в switch класс с реализованным Deconstruct и использовать возможности tuple pattern.


Using declarations

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


  • Нередко, при добавлении using нам приходится вытаскивать код «внутрь» блока, методом copy-paste. Теперь мы об этом попросту не думаем
  • Переменные, объявленные внутри using и используемые после Dispose объекта using, самая настоящая головная боль. Еще на одну проблему меньше
  • В классах, требующих частого вызова Dispose, каждый метод был бы на 2 строчки длиннее. Казалось бы, мелочь, но в условии множества небольших методов эта мелочь не позволяет отобразить достаточное количество этих самых методов на одном экране

В итоге такая простая вещь как using declarations настолько сильно меняет ощущение от кодирования, что попросту не хочется возвращаться на c#7.3.


Static local functions

Если честно, если бы не помощь code analysis, я бы даже не заметил эту возможность. Тем не менее она плотно обосновалась в моем коде: ведь статические локальные функции отлично подходят на роль небольших чистых функций, так как не могут поддержать замыкание переменных метода. Как результат, на сердце легче, так как понимаешь, что на одну потенциальную ошибку в твоем коде меньше.


Nullable reference types

И на десерт хотелось бы упомянуть самую главную фитчу c# 8. По правде говоря, разбор nullable reference types заслуживает отдельной статьи. Мне же хочется просто описать ощущения.


Резюме

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

В момент релиза данной статьи C#8, возможно, уже поселился в Вашем проекте, потому мне было бы интересно, какие Ваши ощущения от новой версии нашего любимого языка?

© Habrahabr.ru