Поделиться через


Улучшения низкоуровневой структуры

Заметка

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

Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия фиксируются в соответствующих собраниях по проектированию языка (LDM).

Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .

Сводка

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

Примечание: Предыдущие версии этой спецификации использовали термины «ref-safe-to-escape» и «safe-to-escape», которые были введены в спецификации функции безопасности диапазона . Комитет по стандарту ECMA изменил имена на "ref-safe-context" и "safe-context"соответственно. Значения безопасного контекста были уточнены для последовательного использования "declaration-block", "function-member" и "caller-context". Спеклеты использовали разные формулировки для этих понятий, а также использовали "безопасное к возвращению" в качестве синонима для "вызывающего контекста". Эта спецификация была обновлена, чтобы использовать термины в стандарте C# 7.3.

Не все функции, описанные в этом документе, реализованы в C# 11. C# 11 включает:

  1. поля ref и scoped
  2. [UnscopedRef]

Эти функции остаются открытыми предложениями для будущей версии C#:

  1. поля ref для ref struct
  2. Ограниченные типы при закате

Мотивация

Более ранние версии C# добавили ряд функций низкой производительности на языке: ref возвращает, ref struct, указатели функций и т. д. Они позволили разработчикам .NET писать высокопроизводительный код, продолжая использовать правила языка C# для обеспечения безопасности типов и памяти. Он также позволил создавать основные типы производительности в библиотеках .NET, таких как Span<T>.

Когда эти функции приобрели популярность в экосистеме .NET, разработчики, как внутренние, так и внешние, предоставили нам информацию о оставшихся точках напряжения в экосистеме. Места, где они по-прежнему должны переходить на код unsafe, чтобы выполнить свою работу, или требуется среда выполнения для обработки особых типов, таких как Span<T>.

Сегодня Span<T> выполняется с помощью типа internalByReference<T>, который среда выполнения эффективно обрабатывает как поле ref. Это обеспечивает преимущество ref полей, но с недостатком, что язык не предоставляет проверки безопасности для этих полей, так как делает это для других использованиях ref. Кроме того, только dotnet/runtime может использовать этот тип, так как он internal, поэтому 3-е стороны не могут разрабатывать собственные примитивы на основе полей ref. Одной из мотиваций для этой работы является удаление ByReference<T> и использование соответствующих полей ref во всех базах кода.

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

  • Разрешить ref struct типам объявлять поля ref.
  • Разрешить среде выполнения полностью задать Span<T> с помощью системы типов C# и удалить специальный случай типа, например ByReference<T>
  • Разрешить struct типам возвращать ref их полям.
  • Разрешить среде выполнения удалять unsafe использование, вызванное ограничениями времени существования по умолчанию
  • Разрешить объявление безопасных буферов fixed для управляемых и неуправляемых типов в struct

Подробный дизайн

Правила безопасности для ref struct определяются в документе по безопасности , используя предыдущие термины. Эти правила были включены в стандарт C# 7 в §9.7.2 и §16.4.12. В этом документе описаны необходимые изменения в этом документе в результате этого предложения. После принятия в качестве утвержденной функции эти изменения будут включены в этот документ.

После завершения этого дизайна определение Span<T> будет следующим:

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

    // This constructor does not exist today but will be added as a part 
    // of changing Span<T> to have ref fields. It is a convenient, and
    // safe, way to create a length one span over a stack value that today 
    // requires unsafe code.
    public Span(ref T value)
    {
        _field = ref value;
        _length = 1;
    }
}

Укажите поля ссылок и их область действия

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

ref struct S 
{
    public ref int Value;
}

Поле ref будет вводиться в метаданные с помощью сигнатуры ELEMENT_TYPE_BYREF. Это ничем не отличается от того, как мы передаем локальные переменные ref или аргументы ref. Например, ref int _field будет выдаваться как ELEMENT_TYPE_BYREF ELEMENT_TYPE_I4. Это потребует от нас обновить ECMA335, чтобы разрешить эту запись, но это должно быть достаточно просто.

Разработчики могут продолжать инициализировать ref struct с полем ref с помощью выражения default, в котором все объявленные поля ref будут иметь значение null. Любая попытка использовать такие поля приведет к тому, что будет выброшена NullReferenceException.

ref struct S 
{
    public ref int Value;
}

S local = default;
local.Value.ToString(); // throws NullReferenceException

Хотя язык C# делает вид, что ref не может быть null это является законным на уровне среды выполнения и имеет четко определенную семантику. Разработчики, которые вводят поля ref в их типы, должны быть осведомлены об этой возможности и должны быть строго не рекомендуется утечки этих сведений в использование кода. Вместо этого поля должны проверяться на непустоту с помощью вспомогательных средств среды выполнения , выбрасывая исключение при неправильном использовании неинициализированного .

ref struct S1 
{
    private ref int Value;

    public int GetValue()
    {
        if (System.Runtime.CompilerServices.Unsafe.IsNullRef(ref Value))
        {
            throw new InvalidOperationException(...);
        }

        return Value;
    }
}

Поле ref можно объединить с модификаторами readonly следующими способами:

  • readonly ref: это поле, которое не может быть переназначено вне конструктора или методов init. Значение может быть присвоено, тем не менее, вне этих контекстов.
  • ref readonly: это поле, которое может быть переназначено, но не может иметь присвоенное значение в любой момент времени. Вот как параметр in можно перенаправить в поле ref.
  • readonly ref readonly: сочетание ref readonly и readonly ref.
ref struct ReadOnlyExample
{
    ref readonly int Field1;
    readonly ref int Field2;
    readonly ref readonly int Field3;

    void Uses(int[] array)
    {
        Field1 = ref array[0];  // Okay
        Field1 = array[0];      // Error: can't assign ref readonly value (value is readonly)
        Field2 = ref array[0];  // Error: can't repoint readonly ref
        Field2 = array[0];      // Okay
        Field3 = ref array[0];  // Error: can't repoint readonly ref
        Field3 = array[0];      // Error: can't assign ref readonly value (value is readonly)
    }
}

Для readonly ref struct требуется, чтобы поля ref были объявлены как readonly ref. Нет требования, что они должны быть объявлены как readonly ref readonly. Это позволяет readonly struct иметь косвенные мутации через такие поля, но это не отличается от поля readonly, которое сегодня указывает на ссылочный тип (дополнительные сведения)

readonly ref будет сгенерирован для метаданных с использованием флага initonly, как и для любого другого поля. Поле ref readonly будет атрибутом System.Runtime.CompilerServices.IsReadOnlyAttribute. readonly ref readonly будет выдаваться с обоими элементами.

Эта функция требует поддержки среды выполнения и изменений в спецификации ECMA. Таким образом, они будут включены только в том случае, если соответствующий флаг компонента установлен в corelib. Отслеживание проблемы отслеживания конкретного API ведется здесь https://github.com/dotnet/runtime/issues/64165

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

Сначала необходимо обновить правила, устанавливающие значения ref-safe-context и для полей ref следующим образом:

Выражение в форме ref e.Fref-safe-context следующим образом:

  1. Если F является полем ref, то его ref-safe-context — это безопасный контекстe.
  2. Иначе, если e имеет ссылочный тип, у него ref-safe-context в контексте вызова
  3. Кроме того, ref-safe-context берется из ref-safe-context .

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

Это означает, что ref поля можно возвращать как ref из ref struct, но обычные поля возвращать нельзя.

ref struct RS
{
    ref int _refField;
    int _field;

    // Okay: this falls into bullet one above. 
    public ref int Prop1 => ref _refField;

    // Error: This is bullet four above and the ref-safe-context of `this`
    // in a `struct` is function-member.
    public ref int Prop2 => ref _field;
}

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

Следующим шагом необходимо скорректировать правила переназначения ссылок с учётом наличия полей ref. Основной сценарий переназначения ссылок — это ref struct конструкторы, передающие параметры ref в поля ref. Поддержка будет более общей, но это основной сценарий. Для этого правила переназначения ссылок будут изменены с учетом полей ref следующим образом:

Правила переназначения ссылок

Левый операнд оператора = ref должен быть выражением, которое привязывается к локальной переменной ссылок, параметру ссылок (кроме this), параметру out, или полю ссылок.

Для переназначения ссылки в форме e1 = ref e2 должны выполняться оба из следующих условий:

  1. e2 должен иметь ref-safe-context, по крайней мере, такой же большой, как ref-safe-context, принадлежащий e1
  2. e1 должны иметь то же безопасного контекста, что и e2примечание

Это означает, что нужный конструктор Span<T> работает без дополнительной заметки:

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

    public Span(ref T value)
    {
        // Falls into the `x.e1 = ref e2` case, where `x` is the implicit `this`. The 
        // safe-context of `this` is *return-only* and ref-safe-context of `value` is 
        // *caller-context* hence this is legal.
        _field = ref value;
        _length = 1;
    }
}

Изменение правил переназначения ссылок означает, что параметры ref теперь могут выходить из метода в качестве поля ref в составе значения ref struct. Как обсуждалось в разделе совместимости, это может изменить правила для существующих API, которые никогда не предназначались для того, чтобы параметры ref становились полем ref. Правила существования параметров основаны исключительно на их объявлении, а не на их использовании. Все параметры ref и in содержат ref-safe-context от контекста вызова и, таким образом, теперь могут быть возвращены через ref или поле ref. Для поддержки API с параметрами ref, которые могут быть экранируемыми или не экранируемыми, и таким образом восстановить семантику мест вызова C# 10, язык будет вводить ограниченные аннотации о времени существования.

модификатор scoped

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

Параметр или локальный ref-safe-context безопасный контекст
Span<int> s элемента функции вызывающего контекста
scoped Span<int> s элемента функции элемента функции
ref Span<int> s вызывающего контекста вызывающего контекста
scoped ref Span<int> s элемента функции вызывающего контекста

В этом отношении ref-safe-context значения никогда не может быть более широким, чем безопасный контекст.

Это позволяет аннотировать API в C# 11 таким образом, чтобы они имели те же правила, что и C# 10:

Span<int> CreateSpan(scoped ref int parameter)
{
    // Just as with C# 10, the implementation of this method isn't relevant to callers.
}

Span<int> BadUseExamples(int parameter)
{
    // Legal in C# 10 and legal in C# 11 due to scoped ref
    return CreateSpan(ref parameter);

    // Legal in C# 10 and legal in C# 11 due to scoped ref
    int local = 42;
    return CreateSpan(ref local);

    // Legal in C# 10 and legal in C# 11 due to scoped ref
    Span<int> span = stackalloc int[42];
    return CreateSpan(ref span[0]);
}

Аннотация scoped также означает, что параметр this в struct теперь можно определить как scoped ref T. Ранее для этого требовалось особое рассмотрение в правилах в качестве параметра ref, который имел различные правила ref-safe-context, отличные от других параметров ref (см. все ссылки на включение или исключение получателя в правилах безопасного контекста). Теперь его можно выразить как общую концепцию в рамках правил, которые еще больше упрощают их.

Заметку scoped также можно применить к следующим расположениям:

  • locals: эта заметка задает время существования как безопасного контекста, или безопасного контекста ссылок в случае локального ref члена функции независимо от времени существования инициализатора.
Span<int> ScopedLocalExamples()
{
    // Error: `span` has a safe-context of *function-member*. That is true even though the 
    // initializer has a safe-context of *caller-context*. The annotation overrides the 
    // initializer
    scoped Span<int> span = default;
    return span;

    // Okay: the initializer has safe-context of *caller-context* hence so does `span2` 
    // and the return is legal.
    Span<int> span2 = default;
    return span2;

    // The declarations of `span3` and `span4` are functionally identical because the 
    // initializer has a safe-context of *function-member* meaning the `scoped` annotation
    // is effectively implied on `span3`
    Span<int> span3 = stackalloc int[42];
    scoped Span<int> span4 = stackalloc int[42];
}

Другие варианты использования scoped на локальных языках рассматриваются ниже.

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

Изменение поведения параметров out

Чтобы дополнительно ограничить влияние изменения и параметров, возвращаемых как поля , язык изменит значение контекста по умолчанию ref-safe-context для параметров , которые будут -член функции. Фактически out параметры неявно scoped out идти вперед. С точки зрения совместимости это означает, что они не могут быть возвращены ref.

ref int Sneaky(out int i) 
{
    i = 42;

    // Error: ref-safe-context of out is now function-member
    return ref i;
}

Это повысит гибкость API, возвращающих значения ref struct и имеющих параметры out, так как ему больше не нужно учитывать параметр, захваченный ссылкой. Это важно, так как это распространенный шаблон в API стиля чтения:

Span<byte> Read(Span<byte> buffer, out int read)
{
    // .. 
}

Span<byte> Use()
{
    var buffer = new byte[256];

    // If we keep current `out` ref-safe-context this is an error. The language must consider
    // the `read` parameter as returnable as a `ref` field
    //
    // If we change `out` ref-safe-context this is legal. The language does not consider the 
    // `read` parameter to be returnable hence this is safe
    int read;
    return Read(buffer, out read);
}

Язык также больше не будет рассматривать аргументы, передаваемые в параметр out, как возвратные. Обработка входных данных к параметру out в качестве возвращаемого объекта крайне запутана разработчикам. Он, по сути, подрывает намерение out, заставляя разработчиков учитывать значение, которое никогда не используется, за исключением языков, не поддерживающих out. Языки, которые поддерживают ref struct, должны гарантировать, что исходное значение, переданное параметру out, никогда не считывается.

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

Span<int> StrangeButLegal(out Span<int> span)
{
    span = default;
    return span;
}

Вместе эти изменения означают, что аргумент для параметра не способствует безопасного контекста или значений ref-safe-context для вызовов методов. Это значительно снижает общее влияние на совместимость полей ref, а также облегчает разработчикам понимание out. Аргумент параметра out не влияет на возвратное значение, это просто является результатом.

Вывод безопасного контекста для объявляющих выражений

безопасного контекста переменной объявления из аргумента () или деконструкции () является самым узким из следующих:

  • вызывающий контекст
  • Если переменная out помечена scoped, тогда для блока объявления (т. е. член-функция или более узкая область).
  • Если тип выходной переменной ref struct, рассмотрите все аргументы в содержащем вызове, включая получатель:
    • безопасного контекста любого аргумента, для которого его соответствующий параметр не out и имеет безопасный контекст, который является только для возврата или более широким
    • ref-safe-context любого аргумента, в котором соответствующий параметр имеет ref-safe-contextвозвращаемых только или более широких

См. также примеры выводимых безопасный контекст выражений объявлений.

Параметры по умолчанию scoped

В целом имеются две ref локации, которые неявно объявлены как scoped:

  • this в методе экземпляра struct
  • параметры out

Правила безопасного контекста ref будут изложены в терминах scoped ref и ref. В целях безопасного контекста ссылки параметр in эквивалентен ref и out эквивалентен scoped ref. Оба in и out будут специально указываться только в том случае, если это важно для смысла правила. В противном случае они просто считаются ref и scoped ref соответственно.

При обсуждении ref-safe-context аргументов, соответствующих in параметрам, они будут обобщены как ref аргументы в спецификации. В случае, если аргумент является lvalue, то ref-safe-context такой же, как у lvalue, в противном случае — это функция-член. Снова in будет упоминаться только здесь, если это важно для семантики текущего правила.

Безопасный контекст только для возврата

Дизайн также требует внедрения нового безопасного контекста: только для возвратов. Это похоже на контекста вызывающего объекта, в котором его можно вернуть, но его можно возвращать только с помощью инструкции return.

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

Существуют три места, которые по умолчанию предназначены только для возврата :

  • Параметр ref или in будет иметь ref-safe-contextтолько возврат. Это делается частично для ref struct, чтобы предотвратить проблемы глупых циклических назначений. Это делается единообразно, чтобы упростить модель, а также свести к минимуму изменения совместимости.
  • Параметр out для ref struct будет иметь безопасного контекста только. Это позволяет как возврату, так и out быть одинаково выразительными. Эта проблема не имеет глупого циклического назначения, так как неявно поэтому ref-safe-context по-прежнему меньше, чем безопасного контекста.
  • Параметр this для конструктора struct будет иметь безопасный контекст , который используется только для возвращения. Это происходит в результате моделирования в качестве out параметров.

Любое выражение или оператор, который явно возвращает значение из метода или лямбда-выражения, должно иметь безопасный контексти, если применимо, безопасный для ссылок контекст, как минимум только для возврата. Это включает в себя return операторы, элементы выражения, лямбда-выражения.

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

Примечание. Выражение, тип которого не является типом ref struct всегда имеет безопасный контекствызывающего контекста.

Правила вызова метода

Правила безопасного ссылочного контекста для вызова метода будут обновляться несколькими способами. Первым является признание влияния, которое scoped имеет на аргументы. Для заданного аргумента expr, передаваемого параметру p:

  1. Если p равно scoped ref, то expr не вносит вклад в ref-safe-context при рассмотрении аргументов.
  2. Если p равно scoped, то expr не вносит в безопасный контекст при рассмотрении аргументов.
  3. Если p это out, тогда expr не вносит вклада в ref-safe-контекст или безопасный контекстподробности

Язык "не вносит вклад" означает, что аргументы просто не учитываются при вычислении значения метода ref-safe-context или безопасного контекста соответственно. Это связано с тем, что значения не могут внести свой вклад в это время жизни, так как аннотация scoped предотвращает это.

Теперь можно упростить правила вызова метода. Получатель больше не должен быть особым, в случае struct теперь это просто scoped ref T. Правила значений нужно изменить, чтобы учитывать результаты возвращающиеся из поля ref.

Значение, полученное из e1.M(e2, ...)вызова метода, где M() не возвращает структуру ref-to-ref-ref, имеет безопасный контекст, взятый из самого узкого из следующих:

  1. вызывающий контекст
  2. Когда возврат имеет значение ref struct, безопасный контекст , созданный всеми выражениями аргументов.
  3. Если возврат является ref struct ref-safe-context, внесенных всеми аргументами ref

Если M() возвращает ссылку на ссылочную структуру, безопасного контекста совпадает с безопасного контекста всех аргументов, которые являются ссылками на ссылочные структуры. Ошибка возникает, если имеются несколько аргументов с разными безопасными контекстами , потому что аргументы метода должны соответствовать.

Правила вызова ref можно упростить следующим способом:

Значение, полученное из вызова метода ref e1.M(e2, ...), где M() не возвращает структуру ref-to-ref-struct, находится в контексте ref-safe-context с наименьшей шириной среди следующих контекстов:

  1. вызывающий контекст
  2. Безопасный контекст , создаваемый всеми выражениями аргументов
  3. ref-safe-context, создаваемый всеми аргументами ref

Если M() возвращает ref-to-ref-struct, то ref-safe-context является самым узким ref-safe-context, определяемым всеми аргументами, которые являются ref-to-ref-struct.

Теперь это правило позволяет определить два варианта требуемых методов:

Span<int> CreateWithoutCapture(scoped ref int value)
{
    // Error: value Rule 3 specifies that the safe-context be limited to the ref-safe-context
    // of the ref argument. That is the *function-member* for value hence this is not allowed.
    return new Span<int>(ref value);
}

Span<int> CreateAndCapture(ref int value)
{
    // Okay: value Rule 3 specifies that the safe-context be limited to the ref-safe-context
    // of the ref argument. That is the *caller-context* for value hence this is not allowed.
    return new Span<int>(ref value);
}

Span<int> ComplexScopedRefExample(scoped ref Span<int> span)
{
    // Okay: the safe-context of `span` is *caller-context* hence this is legal.
    return span;

    // Okay: the local `refLocal` has a ref-safe-context of *function-member* and a 
    // safe-context of *caller-context*. In the call below it is passed to a 
    // parameter that is `scoped ref` which means it does not contribute 
    // ref-safe-context. It only contributes its safe-context hence the returned
    // rvalue ends up as safe-context of *caller-context*
    Span<int> local = default;
    ref Span<int> refLocal = ref local;
    return ComplexScopedRefExample(ref refLocal);

    // Error: similar analysis as above but the safe-context of `stackLocal` is 
    // *function-member* hence this is illegal
    Span<int> stackLocal = stackalloc int[42];
    return ComplexScopedRefExample(ref stackLocal);
}

Правила для инициализаторов объектов

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

  1. безопасный контекст вызова конструктора.
  2. безопасный контекст и ref-безопасный контекст для аргументов к индексаторам инициализатора элементов, которые могут быть переданы получателю.
  3. безопасного контекста RHS назначений в инициализаторах членов для нечитаемых наборов или ref-safe-context при назначении ссылок.

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

Span<int> heapSpan = default;
Span<int> stackSpan = stackalloc int[42];
var x = new S(ref heapSpan)
{
    Field = stackSpan;
}

// Can be modeled as 
var x = new S(ref heapSpan, stackSpan);

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

Аргументы метода должны соответствовать

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

Для любого вызова метода e.M(a1, a2, ... aN)

  1. Вычислите самые узкие безопасного контекста:
    • вызывающего контекста
    • безопасный контекст всех аргументов
    • ref-safe-context всех аргументов-ссылок, параметры которых имеют ref-safe-context вызывающий контекст
  2. Все аргументы ref типа ref struct должны быть присваиваемыми значением в этом безопасном контексте . Это случай, когда refне обобщается для включения in и out

Для любого вызова метода e.M(a1, a2, ... aN)

  1. Вычислите самые узкие безопасного контекста:
    • вызывающего контекста
    • безопасный контекст всех аргументов
    • ref-safe-context всех ссылочных аргументов, соответствующие параметры которых не
  2. Все аргументы out типа ref struct должны быть присваиваемыми значением в этом безопасном контексте .

Наличие scoped позволяет разработчикам уменьшить трение, которое создает это правило путем маркировки параметров, которые не возвращаются как scoped. Это удаляет эти аргументы из (1) в обоих вышеописанных случаях и обеспечивает большую гибкость для вызывающих сторон.

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

Дисперсия диапазона параметров

Модификатор scoped и атрибут [UnscopedRef] (см. ниже) также влияют на параметры, что отражается на переопределении объектов, реализации интерфейса и правилах преобразования delegate. Подпись для переопределения, реализации интерфейса или преобразования delegate может быть следующей:

  • Добавление scoped в параметр ref или in
  • Добавление scoped в параметр ref struct
  • Удаление [UnscopedRef] из параметра out
  • Удалите [UnscopedRef] из параметра ref типа ref struct

Любое другое отличие по отношению к scoped или [UnscopedRef] рассматривается как несоответствие.

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

Предупреждение о несоответствии области может быть сообщено в модуле, скомпилированном по правилам безопасного контекста ref для C#7.2, где scoped недоступен. В некоторых таких случаях может потребоваться отключить предупреждение, если другая несовпадная сигнатура не может быть изменена.

Модификатор scoped и атрибут [UnscopedRef] также имеют следующие последствия для подписей методов:

  • Модификатор scoped и атрибут [UnscopedRef] не влияют на скрытие
  • Перегрузки не могут отличаться только на scoped или [UnscopedRef]

Раздел о поле ref и scoped получился длинным, поэтому хотел завершить кратким описанием предлагаемых серьёзных изменений:

  • Значение, которое имеет ref-safe-context для контекста вызывающего , может быть возвращено полем ref или ref.
  • Параметр out будет иметь безопасный контекст-члена функции.

Подробные заметки:

  • Поле ref можно объявить только внутри ref struct
  • Поле ref нельзя объявить static, volatile или const
  • Поле ref не может иметь тип ref struct
  • Процесс создания эталонной сборки должен сохранить наличие поля ref внутри ref struct
  • readonly ref struct должен объявить поля ref как readonly ref
  • Для значений by-ref модификатор scoped должен отображаться перед in, outили ref
  • Документ правил безопасности диапазона будет обновлен, как описано в этом документе.
  • Новые правила безопасного контекста ссылок вступят в силу, когда либо
    • Основная библиотека содержит флаг функции, указывающий на поддержку полей ref
    • Значение langversion равно 11 или выше

Синтаксис

Объявления локальных переменных 13.6.2: добавлено 'scoped'?.

local_variable_declaration
    : 'scoped'? local_variable_mode_modifier? local_variable_type local_variable_declarators
    ;

local_variable_mode_modifier
    : 'ref' 'readonly'?
    ;

13.9.4 Заявление for: добавлено 'scoped'?косвенно из local_variable_declaration.

13.9.5 Инструкция foreach: добавлена 'scoped'?.

foreach_statement
    : 'foreach' '(' 'scoped'? local_variable_type identifier 'in' expression ')'
      embedded_statement
    ;

списки аргументов 12.6.2: добавлены 'scoped'? для объявления переменной out.

argument_value
    : expression
    | 'in' variable_reference
    | 'ref' variable_reference
    | 'out' ('scoped'? local_variable_type)? identifier
    ;

Деконструкционные выражения 12.7:

[TBD]

15.6.2 параметры метода: добавлено 'scoped'? в parameter_modifier.

fixed_parameter
    : attributes? parameter_modifier? type identifier default_argument?
    ;

parameter_modifier
    | 'this' 'scoped'? parameter_mode_modifier?
    | 'scoped' parameter_mode_modifier?
    | parameter_mode_modifier
    ;

parameter_mode_modifier
    : 'in'
    | 'ref'
    | 'out'
    ;

объявления делегата 20.2: добавлено косвенно из .

12.19 Анонимные выражения функций: добавлено 'scoped'?.

explicit_anonymous_function_parameter
    : 'scoped'? anonymous_function_parameter_modifier? type identifier
    ;

anonymous_function_parameter_modifier
    : 'in'
    | 'ref'
    | 'out'
    ;

Ограниченные типы при закате

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

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

Для поддержки этого правила безопасного контекста ref safe будут обновлены следующим образом:

  • __makeref будет рассматриваться как метод с сигнатурой static TypedReference __makeref<T>(ref T value)
  • __refvalue будет рассматриваться как метод с сигнатурой static ref T __refvalue<T>(TypedReference tr). Выражение __refvalue(tr, int) будет эффективно использовать второй аргумент в качестве параметра типа.
  • __arglist в качестве параметра будет иметь ref-safe-context и safe-context функции-члена .
  • __arglist(...) как выражение будет иметь ref-safe-контекст и безопасный контекст функции-участника .

Соответствующие среды выполнения гарантируют, что TypedReference, RuntimeArgumentHandle и ArgIterator определяются как ref struct. Дополнительные TypedReference должны рассматриваться как наличие поля ref в ref struct для любого возможного типа (он может хранить любое значение). Это в сочетании с приведенными выше правилами гарантирует, что ссылки на стек не выходят за рамки их существования.

Примечание. Строго говоря, это сведения о реализации компилятора и часть языка. Но учитывая связь с ref полями, они включены в языковое предложение для простоты.

Укажите без области действия

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

struct S
{
    int _field;

    // Error: this, and hence _field, can't return by ref
    public ref int Prop => ref _field;
}

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

Чтобы устранить эту проблему, язык предоставит противоположность аннотации времени жизни scoped, поддерживая UnscopedRefAttribute. Это можно применить к любой ref, и это изменит ref-safe-context на один уровень шире, чем по умолчанию. Например:

UnscopedRef, примененный к Исходные ref-safe-context Новые ref-safe-context
Член экземпляра элемент функциональный член Только возврат
параметр in / ref Только возврат вызывающий контекст
параметр out элемент функциональный член Только возврат

При применении [UnscopedRef] к методу экземпляра struct это влияет на изменение неявного параметра this. Это означает, что this выступает в качестве неаннотированного ref такого же типа.

struct S
{
    int field; 

    // Error: `field` has the ref-safe-context of `this` which is *function-member* because 
    // it is a `scoped ref`
    ref int Prop1 => ref field;

    // Okay: `field` has the ref-safe-context of `this` which is *caller-context* because 
    // it is a `ref`
    [UnscopedRef] ref int Prop1 => ref field;
}

Аннотацию также можно поместить в параметры out, чтобы восстановить их поведение как в C# 10.

ref int SneakyOut([UnscopedRef] out int i)
{
    i = 42;
    return ref i;
}

В целях правил безопасного контекста ссылок такой [UnscopedRef] out считается просто ref. Аналогично тому, как in считается ref для целей жизненного цикла.

Аннотация [UnscopedRef] будет запрещена для членов и конструкторов init внутри struct. Эти члены имеют особое значение для семантики ref, так как они рассматривают членов readonly как изменяемых. Это означает, что ref к этим членам представляется как простой ref, а не ref readonly. Это допускается в пределах границ конструкторов и init. Разрешение [UnscopedRef] позволит такому ref неправильно выйти за пределы конструктора и позволит мутацию после применения семантики readonly.

Тип атрибута будет иметь следующее определение:

namespace System.Diagnostics.CodeAnalysis
{
    [AttributeUsage(
        AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Parameter,
        AllowMultiple = false,
        Inherited = false)]
    public sealed class UnscopedRefAttribute : Attribute
    {
    }
}

Подробные заметки:

  • Метод экземпляра или свойство, аннотированное [UnscopedRef], имеет ref-safe-context от this, установленный для caller-context .
  • Элемент, аннотированный с [UnscopedRef] не может реализовать интерфейс.
  • Использование [UnscopedRef] является ошибкой
    • Член, не заявленный в struct
    • Участник static, init или конструктор в struct
    • Параметр, помеченный scoped
    • Параметр, передаваемый по значению
    • Параметр, передаваемый по ссылке, которая неявно ограничена

ScopedRefAttribute

Заметки scoped будут добавляться в метаданные через атрибут типа System.Runtime.CompilerServices.ScopedRefAttribute. Атрибут будет соответствовать имени, соответствующего пространству имен, поэтому определение не должно отображаться в какой-либо конкретной сборке.

Тип ScopedRefAttribute предназначен только для использования компилятором— он не разрешен в источнике. Объявление типа синтезируется компилятором, если он еще не включен в компиляцию.

Тип будет иметь следующее определение:

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
    internal sealed class ScopedRefAttribute : Attribute
    {
    }
}

Компилятор добавляет этот атрибут к параметру с синтаксисом scoped. Это будет возникать только в том случае, если синтаксис приводит к отличию значения от его состояния по умолчанию. Например, scoped out приведет к тому, что атрибут не будет выдан.

RefSafetyRulesAttribute

Существует несколько различий в правилах безопасного контекста для ссылок между C#7.2 и C#11. Любые из этих различий могут привести к критическим изменениям при перекомпиляции с C#11 против ссылок, скомпилированных с помощью C#10 или более ранних версий.

  1. Неограниченные параметры ref/in/out могут уйти из вызова метода как поле ref в ref struct в C#11, а не в C#7.2.
  2. out параметры неявно ограничены в C#11 и не ограничены в C#7.2.
  3. параметры ref/in для типов ref struct неявно ограничены областью видимости в C#11 и без ограничения области видимости в C#7.2.

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

Для этого компилятор сгенерирует новый атрибут [module: RefSafetyRules(11)] при компиляции модуля с -langversion:11 или более поздней версии, либо при компиляции с корлибом, содержащим флаг функции для полей ref.

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

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

Если компилятор загружает модуль, включающий [module: RefSafetyRules(version)]с version, отличным от 11, компилятор выдаст предупреждение о нераспознанной версии, если в этом модуле есть вызовы объявленных методов.

Когда компилятор C#11 анализирует вызов метода:

  • Если модуль, содержащий объявление метода, включает [module: RefSafetyRules(version)]независимо от version, вызов метода анализируется с помощью правил C#11.
  • Если модуль, содержащий объявление метода, взят из исходного кода и компилируется с -langversion:11 или с corlib, содержащей флаг функции для полей ref, вызов метода аналитически оценивается по правилам C#11.
  • Если модуль, содержащий объявление метода, ссылается на System.Runtime { ver: 7.0 }, вызов метода анализируется с помощью правил C#11. Это правило представляет собой временную меру смягчения для модулей, скомпилированных с более ранними версиями C#11 / .NET 7, и будет удалено в будущем.
  • В противном случае вызов метода анализируется с помощью правил C#7.2.

Компилятор до C#11 будет игнорировать все RefSafetyRulesAttribute и анализировать вызовы методов только с помощью правил C#7.2.

RefSafetyRulesAttribute будет соответствовать имени, соответствующего пространству имен, поэтому определение не должно отображаться в какой-либо конкретной сборке.

Тип RefSafetyRulesAttribute предназначен только для использования компилятором— он не разрешен в источнике. Объявление типа синтезируется компилятором, если он еще не включен в компиляцию.

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
    internal sealed class RefSafetyRulesAttribute : Attribute
    {
        public RefSafetyRulesAttribute(int version) { Version = version; }
        public readonly int Version;
    }
}

Безопасные буферы фиксированного размера

Безопасные буферы фиксированного размера не вошли в C# 11. Эта функция может быть реализована в будущей версии C#.

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

internal struct CharBuffer
{
    internal char Data[128];
}

Эти объявления, как и их аналоги с unsafe, определяют последовательность элементов N в содержащем типе. К этим элементам можно получить доступ с помощью индексатора, а также можно преобразовать в Span<T> и ReadOnlySpan<T> экземпляры.

При индексировании в буфер fixed типа T необходимо учитывать состояние readonly контейнера. Если контейнер readonly, индексатор возвращает ref readonly T, иначе возвращает ref T.

Доступ к буферу fixed без индексатора не имеет определённого типа, однако он может быть преобразован в типы Span<T>. В случае, если контейнер readonly буфер неявно преобразуется в ReadOnlySpan<T>. Кроме того, он может неявно преобразоваться в Span<T> или ReadOnlySpan<T> (преобразование Span<T> считается лучше).

Результирующий Span<T> экземпляр будет иметь длину, равную размеру, объявленному в буфере fixed. Будет, безопасный контекст возвращаемого значения равен безопасному контексту контейнера, как если бы доступ к резервным данным осуществлялся как к полю.

Для каждого объявления fixed в типе, где тип элемента T язык создаст соответствующий get только метод индексатора, тип возврата которого ref T. Индексатор будет аннотирован атрибутом [UnscopedRef], поскольку его реализация будет возвращать поля объявляющего типа. Доступность члена будет соответствовать доступности в поле fixed.

Например, подпись индексатора для CharBuffer.Data будет следующей:

[UnscopedRef] internal ref char DataIndexer(int index) => ...;

Если предоставленный индекс находится за пределами объявленных границ массива fixed, будет создан IndexOutOfRangeException. В случае предоставления константного значения он будет заменен прямой ссылкой на соответствующий элемент. Если константы не находятся вне объявленных границ, в этом случае возникнет ошибка времени компиляции.

Кроме того, для каждого буфера fixed будет создан именованный аксессор, реализующий операции по значению get и set. Это означает, что буферы fixed будут ближе к существующей семантике массивов, с использованием метода доступа ref, а также операций по значению get и set. Это означает, что компиляторы будут иметь ту же гибкость при создании кода, потребляющего fixed буферы, что и при использовании массивов. Это должно сделать такие операции, как await по сравнению с буферами fixed проще выдавать.

Это также имеет дополнительное преимущество, которое позволит упростить использование буферов fixed на других языках. Именованные индексаторы — это функция, которая существует с момента выпуска .NET версии 1.0. Даже языки, которые не могут напрямую выдавать именованный индексатор, обычно могут использовать их (C# фактически является хорошим примером этого).

Резервное хранилище для буфера будет создано с помощью атрибута [InlineArray]. Это механизм, рассмотренный в вопросе 12320, который позволяет специально объявлять последовательность полей одного типа. Этот конкретный вопрос по-прежнему находится в активном обсуждении, и ожидается, что реализация этой функции будет производиться в зависимости от того, как пройдет обсуждение.

Инициализаторы со значениями ref в выражениях new и with

В разделе 12.8.17.3 инициализаторов объектовмы обновим грамматику следующим образом:

initializer_value
    : 'ref' expression // added
    | expression
    | object_or_collection_initializer
    ;

В разделе with выражениямы обновим грамматику следующим образом:

member_initializer
    : identifier '=' 'ref' expression // added
    | identifier '=' expression
    ;

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

Мы добавим аналогичное правило для ref local reassignment:
Если левый операнд — это записываемая ссылка (т. е. она обозначает что-либо, отличное от поля ref readonly), то правый операнд должен быть lvalue с возможностью записи.

Правила экранирования для вызовов конструктора остаются прежними.

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

А именно правила вызова метода обновлены выше:

Значение rvalue, полученное из вызова метода, e1.M(e2, ...) использует безопасный контекст из наименьшего из следующих контекстов:

  1. вызывающий контекст
  2. Безопасный контекст , создаваемый всеми выражениями аргументов
  3. Если возврат является ref struct, ref-safe-context, внесенных всеми аргументами ref

Для выражения new с инициализаторами выражения инициализатора считаются аргументами (они вносят свой вклад в безопасный контекст) и выражения инициализатора ref считаются аргументами ref (они вносят свой вклад в ref-safe-context), рекурсивно.

Изменения в небезопасном контексте

Типы указателей (раздела 23.3) расширены, чтобы разрешить управляемые типы в качестве ссылочных типов. Такие типы указателей записываются как управляемый тип, за которым следует маркер *. Они выносят предупреждение.

Адрес оператора (раздела 23.6.5) расслаблен для принятия переменной с управляемым типом в качестве операнда.

Оператор fixed (раздела 23.7) расслаблен для принятия fixed_pointer_initializer, который является адресом переменной управляемого типа T или выражением array_type с элементами управляемого типа T.

Инициализатор выделения стека (раздела 12.8.22) аналогично расслаблен.

Соображения

При оценке этой функции следует учитывать другие части стека разработки.

Рекомендации по совместимости

Вызов в этом предложении заключается в том, что эта конструкция влияет на совместимость с нашими существующими правилами безопасности или §9.7.2. Хотя эти правила полностью поддерживают концепцию ref struct с полями ref, они не позволяют API, за исключением stackalloc, улавливать ref состояние, которое ссылается на стек. Правила безопасного контекста ссылок имеют жесткое предположениеили §16.4.12.8, что конструктор формы Span(ref T value) не существует. Это означает, что правила безопасности не учитывают параметр ref, который может выступать в роли поля ref, что позволяет использовать следующий код.

Span<int> CreateSpanOfInt()
{
    // This is legal according to the 7.2 span rules because they do not account
    // for a constructor in the form Span(ref T value) existing. 
    int local = 42;
    return new Span<int>(ref local);
}

Фактически существует три способа для ref параметра, чтобы избежать вызова метода:

  1. По значению возврата
  2. Возврат по ref
  3. По полю ref в ref struct, возвращаемом или передаваемом в качестве параметра ref / out

Существующие правила учитываются только для (1) и (2). Они не учитывают (3), поэтому пробелы, такие как возврат локальных переменных в виде полей ref, не учитываются. Эта конструкция должна изменить правила, чтобы учитывать (3). Это может оказать небольшое влияние на совместимость существующих API. В частности, это влияет на API, имеющие следующие свойства.

  • Укажите ref struct в подписи
    • Где ref struct является возвращаемым типом, ref или параметром out
    • Имеет дополнительный параметр in или ref, за исключением приемника

В C# 10 вызывающих таких API никогда не приходилось учитывать, что входные данные состояния ref в API можно записать как поле ref. Это позволило существовать нескольким шаблонам, которые безопасны в C# 10, но станут небезопасными в C# 11 из-за возможности того, что состояние ref может выйти наружу как поле ref. Например:

Span<int> CreateSpan(ref int parameter)
{
    // The implementation of this method is irrelevant when considering the lifetime of the 
    // returned Span<T>. The ref safe context rules only look at the method signature, not the 
    // implementation. In C# 10 ref fields didn't exist hence there was no way for `parameter`
    // to escape by ref in this method
}

Span<int> BadUseExamples(int parameter)
{
    // Legal in C# 10 but would be illegal with ref fields
    return CreateSpan(ref parameter);

    // Legal in C# 10 but would be illegal with ref fields
    int local = 42;
    return CreateSpan(ref local);

    // Legal in C# 10 but would be illegal with ref fields
    Span<int> span = stackalloc int[42];
    return CreateSpan(ref span[0]);
}

Ожидается, что влияние этого разрыва совместимости будет очень небольшим. Затронутая форма API имеет мало смысла в отсутствие полей ref, поэтому вряд ли клиенты создали многие из них. Эксперименты, запускающие инструменты для обнаружения этой структуры API в существующих репозиториях, подтверждают это утверждение. Единственный репозиторий с каким-либо существенным количеством этой формы — dotnet/runtime, и это связано с тем, что он может создавать поля ref с помощью встроенного типа ByReference<T>.

Несмотря на это, дизайн должен учитывать существование таких API, поскольку они выражают допустимую, хотя и редкую, схему. Поэтому дизайн должен предоставить разработчикам инструменты для восстановления существующих правил времени жизни при обновлении до C# 10. В частности, он должен предоставлять механизмы, позволяющие разработчикам аннотировать параметры ref, которые не могут выходить за пределы поля ref или ref. Это позволяет клиентам определять API-интерфейсы в C# 11 с теми же правилами вызова, что и в C# 10.

Эталонные сборки

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

Представление полей ref имеет больше нюансов. В идеале поле ref будет отображаться в эталонной сборке, как и любое другое поле. Однако поле ref представляет изменение формата метаданных и может вызвать проблемы с цепочками инструментов, которые не обновляются для понимания этого изменения метаданных. Конкретный пример — C++/CLI, который, скорее всего, выдаст ошибку при обработке поля ref. Поэтому это будет полезно, если поля ref могут быть исключены из ссылочных сборок в наших основных библиотеках.

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

  • ref struct с полем ref никогда не считается unmanaged
  • Тип поля ref влияет на бесконечные универсальные правила расширения. Поэтому, если тип поля ref содержит параметр типа, который необходимо сохранить.

Учитывая эти правила, ниже приведено допустимое преобразование ссылочной сборки для ref struct:

// Impl assembly 
ref struct S<T>
{
    ref T _field;
}

// Ref assembly 
ref struct S<T>
{
    object _o; // force managed 
    T _f; // maintain generic expansion protections
}

Аннотации

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

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

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

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

Существует несколько ниже предопределённых сроков действия для удобства и краткости:

  • $heap: это срок существования любого значения, существующего в куче. Он доступен во всех контекстах и сигнатурах метода.
  • $local: это время существования любого значения, существующего в стеке методов. Это фактически заполнитель имени для члена-функции. Он неявно определен в методах и может отображаться в сигнатурах методов, за исключением любой выходной позиции.
  • $ro: заполнитель имени для возвращает только
  • $cm: заполнителя имени для контекста вызывающего

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

  • where $heap : $a для всех периодов существования $a
  • where $cm : $ro
  • where $x : $local для всех предопределенных сроков службы. Время жизни, заданное пользователем, не имеет связи с локальными элементами, если не определено явно.

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

// $this is covariant
// $a is invariant
ref struct S<out $this, $a> 

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

  • Он должен быть первым параметром времени жизни.
  • Он должен быть ковариантным: out $this.
  • Время существования полей ref должно быть преобразовано в $this
  • Время существования $this всех полей, отличных от ссылок, должно быть $heap или $this.

Время существования ссылки выражается путем предоставления аргумента времени существования для ссылки. Например, ref, ссылающийся на кучу, выражается как ref<$heap>.

При определении конструктора в модели имя new будет использоваться для метода. Необходимо иметь список параметров для возвращаемого значения, а также аргументов конструктора. Это необходимо для выражения связи между входными данными конструктора и созданным значением. Вместо Span<$a><$ro> модель будет использовать Span<$a> new<$ro>. Тип this в конструкторе, включая время существования, будет определенным возвращаемым значением.

Основные правила для времени существования определяются следующим образом:

  • Все параметры времени жизни синтаксически выражаются как универсальные аргументы, предшествующие аргументам типа. Это верно для предопределенных сроков службы, кроме $heap и $local.
  • Все типы T, которые не являются ref struct, неявно имеют время существования T<$heap>. Это неявно, нет необходимости писать int<$heap> в каждом примере.
  • Для поля ref, определенного как ref<$l0> T<$l1, $l2, ... $ln>:
    • Все периоды от $l1 до $ln должны быть инвариантными.
    • Время существования $l0 должно быть совместимо с $this
  • Для ref, определенной как ref<$a> T<$b, ...>, необходимо преобразовать $b в $a
  • ref переменной имеет время существования, определенное следующим образом:
    • Для ref локального, параметра или возврата типа ref<$a> T время существования $a
    • $heap для всех ссылочных типов и полей ссылочных типов
    • $local для всего остального
  • Назначение или возврат законны, если преобразование базового типа законно
  • Время существования выражений можно сделать явным с помощью приведения заметок:
    • (T<$a> expr) время существования значения явно $a для T<...>
    • ref<$a> (T<$b>)expr время жизни значения $b для T<...>, а время жизни ссылки $a.

Для целей правил существования ref считается частью типа выражения для целей преобразования. Он логически представлен преобразованием ref<$a> T<...> в ref<$a, T<...>>, где $a является ковариантным и T является инвариантным.

Далее определим правила, позволяющие сопоставить синтаксис C# с базовой моделью.

Ради краткости тип, который не имеет явных параметров времени жизни, обрабатывается так, как если бы параметр out $this был определён и применён ко всем полям типа. Тип с полем ref должен определять явные параметры времени существования.

Эти правила поддерживают существующую инварианту, согласно которой T можно присвоить scoped T для всех типов. Это сопоставляется с T<$a, ...>, назначаемым T<$local, ...> для всех времен существования, которые могут быть преобразованы в $local. Кроме того, это поддерживает и другие функции, такие как возможность назначать Span<T> из кучи для тех, что находятся в стеке. Это исключает типы, в которых поля имеют различные сроки жизни для значений без ссылок, но такова реальность C# на сегодняшний день. Для этого потребуется значительное изменение правил C#, которые необходимо будет проработать.

Тип this для типа S<out $this, ...> внутри метода экземпляра неявно определяется следующим образом:

  • Для обычного метода экземпляра: ref<$local> S<$cm, ...>
  • Например, метод аннотирован с [UnscopedRef]: ref<$ro> S<$cm, ...>

Отсутствие явного параметра this вызывает использование неявных правил. Для сложных примеров и обсуждений рекомендуется писать как метод static и делать this явным параметром.

ref struct S<out $this>
{
    // Implicit this can make discussion confusing 
    void M<$ro, $cm>(ref<$ro> S<$cm> s) {  }

    // Rewrite as explicit this to simplify discussion
    static void M<$ro, $cm>(ref<$local> S<$cm> this, ref<$ro> S<$cm> s) { }
}

Синтаксис метода C# сопоставляется с моделью следующими способами:

  • ref параметры имеют время существования ссылок $ro
  • Параметры типа ref struct имеют время жизни $cm
  • Возвращаемая ссылка имеет время жизни ссылки $ro
  • Возвращаемое значение типа ref struct имеет время существования $ro
  • scoped в параметре или ref изменяет время существования ссылки на $local

Учитывая, что давайте рассмотрим простой пример, демонстрирующий модель здесь:

ref int M1(ref int i) => ...

// Maps to the following. 

ref<$ro> int Identity<$ro>(ref<$ro> int i)
{
    // okay: has ref lifetime $ro which is equal to $ro
    return ref i;

    // okay: has ref lifetime $heap which convertible $ro
    int[] array = new int[42];
    return ref array[0];

    // error: has ref lifetime $local which has no conversion to $a hence 
    // it's illegal
    int local = 42;
    return ref local;
}

Теперь рассмотрим тот же пример с помощью ref struct:

ref struct S
{
    ref int Field;

    S(ref int f)
    {
        Field = ref f;
    }
}

S M2(ref int i, S span1, scoped S span2) => ...

// Maps to 

ref struct S<out $this>
{
    // Implicitly 
    ref<$this> int Field;

    S<$ro> new<$ro>(ref<$ro> int f)
    {
        Field = ref f;
    }
}

S<$ro> M2<$ro>(
    ref<$ro> int i,
    S<$ro> span1)
    S<$local> span2)
{
    // okay: types match exactly
    return span1;

    // error: has lifetime $local which has no conversion to $ro
    return span2;

    // okay: type S<$heap> has a conversion to S<$ro> because $heap has a
    // conversion to $ro and the first lifetime parameter of S<> is covariant
    return default(S<$heap>)

    // okay: the ref lifetime of ref $i is $ro so this is just an 
    // identity conversion
    S<$ro> local = new S<$ro>(ref $i);
    return local;

    int[] array = new int[42];
    // okay: S<$heap> is convertible to S<$ro>
    return new S<$heap>(ref<$heap> array[0]);

    // okay: the parameter of the ctor is $ro ref int and the argument is $heap ref int. These 
    // are convertible.
    return new S<$ro>(ref<$heap> array[0]);

    // error: has ref lifetime $local which has no conversion to $a hence 
    // it's illegal
    int local = 42;
    return ref local;
}

Далее давайте посмотрим, как это помогает решить проблему циклического самоприсвоения:

ref struct S
{
    int field;
    ref int refField;

    static void SelfAssign(ref S s)
    {
        s.refField = ref s.field;
    }
}

// Maps to 

ref struct S<out $this>
{
    int field;
    ref<$this> int refField;

    static void SelfAssign<$ro, $cm>(ref<$ro> S<$cm> s)
    {
        // error: the types work out here to ref<$cm> int = ref<$ro> int and that is 
        // illegal as $ro has no conversion to $cm (the relationship is the other direction)
        s.refField = ref<$ro> s.field;
    }
}

Далее давайте посмотрим, как это помогает с проблемой глупых параметров захвата:

ref struct S
{
    ref int refField;

    void Use(ref int parameter)
    {
        // error: this needs to be an error else every call to this.Use(ref local) would fail 
        // because compiler would assume the `ref` was captured by ref.
        this.refField = ref parameter;
    }
}

// Maps to 

ref struct S<out $this>
{
    ref<$this> int refField;
    
    // Using static form of this method signature so the type of this is explicit. 
    static void Use<$ro, $cm>(ref<$local> S<$cm> @this, ref<$ro> int parameter)
    {
        // error: the types here are:
        //  - refField is ref<$cm> int
        //  - ref parameter is ref<$ro> int
        // That means the RHS is not convertible to the LHS ($ro is not covertible to $cm) and 
        // hence this reassignment is illegal
        @this.refField = ref<$ro> parameter;
    }
}

Открытые проблемы

Измените дизайн, чтобы избежать нарушения совместимости.

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

Дизайн, сохраняющий совместимость, был значительно сложнее этого. Чтобы сохранить поля compat ref, требуются различные сроки существования для возможности возврата по ref и возврата по полю ref. По сути, это требует от нас предоставления ref-field-safe-context отслеживания всех параметров для метода. Это нужно вычислить для всех выражений и отслеживать во всех значениях практически везде, где сегодня отслеживается ref-safe-context.

Далее это значение имеет связи с ref-safe-context. Например, нелогично, что значение может быть возвращено как поле ref, но не напрямую как ref. Это объясняется тем, что поля ref уже могут быть легко возвращены ref (состояниеref в ref struct может быть возвращено ref, даже если само содержащее значение не может быть возвращено). Таким образом, правила также нуждаются в постоянной корректировке, чтобы убедиться, что эти значения соответствуют друг с другом.

Кроме того, языку требуется синтаксис для представления ref параметров, которые можно возвращать тремя способами: с помощью ref поля, ref и по значению. Значение по умолчанию, которое можно вернуть через ref. В дальнейшем более естественное возвращение, в частности когда участвуют ref struct, ожидается через поле ref или ref. Это означает, что новые API по умолчанию требуют дополнительной аннотации синтаксиса для правильной работы. Это нежелательно.

Эти изменения совместимости влияют на методы, имеющие следующие свойства:

  • Span<T> или ref struct
    • Где ref struct является возвращаемым типом, ref или параметром out
    • Имеет дополнительный параметр in или ref (за исключением получателя)

Чтобы понять влияние, полезно разбить API на категории:

  1. Хотите, чтобы потребители рассматривали ref как поле ref. Основной пример — конструкторы Span(ref T value)
  2. Не хотите, чтобы потребители воспринимали запись ref как поле ref. Эти, однако, делятся на две категории
    1. Небезопасные API. Это API внутри типов Unsafe и MemoryMarshal, из которых MemoryMarshal.CreateSpan является наиболее выдающимся. Эти API небезопасно захватывают ref, но также известны как небезопасные API.
    2. Безопасные API. Это API-интерфейсы, которые принимают ref параметры для эффективности, но это на самом деле нигде не фиксируется. Примеры небольшие, но один — AsnDecoder.ReadEnumeratedBytes

Это изменение в первую очередь дает преимущества (1) выше. Ожидается, что они будут составлять большинство API, которые принимают ref и возвращают ref struct в дальнейшем. Изменения негативно влияют на (2.1) и (2.2), так как она нарушает существующую семантику вызова, так как правила существования изменяются.

API категории (2.1), в основном создаются корпорацией Майкрософт или разработчиками, которые больше всего выигрывают от полей типа ref (например, как Таннер). Разумно предположить, что этот класс разработчиков будет согласен на налог на совместимость при обновлении до C# 11 в виде нескольких аннотаций, чтобы сохранить существующую семантику, если взамен будут предоставлены поля ref.

API категории (2.2) являются самой большой проблемой. Неизвестно, сколько таких API существуют, и неясно, будут ли они более /менее частыми в 3-м стороннем коде. Ожидается, что их будет очень мало, особенно если мы возьмем перерыв совместимости на out. Поиски до сих пор показали очень небольшое число этих объектов, существующих на области поверхности public. Это сложный шаблон для поиска, однако он требует семантического анализа. Перед внесением этого изменения потребуется инструментальный подход для проверки предположений относительно влияния этого на ограниченное число известных случаев.

Для обоих случаев в категории (2) исправление несложное. Параметры ref, которые не должны считаться доступными для захвата, необходимо добавить scoped в ref. В (2.1) это, скорее всего, также заставит разработчика использовать Unsafe или MemoryMarshal, но это ожидается для небезопасных API стилей.

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

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

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

Ключевые слова и атрибуты

Этот проект предусматривает использование атрибутов для аннотирования новых правил срока действия. Это также можно было сделать так же легко с контекстными ключевыми словами. Например, [DoesNotEscape] может сопоставляться с scoped. Однако ключевые слова, даже контекстные, обычно должны соответствовать очень высоким стандартам для включения. Они занимают ценные языковые пространства и являются более заметной частью языка. Эта функция, хотя и ценная, будет служить меньшинству разработчиков C#.

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

  1. Заметки будут влиять на семантику программы. Обладание атрибутами, которые влияют на семантику программы, является границей, которую C# неохотно пересекает, и неясно, является ли эта особенность той самой причиной, чтобы язык сделал этот шаг.
  2. Разработчики, которые с наибольшей вероятностью будут использовать эту функцию, значительно пересекаются с набором разработчиков, использующих указатели функций. Эта функция, хотя и используется меньшинством разработчиков, действительно обосновала новый синтаксис, и это решение по-прежнему считается обоснованным.

Вместе это означает, что синтаксис должен рассматриваться.

Грубый эскиз синтаксиса будет следующим:

  • [RefDoesNotEscape] сопоставляется с scoped ref
  • [DoesNotEscape] сопоставляется с scoped
  • [RefDoesEscape] сопоставляется с unscoped

решение используйте синтаксис для scoped и scoped ref; используйте атрибут для unscoped.

Разрешить фиксированные локальные буферы

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

class FixedBufferLocals
{
    void Example()
    {
        Span<int> span = stackalloc int[42];
        int buffer[42];
    }
}

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

Пример того, где это будет полезно: https://github.com/dotnet/runtime/pull/34149

решение отложить это на потом

Использовать modreqs или нет?

Необходимо принять решение о том, должны ли методы, помеченные новыми атрибутами времени существования, переводиться в modreq при выводе. Эффективно существовало бы сопоставление 1:1 между заметками и modreq, если бы этот подход был принят.

Обоснование для добавления modreq заключается в том, что атрибуты изменяют семантику правил безопасного контекста ссылок. Только языки, которые понимают эту семантику, должны вызывать указанные методы. Далее при применении к сценариям OHI время существования становится контрактом, который должны реализовывать все производные методы. Наличие аннотаций без modreq может привести к ситуациям, когда загружается цепочка методов virtual с конфликтующими аннотациями времени жизни (это может произойти, если компилируется только одна часть цепочки virtual, а другая — нет).

Начальная работа по безопасному контексту ссылок не использовала modreq, а вместо этого полагалась на языки и фреймворк для понимания. В то же время все элементы, которые способствуют правилам безопасного контекста ссылок, являются сильной частью сигнатуры метода: ref, in, ref structи т. д. Поэтому любое изменение существующих правил метода уже приводит к двоичному изменению сигнатуры. Чтобы обеспечить новое влияние аннотаций времени существования, им потребуется контроль modreq.

Озабоченность заключается в том, является ли это чрезмерностью. Это действительно имеет негативное влияние в том плане, что сделать сигнатуры более гибкими, например, добавив [DoesNotEscape] к параметру, приведет к изменению двоичной совместимости. Такой компромисс означает, что с течением времени такие платформы, как BCL, скорее всего, не смогут смягчить такие ограничения. Это можно несколько смягчить, используя подход, который язык применяет с параметрами in, и применять modreq только в виртуальных позициях.

решение не использовать modreq в метаданных. Разница между out и ref заключается не в modreq, а в том, что теперь у них разные значения безопасного контекста ссылок. Нет никакого реального преимущества в лишь частичном применении правил с modreq здесь.

Разрешить многомерные фиксированные буферы

Следует ли расширить дизайн буферов fixed для включения многомерных массивов стилей? По сути, это позволяет делать объявления, как в следующем примере.

struct Dimensions
{
    int array[42, 13];
}

Решение Пока не разрешать

Нарушение области действия

Репозиторий среды выполнения содержит несколько недоступных API, которые записывают параметры ref в виде полей ref. Это небезопасно, так как время существования результирующего значения не отслеживается. Например, конструктор Span<T>(ref T value, int length).

Большинство из этих API, скорее всего, выберет надлежащее отслеживание жизненного цикла при возврате, что будет просто достигнуто обновлением до C# 11. Хотя некоторые из них хотят сохранить текущую семантику не отслеживания возвращаемого значения, так как их все намерение является небезопасным. Наиболее заметными примерами являются MemoryMarshal.CreateSpan и MemoryMarshal.CreateReadOnlySpan. Это будет достигнуто путем маркировки параметров как scoped.

Это означает, что среда выполнения требует установленного шаблона для небезопасного удаления scoped из параметра:

  1. Unsafe.AsRef<T>(in T value) может расширить свою существующую цель, превратившись в scoped in T value. Это позволит удалить in и scoped из параметров. Затем это становится универсальным методом "удаления безопасности ссылок"
  2. Представьте новый метод, цель которого состоит в удалении scoped: ref T Unsafe.AsUnscoped<T>(scoped in T value). Это удаляет in также, потому что если бы не удаляло, вызывающим функциям все равно потребовалось бы сочетание вызовов методов для "удаления безопасности ссылок", в таком случае существующее решение, скорее всего, будет достаточным.

Снятие ограничения по умолчанию?

Дизайн имеет только два местоположения, которые установлены по умолчанию как scoped.

  • this является scoped ref
  • out является scoped ref

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

Решение о this нежелательно, поскольку это означает, что struct не может вернуть поле через ref. Это важный сценарий для высокопроизводительных разработчиков, и атрибут [UnscopedRef] был добавлен именно по этой причине.

Ключевые слова имеют высокую планку, и добавление их для одного сценария вызывает сомнения. С учетом этого возник вопрос, можно ли вообще избежать использования этого ключевого слова, сделав this по умолчанию просто ref, а не scoped ref. Все участники, которым нужно, чтобы this было scoped ref, могут сделать это, пометив метод scoped (так как метод можно пометить readonly для создания readonly ref сегодня).

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

На ref struct это изменение вносит значительно больше проблем с совместимостью. Рассмотрим следующее:

ref struct Sneaky
{
    int Field;
    ref int RefField;

    public void SelfAssign()
    {
        // This pattern of ref reassign to fields on this inside instance methods would now
        // completely legal.
        RefField = ref Field;
    }

    static Sneaky UseExample()
    {
        Sneaky local = default;

        // Error: this is illegal, and must be illegal, by our existing rules as the 
        // ref-safe-context of local is now an input into method arguments must match. 
        local.SelfAssign();

        // This would be dangerous as local now has a dangerous `ref` but the above 
        // prevents us from getting here.
        return local;
    }
}

По сути, это означает, что все вызовы методов экземпляра для изменяемыхref struct локальных переменных будут недопустимыми, если только локальная переменная не была отмечена как scoped. Правила должны учитывать случай, когда поля были переназначированы другим полям в this. У readonly ref struct нет этой проблемы, так как readonly особенность предотвращает переприсвоение ссылки. Тем не менее, это будет значительное изменение, приводящее к обратной несовместимости, так как оно затронет практически все существующие изменяемые ref struct.

Хотя readonly ref struct по-прежнему проблематично, когда мы расширяемся до добавления ref полей к ref struct. Это позволяет решить одну и ту же основную проблему, просто переместив запись в значение поля ref:

readonly ref struct ReadOnlySneaky
{
    readonly int Field;
    readonly ref ReadOnlySpan<int> Span;

    public void SelfAssign()
    {
        // Instance method captures a ref to itself
        Span = new ReadOnlySpan<int>(ref Field, 1);
    }
}

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

  • this как ref: struct, readonly ref struct или readonly member
  • this как scoped ref: ref struct или readonly ref struct с полем ref к ref struct

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

решение сохранить this как scoped ref. Это означает, что приведённые коварные примеры вызывают ошибки компилятора.

Поля ref на структуру ref

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

Первым является то, что readonly ref теперь может хранить состояние ref. Например:

readonly ref struct Container
{
    readonly ref Span<int> Span;

    void Store(Span<int> span)
    {
        Span = span;
    }
}

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

Во-вторых, язык должен рассматривать новый тип безопасного контекста: ref-field-safe-context. Все ref struct, которые транзитивно содержат поле ref, имеют дополнительную область выхода, представляющую значение(я) в поле(ях) ref. В случае нескольких полей ref их можно совместно отслеживать как одно значение. Значение по умолчанию для этого для параметров — вызывающим контекстом.

ref struct Nested
{
    ref Span<int> Span;
}

Span<int> M(ref Nested nested) => nested.Span;

Это значение не связано с безопасного контекста контейнера; Так как контекст контейнера меньше, он не влияет на ref-field-safe-context значений поля ref. Далее ref-field-safe-context никогда не может быть меньше, чем безопасного контекста контейнера.

ref struct Nested
{
    ref Span<int> Span;
}

void M(ref Nested nested)
{
    scoped ref Nested refLocal = ref nested;

    // the ref-field-safe-context of local is still *caller-context* which means the following
    // is illegal
    refLocal.Span = stackalloc int[42];

    scoped Nested valLocal = nested;

    // the ref-field-safe-context of local is still *caller-context* which means the following
    // is still illegal
    valLocal.Span = stackalloc int[42];
}

Это ref-field-safe-context по существу всегда существовало. До сих пор ref поля могли указывать только на обычные struct, следовательно, они тривиально сворачивались в контекст вызова . Для поддержки полей ref и ref struct необходимо обновить существующие правила с учетом нового контекста безопасности ссылок .

В-третьих, необходимо обновить правила переназначения ref, чтобы убедиться, что мы не нарушаем ref-field-context для значений. По сути, для , где тип является , ref-field-safe-context должен равняться .

Эти проблемы очень решаемые. Команда компилятора набросала несколько версий этих правил, и они в значительной степени выходят из нашего существующего анализа. Проблема заключается в отсутствии кода для таких правил, которые помогают доказать правильность и удобство использования. Это делает нас очень нерешительными в добавлении поддержки из-за страха, что мы выберем неправильные значения по умолчанию и загонит среду выполнения в угол с точки зрения удобства использования, когда возникнет такая необходимость. Эта озабоченность особенно сильна, поскольку .NET 8, скорее всего, ведёт нас в этом направлении с allow T: ref struct и Span<Span<T>>. Правила лучше написать, если это сделано в сочетании с кодом потребления.

Решение Задержка, позволяющая ref полю ref struct до .NET 8, где у нас есть сценарии, которые помогут сформулировать правила для этих сценариев. Это не было реализовано по состоянию на .NET 9

Что сделает C# 11.0?

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

  1. поля ref и scoped
  2. [UnscopedRef]
  3. поля ref для ref struct
  4. Ограниченные типы при закате
  5. буферы фиксированного размера

Что реализуется в том или ином выпуске — это лишь определение масштабов.

решение только (1) и (2) создали C# 11.0. Остальные будут рассматриваться в будущих версиях C#.

Рекомендации по будущему

Расширенные заметки о времени жизни

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

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

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

Каждому scoped можно назначить именованный срок службы, добавив обобщенный аргумент стиля в синтаксис. Например, scoped<'a> — это величина с периодом существования 'a. Затем можно использовать такие ограничения, как where, чтобы описать отношения между этими периодами существования.

void M(scoped<'a> ref MyStruct s, scoped<'b> Span<int> span)
  where 'b >= 'a
{
    s.Span = span;
}

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

Вопросы

Следующие вопросы связаны с этим предложением:

Предложения

Следующие предложения связаны с этим предложением:

Существующие примеры

Utf8JsonReader

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

Utf8JsonWriter

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

Веселые примеры

ReadOnlySpan<T>

public readonly ref struct ReadOnlySpan<T>
{
    readonly ref readonly T _value;
    readonly int _length;

    public ReadOnlySpan(in T value)
    {
        _value = ref value;
        _length = 1;
    }
}

Список экономии

struct FrugalList<T>
{
    private T _item0;
    private T _item1;
    private T _item2;

    public int Count = 3;

    public FrugalList(){}

    public ref T this[int index]
    {
        [UnscopedRef] get
        {
            switch (index)
            {
                case 0: return ref _item0;
                case 1: return ref _item1;
                case 2: return ref _item2;
                default: throw null;
            }
        }
    }
}

Примеры и заметки

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

Переназначение ссылок и точки вызова

Демонстрация того, как переназначение и вызов метода работают вместе.

ref struct RS
{
    ref int _refField;

    public ref int Prop => ref _refField;

    public RS(int[] array)
    {
        _refField = ref array[0];
    }

    public RS(ref int i)
    {
        _refField = ref i;
    }

    public RS CreateRS() => ...;

    public ref int M1(RS rs)
    {
        // The call site arguments for Prop contribute here:
        //   - `rs` contributes no ref-safe-context as the corresponding parameter, 
        //      which is `this`, is `scoped ref`
        //   - `rs` contribute safe-context of *caller-context*
        // 
        // This is an lvalue invocation and the arguments contribute only safe-context 
        // values of *caller-context*. That means `local1` has ref-safe-context of 
        // *caller-context*
        ref int local1 = ref rs.Prop;

        // Okay: this is legal because `local` has ref-safe-context of *caller-context*
        return ref local1;

        // The arguments contribute here:
        //   - `this` contributes no ref-safe-context as the corresponding parameter
        //     is `scoped ref`
        //   - `this` contributes safe-context of *caller-context*
        //
        // This is an rvalue invocation and following those rules the safe-context of 
        // `local2` will be *caller-context*
        RS local2 = CreateRS();

        // Okay: this follows the same analysis as `ref rs.Prop` above
        return ref local2.Prop;

        // The arguments contribute here:
        //   - `local3` contributes ref-safe-context of *function-member*
        //   - `local3` contributes safe-context of *caller-context*
        // 
        // This is an rvalue invocation which returns a `ref struct` and following those 
        // rules the safe-context of `local4` will be *function-member*
        int local3 = 42;
        var local4 = new RS(ref local3);

        // Error: 
        // The arguments contribute here:
        //   - `local4` contributes no ref-safe-context as the corresponding parameter
        //     is `scoped ref`
        //   - `local4` contributes safe-context of *function-member*
        // 
        // This is an lvalue invocation and following those rules the ref-safe-context 
        // of the return is *function-member*
        return ref local4.Prop;
    }
}

Переназначение ссылок и небезопасное экранирование

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

e1 должен иметь тот же безопасный контекст, что и e2

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

void Example(ref Span<int> p)
{
    Span<int> local = stackalloc int[42];
    ref Span<int> refLocal = ref local;

    // Error:
    // The safe-context of refLocal is narrower than p. For a non-ref reassignment 
    // this would be allowed as its safe to assign wider lifetimes to narrower ones.
    // In the case of ref reassignment though this rule prevents it as the 
    // safe-context values are different.
    refLocal = ref p;

    // If it were allowed this would be legal as the safe-context of refLocal
    // is *caller-context* and that is satisfied by stackalloc. At the same time
    // it would be assigning through p and escaping the stackalloc to the calling
    // method
    // 
    // This is equivalent of saying p = stackalloc int[13]!!! 
    refLocal = stackalloc int[13];
}

В случае ref, который не является ref struct, это правило тривиально удовлетворено, так как все значения имеют одинаковый безопасный контекст . Это правило действительно вступает только в игру, когда значение является ref struct.

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

локальные переменные с областью видимости

Использование scoped на локальных переменных особенно полезно для шаблонов кода, которые условно назначают значения локальным переменным в разных безопасных контекстах . Это означает, что код больше не должен полагаться на трюки инициализации, такие как = stackalloc byte[0] для определения локального безопасного контекста, но теперь может просто использовать scoped.

// Old way 
// Span<byte> span = stackalloc byte[0];
// New way 
scoped Span<byte> span;
int len = ...;
if (len < MaxStackLen)
{
    span = stackalloc byte[len];
}
else
{
    span = new byte[len];
}

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

Значения параметров, ограниченные областью

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

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

Это правило существует для предотвращения таких сценариев:

ref struct RS
{
    Span<int> _field;
    void Set(Span<int> p)
    {
        _field = p;
    }

    static void DangerousCode(ref RS p)
    {
        Span<int> span = stackalloc int[] { 42 };

        // Error: if allowed this would let the method return a reference to 
        // the stack
        p.Set(span);
    }
}

По сути, это правило существует, так как язык должен предположить, что все входные данные, передаваемые методу, выходят за пределы максимально допустимого безопасного контекста. При наличии параметров ref или out, включая приемники, существует вероятность, что входные данные могут стать полями этих значений ref (как показано на примере RS.Set выше).

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

ref struct JsonReader
{
    Span<char> _buffer;
    int _position;

    internal bool TextEquals(ReadOnlySpan<char> text)
    {
        var current = _buffer.Slice(_position, text.Length);
        return current == text;
    }
}

class C
{
    static void M(ref JsonReader reader)
    {
        Span<char> span = stackalloc char[4];
        span[0] = 'd';
        span[1] = 'o';
        span[2] = 'g';

        // Error: The safe-context of `span` is function-member 
        // while `reader` is outside function-member hence this fails
        // by the above rule.
        if (reader.TextEquals(span))
        {
            ...
        }
    }
}

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

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

ref struct JsonReader
{
    Span<char> _buffer;
    int _position;

    internal bool TextEquals(scoped ReadOnlySpan<char> text)
    {
        var current = _buffer.Slice(_position, text.Length);
        return current == text;
    }
}

class C
{
    static void M(ref JsonReader reader)
    {
        Span<char> span = stackalloc char[4];
        span[0] = 'd';
        span[1] = 'o';
        span[2] = 'g';

        // Okay: the compiler never considers `span` as capturable here hence it doesn't
        // contribute to the method arguments must match rule
        if (reader.TextEquals(span))
        {
            ...
        }
    }
}

Предотвращение сложного назначения ссылок при изменении только для чтения

Когда ref помещается в поле readonly в конструкторе или в элемент init, тип ref не ref readonly. Это устоявшееся поведение, что позволяет использовать код, например, следующий:

struct S
{
    readonly int i; 

    public S(string s)
    {
        M(ref i);
    }

    static void M(ref int i) { }
}

Это создает потенциальную проблему, хотя если такая ref могла храниться в поле ref на том же типе. Это позволит напрямую изменять readonly struct внутри члена экземпляра.

readonly ref struct S
{ 
    readonly int i; 
    readonly ref int r; 
    public S()
    {
        i = 0;
        // Error: `i` has a narrower scope than `r`
        r = ref i;
    }

    public void Oops()
    {
        r++;
    }
}

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

  • ref-safe-context и безопасного контекста является вызывающего контекста. Это оба стандарта для this в элементе struct.
  • ref-safe-context . Это выходит из правил времени существования поля . В частности правило 4.

На этом этапе строка r = ref i является недопустимой согласно правилам переназначения ссылок .

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

Глупое циклическое назначение

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

ref struct S
{
    int field;
    ref int refField;

    static void SelfAssign(ref S s)
    {
        // Error: s.field can only escape the current method through a return statement
        s.refField = ref s.field;
    }
}

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

void M(ref S s)
{
    ...
}

void Usage()
{
    // safe-context to caller-context
    S local = default; 

    // Error: compiler is forced to assume the worst and concludes a self assignment
    // is possible here and must issue an error.
    M(ref local);
}

Чтобы сделать эти API пригодными для использования, компилятор гарантирует, что срок жизни ref для параметра ref меньше срока жизни всех ссылок в связанном значении параметра. Это обоснование для наличия безопасного контекста от ref до ref struct как только для возврата и для out как вызывающего контекста . Это предотвращает циклическое назначение из-за разницы в времени существования.

Обратите внимание, что способствует контекста ref-safe-context, чтобы значений вызывающий контекст и, следовательно, позволяет выполнять циклическое назначение и заставляет вирусное использование в цепочке вызовов:

S F()
{
    S local = new();
    // Error: self assignment possible inside `S.M`.
    S.M(ref local);
    return local;
}

ref struct S
{
    int field;
    ref int refField;

    public static void M([UnscopedRef] ref S s)
    {
        // Allowed: s has both safe-context and ref-safe-context of caller-context
        s.refField = ref s.field;
    }
}

Аналогичным образом [UnscopedRef] out допускает циклическое назначение, так как параметр имеет как безопасный контекст , так и безопасный для ссылок контекст , только для возврата .

Повышение [UnscopedRef] ref в контексте вызывающего полезно, когда тип является неref struct (обратите внимание, что мы хотим сохранить правила простыми, чтобы не различать между ссылками на ссылки и нессылочными структурами).

int x = 1;
F(ref x).RefField = 2;
Console.WriteLine(x); // prints 2

static S F([UnscopedRef] ref int x)
{
    S local = new();
    local.M(ref x);
    return local;
}

ref struct S
{
    public ref int RefField;

    public void M([UnscopedRef] ref int data)
    {
        RefField = ref data;
    }
}

С точки зрения расширенных аннотаций, дизайн [UnscopedRef] создает следующее:

ref struct S { }

// C# code
S Create1(ref S p)
S Create2([UnscopedRef] ref S p)

// Annotation equivalent
scoped<'b> S Create1(scoped<'a> ref scoped<'b> S)
scoped<'a> S Create2(scoped<'a> ref scoped<'b> S)
  where 'b >= 'a

Атрибут readonly не может быть применён через ссылочные поля глубоко.

Рассмотрим приведенный ниже пример кода:

ref struct S
{
    ref int Field;

    readonly void Method()
    {
        // Legal or illegal?
        Field = 42;
    }
}

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

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

readonly ref struct SpanOfOne
{
    readonly ref int Field;

    public ref int this[int index]
    {
        get
        {
            if (index != 1)
                throw new Exception();
            return ref Field;
        }
    }
}

Это означает, что мы должны выбрать поверхностную интерпретацию readonly.

Конструкторы моделирования

Один из вопросов дизайна: Как тела конструкторов моделируются для безопасности ссылок? Как, по сути, анализируется следующий конструктор?

ref struct S
{
    ref int field;

    public S(ref int f)
    {
        field = ref f;
    }
}

Существует примерно два подхода:

  1. Модель как метод static, где this является локальным, где безопасного контекста является вызывающего контекста
  2. Модель как метод static, где this является параметром out.

Далее конструктор должен соответствовать следующим инвариантным:

  1. Убедитесь, что параметры ref могут быть записаны как поля ref.
  2. Убедитесь, что ref в полях this нельзя экранировать с помощью параметров ref. Это нарушило бы сложное назначение ссылок.

Цель состоит в том, чтобы выбрать форму, которая удовлетворяет нашим инвариантным без введения каких-либо специальных правил для конструкторов. Учитывая, что наиболее подходящая модель для конструкторов предполагает рассмотрение this в качестве параметра out. возвращать только характер out позволяет нам удовлетворить все инварианты выше без какого-либо специального регистра:

public static void ctor(out S @this, ref int f)
{
    // The ref-safe-context of `ref f` is *return-only* which is also the 
    // safe-context of `this.field` hence this assignment is allowed
    @this.field = ref f;
}

Аргументы метода должны соответствовать

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

Методы могут довольно либерально возвращать состояние, переданное им в качестве параметров. По сути, любое доступное состояние, не принадлежащее области, может быть возвращено (включая возврат через ref). Это можно вернуть непосредственно с помощью инструкции return или косвенно, назначив значение ref.

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

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

Аргументы метода должны соответствовать процессу, с помощью которого компилятор утверждает эту проверку безопасности.

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

  1. Просмотрите определение метода идентифицируйте все места, где состояние может быть косвенно возвращено: a. Изменяемые параметры ref, указывающие на ref struct b. Изменяемые параметры ref с назначаемыми ref полями c. Назначаемые ref параметры или поля ref, ссылающиеся на ref struct (рекомендуется учитывать рекурсивно)
  2. Посмотрите на точку вызова a. Определите контексты, которые соответствуют расположениям, указанным выше b. Определите контексты всех входных данных метода, возвращаемых (не в соответствии с параметрами scoped)

Если любое значение в 2.b меньше 2.a, вызов метода должен быть незаконным. Рассмотрим несколько примеров, чтобы проиллюстрировать правила:

ref struct R { }

class Program
{
    static void F0(ref R a, scoped ref R b) => throw null;

    static void F1(ref R x, scoped R y)
    {
        F0(ref x, ref y);
    }
}

Рассмотрим вызов F0, который позволяет нам пройти через (1) и (2). Параметры a и b обладают потенциалом для косвенного возврата, так как оба могут быть назначены напрямую. Аргументы, которые соответствуют этим параметрам:

  • a, который сопоставляется с x, имеющим контекст в контексте вызова
  • b, который сопоставляется с y в контексте для функции-элемента

Набор входных данных, подлежащих возврату для метода

  • x с областью действияконтекста вызывающего
  • ref x с областью действияконтекста вызывающего
  • с

Значение ref y не возвращается, так как оно сопоставляется с scoped ref, поэтому он не считается входным. Но учитывая, что есть по крайней мере один вход с меньшим escape-областью (y аргументе), чем один из выходных данных (x аргумент) вызов метода является незаконным.

Ниже приведен другой вариант.

ref struct R { }

class Program
{
    static void F0(ref R a, ref int b) => throw null;

    static void F1(ref R x)
    {
        int y = 42;
        F0(ref x, ref y);
    }
}

Параметры, которые снова имеют потенциал для косвенного возврата, — это a и b, так как оба могут быть назначены напрямую. Но b можно исключить, так как он не указывает на ref struct поэтому нельзя использовать для хранения состояния ref. Таким образом, у нас есть:

  • a, который сопоставляется с x, имеющим контекст в контексте вызова

Набор возвращаемых входных данных для метода:

  • x с контекстом в контексте вызова
  • ref x с контекстом в контексте вызова
  • ref y с контекстом функции-участника

Учитывая, что существует по крайней мере один вход с меньшей областью выхода (ref y аргумент), чем один из выводов (x аргумент), вызов метода недопустим.

Это логика, которую правило, пытаясь охватить, требует, чтобы аргументы метода соответствовали. Далее рассматривается как scoped служит способом исключения входных данных из рассмотрения, так и readonly как средство исключения ref в качестве выходных данных (не может быть назначен в readonly ref и поэтому не может быть источником выходных данных). Эти особые случаи добавляют сложность в правила, но это делается для преимущества разработчика. Компилятор старается удалить все входные и выходные данные, которые, как он знает, не могут способствовать результату, чтобы предоставить разработчикам максимальную гибкость при вызове элемента. Подобно разрешению перегрузки, стоит постараться сделать наши правила более сложными, если это создаёт большую гибкость для потребителей.

Примеры выведенных безопасных контекстов выражений объявления

Связанные с Вывод безопасный контекст выражений объявлений.

ref struct RS
{
    public RS(ref int x) { } // assumed to be able to capture 'x'

    static void M0(RS input, out RS output) => output = input;

    static void M1()
    {
        var i = 0;
        var rs1 = new RS(ref i); // safe-context of 'rs1' is function-member
        M0(rs1, out var rs2); // safe-context of 'rs2' is function-member
    }

    static void M2(RS rs1)
    {
        M0(rs1, out var rs2); // safe-context of 'rs2' is function-member
    }

    static void M3(RS rs1)
    {
        M0(rs1, out scoped var rs2); // 'scoped' modifier forces safe-context of 'rs2' to the current local context (function-member or narrower).
    }
}

Обратите внимание, что локальный контекст, полученный из модификатора scoped, является самым узким, который, возможно, можно использовать для переменной, то есть любое более узкое выражение означает, что выражение относится к переменным, которые объявляются только в более узком контексте, чем выражение.