Что нового будет в C# 9? Результаты исследования Proposals на GitHub
Предупреждён — значит вооружён. Обсуждаем предлагаемые нововведения, которые могут коснуться разработчиков в девятой версии С#.
Следующим релизом C# станет С# 9. В плане развития языка уже описано 39 предложений (proposals) для девятой версии. Но какие предложения будут внедрены и в какой версии? Только команда разработчиков .Net может ответить на этот вопрос. Наиболее важные функции, запланированные в C# 9, это новый тип – записи (records), дизъюнктные объединения (discriminated unions), улучшение сопоставления с образцом (pattern matching), дополнительная целевая типизация существующих конструкций, таких как тернарные выражения и выражения объединения с null
. Бассам Алугили в публикации Глубокое погружение в C # 9 (англ.) описал ключевые изменения, которые могут появиться в новой версии языка. Мы представляем незначительно сокращённый перевод статьи.
Указанные особенности ещё не находятся на заключительной стадии. И окончательный синтаксис может отличаться от описанного в proposals.
Записи
Записи (records) представляют собой новую упрощенную форму объявления классов C# и структурных типов. Описание этого нового типа дано на GitHub. Актуальная версия предложения изложена здесь.
Записи номинально типизированы и могут иметь методы, свойства, операторы и т. д., позволяют проводить структурное сравнение. По умолчанию свойства записи доступны только для чтения. Записи могут быть Value Type
или Reference Type
.
Запись может быть определена следующим образом:
public class Point3D { public int X { get; set; } public int Y { get; set; } public int Z { get; set; } }
В случае неизменного типа предлагается использовать новый модификатор initonly
, который можно применять к свойствам и полям:
public class Point3D { public initonly int X { get; } public initonly int Y { get; } public initonly int Z { get; } ... ... }
Создание объекта записи:
void DoSomething() { var point3D = new Point3D() { X = 1, Y = 1, Z =1 }; }
Использование записей с with-выражениями
Во вводной части заявки на внедрение записей в качестве нового элемента языка предлагаются with
-выражения. С помощью with
можно сразу изменять копию объекта записи:
public class Demo { public void DoIt() { var point3D = new Point3D() { X = 1, Y = 1, Z =1 }; Console.WriteLine(point3D); } }
var newPoint3D = point3D with {X = 42};
Созданная новая точка newPoint3D
аналогична существующей point3D
, но значение X изменено на 42.
Равенство
Записи сравниваются по структуре, а не по ссылке:
void DoSomething() { var point3D1 = new Point3D() { X = 1, Y = 1, Z =1 }; var point3D2= new Point3D() { X = 1, Y = 1, Z =1 }; var compareRecords = point3D1 == point3D2; // true }
Дизъюнктное объединение
Термин дизъюнктное объединение (discriminated union, disjoint union) заимствован из математики. Например, есть связанные множестваA0 = {(5,0), (6,1)}
и A1 = {(7,2)}
. Дизъюнктное объединение заключается в объединении непересекающихся «копий» множеств:
A0 ⊔ A1 = {(5,0), (6,1), (7,2)}
Предлагаемая функциональность дизъюнктного объединения в C#9 аналогична F# и позволяет определять типы, которые могут содержать любое количество различных типов данных, объединения полезны для различных неоднородных данных и создания простых иерархических структур.
Пример дизъюнктного объединения в F#:
type Person = {firstname:string; lastname:string} // определяем запись type ByteOrBool = Y of byte | B of bool type MixedType = | P of Person // используем запись, введенную выше | U of ByteOrBool // используем запись, введенную выше let unionRecord = MixedType.P({firstname="Bassam"; lastname= "Alugili"}); let unionType1 = MixedType.U( B true); // Boolean type let unionType2 = MixedType.U( Y 86uy); // Byte type
Возьмём для дизъюнктного объединения в С# 9 пару сущностей:
// Определяем запись public class Person { public initonly string Firstname { get; } public initonly string Lastname { get; } }; enum class ByteOrBool { byte Y; bool B;} // Возможный вариант синтаксиса
Во втором случае мы хотим тип, который отражает все возможные целые числа и все возможные булевы значения:
В нашем случае новый тип ByteOrBool – это «сумма» байтового и логического типов. Как и в F#, суммарный тип называется discriminated union
(дизъюнктное объединение).
enum class MixedType { Person P; ByteOrBool U; }
Создание экземпляра объединения:
var person = new Person() { Firstname = ”Bassam”; Lastname = “Alugili”; }; var unionRecord = new MixedType.P(person); // Record C# 9 var unionType1 = new MixedType.U( B true); // Boolean type var unionType2 = new MixedType.U( Y 86uy); // Byte type
Использование дизъюнктных объединений
Приведенные ниже примеры демонстративные, только для лучшего понимания предлагаемых новшеств.
1. Обработка исключений, как в Java:
try { … … } catch (CommunicationException | SystemException ex) { // Здесь обрабатываем CommunicationException и SystemException }
2. Ограничение типа:
public class GenericClass<T> where T : T1 | T2 | T3
Универсальный класс может принадлежать одному из типов T1
, T2
или T3
.
3. Гетерогенные коллекции:
var crazyCollectionFP = new List<int|double|string>{1, 2.3, "bassam"};
4. Комбинация переменных/значений/выражений разных типов через операторы ? :
, ??
или выражение switch
:
var result = x switch { true => "Successful", false => 0 };
Тип результата здесь будет string | int
.
5. Если несколько перегрузок какого-либо метода имеют одинаковые реализации, их можно объединить. Например, следующий набор
void logInput(int input) => Console.WriteLine($"The input is {input}"); void logInput(long input) => Console.WriteLine($"The input is {input}"); void logInput(float input) => Console.WriteLine($"The input is {input}");
может быть заменен на единственную строку:
void logInput(int|long|float input) => Console.WriteLine($"The input is {input}");
6. Можно использовать подход для возвращаемых типов:
public int|Exception Method() // возвращение исключения public class None {} public typealias Option<T> = T | None; // Option type public typealias Result<T> = T | Exception; // Result type
Оператор объединения с null (??)
Речь идет о разрешении неявного преобразования для выражений, в которых происходит объединение с null
. Вот пример в C# 8:
void M(List<int> list, uint? u) { IEnumerable<int> x = list ?? (IEnumerable<int>)new[] { 1, 2 }; // C# 8 var l = u ?? -1u; // C# 8 }
В C# 9 тот же код будет выглядеть так:
void M(List<int> list, uint? u) { IEnumerable<int> x = list ?? new[] { 1, 2 }; // C# 9 var l = u ?? -1; // C# 9 }
Выражение new
Рассмотрим пример из официального предложения.
IEnumerable<KeyValuePair<string, string>> Headers = new[] { new KeyValuePair<string, string>("Foo", foo), new KeyValuePair<string, string>("Bar", bar), }
Приведённый код может быть упрощен до следующего
IEnumerable<KeyValuePair<string, string>> Headers = new KeyValuePair<string, string>[] { new("Foo", foo), new("Bar", bar), }
Но вам всё равно нужно повторно указать тип после инициализации поля/свойства. Самое близкое, что можно получить, это что-то вроде:
IEnumerable<KeyValuePair<string, string>> Headers = new[] { new KeyValuePair<string, string>("Foo", foo), new("Bar", bar), }
Для полноты картины можно предложить также сделать new[]
выражением с типом целевого объекта.
IEnumerable<KeyValuePair<string, string>> Headers = new[] { new("Foo", foo), new("Bar", bar), }
Атрибут вызываемого выражения
Идея состоит в том, чтобы позволить вызывающему объекту «структурировать» выражения, переданные на место вызова. Конструктор атрибута примет строковый аргумент, определяющий имя аргумента для строкового преобразования.
public static class Verify { public static void InRange(int argument, int low, int high, [CallerArgumentExpression("argument")] string argumentExpression = null, [CallerArgumentExpression("low")] string lowExpression = null, [CallerArgumentExpression("high")] string highExpression = null) { if (argument < low) { throw new ArgumentOutOfRangeException(paramName: argumentExpression, message: $ " {argumentExpression} ({argument}) cannot be less than {lowExpression} ({low})."); } if (argument > high) { throw new ArgumentOutOfRangeException(paramName: argumentExpression, message: $ "{argumentExpression} ({argument}) cannot be greater than {highExpression} ({high})."); } } public static void NotNull < T > (T argument, [CallerArgumentExpression("argument")] string argumentExpression = null) where T: class { if (argument == null) throw new ArgumentNullException(paramName: argumentExpression); } } // CallerArgumentExpression: преобразует выражения в строку! Verify.NotNull(array); // paramName: "массив" // paramName: "индекс" // Сообщение об ошибке из-за неверного Index: "index (-1) cannot be less than 0 (0).", or // "index (6) cannot be greater than array.Length - 1 (5)." Verify.InRange(index, 0, array.Length - 1);
Упрощённая default-деконструкция кортежа:
В C# 7 сопоставляем каждой переменной default
:
(int x, string y) = (default, default);
В C# 9 проще:
(int x, string y) = default;
Свободный порядок модификаторов ref и partial
При объявлении класса можно указывать partial
перед ref
:
public ref partial struct {} // C# 7 public partial ref struct {} // C# 9
Проверка на null
Задача – упростить стандартную null проверку параметров, используя небольшую аннотацию параметров. Эта функция относится к улучшению качества кода. Подробнее смотрите в заметках об обсуждении.
// Ранее в C# 1..7.x void DoSomething(string txt) { if (txt is null) { throw new ArgumentNullException(nameof(txt)); } … } // Кандидат для C# 9 void DoSomething (string txt!) { … }
Пустые параметры для лямбда-выражений
Идея состоит в том, чтобы разрешить при вводе лямбда-выражений множественные объявления параметров с именем _
. В этом случае параметры являются «сброшенными» (discard) и не могут использоваться внутри лямбды:
Func zero = (_,_) => 0; (_,_) => 1, (int, string) => 1, void local(int , int);
Атрибуты локальных функций
Идея состоит в том, чтобы позволить атрибутам быть частью объявления локальной функции.
Пример:
static void Main(string[] args) { static bool LocalFunc([NotNull] data) { return true; } }
Еще один пример использования – с EnumeratorCancellation
для параметра CancellationToken
локальной функции, реализующей асинхронный итератор, что часто встречается при реализации операторов запросов.
public static IAsyncEnumerable Where(this IAsyncEnumerable source, Func predicate) { if (source == null) throw new ArgumentNullException(nameof(source)); if (predicate == null) throw new ArgumentNullException(nameof(predicate)); return Core(); async IAsyncEnumerable<T> Core([EnumeratorCancellation] CancellationToken token = default) { await foreach (var item in source.WithCancellation(token)) { if (predicate(item)) { yield return item; } } } }
Native Int
Предлагается ввести набор нативных типов (nint
, nuint
). Планируется, что дизайн новых типов данных позволит одному исходному файлу C# использовать 32- или 64-разрядные хранилища в зависимости от типа платформы хоста и настроек компиляции.
Тип определяется ОС:
nint nativeInt = 55; // 4 байта при компиляции в 32 бит системе nint nativeInt = 55; // 8 байт при компиляции в 64 бит системе с x64 настройками компиляции
Указатели на функции
Термин «указатель на функцию» многим известен из С/С++. В переменной хранится адрес функции, которая впоследствии может быть вызвана через указатель этой функции. Указатели на функции можно вызывать и передавать им аргументы, как при обычном вызове функции. Подробнее читайте в описании предложения.
Указатель на функцию C# позволяет объявлять указатели на функции с использованием синтаксиса func*
. Это похоже на синтаксис, используемый при объявлении делегатов:
unsafe class Example { void Example(Action<int> a, delegate*<int, void> f) { a(42); f(42); } }
Заключение
Итак, вы прочитали о кандидатах в C# 9. Также вы можете посмотреть список рабочих изменений C# NEXT. Только если функции-кандидаты находятся в ветви master
, они будут выпущены в следующей версии. Многое ещё обсуждается, предложенные функции и синтаксис/семантика могут быть изменены.