Новые возможности в C# версий 7.0–7.3

В C# версий 7.0–7.3 был реализован ряд функций и улучшены возможности разработки на C#. В этой статье приводятся общие сведения о новых функциях языка и параметрах компилятора. Приводится описание поведения C# 7.3, то есть самой последней версии, поддерживаемой для приложений на основе .NET Framework.

В C# 7.1 добавлен элемент конфигурации выбор версии языка, что позволяет указать версию языка компилятора в файле проекта.

В C# версий 7.0–7.3 добавлены следующие функции и темы в язык C#:

  • Кортежи и пустые переменные
    • Вы можете создать простые, неименованные типы, содержащие несколько открытых полей. Компиляторы и инструменты IDE понимают семантику этих типов.
    • Пустые переменные представляют собой временные переменные, доступные только для записи, которые используются при присвоении в тех случаях, когда присваиваемое значение не важно. Они особенно полезны при деконструкции кортежей и пользовательских типов, а также при вызове методов с параметрами out.
  • Соответствие шаблону
    • На основе произвольных типов и значений их членов можно создать логику ветвления.
  • Метод async Main
    • Точка входа для приложения может иметь модификатор async.
  • Локальные функции
    • Функции можно вкладывать в другие функции, чтобы ограничить область их действия и видимость.
  • Другие элементы, воплощающие выражение
    • Список элементов, которые можно создавать с помощью выражений, увеличился.
  • Выражения throw
    • Исключения могут возникать в конструкциях кода, которые ранее не допускались, поскольку throw был оператором.
  • Литеральные выражения default
    • Литеральные выражения по умолчанию можно использовать в выражениях значения по умолчанию, если можно вывести тип целевого объекта.
  • Усовершенствования в синтаксисе числовых литералов
    • Новые маркеры делают числовые константы более удобочитаемыми.
  • Переменные out
    • Значения out можно объявлять встроенными как аргументы для метода, в котором они используются.
  • Неконечные именованные аргументы
    • После именованных аргументов могут следовать позиционные аргументы.
  • Модификатор доступа private protected
    • Модификатор доступа private protected разрешает доступ для производных классов в одной сборке.
  • Улучшенное разрешение перегрузки
    • Новые правила для устранения неоднозначности разрешения перегрузки.
  • Методы написания безопасного и эффективного кода
    • Ряд улучшений синтаксиса, обеспечивающих работу с типами значений с использованием семантики ссылок.

Наконец, компилятор получил новые параметры:

  • -refout и -refonly, которые управляют созданием базовой сборки.
  • -publicsign позволяет включить подписывание сборок как программного обеспечения с открытым кодом;
  • -pathmap позволяет предоставить сопоставление для исходных каталогов.

В оставшейся части этой статьи представлены общие сведения об этих функциях. Каждая функция сопровождается обоснованием и описанием синтаксиса. Эти функции можно изучить в своей среде с помощью глобального средства dotnet try:

  1. Установите глобальное средство dotnet-try.
  2. Клонируйте репозиторий dotnet/try-samples.
  3. Для репозитория try-samples установите в качестве текущего каталога подкаталог csharp7.
  4. Запустите dotnet try.

Кортежи и пустые переменные

C# предоставляет расширенный синтаксис для классов и структур, который используется для объяснения цели проекта. Однако в некоторых случаях расширенный синтаксис требует дополнительной работы с минимальной результативностью. Зачастую требуется написание методов, которым нужна простая структура, состоящая из более чем одного элемента данных. Для поддержки этих сценариев в C# были добавлены кортежи. Кортежи — это упрощенные структуры данных, содержащие несколько полей для представления элементов данных. Поля не проверяются, и собственные методы определять нельзя. Типы кортежей в C# поддерживают == и !=. Дополнительные сведения см. в записи блога

Примечание

Кортежи существовали и в версиях C#, предшествовавших версии 7.0, но были неэффективны и не имели языковой поддержки. Это означает, что ссылки на элементы кортежа можно было задавать только в виде Item1, Item2 и т. д. В C# 7.0 реализуется языковая поддержка кортежей, что позволяет работать с семантическими именами полей кортежа с использованием новых, более эффективных типов кортежей.

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

(string Alpha, string Beta) namedLetters = ("a", "b");
Console.WriteLine($"{namedLetters.Alpha}, {namedLetters.Beta}");

Кортеж namedLetters содержит поля, которые называются Alpha и Beta. Эти имена существуют только во время компиляции и не сохраняются, например при проверке кортежа посредством отражения во время выполнения.

В назначении кортежа можно также указать имена полей в правой части назначения:

var alphabetStart = (Alpha: "a", Beta: "b");
Console.WriteLine($"{alphabetStart.Alpha}, {alphabetStart.Beta}");

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

(int max, int min) = Range(numbers);
Console.WriteLine(max);
Console.WriteLine(min);

Аналогичную деконструкцию можно обеспечить для любого типа в .NET. Можно написать метод Deconstruct в качестве члена класса. Метод Deconstruct предоставляет набор аргументов out для каждого из свойств, которые нужно извлечь. Рассмотрим этот класс Point, предоставляющий метод deconstructor, который извлекает координаты X и Y:

public class Point
{
    public Point(double x, double y)
        => (X, Y) = (x, y);

    public double X { get; }
    public double Y { get; }

    public void Deconstruct(out double x, out double y) =>
        (x, y) = (X, Y);
}

Отдельные поля можно извлекать, назначая кортежу метод Point:

var p = new Point(3.14, 2.71);
(double X, double Y) = p;

Очень часто при инициализации кортежа переменные в правой части задания совпадают с именами для элементов кортежа: Имена элементов кортежа можно вывести из переменных, используемых для инициализации кортежа:

int count = 5;
string label = "Colors used in the map";
var pair = (count, label); // element names are "count" and "label"

Дополнительные сведения об этой функции см. в статье Типы кортежей.

При деконструкции кортежа или вызове метода с параметрами out часто требуется определить переменную, которую вы не планируете использовать и значение которой не важно. Для работы в таких сценариях в C# реализована поддержка пустых переменных. Пустая переменная представляет собой доступную только для записи переменную с именем _ (знак подчеркивания). Вы можете назначить одной переменной все значения, которые не потребуются в дальнейшем. Пустая переменная является аналогом неприсвоенной переменной и не может использоваться в коде где-либо, за исключением оператора присваивания.

Пустые переменные поддерживается в следующих случаях.

  • При деконструкции кортежей или пользовательских типов.
  • При вызове методов с параметрами out.
  • В операции сопоставления шаблонов с выражениями is и switch.
  • В качестве автономного идентификатора в тех случаях, когда требуется явно идентифицировать значение присваивания как пустую переменную.

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

using System;
using System.Collections.Generic;

public class Example
{
    public static void Main()
    {
        var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010);

        Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}");
    }

    private static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2)
    {
        int population1 = 0, population2 = 0;
        double area = 0;

        if (name == "New York City")
        {
            area = 468.48;
            if (year1 == 1960)
            {
                population1 = 7781984;
            }
            if (year2 == 2010)
            {
                population2 = 8175133;
            }
            return (name, area, year1, population1, year2, population2);
        }

        return ("", 0, 0, 0, 0, 0);
    }
}
// The example displays the following output:
//      Population change, 1960 to 2010: 393,149

Дополнительные сведения см. в разделе Пустые переменные.

Регулярные выражения

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

Сопоставление шаблонов поддерживает выражения is и switch. Каждое из них позволяет проверять объект и его свойства и определять, соответствует ли этот объект искомому шаблону. Для добавления правил в шаблон используется ключевое слово when.

Выражение шаблона is позволяет использовать знакомый оператор is для запроса объекта о типе и присваивания результата в одной инструкции. Следующий код проверяет, является ли переменная int; если да, добавляет ее к текущей сумме:

if (input is int count)
    sum += count;

Предыдущий небольшой пример показывает улучшенное выражение is. Можно проверять типы значений, а также ссылочные типы; успешный результат можно назначить переменной соответствующего типа.

Выражение сопоставления со switch имеет знакомый синтаксис, основанный на операторе switch, который уже является частью языка C#. Обновленный оператор switch имеет несколько новых конструкций:

  • Определяющий тип выражения switch больше не ограничен интегральными типами, типами Enum, string или типами, принимающими значения NULL, соответствующими одному из таких типов. Может использоваться любой тип.
  • Можно проверить тип выражения switch в каждой метке case. Как и в выражении is, можно назначить новую переменную этого типа.
  • Можно добавить предложение when для дальнейшей проверки условий по этой переменной.
  • Порядок меток case становится важным. Будет выполнена первая совпавшая ветвь; другие пропускаются.

Это демонстрируется в следующем коде:

public static int SumPositiveNumbers(IEnumerable<object> sequence)
{
    int sum = 0;
    foreach (var i in sequence)
    {
        switch (i)
        {
            case 0:
                break;
            case IEnumerable<int> childSequence:
            {
                foreach(var item in childSequence)
                    sum += (item > 0) ? item : 0;
                break;
            }
            case int n when n > 0:
                sum += n;
                break;
            case null:
                throw new NullReferenceException("Null found in sequence");
            default:
                throw new InvalidOperationException("Unrecognized type");
        }
    }
    return sum;
}
  • case 0: — шаблон константы.
  • case IEnumerable<int> childSequence: — шаблон объявления.
  • case int n when n > 0: — шаблон объявления с дополнительным условием when.
  • case null: — шаблон константы null.
  • default: — знакомый вариант по умолчанию.

Начиная с версии C# 7.1, выражение шаблона для шаблона типа is и switch может быть типом параметра универсального типа. Эта возможность особенно полезна при проверке типов, которые могут представлять типы struct или class, когда вы хотите избежать упаковки-преобразования.

Дополнительные сведения о сопоставлении шаблонов см. в разделе Сопоставление шаблонов в C#.

Async main

Метод async main позволяет использовать await в методе Main. Раньше пришлось бы написать:

static int Main()
{
    return DoAsyncWork().GetAwaiter().GetResult();
}

Теперь можно написать:

static async Task<int> Main()
{
    // This could also be replaced with the body
    // DoAsyncWork, including its await expressions:
    return await DoAsyncWork();
}

Если программа не возвращает код выхода, объявите метод Main, возвращающий Task:

static async Task Main()
{
    await SomeAsyncMethod();
}

См. подробнее в описании async main в руководстве по программированию.

Локальные функции

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

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

public static IEnumerable<char> AlphabetSubset3(char start, char end)
{
    if (start < 'a' || start > 'z')
        throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter");
    if (end < 'a' || end > 'z')
        throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter");

    if (end <= start)
        throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}");

    return alphabetSubsetImplementation();

    IEnumerable<char> alphabetSubsetImplementation()
    {
        for (var c = start; c < end; c++)
            yield return c;
    }
}

Та же технология может применяться с методами async для того, чтобы исключения, возникающие при проверке параметров, выдавались до начала асинхронной работы:

public Task<string> PerformLongRunningWork(string address, int index, string name)
{
    if (string.IsNullOrWhiteSpace(address))
        throw new ArgumentException(message: "An address is required", paramName: nameof(address));
    if (index < 0)
        throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-negative");
    if (string.IsNullOrWhiteSpace(name))
        throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));

    return longRunningWorkImplementation();

    async Task<string> longRunningWorkImplementation()
    {
        var interimResult = await FirstWork(address);
        var secondResult = await SecondStep(index, name);
        return $"The results are {interimResult} and {secondResult}. Enjoy.";
    }
}

Теперь поддерживается такой синтаксис:

[field: SomeThingAboutFieldAttribute]
public int SomeProperty { get; set; }

Атрибут SomeThingAboutFieldAttribute применяется к резервному полю, созданному компилятором для SomeProperty. Дополнительные сведения см. в статье об атрибутах в руководстве по программированию на C#.

Примечание

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

Другие элементы, воплощающие выражение

В C# версии 6 появились элементы, воплощающие выражение, для функций-членов и свойств, доступных только для чтения. В C# 7.0 расширен список допустимых членов, которые могут быть реализованы как выражения. В C# 7.0 можно реализовать конструкторы, методы завершения, а также методы доступа get и set для свойств и индексаторов. В следующем коде показаны примеры каждого из них:

// Expression-bodied constructor
public ExpressionMembersExample(string label) => this.Label = label;

// Expression-bodied finalizer
~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!");

private string label;

// Expression-bodied get / set accessors.
public string Label
{
    get => label;
    set => this.label = value ?? "Default label";
}

Примечание

В этом примере метод завершения не требуется, он приводится для демонстрации синтаксиса. Метод завершения следует реализовывать в классе только в том случае, если это необходимо для высвобождения неуправляемых ресурсов. Кроме того, вместо управления неуправляемыми ресурсами напрямую можно воспользоваться классом SafeHandle.

Новые расположения для элементов, составляющих выражение, представляют важный этап развития языка C#. Эти функции были реализованы членами сообщества, работающими над проектом Roslyn с открытым исходным кодом.

Изменение метода на элемент, воплощающий выражение, является совместимым на уровне двоичного кода.

Выражения throw

В C# throw всегда был оператором. Поскольку throw — оператор, а не выражение, конструкции C# находились там, где использовать их было невозможно. Они включали условные выражения, выражения объединения со значением NULL и некоторые лямбда-выражения. Добавление элементов, воплощающих выражение, расширяет список мест, в которых могут пригодиться выражения throw. Для записи этих конструкций в C# 7.0 представлены выражения throw.

Это добавление упрощает написание кода на основе выражений. Дополнительные инструкции для проверки на наличие ошибок не требуются.

Литеральные выражения по умолчанию

Литеральные выражения по умолчанию — это усовершенствование выражения значения по умолчанию. Эти выражения инициализируют переменную до значения по умолчанию. Раньше пришлось бы написать:

Func<string, bool> whereClause = default(Func<string, bool>);

Теперь можно опустить тип с правой стороны инициализации:

Func<string, bool> whereClause = default;

Дополнительные сведения см. в разделе о литерале default в статье об операторе default.

Усовершенствования в синтаксисе числовых литералов

Неправильное толкование числовых констант затрудняет понимание кода при первом прочтении. Битовые маски или другие символьные значения могут вызывать затруднения. C# 7.0 содержит две новые возможности для записи чисел в удобочитаемом виде: двоичные литералы и разделители цифр.

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

public const int Sixteen =   0b0001_0000;
public const int ThirtyTwo = 0b0010_0000;
public const int SixtyFour = 0b0100_0000;
public const int OneHundredTwentyEight = 0b1000_0000;

0b в начале константы означает, что число записано в двоичном формате. Двоичные числа могут быть длинными, поэтому для удобства работы с битовыми шаблонами можно разделять разряды с помощью символа _, как показано в двоичной константе в предыдущем примере. Разделитель разрядов может находиться в любом месте константы. В десятичных числах он обычно используется для разделения тысяч. Шестнадцатеричные и двоичные числовые литералы могут начинаться со знака _:

public const long BillionsAndBillions = 100_000_000_000;

Разделитель разрядов можно также использовать с типами decimal, float и double:

public const double AvogadroConstant = 6.022_140_857_747_474e23;
public const decimal GoldenRatio = 1.618_033_988_749_894_848_204_586_834_365_638_117_720_309_179M;

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

Переменные out

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

if (int.TryParse(input, out int result))
    Console.WriteLine(result);
else
    Console.WriteLine("Could not parse input");

Для ясности можно указать тип переменной out, как показано в предыдущем примере. В то же время язык поддерживает использование неявно типизированной локальной переменной:

if (int.TryParse(input, out var answer))
    Console.WriteLine(answer);
else
    Console.WriteLine("Could not parse input");
  • Код проще читать.
    • Переменная out объявляется при использовании, а не в предыдущей строке кода.
  • Назначать начальное значение не нужно.
    • Объявляя переменную out, когда она используется при вызове метода, ее нельзя случайно использовать прежде, чем она будет назначена.

Синтаксис, который с версии C# 7.0 позволяет объявлять переменные out, теперь также поддерживает инициализаторы полей, инициализаторы свойств, инициализаторы конструктора и предложения запроса. Он позволяет создать такой код, как в следующем примере:

public class B
{
   public B(int i, out int j)
   {
      j = i;
   }
}

public class D : B
{
   public D(int i) : base(i, out var j)
   {
      Console.WriteLine($"The value of 'j' is {j}");
   }
}

Неконечные именованные аргументы

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

private protected — модификатор доступа

Новый составной модификатор доступа private protected указывает, что доступ к члену может осуществляться содержащим классом или производными классами, которые объявлены в рамках одной сборки. В отличие от модификатора protected internal, который разрешает доступ производным классам или классам из той же сборки, private protected ограничивает доступ только для производных классов, объявленных в рамках одной сборки.

Дополнительные сведения см. в разделе Модификаторы доступа в справочнике по языку.

Улучшенный отбор потенциальных перегрузок

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

  1. Если группа методов содержит элементы экземпляра и статические элементы, компилятор отклоняет все элементы экземпляра при вызове метода без экземпляра-получателя и вне контекста экземпляра. Компилятор отклоняет статические элементы, если метод был вызван с экземпляром-получателем. Если получатель не указан, компилятор включает в статический контекст только статические элементы, а в противном случае — статические элементы и элементы экземпляра. Если получатель невозможно однозначно определить как экземпляр или тип, компилятор включает и те, и другие элементы. В статический контекст, в котором невозможно использовать неявный экземпляр-получатель this, включается текст тех элементов, для которых не определено this, например статические элементы, а также все места, где не может использоваться this, такие как инициализаторы полей и конструкторы-инициализаторы.
  2. Если группа методов содержит некоторые универсальные методы, у которых аргументы типа не удовлетворяют ограничениям, такие элементы удаляются из набора кандидатов.
  3. При преобразовании группы методов из набора удаляются методы-кандидаты, у которых возвращаемый тип не соответствует возвращаемому типу делегата.

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

Повышение эффективности безопасного кода

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

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

  • доступ к полям фиксированной ширины без закрепления;
  • возможность переназначать локальные переменные ref;
  • возможность использовать инициализаторы для массивов stackalloc;
  • возможность использовать инструкции fixed с любым типом, который поддерживает шаблон;
  • возможность использовать дополнительные универсальные ограничения.
  • модификатор in для параметров, указывающий, что аргумент передается по ссылке, но не изменяется вызываемым методом; Добавление модификатора in к аргументу является изменением, совместимым на уровне исходного кода.
  • модификатор ref readonly для возвращаемого значения метода, указывающий, что метод возвращает значение по ссылке, но не допускает операции записи в соответствующий объект; Добавление модификатора ref readonly к аргументу является изменением, совместимым на уровне исходного кода, если оператору return присваивается значение. Добавление модификатора readonly к существующему оператору return ref является несовместимым изменением. Требуется указать вызывающие объекты, чтобы добавить модификатор readonly в объявление локальных переменных ref.
  • объявление readonly struct, указывающее, что структура является неизменяемой и должна передаваться в методы члена как параметр in; Добавление модификатора readonly к существующему объявлению структуры является двоично совместимым изменением.
  • объявление ref struct, указывающее, что тип структуры обращается напрямую к управляемой памяти и всегда должен обрабатываться с выделением стека. Добавление модификатора ref к существующему объявлению struct является двоично совместимым изменением. Объект ref struct не может быть членом класса или использоваться в других местах, где он может выделяться в куче.

Дополнительные сведения обо всех эти изменениях см. в статье о том, как писать безопасный и эффективный код.

Локальные переменные и возвращаемые значения Ref

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

public static ref int Find(int[,] matrix, Func<int, bool> predicate)
{
    for (int i = 0; i < matrix.GetLength(0); i++)
        for (int j = 0; j < matrix.GetLength(1); j++)
            if (predicate(matrix[i, j]))
                return ref matrix[i, j];
    throw new InvalidOperationException("Not found");
}

Можно объявить возвращаемое значение как ref и изменять это значение в матрице, как показано в следующем коде:

ref var item = ref MatrixSearch.Find(matrix, (val) => val == 42);
Console.WriteLine(item);
item = 24;
Console.WriteLine(matrix[4, 2]);

Язык C# включает правила, которые защищают вас от неправильного использования локальных переменных и возвращаемых значений ref:

  • Необходимо добавить ключевое слово ref в сигнатуру метода и все инструкции return в методе.
    • Это позволяет уточнить, что метод возвращает значение по ссылке, во всех местах.
  • Объект ref return может быть назначен переменной-значению или переменной ref.
    • Вызывающий объект определяет, копируется ли возвращаемое значение. Пропуск модификатора ref при присваивании возвращаемого значения указывает, что вызывающий объект хочет получить копию значения, а не ссылку на хранилище.
  • Присвоить локальной переменной ref стандартное возвращаемое значение метода невозможно.
    • Это запрещает использовать операторы вида ref int i = sequence.Count();
  • Переменную ref невозможно возвращать переменной, которая продолжает существовать даже после того, как метод будет выполнен.
    • Это означает невозможность возвращения ссылки на локальную переменную или переменную с аналогичной областью.
  • Возвращаемые значения и локальные переменные ref не могут использоваться с асинхронными методами.
    • На момент, когда асинхронный метод возвращает значение, компилятору неизвестно, присвоено ли переменной, на которую указывает ссылка, окончательное значение.

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

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

Теперь локальные переменные ref можно переназначить другим экземплярам после инициализации. Следующая команда теперь успешно компилируется:

ref VeryLargeStruct refLocal = ref veryLargeStruct; // initialization
refLocal = ref anotherVeryLargeStruct; // reassigned, refLocal refers to different storage.

Дополнительные сведения см. в статье о возвращаемых значениях ref и локальных переменных ref, а также в статье foreach.

Дополнительные сведения см. в статье ref (Справочник по C#).

Условные выражения ref

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

ref var r = ref (arr != null ? ref arr[0] : ref otherArr[0]);

Переменная r — это ссылка на первое значение в arr или otherArr.

Дополнительные сведения см. в описании условного оператора (?:) в справочнике по языку.

Модификатор параметра in

При передаче аргументов по ссылке можно использовать ключевое слово in как дополнение к существующим ключевым словам ref и out. Ключевое слово in указывает, что аргумент передается по ссылке, но вызванный метод не изменяет это значение.

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

static void M(S arg);
static void M(in S arg);

Перегрузка по значению (первая в предыдущем примере) считается лучше, чем перегрузка по атрибуту "только для чтения". Чтобы вызвать версию со ссылочным аргументом "только для чтения", необходимо при вызове метода указать модификатор in.

Дополнительные сведения см. в статье, посвященной модификатору параметра in.

Больше типов поддерживают инструкцию fixed

Инструкция fixed ранее поддерживала лишь ограниченный набор типов. Начиная с C# 7.3 любой тип, содержащий метод GetPinnableReference(), который возвращает ref T или ref readonly T, может иметь инструкцию fixed. Добавление этой возможности означает, что fixed можно применять для System.Span<T> и связанных типов.

Дополнительные сведения см. в статье об инструкции fixed в справочнике по языку.

Индексирование полей fixed не требует закрепления

Давайте рассмотрим такую структуру:

unsafe struct S
{
    public fixed int myFixedField[10];
}

В более ранних версиях C# переменную необходимо закрепить, чтобы получить доступ к целым числам, входящим в myFixedField. Теперь приведенный ниже код компилируется без закрепления переменной p внутри отдельного оператора fixed:

class C
{
    static S s = new S();

    unsafe public void M()
    {
        int p = s.myFixedField[5];
    }
}

Переменная p обращается к одному элементу в myFixedField. Для этого не нужно объявлять отдельную переменную int*. Контекст unsafe по-прежнему является обязательным. В более ранних версиях C# необходимо объявить второй фиксированный указатель:

class C
{
    static S s = new S();

    unsafe public void M()
    {
        fixed (int* ptr = s.myFixedField)
        {
            int p = ptr[5];
        }
    }
}

Дополнительные сведения см. в статье, посвященной инструкции fixed.

Массивы stackalloc поддерживают инициализаторы

Раньше вы могли задавать значения для элементов массива при его инициализации:

var arr = new int[3] {1, 2, 3};
var arr2 = new int[] {1, 2, 3};

Теперь такой же синтаксис можно применять к массивам, в объявлении которых есть stackalloc:

int* pArr = stackalloc int[3] {1, 2, 3};
int* pArr2 = stackalloc int[] {1, 2, 3};
Span<int> arr = stackalloc [] {1, 2, 3};

Дополнительные сведения см. в статье Оператор stackalloc.

Расширенные универсальные ограничения

Теперь вы можете указать тип System.Enum или System.Delegate в качестве ограничения базового класса для параметра типа.

Вы также можете использовать новое ограничение unmanaged, чтобы указать, что параметр типа должен быть неуправляемым типом, не допускающим значения NULL.

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

Добавление этих ограничений в существующие типы — это несовместимое изменение. Закрытые универсальные типы не будут соответствовать подобным ограничениям.

Обобщенные асинхронные типы возвращаемых значений

В некоторых случаях возврат объекта Task из асинхронных методов может вызывать сложности. Task — это тип ссылки, поэтому его применение означает распределение объекта. В случаях, когда метод, объявленный с модификатором async, возвращает кэшированный результат или завершается синхронно, лишние распределения могут вызывать серьезные потери времени при выполнении фрагментов кода, зависящих от производительности. Эта проблема встает серьезно, если распределения происходят в коротких циклах.

Новая возможность языка означает, что этот асинхронный метод возвращает типы, не ограниченные Task, Task<T> и void. Возвращаемый тип должен по-прежнему соответствовать асинхронному шаблону, а значит, метод GetAwaiter должен быть доступен. Конкретный пример. В .NET добавлен новый тип ValueTask, позволяющий применять эту новую возможность языка:

public async ValueTask<int> Func()
{
    await Task.Delay(100);
    return 5;
}

Примечание

Чтобы использовать тип ValueTask<TResult>, необходимо добавить пакет NuGet System.Threading.Tasks.Extensions >.

Это улучшение особенно полезно для авторов библиотек, которые хотят избежать выделения Task в критическом по производительности коде.

Новые параметры компилятора

Новые параметры компилятора поддерживают сценарии сборки и DevOps для программ на C#.

Создание базовой сборки

Доступно два новых параметра компилятора, которые создают сборки только со ссылками: ProduceReferenceAssembly и ProduceOnlyReferenceAssembly. В соответствующих статьях подробно рассматриваются эти параметры и базовые сборки.

Подписывание открытым ключом или с открытым исходным кодом

Параметр компилятора PublicSign указывает, что сборку нужно подписать открытым ключом. Такая сборка будет помечена как подписанная, но подпись для нее берется из открытого ключа. Этот параметр позволяет создавать подписанные сборки из проектов с открытым кодом с помощью открытого ключа.

Дополнительные сведения см. в статье о параметре компилятора PublicSign.

pathmap

Параметр PathMap указывает компилятору, что исходные пути в среде сборки следует заменить сопоставленными исходными путями. Параметр PathMap управляет исходными путями, которые компилятор записывает в PDB-файлы или для CallerFilePathAttribute.

Дополнительные сведения см. в статье о параметре компилятора PathMap.