Больше возможностей с C# 8.0

Недавно была выпущена превью версия Visual Studio 2019. Вместе с ней стали доступны новые фичи языка C# и появилась интересная статья в .NET Blog на эту тему: Do more with patterns in C# 8.0. Ниже вы найдете вольный перевод этой статьи.

Вышел Visual Studio 2019 Preview 2! А вместе с новой IDE появились несколько дополнительных функций C # 8.0 которые вы можете попробовать прямо сейчас. В основном речь идет о сопоставлении с образцом (pattern matching), хотя в конце статьи будет немного больше информации и по другим изменениям.

Когда в C # 7.0 ввели сопоставление с образцом, команда разработчиков C# сказала, что ожидаем они собираются добавить больше образцов в большем количестве мест программы в будущем. И это время пришло! В C# 8.0 добавляется то, что мы называем рекурсивными шаблонами (recursive patterns), а также более компактную форму записи оператора switch которая называется (как вы уже догадались) выражениями switch (switch expressions).
Ниже пример кода с использованием pattern matching , с которого мы и начнем:

class Point
{
    public int X { get; }
    public int Y { get; }
    public Point(int x, int y) => (X, Y) = (x, y);
    public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}

static string Display(object o)
{
    switch (o)
    {
        case Point p when p.X == 0 && p.Y == 0:
            return "origin";
        case Point p:
            return $"({p.X}, {p.Y})";
        default:
            return "unknown";
    }
}
 

Если вы вдруг не совсем понимаете, что тут происходит, то вот вам краткое описание:
Мы объявили простой класс с двумя полями X и Y. У этого класса есть конструктор, который может конструировать класс используя запись в виде кортежа (tuples). У класса есть деконструктор, который конвертирует класс в форму кортежа. Есть некий метод, который принимает любую переменную ссылочного типа в виде ссылки на object, и в зависимости от типа выводит различную информацию.

Если это переменная типа Point у которой поля X и Y равны 0, то выполниться первый case и вернется строка “origin”.
Если это переменная типа Point у которой поля X и/или Y не равны 0, то будет выполнен второй case и вернется строка со значениями координат.
Если это переменная не типа Point, то выполнится третий case и вернется строка «unknown».
Это и есть сопоставление с образцом.

Switch выражение

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

Поэтому в команде C# решили, что пришло время добавить switch в виде выражения. Вот так будет выглядеть вышеприведённый пример с новыми возможностями C# 8:

static string Display(object o)
{
    return o switch
    {
        Point p when p.X == 0 && p.Y == 0 => "origin",
        Point p                           => $"({p.X}, {p.Y})",
        _                                 => "unknown"
    };
}

Как видите, код отличается от стандартного оператора switch:

  • Ключевое слово switch — это «infix» между проверяемым значением и списком case выражений {…}. Это делает его более похожим на другие выражения, а также визуально отличает switch выражение от оператора switch.
  • Ключевое слово case и символ : были заменены лямбда-стрелкой => для краткости.
  • Для краткости значение по умолчанию было заменено шаблоном сброса _.

Тело замененного case — это выражение. Результат выбранного тела становится результатом switch выражения.

Поскольку выражение должно иметь значение или бросать исключение, switch выражение, которое достигает конца без совпадения, сгенерирует исключение. Компилятор отлично отлавливает такие случаи и предупреждает вас. Но компилятор не заставит вас вставлять в конце catch-all выражение – вы, как разработчик программы, всегда знаете лучше, как должна быть построена программа.

Вернемся к примеру. Так как наш метод Display теперь состоит из одного оператора return, мы можем упростить его до метода с телом — выражением:

    static string Display(object o) => o switch
    {
        Point p when p.X == 0 && p.Y == 0 => "origin",
        Point p                           => $"({p.X}, {p.Y})",
        _                                 => "unknown"
    };

Если честно, я не уверен, какое форматирование лучше подходит, но очевидно, что это намного более краткая и понятная запись. Такая краткость позволяет форматировать switch выражение в «табличном» виде, как сделано выше – условия выровнены по левому краю, оператор => выравнен справа, а за ним в столбец перечислены возвращаемые значения.

Шаблоны свойств

Если говорить о краткости, то шаблоны внезапно становятся самыми тяжелыми элементами switch выражения. Давайте попробуем что-то сделать с этим.
Обратите внимание, выражение switch использует шаблон типа Point p (дважды), а также предложение when для добавления дополнительных условий для первого случая.
В C # 8.0 в шаблоны типов добавляются больше опциональных элементов. Это позволяет шаблону больше сосредоточиться на значении сопоставляемого шаблона. Вы можете сделать шаблон свойства, добавив блок {…}, содержащий вложенные шаблоны, для применения к доступным свойствам или полям значения шаблона. Это позволяет нам переписать выражение switch из примера следующим образом (код немного прояснит эти запутанные предложения):

static string Display(object o) => o switch
{
    Point { X: 0, Y: 0 }         p => "origin",
    Point { X: var x, Y: var y } p => $"({x}, {y})",
    _                              => "unknown"
};

Оба случая все еще проверяют, что o является объектом типа Point. В первом случае рекурсивно применяется шаблон константы 0 к свойствам X и Y переменной p, проверяя, имеют ли они это значение. Таким образом, мы можем исключить условие when в этом и многих распространенных случаях.

Во втором случае шаблон var применяется к каждому из X и Y. Напомним, что шаблон var в C # 7.0 всегда выполняется успешно, и просто объявляет новую переменную для хранения значения. Таким образом, x и y содержат значения int для p.X и p.Y.

Мы никогда не используем p, и фактически можем опустить его в этом примере:

    Point { X: 0, Y: 0 }         => "origin",
    Point { X: var x, Y: var y } => $"({x}, {y})",
    _                            => "unknown"

Единственная вещь, которая требуется для всех шаблонов типов, включая шаблоны свойств, состоит в том, что значение должно быть не ненулевым. Это открывает возможность использования «пустого» шаблона свойств {} в качестве компактного «не нулевого» шаблона. Например, мы могли бы заменить вариант по умолчанию следующими двумя случаями:

    {}                           => o.ToString(),
    null                         => "null"

Шаблон {} имеет дело с оставшимися ненулевыми объектами, а null обьекты попадут в последний case, поэтому такой switch является исчерпывающим, и компилятор не будет жаловаться на возможные необработанные случаи.

static string Display(object o) => o switch
{
	Point { X: 0, Y: 0 }         p => "origin",
	Point { X: var x, Y: var y } p => $"({x}, {y})",
	{}                           => o.ToString(),
	null                         => "null"
};

Позиционные шаблоны

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

Обратите внимание, что класс Point имеет метод Deconstruct, так называемый деконструктор. В C # 7.0 деконструкторы позволяли деконструировать значение при присваивании, чтобы вы могли написать, например так:

(int x, int y) = GetPoint();

C # 7.0 не интегрировал деконструкцию с шаблонами. Но в C# 8.0 это меняется и деконструкторы интегрированы с позиционными шаблонами, которые являются дополнительным способом расширения шаблонов типов в C # 8.0. Если совпавший тип является типом кортежа или имеет деконструктор, мы можем использовать позиционные шаблоны как компактный способ применения рекурсивных шаблонов без необходимости указывания имени свойства:

static string Display(object o) => o switch
{
    Point(0, 0)         => "origin",
    Point(var x, var y) => $"({x}, {y})",
    _                   => "unknown"
};

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

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

Шаблоны кортежей

Очень полезный особый случай позиционных паттернов — это когда они применяются к кортежам. Если оператор switch применяется непосредственно к выражению кортежа, мы даже разрешаем опускать дополнительный набор скобок, как switch (x, y, z) вместо switch ((x, y, z)).

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

static State ChangeState(State current, Transition transition, bool hasKey) =>
    (current, transition) switch
    {
        (Opened, Close)              => Closed,
        (Closed, Open)               => Opened,
        (Closed, Lock)   when hasKey => Locked,
        (Locked, Unlock) when hasKey => Closed,
        _ => throw new InvalidOperationException($"Invalid transition")
    };

Конечно, мы могли бы включить hasKey внутрь кортежа, вместо использования выражения when — это действительно дело вкуса:

static State ChangeState(State current, Transition transition, bool hasKey) =>
    (current, transition, hasKey) switch
    {
        (Opened, Close,  _)    => Closed,
        (Closed, Open,   _)    => Opened,
        (Closed, Lock,   true) => Locked,
        (Locked, Unlock, true) => Closed,
        _ => throw new InvalidOperationException($"Invalid transition")
    };

Другие новшества в C# 8.0 Preview 2

Несмотря на то, что в VS 2019 Preview 2 основные функции для работы с шаблонами являются наиболее важными, есть несколько более мелких функций, которые, я надеюсь, вы также найдете полезными и интересными. Я не буду вдаваться в подробности здесь, но просто дам вам краткое описание каждого.

Объявление Using

В C # использование операторов всегда вызывает уровень вложенности, что может сильно раздражать и ухудшать читабельность. Для простых случаев, когда вы просто хотите, чтобы ресурс был очищен в конце области, теперь вы используете вместо этого объявления. Using объявления — это просто объявления локальных переменных с ключевым словом using, а их содержимое освобождается в конце текущего блока операторов. Так что вместо:

static void Main(string[] args)
{
    using (var options = Parse(args))
    {
        if (options["verbose"]) { WriteLine("Logging..."); }
        ...
    } // options disposed here
}

Вы можете теперь писать:

static void Main(string[] args)
{
    using var options = Parse(args);
    if (options["verbose"]) { WriteLine("Logging..."); }

} // options disposed here

Освобождаемые ссылочные структуры

Ссылочные структуры (ref struct) были введены в C # 7.2. Это было крайне полезное нововведение, но они шли с некоторыми серьезными ограничениями, такими как неспособность реализовать интерфейсы. Ссылочные структуры теперь можно использовать без реализации интерфейса IDisposable, просто используя в них метод Dispose.

Статические локальные функции

Если вы хотите убедиться, что ваша локальная функция не несет затрат времени выполнения, связанных с «захватом» переменных из внешней области, вы можете объявить ее как статическую. Тогда компилятор не будет создавать ссылки на все, что объявлено во включающих функциях — кроме других статических локальных функций!

Добавить комментарий

Ваш e-mail не будет опубликован.