22 новых фичи C# — каким будет C# 11+

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Tl;dr:

  1. Гибкость:

    1. Из предварительной версии c# 11 удалили parameter null-checking с помощью оператора !! — дизайн фичи сочли неподходящим.

    2. Полуавтоматические свойства aka возможность сделать автоматическое свойство, в котором можно обращаться к backing field с помощью ключевого слова field.

    3. Модификатор для типов с областью видимости только в текущем файле.

    4. Первичные конструкторы для классов и структур.

    5. Паттерн-матчинг для списков.

    6. Атрибуты для Main в программах с top level statement

  2. Работа со строками:

    1. Сырые строки без экранирования внутри строки — для удобства работы с строковым представлением json, xml, html и регулярных выражений.

    2. Строковые литералы для UTF-8.

    3. Паттнерн-матчинг для Span<char>.

    4. Перенос строк в выражениях интерполяции.

  3. Обобщенная математка aka generic math:

    1. Возможность перегрузки операторов с проверкой на переполнение. Теперь можно делать checked и unchecked версии операторов ++--+-/* , нужная версия будет выбираться из контекста.

    2. Оператор побитового сдвига вправо без знака >>> для поддержки операции в обобщенной математике.

  4. Изменения в nameof:

    1. nameof для параметров методов — сильно пригодится в CallerArgumentExpressionAttribute и атрибутах про nullable-типы вроде NotNullIfNotNullAttribute  .

    2. Возможность обратиться к членам экземпляра другого объекта в nameof.

  5. Безопасность языка:

    1. Ключевое слово required для свойств и полей, которые должны быть инициализированы в клиентом типа при создании экземпляра.

    2. Новый warning CS8981 для имен типов целиком в нижнем регистре — для уменьшения вероятности использования в качестве имени типа нового зарезервированного слова.

    3. Автоматическая инициализация свойств структур значением по-умолчанию.

    4. Generic-атрибуты.

    5. Локальные переменные и параметры только для чтения.

  6. Производительность:

    1. Кэширование делегатов при использовании method group.

    2. Поля, хранящиеся по ссылке для ref struct.

    3. params Span<T>, params ReadOnlySpan<T>, params IEnumerable<T>. в объявлении методов чтобы избежать лишних неявных созданий массива в куче и копирований коллекций.

Команда C# активно работает над следующей версий языка и уже выпускает предварительные версии C# 11, которые можно попробовать вместе Visual studio 2022 Preview (и частично в Rider 2022.1).

Это обзор фич, которые прямо сейчас активно обсуждаются и находятся в работе — тестируются, разрабатываются или уточняется дизайн.

Remove parameter null-checking from C# 11

Из C# убрали добавленный в предварительной версии оператор !!, для проверки аргументов на null предлагается ArgumentNullException.ThrowIfNull(myString);

"Тот самый" оператор !! для проверки параметра функции и индексатора на не null удаляют из C# 11. Предполагалось, что при использовании оператора для параметра функции в рантайме, если передано значение null, то будет возникать исключение ArgumentNullException:

public static void M(string s!!) // throw ArgumentNullException, если s == null
Func<string, string> s = x!! => x; // throw ArgumentNullException при вызове s(null)
public string this[string key!!] { get { ... } set { ... } } 
void WarnCase<T>(string? name!!)   // warn CS8995   Nullable type 'string?' is null-checked and will throw if null.

Авторы не уверены, что фича оказалась достаточно продуманно задизайнена и должна быть реализована именно таким способом. Пока рекомендованный способ проверки параметров такой:

public static void M(string myString)
{
    ArgumentNullException.ThrowIfNull(myString);
    // method 
}

Если хочется уменьшить количество кода, то есть библиотека Fody/NullGuard, проверяющая параметры на null, работающая с атрибутами [AllowNull] и [NotNull] и 3 режимами:

  • В imlicit mode все параметры считаются не поддерживающими значение null, кроме помеченных [AllowNull] .

  • В explicit mode все параметры считаются поддерживающими значение null, кроме помеченных [NotNull].

  • Если в проекте включены nullable reference types, то возможность параметра принимать значение null вычисляется исходя из сигнатуры.

Пример работы в Nullable Reference Types Mode:

public class Sample
{
    // Возвращаемое значение может быть null
    public string? MaybeGetValue()
    {
        return null;
    }

    // Выбрасывает InvalidOperationException, если возвращаемое значение null
    public string MustReturnValue()
    {
        return null;
    }

    // Выбрасывает InvalidOperationException, если результат выполнения задачи null 
    public async Task<string> GetValueAsync()
    {
        return null;
    }

    // Возвращаемое задачей значение может быть null
    public async Task<string?> GetValueAsync()
    {
        return null;
    }

    public void WriteValue(string arg)
    {
        // Выбрасывает ArgumentNullException, если arg имеет значение null
    }

    public void WriteValue(string? arg) 
    {
        // Значение arg может быть null
    }

    public void GenericMethod<T>(T arg) where T : notnull
    {
        // Выбрасывает ArgumentNullException, если arg имеет значение null
    }

    public bool TryGetValue<T>(string key, [MaybeNullWhen(false)] out T value)
    {
        // Выбрасывает ArgumentNullException, если key имеет значение null
        // Значение out value не проверяется
    }
}

Полуавтоматические свойства (Semi-auto-properties)

Можно реализовать getter и setter для свойства без явного задания backing field. Доступ к полю будет с помощью ключевого слова feidld. Сокращает код.

Сейчас автоматические свойства не могут иметь логики внутри геттера и сеттера, при необходимости реализовать такую логику нужно явно объявлять в классе backing field для такого свойства. Иногда требуется больше контроля при доступе к автоматическому свойству, например, вы хотите проверить значение в сеттере или вызывать событие, которое информирует об изменении свойства.

Полуавтоматические свойства позволяют добавлять логику в свойства без явного объявления поля для этого свойства — внутри геттера и сеттера обратиться к backing fileled можно будет с помощью ключевого слова field:

public class SemiAutoPropertiesExample
{
    public int Salary 
    { 
        get { return field; } 
        set 
        {
            if(value < 0)
                throw new ArgumentOutOfRangeException("Зарплата не может иметь отрицательное значение");
            field = value;
            NotifyPropertyChanged(nameof(Salary));
        } 
    }
    
    public string LazyValue => field ??= ComputeValue();
    private static string ComputeValue() { /*...*/ }
}

Proposal на github
Применимость: иногда
Текущий статус: In Progress

Модификатор для типов с областью видимости только в текущем файле

Можно делать невложенные типы private — область видимости ограничена файлом объявления. Полезно для локальных хелперов и source generators.

Генераторы исходного кода (source generators) иногда создают вспомогательные типы, которые используются только другим сгенерированным исходным кодом. Разработчики иногда тоже могут писать хелперы, которые используются только в рамках текущего файла. Сейчас такие типы можно пометить с помощью EditorBrowsableAttribute, но полностью гарантировать, что ими не воспользуются в другой части кода нельзя. Поэтому при модификации нужно будет учитывать учитывать все использования кода, а изменение реализации внутри генератора может привести к ошибкам.

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

// File1.cs
internal partial class UserType
{
    internal partial void SomePartialMethod()
    {
        My.Own.Stuff.Zoo.Blah(); // OK
    }    
}

private class Zoo
{
    public static void Blah() { }
}

// File2.cs
class C
{
    public static void N() 
        => My.Own.Stuff.Zoo.Blah(); // ERROR: Zoo isn’t defined/visible/etc.
}

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

private class C1 { }

internal class C2 { 
  private void M(C1 c) { } // OK, поскольку метод M доступен только внутри C2
}

private class C3 { 
  internal void M(C1 c) { } // OK, поскольку C3 и его метод M доступен только в пределах файла
}

internal class C4 { 
  internal void M(C1 c) { } // Ошибка компиляции, поскольку C2.M доступен за пределами файла, а C1 нет
}

Ещё возможно будет использовать в рамках partial-классов приватные типы, имеющие одно имя, если такие типы объявлены в разных файлах. Это может сбивать с толку, но такой код будет компилироваться:

// file1.cs 
private class Zoo { }
public partial class C { 
  private void M(Zoo z) { }  // OK, используется Zoo из file1.cs
}

// file2.cs
private class Zoo { }
public partial class C {
  private void M(Zoo z) { } // OK, используется Zoo из file2.cs
}

Proposal на github
Применимость: редко в пользовательском коде, часто в source generators
Текущее состояние: In Progress

Первичные конструкторы для классов и структур

Для классов и структур можно сделать первичный конструктор, для каждого параметра будет сгенерировано приватное поле. Сокращает код и выносит список зависимостей в начало объявления типов.

Первичные конструкторы сейчас доступны только в типах record. Они позволяют сократить код — для каждого параметра такого конструктора генерируется публичное свойство. Теперь предлагается расширить эту функциональность на классы и структуры. Первичные конструкторы в классах и структурах будут иметь отличия от них же в record:

  • Вместо публичных свойств для каждого параметра будут генерироваться приватные поля.

  • Если на поле нет ссылки внутри объявления типа, то оно не будет генерироваться (но параметр всё ещё можно будет использовать в инициализаторе).

  • Деконструктор не будет сгенерирован для типа с первичным конструктором.

  • Любые другие конструкторы типа обязаны вызывать первичный конструктор.

internal class CustomerRepository(string connectionString, string companyName) 
    : ICustomerRepository
{
    public string CompanyName { get; set; } = companyName;

    public CosmosCustomerRepository(string connectionString)
        : this(connectionString, "DefaultCompanyName")
    { }
    
  	public Customer Retrieve(CustomerId id)
	  {
      var dbContext = CreateDbContext(connectionString);
      return dbContext
          .Customers
          .SingleOrDefault(c => c.company == collectionName && c.id == id);
	  }
}

Proposal на github
Применимость: очень часто
Текущее состояние: In Progress

Паттерн-матчинг для списков

Можно сопоставлять списки и их элементы с помощью паттерн-матчинга. Синтаксис условий похож на использование Range и Index, которые появились в C# 8.

В паттерн-матчинг добавляются шаблоны списков (list_pattern), которые позволяют сопоставлять списки и элементы списка. Так же внутри шаблона списка можно сопоставлять срезы из 0+ элементов с помощью шаблонов среза (slice_pattern).

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

Шаблоны списков работают со всеми счетными и индексируемыми типами, а шаблоны срезов со всеми типами, где есть индексатор с аргументом System.Range или метод Slice с двумя целочисленными параметрами. В будущем возможно добавление шаблона списков, который работает с IEnumerable.

public static int CheckSwitch(int[] values)
    => values switch
    {
        [1, 2, .., 10] => 1, // values.Length >= 4 && values[0] == 1 && values[1] == 2 && values[^1] == 10
        [1, 2] => 2, // values.Length == 2 && values[0] == 1 && values[1] == 2
        [1, _] => 3, // values.Length >= 4 && values[0] == 1
        [1, ..] => 4, // values.Length >= 2 && values[0] == 1
        [..] => 50 // values.Length >= 0 
    };

WriteLine(CheckSwitch(new[] { 1, 2, 10 }));          // 1
WriteLine(CheckSwitch(new[] { 1, 2, 7, 3, 3, 10 })); // 1
WriteLine(CheckSwitch(new[] { 1, 2 }));              // 2
WriteLine(CheckSwitch(new[] { 1, 3 }));              // 3
WriteLine(CheckSwitch(new[] { 1, 3, 5 }));           // 4
WriteLine(CheckSwitch(new[] { 2, 5, 6, 7 }));        // 50

// Пример захвата результата сопоставления шаблона
public static string CaptureSlice(int[] values)
    => values switch
    {
        [1, .. var middle, _] => $"Middle {String.Join(", ", middle)}",
        [.. var all] => $"All {String.Join(", ", all)}"
    };

Proposal на github
Применимость: иногда
Текущее состояние: доступно в C# 11 Preview

Атрибуты для Main в программах с top level statement

У атрибутов появится новый глобальный target (как сейчас для assembly) — main.

В C# 10 появились top level statement — способ избавиться от шаблонного кода при объявлении точки входа в приложении. Если нужно пометить Entry point атрибутом, то использовать новую функциональность не получится. Изменение предлагает решение для этой проблемы по аналогии с тем, как атрибутом может помещаться сборка — через новый тип target для атрибута.

Пример использования для Win Forms приложения: [main: STAThread].

Issue на github
Применимость: редко
Текущее состояние: In Progress

Сырые строки

Появилась возможность объявить строковый литерал без экранирования внутри. Полезно для работы с html/json/xml/regex/sql.

Это удобный способ работы со строками, которые содержат много кавычек, фигурных скобок и других символов, которые нужно экранировать. Например, html, json, xml, sql, регулярные выражения.

В C# 11 появляется способ работать с такими строками без большого числа экранирований. Сырые строки (Raw string literals) не имеют экранирования совсем. То есть, \ выводится как \, а \t выводится как \t, а не заменяется на символ табуляции.

Сырые строки начинаются и заканчиваются на как минимум три двойных кавычки. Например, """My string""".

Почему "как минимум"? Потому что экранироваться по-умолчанию в таких строках будут все подряд идущие двойные кавычки, число которых меньше открывающих и закрывающих. То есть, если в строке возможны 3 подряд идущие двойные кавычки и не хочется их экранировать явно, то такая строка может начинаться и заканчиваться 4 двойными кавычками. Тоже правило, кстати. действует на фигурные скобки и обозначение строки как interpolated через $ — экранироваться по-умолчанию будет количество меньше указанного перед строкой.

const int comfortable = 20;
const int veryCold = -30;
string jsonString = 
    $$"""
    {
      "TemperatureRanges": {
        "Cold": {
          "High": {{comfortable}},
          "Low": {{veryCold}}
        }
      }
    }
    """;

Из-за правила с произвольным количеством символов интерполированной строки и открывающих/закрывающих кавычек такой код тоже корректно компилируется:

var myString = $$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$"""""""Very rich string""""""";

Proposal на github
Применимость: иногда
Текущее состояние: доступно в C# 11 preview

Строковые литералы для UTF-8

Можно объявлять константные строки в виде их байтового представления в UTF-8. Позволит не тратить время на конвертацию во время выполнения.

Это способ преобразовывать строковые литералы, содержащие символы UTF-8, в их байтовое представление во время объявления. Байтовое представление будет сформировано во время компиляции, что избавляет от затрат на преобразование кодировки во время выполнения. Неявно UTF-8 литералы конвертируются в byte[], поэтому их можно присводить Span<byte> и ReadOnlySpan<byte>.

byte[] array = "hello";             // == new byte[] { 0x68, 0x65, 0x6c, 0x6c, 0x6f }
Span<byte> span = "dog";            // == new byte[] { 0x64, 0x6f, 0x67 }
ReadOnlySpan<byte> span = "cat";    // == new byte[] { 0x63, 0x61, 0x74 }
string s1 = "hello"u8;              // Ошибка компиляции
var s2 = "hello"u8;                 // == new byte[] { 0x68, 0x65, 0x6c, 0x6c, 0x6f }

Proposal на github
Применимость: редко
Текущее состояние: In Progress

Pattern matching для Span<char>

Можно использовать паттерн-матчинг сSpan<char> и ReadOnlySpan<char> для сопоставления с константной строкой.

Из соображений производительности может быть предпочтительнее использовать типы Span<char> или ReadOnlySpan<char> вместо string. Одной из типичных операций со строками является сопоставление с значением с помощью switch , а компилятор оптимизирует такие использования switch. Теперь можно будет сопоставлять значения Span<char> и ReadOnlySpan<char> с константной строкой:

static bool Is123(ReadOnlySpan<char> s) => s is "123";
static bool IsABC(Span<char> s) => s switch { "ABC" => true, _ => false };

Как и в случае с типом string, строковые шаблоны нельзя комбинировать с шаблонами списков.

Proposal на github
Применимость: редко
Текущее состояние: In Progress

Перенос строк в выражениях интерполяции

В выражениях интерполяции можно использовать перенос строки и улучшить этим читаемость кода.

Сейчас при использовании интерполяции в строковых литералах нельзя использовать перенос строки внутри выражения интерполяции — для chained-операций или других длинных выражений это может приводить к длинным строкам в исходном коде. При этом элементы формата сами по себе не часть текста и не должны следовать правилам экранирования и переноса строк в интерполированном строком литерале.

Это изменение позволяет сделать код более читаемым и использовать переносы строк в выражениях интерполяции как в любых других выражениях в коде:

var v = $"Count ist: { this.Is.Really.Something()
                            .That.I.Should(
                                be + able)[
                                    to.Wrap()] }.";

Proposal на github
Применимость: часто
Текущее состояние: доступно в C# 11 preview

Перегрузка операторов с проверкой на переполнение

Возможность объявить checked и unchecked версии математических операторов для пользовательских типов — нужный метод оператора будет выбран исходя из текущего контекста. Полезно для generic math.

Одной из причин использования статических абстрактных членов интерфейсах C# 11 является возможность поддержки обобщенной математики (generic math). Разработчики могут писать алгоритмы, которые полагаются на интерфейсы, включающие статические абстрактные члены в качестве generic-параметра. Одним из таких встроенных интерфейсов является INumber<TSelf>, который обеспечивает доступ к операциям +, -, *, / и методам, например Max, Min, Parse.

T Add<T>(T lhs, T rhs)
    where T : INumber<T>
{
    // Поскольку в INumber<T> определен оператор сложения, то мы можем использовать его в теле метода
    return lhs + rhs;
}

Можно писать свои generic-интерфейсы для generic-методов, которые будут универсально работать с типами, заданными в качестве generic-параметра:

interface IAddable<T> where T : IAddable<T>
{
	static abstract T Zero { get; }
	static abstract T operator +(T t1, T t2);
}

record struct ComplexIntNumber(int Real, int Imaginary) : IAddable<ComplexNumber>
{
	public static ComplexIntNumber Zero => new ComplexIntNumber(0, 0);
	static ComplexIntNumber IAddable<ComplexIntNumber>.operator +(ComplexIntNumber x, ComplexIntNumber y)
		=> new ComplexIntNumber(x.Real + y.Real, x.Imaginary + y.Imaginary);
}

// Generic-алгоритм может использовать статические члены T, в том числе имеет доступ к операторам
public static T AddAll<T>(T[] ts) where T : IAddable<T>
{
    T result = T.Zero;                   
    foreach (T t in ts) { result += t; } 
    return result;
}

ComplexNumber result = AddAll(new [] 
		{ 
    		new ComplexIntNumber(1, 2),
        new ComplexIntNumber(-6, 14),
        new ComplexIntNumber(-3, 0),
        new ComplexIntNumber(1, -2)
    });

Для проверки на переполнение в C# используется свойство на уровне проекта <CheckForOverflowUnderflow> или checked/unchecked области или операторы. До C# 11 определенный пользователем оператор не знал о контексте, в котором он использовался. Теперь добавлена ​​возможность объявлять определенные операторы как checked явно. Это позволит использовать разные операторы в checked и unchecked контексте и более точно определить поведение в разных контекстах.

Сейчас нет требования чтобы checked версии операторов генерировали ошибки, если превышены границы типа, или чтобы непроверенные операторы не генерировали ошибки, но это ожидаемое поведение при использовании контекста проверки на переполнение.

Эта функция полезна, если вы определяете операторы в типах, для которых есть понятие арифметического переполнения. Для определения checked-оператора достаточно добавить ключевое слово checked при его объявлении:

static int IAdditionOperators<int, int, int>.operator +(int left, int right) => left + right;
static int IAdditionOperators<int, int, int>.operator checked +(int left, int right) => checked(left + right);

Proposal на github
Применимость: редко
Текущее состояние: In Progress

Оператор побитового сдвига вправо без знака >>>

Новый оператор побитового сдвига вправо без знака, который решает проблему такой операции для пользовательских типов, у которых нет беззнакового аналога и упрощает код для тех, в которых такой эквивалент есть. Полезно для generic math.

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

В контексте обобщенной математики это потенциально более проблематично, так как тип может не обязательно иметь беззнаковый аналог или такой аналог может быть проблемно определить для обобщенного алгоритма. Но побитовый сдвиг вправо без знака всё ещё необходим для такого кода, поэтому в C# появится новый оператор сдвига >>>:

int x = -14;
Console.WriteLine(x >> 1); // - 7
Console.WriteLine((uint) x >> 1); // 2147483641
Console.WriteLine(x >>> 1); // 2147483641

Proposal на github
Применимость: редко
Текущее состояние: In Progress

nameof для параметров методов

В конструкции nameof можно обращаться к параметрам метода, если конструкция используется в объявлении метода. Удобно, если нужно имя параметра или значение параметра по имени, как в CallerArgumentExpression.

В некоторых атрибутах для параметров, например NotNullIfNotNull  или  CallerExpression нужно использовать имена параметров метода, но сейчас они недоступны в выражении nameof, поэтому приходится указывать их с помощью строкового литерала, что может быть небезопасно при рефакторингах.

Предлагается расширить область видимости оператора nameof так, чтобы можно было использовать этот оператор внутри атрибутов:

[MyAttribute(nameof(parameter))] void M(int parameter) { }
[MyAttribute(nameof(TParameter))] void M<TParameter>() { }
void M(int parameter, [MyAttribute(nameof(parameter))] int other) { }

Теперь может возникнуть ситуация, когда в типе определен член с именем, который совпадает с именем параметра, и nameof в таком случае захватит именно параметр метода, а не другой член типа:

public class MyClass
{
    private int someValue;
    
    // Будет захвачен параметр метода M
    [MyAttribute(nameof(someValue))] 
    void M(int someValue) { }
}

Proposal на github
Применимость: иногда
Текущее состояние: доступно в C# 11 preview

nameof для членов экземпляра другого типа

В конструкции nameof можно обращаться к членам экземпляра другого типа.

Если атрибуту нужен доступ к члену экземпляра другого класса, то сейчас этого сделать не получится.

public class C
{
	public string S { get; private set; }
	public static string M() => nameof(S.Count); // error CS0120 An object reference is required
}

Обычно в таких случаях можно указать имя класса, но в случае рефакторинга выражение внутри nameof может стать неактуальным, если член экземпляра изменился. В новой версии C# код выше будет компилироваться и рефакторинг станет более безопасным.

Issue на github
Применимость: иногда
Текущее состояние: In Progress

Ключевое слово required для свойств и полей

Можно объявить в типе список обязательных членов, которые пользователь обязан будет инициализировать при создании объекта. Заменяет необходимость в конструкторе, который инициализирует обязательные поля, перекладывая эту обязанность на пользователя и object initializer.

Исторически в C# не было концепции обязательно инициализируемых членов типа. У свойств можно было ограничинить время жизненного цикла для инициализации с помощью init;. Поля могли иметь ограничение readonly, что позволяло ограничить конструктором область, в котором они могут быть инициализированы. Единственным способом гарантировать, что член типа инициализирован было использование конструктора с параметрами, который бы инициализировал эти поля внутри.

В сложных иерархиях наследования конструкторы, которые инициализируют необходимые члены типов приводят к большому дублированию кода — в конструкторе дублируются типы членов и имена членов, а наследники должны заново объявлять конструктор с тем же списком параметров чтобы вызвать базовый конструктор:

class Person
{
    public string FirstName { get; }
    public string MiddleName { get; }
    public string LastName { get; }

    public Person(string firstName, string lastName, string? middleName = null)
    {
        FirstName = firstName;
        LastName = lastName;
        MiddleName = middleName ?? string.Empty;
    }
}

class Student : Person
{
    public int ID { get; }
    public Person(int id, string firstName, string lastName, string? middleName = null)
        : base(firstName, lastName, middleName)
    {
        ID = id;
    }
}

Nullable reference types в C# 8 позволили делать члены класса не допускающими значения null. Но всё ещё возможно создать экземпляр класса с значением null в члене класса, просто не инициализировав его:

public class Person
{
		public string FirstName { get; init; }
    public string? MiddleName { get; init; }
    public string LastName { get; init; }
}

var person = new Person();
var px = person.FirstName;

Console.WriteLine(px.Length); // NullReferenceException

Новая фича предлагает определить список обязательных полей и свойств, которые должны быть инициализированы при создании объекта, а обязаность инициировать обязательные члены типа перенести на клиента типа.

Ключевое слово required для полей и свойств позволяет объявить список членов типа, которые являются обязательными и должны быть инициализированы во время конструирования экземпляра.

public class Person
{
    // Конструктор по-умолчанию обяжет инициализировать FirstName and LastName при создании экземпляра
    public required string FirstName { get; init; }
    public string MiddleName { get; init; } = "";
    public required string LastName { get; init; }
}

// Ошибка компиляции — не все обязательные поля инициализированы
var p1 = new Person { FirstName = "Иван" };

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

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

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

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

  • В типе не может быть конструктора, который имеет область видимости шире, чем инициализатор обязательного свойства — потому что internal-инициализатор не будет доступен в другой сборке, если объявлен публичный конструктор, а protected-инициализатор не будет доступен для клиента типа при использовании internal или public конструктора.

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

  • Обязательные свойства должны иметь сеттер или инициализатор.

  • Явные реализации свойств интерфейсов не могут быть назначены через инициализатор, поэтому не могут быть обязательными.

interface I
{
    int Prop1 { get; }
}
public class Base
{
    public virtual int Prop2 { get; set; }
    protected required int _field; // Ошибка: _field имеет область видимости ниже, чем тип Base
    public required readonly int _field2; // Ошибка: обязательные поля не могут иметь доступ только для чтения
}
public class Derived : Base, I
{
    required int I.Prop1 { get; } // Ошибка: явные реализации интерфейса не могут быть обязательными потому что не могут быть назначены через object initializer
    public required override int Prop2 { get; set; } // Ошибка: это свойство сокрыто Derived.Prop2 и не может быть назначено через object initializer
    public new int Prop2 { get; }
    public required int Prop3 { get; } // Ошибка: Обязательное свойство должно иметь сеттер или инициализатор
    public required int Prop4 { get; internal set; } // Ошибка: сеттер обязательного свойства должен иметь модификатор доступа не ниже, чем конструктор типа
}

Это изменение фактически предполагает переход на другой способ инициализации экземпляров класса — не с помощью конструктора, а с помощью инициализаторов объектов (object initializer). Обязательные члены при этом должны иметь модификатор доступа достаточный для доступа к ним из этого инициализатора объекта, что плохо подходит для классов с логикой и полей, содержащих внутреннюю реализацию, которая должна быть сокрыта от пользователей. Пока непонятно, как такая фича может сочетаться с DI-контейнерами. Поэтому прямо сейчас говорить о широкой применимости рано, а для типов данных может быть достаточно использования типов-записей record.


Proposal на github
Применимость: иногда
Текущее состояние: In Progress

Новый warning для имен типов целиком в нижнем регистре

Для имен типов, целиком состоящих из ASCII-символов в нижнем регистре компилятор теперь будет выдавать предупреждение.

Представлен новый набор предупреждений Warning Wave 7, который включает предупреждение для имени типа целиком в нижнем регистре:

warning CS8981: The type name '...' only contains lower-cased ascii characters. Such names may become reserved for the language.

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

Автоматическая инициализация свойств структур значением по-умолчанию

Раньше в конструкторе нужно было обязательно инициализировать все поля структуры. Теперь ограничение снято, а для всех членов структуры, которые не инициализировал пользователь, будет установлено значение default.

Это изменение в поведении структур — раньше при использовании конструкторов нужно было инициализировать все поля структуры явно прямо в конструкторе, а теперь компилятор будет делать это сам значением по-умолчанию для полей и автосвойств. Можно включить предупреждения для мест, где не все поля инициализируются явно, а код, который явно инициализировал поля значениями по-умолчанию можно удалить. Следующий код не компилируется в C# 10, но будет корректным с выходом этой фичи:

struct MyStruct
{
	  public int IntValue { get; init; } // default == 0
    public int? NullableIntValue { get; init; } // default == null
	  public string TextValue { get; init; } // default == null

	  public MyStruct()
	  {
	  }
	
	  public MyStruct(int intValue, int? nullableIntValue, string textValue)
	  {
	  }
}

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

Вместе с этим изменением в структурах исправят и ещё одну проблему, которая появилась в C# 10 — если члены структуры имели конструктор с параметрами, то компилятор не генерировал для таких структур конструктор без параметров. А при создании объекта вместо вызова конструктора с помощью OpCodes.Newobj объект конструировался при помощи OpCodes.Initobj, который просто инициализирует каждое поле типа значения null или значением 0 соответствующего примитивного типа. Из-за этого инициализаторы свойств не вызывались.

В будущих версиях в таком случае всегда будет генерироваться и вызываться конструктор без параметров, который приводит к вызову инициализаторов свойств.

struct S1
{
	public int Value = 42;
	public S1() { }
}

struct S2
{
	public int Value = 42;
	public S2(String s) { }
}

Console.WriteLine(new S1().Value); // Value == 42
Console.WriteLine(new S2().Value); // В С# 10 Value == 0, в будущих версиях Value == 42

Proposal на github
Применимость: иногда
Текущее состояние: In Progress

Generic-атрибуты

Можно объявлять атрибуты с generic-параметрами и ограничениями для этих параметров.

Сейчас, если требуется работать с типом в атрибуте, то единственный способ задать это тип — принимать System.Type в качестве параметра и использовать typeof для предоставления атрибуту необходимых ему типов. Но у авторов атрибутов нет возможности ограничить, какие типы разрешено передавать атрибуту через typeof. Если дать возможность объявлять generic-атрибуты, то их авторы смогут использовать существующую систему ограничений параметров типов, которые они принимают в качестве входных данных, а возможные ошибки при нарушении ограничений будут возникать на этапе компиляции, что повысит безопасность кода.

public class MyAttr<T1> : Attribute
	  where T1 : ValidationRuleBase
{
}

public class MyAttrForNumeric<T1> : Attribute
	  where T1 : INumber<T1>
{
}

Тип атрибута должен быть полностью определен на этапе компиляции, поэтому использование generic-параметров типа, в котором объявлен атрибут не допускается.

public class MyAttr<T1> : Attribute { }

public class Program<T2>
{
    [Attr<string>] // OK
    [Attr<T2>] // Ошибка компиляции
    [Attr<List<T2>>] // Ошибка компиляции
    void M() { }
}

Proposal на github
Применимость: иногда
Текущее состояние: доступно в C# 11 preview

Локальные переменные и параметры только для чтения

Ключевое слово readonly можно использовать в локальных переменных и параметрах методов — их значение нельзя будет изменить после объявления.

Предложение внесли в 2019 году — для уменьшения числа ошибок предлагается дать возможность объявлять локальные переменные и параметры как неизменяемые. Для локальных переменных есть 2 возможных варианта реализации: с помощью добавления ключевого слова readonly или нового ключевого слова для неявного типа val (== value по аналогии с существующим var == variable). Первый способ более универсален и не ограничивается выводимыми типами, но более многословен.

// Вариант 1
readonly long maxBytesToDelete = (stream.LimitBytes - stream.MaxBytes) / 10;
maxBytesToDelete = 0; // Ошибка: невозможно установить значение для локальной переменной только для чтения после объявления переменной

// Вариант 2
val maxBytesToDelete = (stream.LimitBytes - stream.MaxBytes) / 10;
maxBytesToDelete = 0; // Ошибка: невозможно установить значение для локальной переменной только для чтения после объявления переменной

Учитывая расширение применения ключевого слова readonly с типами, доступными по ссылке, и появлению сочетаний readonly ref / ref readonly / readonly ref readonly, вопросы к возможному дизайну подобной фичи только усложняется. Читаемость и однозначность интерпретации конструкций с readonly может сильно страдать при использовании в разных контекстах:

var x0 = 14;

// ref readonly или readonly int?
ref readonly int y = ref x0;

Над фичей прямо сейчас не идёт активной работы, но она хорошо показывает проблему сочетаемости разной функциональности при проектировании языка.

Proposal на github
Применимость: часто
Текущее состояние: в списке предложений, не рассматривается для C# 11

Кэширование делегатов при использовании method group

Делегаты заданные как method group кэшируются и переиспользуются вместо создания нового делегата при каждом использовании.

При использовании method group для статического метода в месте, где ожидается делегат, всегда создавался новый экземпляр делегата. Из-за этого код, который работал одинаково и имел более краткую запись работал медленнее явного лямбда-выражения.

В C# 11 preview 2 добавили кэширование таких делегатов.

Action writeLine = () => Console.WriteLine(); // так кэшируется
Action writeLineNew = Console.WriteLine; // и так теперь кэшируется

Issue на github
Применимость: часто
Текущее состояние: доступно в C# 11 preview

Поля, хранящиеся по ссылке для ref struct

В ref struct должна быть возможность создать поля, хранящиеся по ссылке — это позволит делать безопасные эффективные структуры данных, целиком хранящиеся на стеке.

Тут всё непросто и про производительность. В предыдущих версиях C# добавлялись фичи для производительности — возврат значения по ссылке, ref struct, которые целиком хранятся в стеке и не могут быть перемещены в кучу, указатели на функции. Это позволяет создавать высокопроизводительный код, оставаясь в рамках статической типизации и безопасной работы с памятью в .NET. Ещё это позволило создать новые типы для оптимизации производительности вроде Span<T> .

Span<T> — это такая безопасная обертка над непрерывной областью памяти. На примере его внутренней реализации видно, что текущих возможностей языка не хватает для полноценной безопасной работы с памятью — сейчас для ссылки на начало области в памяти используется внутренний тип ByReference<T> , который даёт функциональность полей, хранящихся по ссылке, но без проверки безопасности, которая используется в других значениях по ссылке.

Текущее изменение сделает возможным для ref struct иметь поля, которые хранятся по ссылке. Это в большой степени внутреннее изменение, которое позволит избавиться от особого случая с ByReference<T> .

readonly ref struct Span<T>
{
    readonly ref T _field;
    readonly int _length;

    // Этот конструктор не существует сейчас, но будет добавлен как часть  
    // изменений в Span<T> в рамках задачи появления ref fields. Это простой
    // и безопасный способ создать область длины 1 для значения в стеке, 
    // что раньше требовало использования небезопасного кода.
    public Span(ref T value)
    {
        _field = ref value;
        _length = 1;
    }
}

Это изменение может казаться незначительным, учитывая уже имеющиеся в языке возвращаемые ссылочные значения и ссылочные локальные переменные, но внутри есть много нюансов, связанных с безопасностью кода, проверками и сценариями использования. Например, ссылочные поля могут сочетаться с модификатором readonly разными способами из-за того, что понятие значения только для чтения и ссылки только для чтения независимы друг от друга:

  • readonly ref: можно переназначить значение поля, хранящееся по ссылке, но не саму хранящуюся ссылку.

  • ref readonly: можно переназначить ссылку, но нельзя изменить значение по ссылке.

  • readonly ref readonly: нельзя изменить ни значение по ссылке, ни переназначить ссылку, на которую указывает поле.

ref struct ReadOnlyExample
{
    ref readonly int Field1;
    readonly ref int Field2;
    readonly ref readonly int Field3;

    void Uses(int[] array)
    {
        Field1 = ref array[0];  // OK, теперь Field1 указывает на array[0]
        Field1 = array[0];      // Ошибка: невозможно изменить значение поля по текущей ссылке
        Field2 = ref array[0];  // Ошибка: нельзя изменить ссылку, на которую указывает поле Field2
        Field2 = array[0];      // ОК, теперь в значении, на которое указывает Field2 находится значение array[0]
        Field3 = ref array[0];  // Ошибка: нельзя изменить ссылку, на которую указывает поле Field3
        Field3 = array[0];      // Ошибка: невозможно изменить значение поля по текущей ссылке
    }
}

Кроме того, теперь значение переданное по ссылке может быть принято в метод, передано в качестве ссылки в ref struct и возвращено из метода как ref field этой структуры. Это может приводить к тому, что значение по этой ссылке изменится неожиданно вне контекста вызываемого метода. Чтобы иметь возможность сделать работу с значениями по ссылке более предсказуемой, вводится ключевое слово scoped, позволяющее ограничить жизненный цикл ссылки текущим методом.

Span<int> CreateAndCapture(ref int value)
{
    // ОК: жизненный цикл ссылки позволяет ей выходить за пределы метода
    return new Span<int>(ref value)
}

Span<int> CreateWithoutCapture(scoped ref int value)
{
    // Ошибка: при применении ключевого слова scoped жизненный цикл ссылки ограничивается текущим методом
    return new Span<int>(ref value);
}

Одно из критических изменений, которые ломают обратную совместимость — теперь ссылки out-параметров имеют жизненный цикл ограниченный текущим методом, поэтому ссылку на out-параметр нельзя вернуть из метода в C# 11. Но для мест, где жизненный цикл ссылки уже был ограничен текущим методом теперь можно изменить это поведение с помощью ключевого словаunscoped:

struct S1
{
    int field; 
    ref int Prop1 => ref field; // Ошибка
    unscoped ref int Prop1 => ref field; // ОК
}

unscoped struct S2
{
    int field;
    ref int Prop => ref field; // ОК поскольку unscoped применен к самой структуре
}
ref var refX = ref SneakyOut(out var x);
Console.WriteLine($"{refX}, {x}"); // 42, 42
x = 10;
Console.WriteLine($"{refX}, {x}"); // 10, 10
refX = 12;
Console.WriteLine($"{refX}, {x}"); // 12, 12

// unscoped для out-параметра возвращает поведение C# 10
ref int SneakyOut(unscoped out int i)
{
	  i = 42;
	  return ref i;
}

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

// Односвязный список с доступом между элементами по ссылке
ref struct StackLinkedListNode<T>
{
    T _value;
    ref StackLinkedListNode<T> _next;

    public T Value => _value;

    public bool HasNext => !Unsafe.IsNullRef(ref _next);

    public ref StackLinkedListNode<T> Next 
    {
        get
        {
            if (!HasNext)
            {
                throw new InvalidOperationException("No next node");
            }

            return ref _next;
        }
    }

    public StackLinkedListNode(T value)
    {
        this = default;
        _value = value;
    }

    public StackLinkedListNode(T value, ref StackLinkedListNode<T> next)
    {
        _value = value;
        _next = ref next;
    }
}

Proposal на github
Применимость: очень редко
Текущее состояние: In Progress

Span<T> / ReadOnlySpan<T> / IEnumerable<T> для params в объявлении методов

При объявления метода с переменным числом аргументов можно использовать params Span<T> или params ReadOnlySpan<T>, что позволит избавиться от неявного создания массива в куче при каждом вызове метода.

Можно использовать params IEnumerable<T> , что может уменшить копирование коллекций и число аллокаций при передаче параметров, отличных от T[].

params дает удобный способ вызова метода с переменным числом параметров. Но из-за того, что в качестве типа params сейчас используется массив, каждый вызов такого метода приводит к аллокации нового массива в куче.

Вместо этого можно будет задавать переменный список параметров с помощью ref struct типов Span<T> и ReadOnlySpan<T>, которые хранятся на стеке и массив таких параметров тоже может быть выделен на стеке.

Кроме этого можно использовать для списка параметров params IEnumerable<T>, что может помочь избежать лишних аллокаций и копирования коллекций, отличных от T[] в местах вызова.

Для ReadOnlySpan<T> или Span<T> массивы для списка параметров будут созданы в стеке, если длина массива находится пределах (если таковые имеются), установленных компилятором. В противном случае массив будет размещен в куче.

public void WriteLine(string format, params ReadOnlySpan<object?> arg) { ... }

WriteLine(fmt, x, y, z); // object[] на стеке
WriteLine(fmt, new[] { x, y, z }); // object[] на стеке 

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

  • Вызов происходит из одного места (внутри цикла).

  • Жизненный цикл использования этих массивов не пересекается, длина массива достаточна, а тип элементов массива имеет идентичный размер.

Это сделано чтобы эффективнее использовать стек и избежать проблем при использовании в цикле:

int count = 100;
for(int i = 0; i < count; i++)
{
    // Массив на стеке не будет выделяться каждый раз, а будет переиспользоваться
    Test(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); 
}

void Test(params Span<int> span) { ... }

Proposal на github
Применимость: иногда
Текущее состояние: In Progress


Полезные ссылки:

  • Папка proposals в репозитории C# — можно найти рассматриваемые в текущий момент изменения в языке.

  • Feature Status page

  • SharpLab — онлайн-песочница для C# от @ashmind , в которой можно посмотреть результирующий IL-код, JIT Asm, синтаксическое дерево и результат исполнения. Можно переключаться между разными ветками репозитория Roslyn, в том числе с фичами, которые сейчас находятся в разработке.

Источник: https://habr.com/ru/post/663150/


Интересные статьи

Интересные статьи

Я нашёл ещё один неплохой вариант для простейшей переделки в «вечную лампу». Это лампы Lexman 10 Вт 1000 лм, продающиеся по 85 рублей в магазинах Леруа Мерлен.
В конце ноября коллекционер устройств Apple и инсайдер Джулио Зомпетти в своем Twitter выложил фото прототипов зарядного устройства от Apple на 29 Вт и наушников AirPods с прозрачным корпусом. 
Возможно, вы уже сталкивались с агрессивным стилем вождения популярных электросамокатов. На тротуаре меньше всего ожидаешь, что здесь будут неожиданно подрезать, сигналит...
Короткий мануал — как реализовать поддержку загрузочного NVMe SSD на старых материнских платах с Legacy BIOS и с использованием Clover (для любых ОС). По следам вот этого поста, где на мой взгляд...
Сегодня делимся с вами рекомендациями Люка Геттинга (Luke Goetting) — признанного эксперта по созданию бизнес-презентаций, директора агентства Puffingston Presentations. Но начнем мы со слов Дж...