Pull to refresh

Что нового появилось в C# 7 и уже поддерживается в Visual Studio “15” Preview 4

Reading time 11 min
Views 69K
Original author: Mads Torgersen
С релизом Visual Studio “15” Preview 4 многие новые фичи C# 7 можно попробовать самому. Основные новшества C# 7 призваны облегчить работу с данными, упростить код и улучшить производительность. От себя скажу, что C# движется в сторону функционального языка, добавляя такие вещи, как кортежи и сопоставления с шаблоном (pattern matching). Не все из новой функциональности работает так, как задумано, в Preview 4, в этих случаях будет указано, что именно вы можете пока использовать и как это будет работать в будущем. Что ж, приступим.

Out переменные


Сейчас использовать out переменные не так легко, как нам хотелось бы. Перед тем как вызвать метод с out аргументами, необходимо объявить переменные, которые будут переданы в этот метод. Так как обычно значения этим переменным во время объявления не присваиваются (что логично — они все равно будут перезаписаны методом), ключевое слово var использовать не получится. Надо объявлять переменные с указанием их типа:

public void PrintCoordinates(Point p)
{
    int x, y; // нужно объявить переменные
    p.GetCoordinates(out x, out y);
    WriteLine($"({x}, {y})");
}

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

public void PrintCoordinates(Point p)
{
    p.GetCoordinates(out int x, out int y);
    WriteLine($"({x}, {y})");
}

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

Внимание: В Preview 4 действуют более строгие ограничения на область видимости: 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("Облачно - звезд нет!"); }
}

Внимание: В данном примере i используется только внутри блока if, в котором определена, поэтому в Preview 4 этот пример тоже работает.

Одним из возможных улучшений (которое необязательно попадет в C# 7), может стать использование подстановочных символов ( * ) вместо тех out параметров, которые не будут использоваться далее. Например:

p.GetCoordinates(out int x, out *); // Нам нужен только x

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


В седьмой версии C# появляется понятие шаблона (pattern), который в общем случае представляет собой синтаксическую конструкцию, позволяющую проверить соответствие переменной определенному шаблону и извлечь из нее информацию, если такое соответствие имеется.

В C# 7 есть следующие шаблоны:

  • Константные шаблоны: c (где c — константное выражение C#); проверяют, равна ли переменная этой константе или нет.
  • Шаблоны типа: T x (где T — тип и x — переменная); проверяют, имеет ли переменная тип T, и если да, то извлекает ее значение в новую переменную x типа T.
  • Var шаблоны: var x (где x — переменная); этот шаблон всегда вычисляется в true, используется для создания новой переменной того же типа и с тем же значением.

Это всего лишь начало, и в будущем мы обязательно добавим новые шаблоны в C#. Для поддержки шаблонов были изменены 2 уже существующих языковых конструкции:

  • is теперь может использоваться не только с типом, но и с шаблоном (в качестве правого аргумента).
  • case в операторе switch теперь может использовать шаблоны, а не только константы.

В будущем мы добавим еще возможности использования шаблонов.

Шаблоны с is


Рассмотрим простой пример, в котором используются и константный шаблон, и шаблон типа.

public void PrintStars(object o)
{
    if (o is null) return;     // константный шаблон "null"
    if (!(o is int i)) return; // шаблон типа "int i"
    WriteLine(new string('*', i));
}

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

Внимание: В Preview 4 для переменных шаблона так же, как и для out переменных, действуют более строгие правила видимости, поэтому пример заработает только в следующих релизах.

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

if (o is int i || (o is string s && int.TryParse(s, out i)) { /* можно использовать i типа int */ }

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


Варианты использования switch были расширены, теперь можно:

  • Использовать любые типы (не только примитивные).
  • Использовать шаблоны в выражениях case.
  • Добавлять дополнительные условия к выражениям case (используя ключевое слово when).

Теперь рассмотрим пример:

switch(shape)
{
    case Circle c:
        WriteLine($"круг с радиусом {c.Radius}");
        break;
    case Rectangle s when (s.Length == s.Height):
        WriteLine($"{s.Length} x {s.Height} квадрат");
        break;
    case Rectangle r:
        WriteLine($"{r.Length} x {r.Height} прямоугольник");
        break;
    default:
        WriteLine("<неизвестная фигура>");
        break;
    case null:
        throw new ArgumentNullException(nameof(shape));
}

Отметим следующие особенности нового расширенного switch:

  • Порядок выражений case теперь имеет значение. Теперь логика сопоставления такая же, как у выражений catch: будет выбрано первое по порядку выражение, удовлетворяющее условию. Поэтому в данном примере важно, что более специфичное условие для квадрата идет перед более общим условием для прямоугольника, если поменять их местами, то условие для квадрата никогда не сработает. В таких случаях на помощь придет компилятор, который будет помечать явные недостижимые условия (так же, как и для catch). Данное изменение не является изменением уже существующего поведения: до C# 7 порядок выполнения выражений case не был определен.

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

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

Областью видимости для переменных шаблона, объявленных в case, является выражение switch.

Кортежи


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

  • Out параметры: синтаксис выглядит перегруженным (даже если использовать рассмотренные выше нововведения), неприменимо к асинхронным методам.

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

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

  • Объект dynamic: потери в производительности и отсутствие проверки типов на этапе компиляции.

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

(string, string, string) LookupName(long id) // возвращаемый тип - кортеж
{
    ... // инициализируем данные
    return (first, middle, last); // литерал кортежа
}

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

var names = LookupName(id);
WriteLine($"найдены {names.Item1} {names.Item3}.");

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

(string first, string middle, string last) LookupName(long id) // элементы кортежа теперь имеют собственные имена

Теперь к элементам кортежа можно обратиться так:

var names = LookupName(id);
WriteLine($"найдены {names.first} {names.last}.");

Также имена элементов можно указать сразу в литерале кортежа:

return (first: first, middle: middle, last: last); //указываем имена элементов в литерале кортежа

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

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

Внимание: Кортежи в своей работе опираются на типы, которых еще нет в Preview 4, но их можно добавить в проект с помощью NuGet (не забудьте выбрать «Include prerelease» и указать «nuget.org» в качестве «Package source»), пакет называется System.ValueTuple.

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


Еще один способ работы с кортежем — его распаковка, которая заключается в присваивании его элементов новым переменным:

(string first, string middle, string last) = LookupName(id1); // deconstructing declaration
WriteLine($"найдены {first} {last}.");

Также можно использовать ключевое слово var вместо типа для каждой переменной:

(var first, var middle, var last) = LookupName(id1); // var внутри

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

var (first, middle, last) = LookupName(id1); // var снаружи

Также распаковать кортеж можно в уже объявленные переменные:

(first, middle, last) = LookupName(id2); //переменные уже объявлены

Распаковать можно не только кортеж, любой тип может быть распакован. Для этого он должен иметь метод следующего вида:

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(); // вызывает Deconstruct(out myX, out myY);

Такой подход позволит создавать «симметричные» конструктор и метод распаковки.
Так же как и для out переменных мы планируем добавить подстановочные символы для игнорирования некоторых возвращаемых параметров.

(var myX, *) = GetPoint(); // нам нужен только myX

Внимание: До сих пор неизвестно, будут ли добавлены подстановочные символы в C# 7.

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


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

public int Fibonacci(int x)
{
    if (x < 0) throw new ArgumentException("Не надо негатива!", 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<T> Filter<T>(IEnumerable<T> source, Func<T, bool> filter)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (filter == null) throw new ArgumentNullException(nameof(filter));

    return Iterator();

    IEnumerable<T> Iterator()
    {
        foreach (var element in source) 
        {
            if (filter(element)) { yield return element; }
        }
    }
}

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

Внимание: В Preview 4 локальные функции должны быть объявлены перед вызовом. В будущем это ограничение будет ослаблено: локальные функции можно будет вызвать после того, как всем локальным переменные, ими используемые, будут присвоены значения.

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


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

var d = 123_456;
var x = 0xAB_CD_EF;

Разделитель можно добавить в любом месте между цифрами, на значение он не влияет.
Также в C# 7 появились бинарные литералы:

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]; // возвращаем ссылку на место хранения, а не значение элемента массива
        }
    }
    throw new IndexOutOfRangeException($"{nameof(number)} не найден");
}

int[] array = { 1, 15, -39, 0, 7, 14, -12 };
ref int place = ref Find(7, array); // ссылка на место, где находится 7 в массиве
place = 9; // заменяем 7 на 9
WriteLine(array[4]); // выведет 9

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

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

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

Расширение списка типов, возвращаемых асинхронными методами


До сегодняшнего дня, async методы могли возвращать только void, Task or Task<T>. В C# 7 появилась возможность создания типов, которые также могут быть возвращены асинхронным методом. Например, можно создать структуру ValueTask<T>, которая поможет избежать создания объекта Task<T> в случае, когда результат асинхронной операции уже доступен. Для многих асинхронных сценариев, например, где используется буферизация, такой подход может значительно уменьшить число выделений памяти и таким образом повысить производительность.

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

Внимание: В Preview 4 эти типы еще не доступны.

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


Методы и свойства в виде выражений (expression bodied members), появившиеся в C# 6, часто использовались, но не все типы членов класса можно было так объявлять. Теперь в C# 7 добавилась поддержка сеттеров, геттеров, конструкторов и деструкторов:

class Person
{
    private static ConcurrentDictionary<int, string> names = new ConcurrentDictionary<int, string>();
    private int id = GetId();

    public Person(string name) => names.TryAdd(id, name); // конструктор
    ~Person() => names.TryRemove(id, out *);              // декструктор
    public string Name
    {
        get => names[id];                                 // геттер
        set => names[id] = value;                         // сеттер
    }
}

Это пример новой функциональности, добавленной сообществом, а не командой разработки компилятора! Ура, опен сорс!

Внимание: В Preview 4 поддержка этих членов класса недоступна.

Throw выражения


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

class Person
{
    public string Name { get; }
    public Person(string name) => Name = name ?? throw new ArgumentNullException(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();
}

Внимание: В Preview 4 такие выражения еще не доступны.

Заключение


Хоть до релиза C# 7 еще и далеко, уже сейчас можно поиграться с большинством новых фич и понять, куда C# и .Net в целом движутся (как по мне, так C# берет часть фишек из функциональных языков, и зачастую это делает код более читаемым и менее многословным. Но везде нужно знать меру, конечно).
Tags:
Hubs:
+36
Comments 69
Comments Comments 69

Articles