Что нового будет в 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, они будут выпущены в следующей версии. Многое ещё обсуждается, предложенные функции и синтаксис/семантика могут быть изменены.

Какие из предлагаемых нововведений для вас наиболее актуальны?

Источники

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

matyushkin
18 марта 2020

ТОП-10 книг по C#: от новичка до профессионала

Отобрали актуальные книги по C#, .NET, Unity c лучшими оценками. Расположил...
Библиотека программиста
25 августа 2019

Почему C# программисты скоро будут нарасхват

C# программисты становятся более востребованными благодаря развивающейся эк...
Библиотека программиста
12 марта 2018

Видеокурс по C# с нуля: от основ до полноценного приложения

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