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


параметры ref readonly

Заметка

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

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

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

Проблема чемпиона: https://github.com/dotnet/csharplang/issues/6010

Сводка

Разрешить модификатор для места объявления параметров ref readonly и изменить правила для вызовов следующим образом:

Аннотация вызова параметр ref параметр ref readonly параметр in параметр out
ref Разрешенный разрешенные предупреждение Ошибка
in Ошибка разрешенные Разрешенный Ошибка
out Ошибка ошибка Ошибка Разрешенный
Нет заметки Ошибка предупреждение Разрешённый Ошибка

(Обратите внимание, что существует одно изменение существующих правил: параметр in с аннотацией точки вызова ref выводит предупреждение вместо ошибки.)

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

Тип значения параметр ref параметр ref readonly параметр in параметр out
rvalue Ошибка предупреждение Разрешено Ошибка
lvalue Разрешённый Разрешено Разрешённый Разрешённый

Если lvalue означает переменную (т. е. значение с расположением; не должно быть записываемым или назначаемым) и rvalue означает любой вид значения.

Мотивация

В C# 7.2 были введены параметры in для передачи ссылок только для чтения. in параметры позволяют использовать как lvalues, так и rvalues и могут использоваться без каких-либо аннотаций в месте вызова. Однако API, которые фиксируют или возвращают ссылки из параметров, стремятся запретить использование rvalue и принудительно применить некоторые указания на месте вызова, что захватывается ссылка. ref readonly параметры идеально подходят в таких случаях, так как они предупреждают, если используются с rvalue или без каких-либо аннотаций на месте вызова.

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

  • ref параметры, так как они были введены до того, как in стали доступными, а изменение на in вызовет несовместимость на уровне исходного кода и бинарного представления, например QueryInterface, или
  • in параметры, допускающие прием ссылок только для чтения, несмотря на то, что передача rvalues в них действительно не имеет смысла, например, ReadOnlySpan<T>..ctor(in T value)или
  • ref параметры, запрещающие использование rvalue, даже если они не изменяют переданную ссылку, например, Unsafe.IsNullRef.

Эти API могут перенестись в ref readonly параметры без нарушения работы пользователей. Дополнительные сведения о совместимости двоичных файлов см. в предлагаемой кодировке метаданных . Конкретно, изменение

  • refref readonly будет изменением, нарушающим двоичную совместимость, только для виртуальных методов.
  • refin также представляет собой бинарное изменение для виртуальных методов, но не изменение в исходном коде (так как правила изменяются таким образом, что только предупреждают о передаче аргументов ref в параметры in),
  • inref readonly не будет являться критическим изменением (но отсутствие аннотации вызова или rvalue приведет к предупреждению);
    • Обратите внимание, что это будет важным изменением для пользователей, использующих более старые версии компилятора (так как они интерпретируют ref readonly параметры как ref параметры, не разрешая in или без аннотации в точке вызова) и новые версии компилятора с LangVersion <= 11 (для согласованности с более старыми версиями компилятора, будет выдаваться ошибка, что ref readonly параметры не поддерживаются, если только соответствующие аргументы не передаются с модификатором ref).

Изменение в противоположном направлении

  • ref readonlyref может стать потенциально несовместимым изменением на уровне исходного кода (если только использовалась аннотация вызова ref и только ссылки только для чтения использовались в качестве аргументов) и вызвать несовместимость двоичного формата для виртуальных методов.
  • ref readonlyin не будет критическим изменением (но ref аннотации места вызова приведут к предупреждению).

Обратите внимание, что приведенные выше правила применяются к сигнатурам методов, но не к сигнатурам делегатов. Например, изменение ref на in в подписи делегата может быть изменением, нарушающим совместимость (если пользователь назначает метод с параметром ref для этого типа делегата, это станет ошибкой после изменения API).

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

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

Объявления параметров

Никаких изменений в грамматике не требуется. Модификатор ref readonly будет разрешен для параметров. Помимо обычных методов, ref readonly будут разрешены для параметров индексатора (например, in, но в отличие от ref), но запрещено для параметров оператора (например, ref, но в отличие от in).

Значения параметров по умолчанию будут разрешены для параметров ref readonly, но с предупреждением, так как они эквивалентны передаче rvalues. Это позволяет авторам API изменять параметры in со значениями по умолчанию на параметры ref readonly, не вводя критических изменений в исходный код.

Проверки типов значений

Обратите внимание, что хотя модификатор аргумента ref допускается для ref readonly параметров, ничего не меняется в отношении проверок типа значений, т. е.

  • ref можно использовать только с назначаемыми значениями;
  • для передачи ссылок только для чтения необходимо использовать модификатор аргумента in;
  • для передачи rvalues необходимо использовать отсутствие модификатора (что вызывает предупреждение для параметров ref readonly, как описано в сводке этого предложения).

Разрешение перегрузки

Разрешение перегрузки позволит смешивать ref/ref readonly/in/без аннотаций вызова и модификаторов параметров, как указано в таблице в сводке этого предложения, т. е. все разрешенные и предупредительные случаи будут рассматриваться как возможные кандидаты во время разрешения перегрузки. В частности, существует изменение существующего поведения, в котором методы с параметром in будут соответствовать вызовам с соответствующим аргументом, помеченным как ref, — это изменение будет включено в LangVersion.

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

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

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

Преобразования методов

Аналогичным образом, для анонимной функции [§10.7] и группы методов [§10.8] преобразования, эти модификаторы считаются совместимыми (но любое разрешенное преобразование между различными модификаторами приводит к предупреждению):

  • ref readonly параметр целевого метода может соответствовать in или ref параметру делегата.
  • in параметр целевого метода может соответствовать ref readonly или, в зависимости от версии языка (LangVersion), параметру ref делегата.
  • Примечание. ref параметр целевого метода не разрешено соответствовать in или ref readonly параметра делегата.

Например:

DIn dIn = (ref int p) => { }; // error: cannot match `ref` to `in`
DRef dRef = (in int p) => { }; // warning: mismatch between `in` and `ref`
DRR dRR = (ref int p) => { }; // error: cannot match `ref` to `ref readonly`
dRR = (in int p) => { }; // warning: mismatch between `in` and `ref readonly`
dIn = (ref readonly int p) => { }; // warning: mismatch between `ref readonly` and `in`
dRef = (ref readonly int p) => { }; // warning: mismatch between `ref readonly` and `ref`
delegate void DIn(in int p);
delegate void DRef(ref int p);
delegate void DRR(ref readonly int p);

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

Сопоставление подписей

Члены, объявленные в одном типе, не могут отличаться по сигнатуре только по критериям ref/out/in/ref readonly. В других целях сопоставления подписей (например, скрытия или переопределения) ref readonly можно заменить модификатором in, но это вызовет предупреждение в месте объявления [§7.6]. Это не применяется при сопоставлении partial объявления с его реализацией и при сопоставлении подписи перехватчика с перехватываемой подписью. Обратите внимание, что переопределение для пар модификаторов ref/in и ref readonly/ref не может быть изменено, так как подписи не совместимы с двоичными файлами. Для согласованности то же самое верно для других целей сопоставления подписей (например, скрытие).

Кодировка метаданных

Как напоминание,

  • ref параметры испускаются как обычные типы byref (T& в IL),
  • in параметры похожи на ref плюс они аннотированы с System.Runtime.CompilerServices.IsReadOnlyAttribute. В C# 7.3 и более поздних версиях они тоже создаются с [in], а если они виртуальные — с modreq(System.Runtime.InteropServices.InAttribute).

ref readonly параметры будут выдаваться как [in] T&и аннотированы следующим атрибутом:

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

Кроме того, если виртуальные, они будут испускаться с modreq(System.Runtime.InteropServices.InAttribute) для обеспечения двоичной совместимости с параметрами in. Обратите внимание, что в отличие от параметров in, [IsReadOnly] не будут использоваться для параметров ref readonly, чтобы избежать увеличения размера метаданных, а также для того, чтобы старые версии компилятора интерпретировали параметры ref readonly как параметры ref (и, следовательно, замена ref на ref readonly не будет нарушать совместимость исходного кода даже между разными версиями компилятора).

RequiresLocationAttribute будет сопоставлен с именем, квалифицированным пространством имен, и будет синтезирован компилятором, если он еще не включен в компиляцию.

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

Указатели функций

В указателях функций параметры in передаются с modreq(System.Runtime.InteropServices.InAttribute) (см. предложение о указателях функций). ref readonly параметры будут выдаваться без этого modreq, но вместо этого с modopt(System.Runtime.CompilerServices.RequiresLocationAttribute). Старые версии компилятора игнорируют modopt и поэтому интерпретируют ref readonly параметры как ref параметры (в соответствии с более старым поведением компилятора для обычных методов с параметрами ref readonly, как описано выше), и новые версии компилятора, осведомленные о modopt, будут использовать его для распознавания ref readonly параметров для выдачи предупреждений во время преобразований и вызовов. Для согласованности с более старыми версиями компилятора новые версии компилятора с LangVersion <= 11 будут сообщать об ошибках, что параметры ref readonly не поддерживаются, если только соответствующие аргументы не передаются с модификатором ref.

Обратите внимание, что это двоичный разрыв для изменения модификаторов в сигнатурах указателя функции, если они являются частью общедоступных API, поэтому это будет двоичный разрыв при изменении ref или in на ref readonly. Однако разрыв источника произойдет только для вызывающих с LangVersion <= 11 при изменении inref readonly (если указатель вызывается с модификатором вызова in), что соответствует обычным методам.

Критические изменения

ref / in несоответствие в разрешении перегрузки приводит к критическому изменению поведения, показанным в следующем примере:

class C
{
    string M(in int i) => "C";
    static void Main()
    {
        int i = 5;
        System.Console.Write(new C().M(ref i));
    }
}
static class E
{
    public static string M(this C c, ref int i) => "E";
}

В C# 11 вызов привязывается к E.M, поэтому выводится "E". В C# 12 C.M можно привязать (с предупреждением), и пространства имен расширения не ищутся, так как у нас есть подходящий вариант, поэтому "C" будет напечатан.

Существует также изменение, нарушающее совместимость источника, по той же самой причине. Пример ниже выводит на экран "1" в C# 11, но не компилируется из-за ошибки неоднозначности в C# 12.

var i = 5;
System.Console.Write(C.M(null, ref i));

interface I1 { }
interface I2 { }
static class C
{
    public static string M(I1 o, ref int x) => "1";
    public static string M(I2 o, in int x) => "2";
}

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

Альтернативы

объявления параметров

Авторы API могут аномировать in параметры, предназначенные для принятия только lvalues с пользовательским атрибутом и предоставления анализатора для флага неправильного использования. Это не позволит авторам API изменять подписи существующих API, которые решили использовать параметры ref для запрета значений rvalue. Вызывающим такие API по-прежнему потребуется проделать дополнительную работу, чтобы получить ref, если у них есть доступ только к переменной ref readonly. Изменение этих API с ref на [RequiresLocation] in будет разрушающим изменением кода (а в случае виртуальных методов — также изменением двоичной совместимости).

Вместо разрешения модификатора ref readonlyкомпилятор может распознать, когда к параметру применяется специальный атрибут (например, [RequiresLocation]). Это обсуждалось в LDM 2022-04-25, где решили, что это языковая функция, а не анализатор, и так она должна выглядеть как таковая.

Типы значений проверяют

Передача lvalues без каких-либо модификаторов в параметры ref readonly может быть разрешена без предупреждений, как в случае с неявными параметрами C++ byref. Это обсуждалось в LDM 2022-05-11, с учетом того, что основная мотивация для параметров ref readonly — это API, которые фиксируют или возвращают ссылки из этих параметров, поэтому полезно иметь какой-либо маркер.

Передача rvalue в ref readonly может быть ошибкой, а не предупреждением. Это было первоначально принято в LDM 2022-04-25, но позже обсуждения по электронной почте смягчили это, потому что мы потеряем возможность изменять существующие API без нарушения пользователей.

in может быть «естественным» модификатором вызовов для параметров ref readonly, и использование ref может привести к предупреждениям. Это позволит обеспечить согласованный стиль кода и сделать его очевидным на объекте вызовов, что ссылка будет прочитана (в отличие от ref). Первоначально это было принято в LDM 2022-04-25. Однако предупреждения могут быть препятствием для авторов API при переходе от ref к ref readonly. Кроме того, in был переопределен как ref readonly + удобные функции, поэтому это было отклонено в LDM 2022-05-11.

В ожидании проверки LDM

Ни один из следующих вариантов не реализован в C# 12. Они остаются потенциальными предложениями.

Объявления параметров

Можно разрешить обратное упорядочение модификаторов (readonly ref вместо ref readonly). Это было бы несовместимо с тем, как readonly ref возвращается, и с тем, как ведут себя поля (обратное упорядочение запрещено или соответственно означает что-то другое) и может конфликтовать с параметрами только для чтения, если они будут реализованы в будущем.

Значения параметров по умолчанию могут быть ошибкой для параметров ref readonly.

Проверка типов значений

Ошибки могут выдаваться вместо предупреждений при передаче rvalue в ref readonly параметры или при несоответствии аннотаций места вызова и модификаторов параметров. Аналогичным образом можно использовать специальные modreq вместо атрибута, чтобы гарантировать, что параметры ref readonly отличаются от параметров in на двоичном уровне. Это обеспечит более надежные гарантии, поэтому это будет хорошо для новых API, но предотвратит внедрение в существующие API среды выполнения, так как они не могут позволить себе критические изменения.

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

разрешение перегрузки

Разрешение перегрузки, переопределение и преобразование могут привести к невозможности взаимозаменяемости модификаторов ref readonly и in.

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

Вызов метода расширения с помощью приемника ref readonly может привести к предупреждению "Аргумент 1 должен передаваться с ключевыми словами ref или in", как это произойдет для вызовов без модификаторов вызова. Пользователь может исправить такое предупреждение, превратив вызов метода расширения в вызов статического метода. То же предупреждение может появляться при использовании пользовательского инициализатора коллекции или интерполированного обработчика строк с параметром ref readonly, хотя пользователь не может избежать его.

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

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

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

class Program
{
    static readonly int f = 123;
    static void Main()
    {
        var d = (in int x) => { };
        d = (ref int x) => { x = 42; }; // warning: mismatch between `ref` and `in`
        d(f); // changes value of `f` even though it is `readonly`!
        System.Console.WriteLine(f); // prints 42
    }
}

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

кодировка метаданных

Указание RequiresLocationAttribute в источнике может быть разрешено аналогично In и Out атрибутам. Кроме того, это может быть ошибка при применении в других контекстах, а не только в параметрах, аналогично атрибуту IsReadOnly, чтобы сохранить дополнительное пространство для проектирования.

Параметры указателя функции ref readonly можно создать с различными сочетаниями modopt/modreq (обратите внимание, что "исходный разрыв" в этой таблице означает для вызывающих с LangVersion <= 11):

Модификаторы Можно распознать во всех компиляциях Старые компиляторы видят их как refref readonly inref readonly
modreq(In) modopt(RequiresLocation) да in двоичный, ошибка в исходном коде нарушение двоичной последовательности
modreq(In) Нет in двоичный код, нарушение исходника Хорошо
modreq(RequiresLocation) да Неподдерживаемый двоичный, разрыв исходного кода бинарный, разрыв источника
modopt(RequiresLocation) да ref двоичный разрыв бинарный, разрыв исходного кода

Мы могли бы сгенерировать атрибуты [RequiresLocation] и [IsReadOnly] для параметров ref readonly. Тогда inref readonly не будет изменением, нарушающим совместимость, даже для старых версий компилятора, но refref readonly станет изменением, нарушающим совместимость исходников, для старых версий компилятора (так как они интерпретируют ref readonly как in, запрещая модификаторы ref) и для новых версий компиляторов с LangVersion <= 11 (для согласованности).

Мы могли бы сделать так, чтобы поведение для LangVersion <= 11 отличалось от поведения для более старых версий компилятора. Например, это может быть ошибкой всякий раз, когда вызван параметр ref readonly (даже при использовании модификатора ref в месте вызова), или может всегда допускаться без каких-либо ошибок.

Критические изменения

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

  • запретить несоответствие ref/in (это позволит предотвратить миграцию только на in для старых API, которые использовали ref, так как in еще недоступен),
  • изменение правил разрешения перегрузки для продолжения поиска лучшего соответствия (определяется правилами улучшения, указанными ниже) при наличии несоответствия типа ссылок, введенного в этом предложении,
    • или продолжить только для несоответствия между ref и in, исключая других (ref readonly против ref/in/по значению).
Правила улучшения качества

В следующем примере в настоящее время возникают три неоднозначности для трех вызовов M. Мы могли бы добавить новые правила улучшения для устранения неоднозначности. Это также позволит устранить критическое изменение источника, описанное ранее. Одним из способов было бы сделать пример, печатающий 221 (где параметр ref readonly сопоставляется с аргументом in, так как это будет предупреждением для вызова его без модификатора, в то время как для параметра in это разрешено).

interface I1 { }
interface I2 { }
class C
{
    static string M(I1 o, in int i) => "1";
    static string M(I2 o, ref readonly int i) => "2";
    static void Main()
    {
        int i = 5;
        System.Console.Write(M(null, ref i));
        System.Console.Write(M(null, in i));
        System.Console.Write(M(null, i));
    }
}

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

аргумент лучший параметр худший параметр
ref/in ref readonly in
ref ref ref readonly/in
по значению по значению/in ref readonly
in in ref

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

class C
{
    void M(I1 o, ref readonly int x) => System.Console.Write("1");
    void M(I2 o, ref int x) => System.Console.Write("2");
    void Run()
    {
        D1 m1 = this.M;
        D2 m2 = this.M; // currently ambiguous

        var i = 5;
        m1(null, in i);
        m2(null, ref i);
    }
    static void Main() => new C().Run();
}
interface I1 { }
interface I2 { }
class X : I1, I2 { }
delegate void D1(X s, ref readonly int x);
delegate void D2(X s, ref int x);

Совещания по дизайну

  • LDM 2022-04-25: функция принята
  • LDM 2022-05-09: обсуждение разделено на три части
  • LDM 2022-05-11: разрешено ref и нет аннотации для ref readonly параметров