Как стать автором
Обновить

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

Время на прочтение5 мин
Количество просмотров10K

В недавнем выпуске подкаста 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 заслуживает отдельной статьи. Мне же хочется просто описать ощущения.


  • Во-первых, это прекрасно. Я и раньше мог описать явное свое намерение объявить поле или свойство nullable, но теперь эта функция встроена в язык.
  • Во-вторых, это совершенно не спасает от NullReferenceException. И я не говорю про пресловутое "забивание" на warnings. Просто в runtime Вам никто не генерирует никаких проверок аргументов на null, так что не спешите выкидывать код вида throw new ArgumentNullException()
  • В третьих, возникает серьёзная проблема с DTO. Например, вы аннотируете свойство атрибутом Required. Соответственно, в Ваш WebAPI контроллер попадет объект с 100% not null свойством. Однако, невозможно связать данный атрибут и все похожие атрибуты с проверками nullable reference types. Все дело в том, что если вы объявите стандартное MyProperty {get; set;} свойство с NotNull типом, то Вы получите warning: "[CS8618] Non-nullable property 'MyProperty' is uninitialized. Consider declaring the property as nullable". Что достаточно справедливо, так как вы не можете в процессе инициализации объекта гарантировать not null семантику. Только результатом данной особенности является невозможность использовать not null свойства в любых DTO. Но есть хорошая новость, существует простой workaround — достаточно проинициализировать ваше поле значением по-умолчанию:
    public string MyProperty { get; set; } = "";
  • В четвертых, атрибуты, позволяющие обработать сложные случаи, типа TryGetValue, сами по себе достаточно непросты. Как результат, высока вероятность, что не особо сознательные разработчики будут злоупотреблять операторами (!), тем самым нивелируя возможности nullable reference types. Одна надежда на анализаторы.
  • В пятых, и самое главное, лично меня эта возможность уже много раз спасла от NullReferenceException ошибок. Получается банальная экономия времени — масса ошибок ловится на этапе компиляции, а не тестов или отладки. Особенно это актуально не только в процессе разработки сложной бизнес логики, но и в случае банальной работы с внешними библиотеками, DTO, и прочими зависимостями, возможно, содержащими null.

Резюме


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


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

Теги:
Хабы:
+17
Комментарии29

Публикации

Истории

Работа

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн