Microsoft corporate blog
Programming
.NET
C#
February 20

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

Original author: Мэдс Торгерсен
Translation

Совсем недавно вышла Visual Studio 2019 Preview 2. И вместе с ней пара дополнительных функций C# 8.0 готовы к тому, чтобы вы их опробовали. В основном речь идет о сопоставлении с образцом, хотя в конце я коснусь и некоторых других новостей и изменений.


Эта статья на английском




За перевод спасибо нашему MSP, Льву Буланову.

Больше паттернов в большем количестве мест


Когда в C# 7.0 появилось сопоставление с образцом, мы отметили, что в будущем ожидается увеличение числа паттернов в большем количестве мест. Это время пришло! Мы добавляем то, что мы называем рекурсивными паттернами, а также более компактную форму выражений switch, называемых (как вы уже догадались) switch expressions.


Для начала вот простой пример паттернов C# 7.0:


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";
    }
}

Switch expressions


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


Мы решили, что пришло время добавить форму выражений switch. Она применима к приведенному ниже примеру:


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» между тестируемым значением и списком {...} кейсов. Это делает его более композиционным с другими выражениями, а также его легче визуально отличить от оператора switch.
  • Ключевое слово case и символ: были заменены лямбда-стрелкой => для краткости.
  • Default для краткости был заменен паттерном сброса _ .
  • Bodies — это выражения. Результат выбранного body становится результатом выражения switch.

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


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


 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 «табличным» способом, как указано выше, с паттернами и bod на одной линии, и => выстроившимися друг под другом.


Кстати, мы планируем разрешить использование запятой после последнего кейса в соответствии со всеми другими «списками, разделенными запятыми в фигурных скобках» в C#, но в Preview 2 это пока не разрешено.


Свойства паттернов


 К слову, о краткости — паттерны внезапно становятся самыми тяжелыми элементами выражений выбора. Давайте что-нибудь с этим сделаем.


Обратите внимание, что выражение выбора использует паттерн типа Point p (дважды), а также when для добавления дополнительных условий в первом case.


В C# 8.0 мы добавляем дополнительные необязательные элементы в тип паттернов, что позволяет самому паттерну углубляться в значение, которое сопоставляется с образцом. Вы можете сделать его паттерном свойств, добавив {...}, содержащие вложенные паттерны, применяя к доступным свойствам или полям значения. Это позволяет нам переписать switch expression следующим образом:


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"

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


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

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


Позиционные паттерны


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


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


(int x, int y) = GetPoint(); // split up the Point according to its deconstructor

C# 7.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 expression   понятно и легко читается.


Паттерны кортежей


Очень полезным частным случаем позиционных паттернов является их применение к кортежам. Если оператор 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")
    };

В целом вы видите, что рекурсивные паттерны и switch expressions могут привести к более ясной и декларативной логике программы.


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


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


Использование объявлений


В C# using операторы всегда увеличивают уровень вложенности, что может сильно раздражать и ухудшать читаемость. В простых случаях, когда требуется просто очистить ресурс в конце области, используются объявления using. Объявления 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 structs


Ref structs были введены в C # 7.2, и вроде бы здесь не место повторяться о них. Но все-таки стоит отметь кое-что: они имеют некоторые ограничения, такие как невозможность имплементации интерфейсов. Ref structs теперь можно использовать без имплементации интерфейса IDisposable, просто используя в них метод Dispose.


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


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


Изменения по сравнению с Preview 1


Основными функциями Preview 1 были обнуляемые ссылочные типы и асинхронные потоки. Обе фичи немного изменились в Preview 2, поэтому, если вы начали их использовать, полезно знать следующее.


Обнуляемые ссылочные типы


Мы добавили больше опций для управления nullable- предупреждениями как в источнике (через #nullable и #pragma warning директивы), так и на уровне проекта. Мы также изменили подписку на файл проекта на <NullableContextOptions> enable </ NullableContextOptions>.


Асинхронные потоки


Мы изменили форму интерфейса IAsyncEnumerable <T>, которого ожидает компилятор. Это приводит к тому, что компилятор не синхронизируется с интерфейсом, предусмотренным в .NET Core 3.0 Preview 1, что может вызвать некоторые проблемы. Однако скоро выйдет .NET Core 3.0 Preview 2, и это вернет синхронизацию.

+23
9.5k 76
Comments 15
Top of the day