В этой статье мы расскажем о нововведениях в языке 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(); }
Статьи по схожей тематике