В этой статье мы расскажем о нововведениях в языке C# 7.0, которые были представлены в марте 2017 года как часть релиза Visual Studio 2017.

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

Если вас интересует процесс разработки, который привел к этому набору функций, вы можете найти заметки, предложения и множество обсуждений на эту тему на сайте C# language design GitHub.

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

Получайте удовольствие от C# 7.0 и удачного хакинга!

Out переменные

В более ранних версиях C# использование out параметров является не таким легким, как нам хотелось бы. Прежде чем вызвать метод с out параметрами, сначала необходимо объявить переменные, чтобы перейти к нему. Поскольку вы обычно не инициализируете эти переменные (они все равно будут перезаписаны методом), вы также не можете использовать ключевое слово var, но вам нужно указать полный тип:

public void PrintCoordinates(Point p)

{

    int x, y; // have to "predeclare"

    p.GetCoordinates(out x, out y);

    WriteLine($"({x}, {y})");

}

В C# 7.0 мы добавили out переменные, что позволяет объявлять переменную прямо в точке, где она передается как out аргумент:

public void PrintCoordinates(Point p)

{

    p.GetCoordinates(out int x, out int y);

    WriteLine($"({x}, {y})");

}

 Обратите внимание, что переменные находятся в области видимости в окружающем блоке, поэтому последующая строка может их использовать. Многие виды утверждений не устанавливают свою собственную область действия, поэтому out переменные, объявленные в них, часто вводятся в область видимости.

Поскольку out переменные объявляются непосредственно в качестве аргументов для out параметров, компилятор может обычно указывать, каков должен быть их тип (если только не существует конфликтующих перегрузок), поэтому вместо типа можно использовать ключевое слово var:

p.GetCoordinates(out var x, out var y);

 Общим использованием out параметров является шаблон Try..., где логическое возвращаемое значение указывает на успех, а out параметры переносят полученные результаты:

public void PrintStars(string s)

{

    if (int.TryParse(s, out var i)) { WriteLine(new string('*', i)); }

    else { WriteLine("Cloudy - no stars tonight!"); }

}

 Мы также допускаем «сбрасывание» в качестве out параметров в виде «_», что позволит вам проигнорировать параметры, которые вам не нужны:

p.GetCoordinates(out var x, out _); // I only care about x

Соответствие с шаблоном

В C# 7.0 вводится понятие шаблонов, которые являются синтаксическими элементами, позволяющими проверить соответствие значения определенной «форме» и извлечь информацию из значения, если такое соответствие имеется.

Примеры шаблонов в C# 7.0:

• Константные шаблоны c (где c – константное выражение в C#), которые проверяют, равняется ли переменная этой константе.

• Шаблоны типа T x (где T – тип и x – идентификатор), которые проверяют, имеет ли переменная тип T, и если да, то извлекают значение в новую переменную x типа T.

• Var шаблоны var x (где x – идентификатор), которые всегда совпадают и просто помещают значение ввода в новую переменную x с тем же типом.

Это только начало; шаблоны являются новым типом элемента языка C#, и в будущем мы обязательно добавим новые шаблоны в C#.

В C# 7.0 мы улучшаем две существующие языковые конструкции с шаблонами:

is теперь может использоваться не только с типом, но и с шаблоном;

case в операторе switch теперь может использовать шаблоны, а не только константы.

В будущих версиях C#, вероятно, мы добавим больше мест, где можно использовать шаблоны.

Шаблоны с is

Рассмотрим пример использования is с константным шаблоном и шаблоном типа:

public void PrintStars(object o)

{

    if (o is null) return;     // constant pattern "null"

    if (!(o is int i)) return; // type pattern "int i"

    WriteLine(new string('*', i));

}

Как видно из примера, переменные шаблона, представленные шаблоном, аналогичны out переменным, описанным ранее, поэтому могут быть объявлены в середине выражения и использоваться в ближайшей окружающей области. Также как out переменные, переменные шаблона изменяемы. Мы часто ссылаемся на out переменные и переменные шаблона совместно как «переменные выражения».

Шаблоны и Try-методы часто используются вместе:

if (o is int i || (o is string s && int.TryParse(s, out i)) { /* use i */ }

Шаблоны с выражениями switch

Мы обобщаем варианты использования switch:

• Вы можете использовать любой тип (не только простые типы).

• Шаблоны могут использоваться в выражениях case.

• Вы можете добавлять дополнительные условия к выражениям case.

Вот простой пример:

switch(shape)

{

    case Circle c:

        WriteLine($"circle with radius {c.Radius}");

        break;

    case Rectangle s when (s.Length == s.Height):

        WriteLine($"{s.Length} x {s.Height} square");

        break;

    case Rectangle r:

        WriteLine($"{r.Length} x {r.Height} rectangle");

        break;

    default:

        WriteLine("");

        break;

    case null:

        throw new ArgumentNullException(nameof(shape));

}

Существует несколько особенностей, которые следует отметить в этом новом расширенном выражении switch:

• Порядок выражений case теперь имеет значение: как и в случае с выражениями catch, у выражений case выбирается первое по порядку выражение, удовлетворяющее условию. Поэтому важно, чтобы условие квадрата было перед условием прямоугольника. Кроме того, как и в случае с выражениями catch, компилятор поможет вам пометить явные недостижимые условия. До этого вы не могли определить порядок выполнения, так что это не является нарушением существующего поведения.

• Условие по умолчанию (default) всегда вычисляется последним: несмотря на то, что после него идет условие null, условие default будет проверено после него. Это сделано для совместимости с существующей семантикой. Однако, как правило, вы помещаете условие default  в конце.

• Условие null в конце достижимо, потому что шаблоны типов следуют примеру текущего is  и не срабатывают для null. Это гарантирует, что null значения не будут случайно сопоставлены с первым шаблоном типа; вы должны явно указать, как им управлять (или оставить логику для условия default).

Переменные шаблона, объявленные ключевым словом case..., находятся в области видимости только в соответствующем разделе switch.

Кортежи

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

Out параметры: использование является неэффективным (даже при использовании рассмотренных нововведений) и они не работают с асинхронными методами.

System.Tuple <...>: выглядит многословным для использования и требует выделения кортежного объекта.

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

Анонимные типы, возвращаемые через тип возврата dynamic: потери в производительности и отсутствие проверки статического типа.

 

 

 

Для упрощения этой задачи в C# 7.0 были добавлены кортежи и литералы кортежей:

(string, string, string) LookupName(long id) // tuple return type

{

    ... // retrieve first, middle and last from data storage

    return (first, middle, last); // tuple literal

}

 Теперь метод эффективно возвращает три строки, объединенные как элементы кортежа.

Вызывающий код метода получит кортеж и может индивидуально иметь доступ к элементам:

var names = LookupName(id);

WriteLine($"found {names.Item1} {names.Item3}.");
 

Имена полей Item1 и т. д. являются именами по умолчанию для элементов кортежа и могут использоваться всегда. Но они не очень наглядны, поэтому вы можете добавить лучшие имена:

(string first, string middle, string last) LookupName(long id) // tuple elements have names
 

Теперь получатель этого кортежа имеет более описательные имена для дальнейшей работы:

var names = LookupName(id);

WriteLine($"found {names.first} {names.last}.");

Вы также можете указать имена элементов непосредственно в литералах кортежей:

return (first: first, middle: middle, last: last); // named tuple elements in a literal

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

Кортежи – это типы значений, а их элементы – общедоступные изменяемые поля. Они имеют значение равенства, а это значит, что два кортежа являются равными (и имеют одинаковый хэш-код), если все их элементы попарно равны (и имеют одинаковый хэш-код).

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

Кортежи полагаются на базовые структурные типы, которые называются ValueTuple <...>. Если вы выявите модель, что еще не включает эти типы, вы можете вместо этого выбрать их с помощью NuGet:

• Щелкните правой кнопкой мыши проект в обозревателе решений и выберите «Manage NuGet Packages…».

• Выберите вкладку «Browse» и выберите «nuget.org» в качестве «Package source».

• Найдите «System.ValueTuple» и установите его.

Распаковка кортежей

Еще один способ использования кортежа – это его распаковка. Объявление распаковки является синтаксисом для разделения кортежа (или другого значения) на его части и назначения этих частей по отдельности новым переменным:

(string first, string middle, string last) = LookupName(id1); // deconstructing declaration

WriteLine($"found {first} {last}.");

В объявлении распаковки можно использовать ключевое слово var для отдельных переменных:

(var first, var middle, var last) = LookupName(id1); // var inside

Или даже поместить var перед скобками как аббревиатуру:

var (first, middle, last) = LookupName(id1); // var outside

Вы также можете распаковать в уже существующие переменные с помощью присвоения распаковки:

(first, middle, last) = LookupName(id2); // deconstructing assignment

Распаковка выполняется не только для кортежей. Любой тип может быть распакован, если у него есть метод распаковки (образец или расширение):

public void Deconstruct(out T1 x1, ..., out Tn xn) { ... }

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

(Почему используются out параметры, а не кортежи? Чтобы можно было иметь несколько перегрузок метода с разным количеством параметров).

class Point

{

    public int X { get; }

    public int Y { get; }

    public Point(int x, int y) { X = x; Y = y; }

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

}

(var myX, var myY) = GetPoint(); // calls Deconstruct(out myX, out myY);

Это будет обычный шаблон для создания «симметричных» конструкторов и методов распаковки таким способом.

Так же, как и для out переменных, мы разрешаем «сбрасывать» в распаковке параметры, которые вам не нужны:

(var myX, _) = GetPoint(); // I only care about myX

Локальные функции

Иногда вспомогательная функция имеет смысл только внутри одного метода, в котором вызывается. Теперь вы можете объявить такие функции внутри других функций как локальную функцию:

public int Fibonacci(int x)

{

    if (x < 0) throw new ArgumentException("Less negativity please!", nameof(x));

    return Fib(x).current;

    (int current, int previous) Fib(int i)

    {

        if (i == 0) return (1, 0);

        var (p, pp) = Fib(i - 1);

        return (p + pp, p);

    }

}

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

В качестве примера рассмотрим методы, реализованные как итераторы, что обычно нуждаются в неитераторном методе-оболочке для точной проверки аргументов в момент их вызова (так как сам итератор не запускается, пока не будет вызван MoveNext). Локальные функции идеально подходят для этого сценария:

public IEnumerable Filter(IEnumerable source, Func filter)

{

    if (source == null) throw new ArgumentNullException(nameof(source));

    if (filter == null) throw new ArgumentNullException(nameof(filter));

    return Iterator();

    IEnumerable Iterator()

    {

        foreach (var element in source) 

        {

            if (filter(element)) { yield return element; }

        }

    }

}

Если бы Iterator был приватным методом рядом с Filter, то мог быть доступен для других членов в использовании напрямую (без проверки аргументов). Кроме того, необходимо было бы передавать все те же аргументы, что и Filter, вместо того, чтобы иметь их только в области видимости.

Улучшения литералов

В C# 7.0 появилась возможность добавлять «_» в качестве разделителя в числовые литералы:

var d = 123_456;

var x = 0xAB_CD_EF;

 Вы можете поместить разделитель в любом месте между цифрами, чтобы улучшить читабельность. Они не влияют на значение.

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

var b = 0b1010_1011_1100_1101_1110_1111;

 Локальные переменные и возвращаемые значения по ссылке

Теперь можно не только передать параметры в метод по ссылке в  С# (с помощью ключевого слова ref), но и возвратить данные из метода по ссылке, а также сохранить в локальной переменной тоже по ссылке.

public ref int Find(int number, int[] numbers)

{

    for (int i = 0; i < numbers.Length; i++)

    {

        if (numbers[i] == number) 

        {

            return ref numbers[i]; // return the storage location, not the value

        }

    }

    throw new IndexOutOfRangeException($"{nameof(number)} not found");

}

int[] array = { 1, 15, -39, 0, 7, 14, -12 };

ref int place = ref Find(7, array); // aliases 7's place in the array

place = 9; // replaces 7 with 9 in the array

WriteLine(array[4]); // prints 9

Очень удобно передавать ссылки на определенные места в больших структурах данных. Например, в игре информация содержится в большом заранее выделенном массиве структур (во избежание пауз на сбор мусора). Теперь методы могут вернуть ссылку непосредственно на одну из таких структур, с помощью которой вызывающий код может читать и изменять эту структуру.

Существуют некоторые ограничения для обеспечения безопасности:

• Можно возвращать только ссылки, которые возвращать безопасно: ссылки, переданные в метод и ссылки на поля объектов.

• Локальные переменные инициализируются определенной ячейкой памяти и в будущем не меняются.

Обобщенные типы асинхронных возвратов

До сегодняшнего дня асинхронные методы могли возвращать только void, Task или Task. В C# 7.0 позволяется создавать типы, которые также могут быть возвращены асинхронным методом.

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

Конечно, можно придумать и другие ситуации, в которых task-подобные объекты будут полезны. Правильное создание таких типов не будет простой задачей, поэтому мы не ожидаем, что большое количество разработчиков будут создавать их. Однако мы полагаем, что они будут появляться в различных моделях и прикладных интерфейсах, и вызывающий код сможет просто использовать await, как сейчас для Task.

Больше членов в виде выражений

Методы и свойства в виде выражений используются в C# 6.0, но не все типы членов можно было так объявлять. В C# 7.0 к списку членов в виде выражений добавилась поддержка аксессоров, конструкторов и финализаторов:

class Person

{

    private static ConcurrentDictionary names = new ConcurrentDictionary();

    private int id = GetId();

 

    public Person(string name) => names.TryAdd(id, name); // constructors

    ~Person() => names.TryRemove(id, out _);              // finalizers

    public string Name

    {

        get => names[id];                                 // getters

        set => names[id] = value;                         // setters

    }

}

Это пример функции, которая была предоставлена сообществом, а не командой компилятора Microsoft C#. Ура, открытый код!

Throw выражения

Выбросить исключение в середине выражения очень легко: достаточно вызвать метод, который это сделает! Но в C# 7 теперь можно использовать throw как часть выражения в определенном месте:

class Person

{

    public string Name { get; }

    public Person(string name) => Name = name ?? throw new ArgumentNullException(nameof(name));

    public string GetFirstName()

    {

        var parts = Name.Split(" ");

        return (parts.Length > 0) ? parts[0] : throw new InvalidOperationException("No name!");

    }

    public string GetLastName() => throw new NotImplementedException();

}

Источник