proglib
Сообщение

Почему специалистом по кибербезопасности быть интереснее, чем разработчиком или сисадмином? Приглашаем на вебинар от HackerU

Почему специалистом по кибербезопасности быть интереснее, чем разработчиком или сисадмином? Приглашаем на вебинар от HackerU

matyushkin 02 февраля 2020

Что нового будет в C# 9? Результаты исследования Proposals на GitHub

Предупреждён — значит вооружён. Обсуждаем предлагаемые нововведения, которые могут коснуться разработчиков в девятой версии С#.
0
4328

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

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

Источники

РУБРИКИ В СТАТЬЕ

МЕРОПРИЯТИЯ

Комментарии 0

ВАКАНСИИ

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

BUG