Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Заметка
Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Включает в себя предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и создания функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.
Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия фиксируются в заседаниях по проектированию языка (LDM) , как указано в соответствующих заметках.
Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .
Проблема чемпиона: https://github.com/dotnet/csharplang/issues/7608
Сводка
Это предложение расширит возможности ref struct таким образом, чтобы они могли реализовывать интерфейсы и участвовать в качестве аргументов универсального типа.
Мотивация
Неспособность ref struct реализовать интерфейсы означает, что они не могут участвовать в довольно фундаментальных методах абстракции .NET.
Span<T>, несмотря на то, что у него есть все атрибуты последовательного списка, не могут участвовать в методах, которые принимают IReadOnlyList<T>, IEnumerable<T>и т. д. Вместо этого определенные методы должны быть закодированы для Span<T>, которые имеют практически ту же реализацию. Разрешение ref struct реализовывать интерфейсы позволит абстрагировать операции над ними так же, как и для других типов.
Подробный дизайн
Интерфейсы структуры ref
Язык позволит типам ref struct реализовывать интерфейсы. Синтаксис и правила такие же, как и для обычных типов struct, за исключением нескольких ограничений типов ref struct.
Возможность реализации интерфейсов не влияет на существующие ограничения на упаковку экземпляров ref struct. Это означает, что даже если ref struct реализует определенный интерфейс, он не может быть напрямую приведение к нему, как это представляет действие бокса.
ref struct File : IDisposable
{
private SafeHandle _handle;
public void Dispose()
{
_handle.Dispose();
}
}
File f = ...;
// Error: cannot box `ref struct` type `File`
IDisposable d = f;
Возможность реализации интерфейсов полезна только в сочетании с возможностью ref struct участвовать в обобщенных аргументах (как будет объяснено позже).
Чтобы интерфейсы охватывали полную экспрессивность ref struct и проблемы жизненного цикла, которые они могут представить, язык позволит [UnscopedRef] использоваться в методах и свойствах интерфейса. Это необходимо, так как это позволяет интерфейсам, которые абстрагируются через struct иметь ту же гибкость, что и использование struct напрямую. Рассмотрим следующий пример:
interface I1
{
[UnscopedRef]
ref int P1 { get; }
ref int P2 { get; }
}
struct S1
{
[UnscopedRef]
internal ref int P1 { get {...} }
internal ref int P2 { get {...} }
}
ref int M<T>(T t, S1 s)
where T : I1, allows ref struct
{
// Error: may return ref to t
return ref t.P1;
// Error: may return ref to t
return ref s.P1;
// Okay
return ref t.P2;
// Okay
return ref s.P2;
}
Если элемент struct / ref struct реализует элемент интерфейса с атрибутом [UnscopedRef], он также может быть украшен [UnscopedRef], но это не обязательно. Однако элемент с [UnscopedRef] не может использоваться для реализации элемента, который не имеет атрибута (подробных сведений).
interface I1
{
[UnscopedRef]
ref int P1 { get; }
ref int P2 { get; }
}
struct S1
{
internal ref int P1 { get {...} }
internal ref int P2 { get {...} }
}
struct S2
{
[UnscopedRef]
internal ref int P1 { get {...} }
internal ref int P2 { get {...} }
}
struct S3 : I1
{
internal ref int P1 { get {...} }
// Error: P2 is marked with [UnscopedRef] and cannot implement I1.P2 as is not marked
// with [UnscopedRef]
[UnscopedRef]
internal ref int P2 { get {...} }
}
class C1 : I1
{
internal ref int P1 { get {...} }
internal ref int P2 { get {...} }
}
Методы интерфейса по умолчанию создают проблему для ref struct, так как нет защиты от упаковки члена this реализацией по умолчанию.
interface I1
{
void M()
{
// Danger: both of these box if I1 is implemented by a ref struct
I1 local1 = this;
object local2 = this;
}
}
// Error: I1.M cannot implement interface member I1.M() for ref struct S
ref struct S : I1 { }
Чтобы справиться с этой задачей, ref struct придется реализовать все члены интерфейса, даже если они имеют стандартные реализации.
Среда выполнения также будет обновлена, чтобы создать исключение, если член интерфейса по умолчанию вызывается в типе ref struct.
Чтобы избежать исключения во время выполнения, компилятор сообщит об ошибке вызова невиртуального метода или свойства в параметре типа с ограничениями, допускающими ref struct. Ниже приведен пример:
public interface I1
{
sealed void M3() {}
}
class C
{
static void Test2<T>(T x) where T : I1, allows ref struct
{
#line 100
x.M3(); // (100,9): error: A non-virtual instance interface member cannot be accessed on a type parameter that allows ref struct.
}
}
Существует также открытый вопрос о создании отчета о предупреждении для вызова виртуального (не абстрактного) метода экземпляра (или свойства) для параметра типа, допускающего `ref struct`.
Подробные заметки:
-
ref structможет реализовать интерфейс -
ref structне может участвовать в элементах интерфейса по умолчанию -
ref structнельзя привести к интерфейсам, которые он реализует, так как это операция бокса
ref struct Обобщенные Параметры
type_parameter_constraints_clause
: 'where' type_parameter ':' type_parameter_constraints
;
type_parameter_constraints
: restrictive_type_parameter_constraints
| allows_type_parameter_constraints_clause
| restrictive_type_parameter_constraints ',' allows_type_parameter_constraints_clause
restrictive_type_parameter_constraints
: primary_constraint
| secondary_constraints
| constructor_constraint
| primary_constraint ',' secondary_constraints
| primary_constraint ',' constructor_constraint
| secondary_constraints ',' constructor_constraint
| primary_constraint ',' secondary_constraints ',' constructor_constraint
;
primary_constraint
: class_type
| 'class'
| 'struct'
| 'unmanaged'
;
secondary_constraints
: interface_type
| type_parameter
| secondary_constraints ',' interface_type
| secondary_constraints ',' type_parameter
;
constructor_constraint
: 'new' '(' ')'
;
allows_type_parameter_constraints_clause
: 'allows' allows_type_parameter_constraints
allows_type_parameter_constraints
: allows_type_parameter_constraint
| allows_type_parameter_constraints ',' allows_type_parameter_constraint
allows_type_parameter_constraint
: ref_struct_clause
ref_struct_clause
: 'ref' 'struct'
Язык позволяет универсальным параметрам выбирать поддержку ref struct в качестве аргументов с помощью синтаксиса allows ref struct внутри предложения where:
T Identity<T>(T p)
where T : allows ref struct
=> p;
// Okay
Span<int> local = Identity(new Span<int>(new int[10]));
Это похоже на другие элементы в предложении where, что указывает возможности универсального параметра. Разница заключается в том, что другие элементы синтаксиса ограничивают набор типов, которые могут выполнять универсальный параметр, а allows ref struct расширяет набор типов. Это фактически анти-ограничение, так как оно удаляет неявное ограничение на то, что ref struct не может удовлетворить обобщенный параметр. Таким образом, этому синтаксису присвоен новый префикс allows, чтобы сделать это более понятным.
Параметр типа, привязанный allows ref struct, имеет все поведение типа ref struct:
- Экземпляры не могут быть заключены в оболочку
- Экземпляры участвуют в правилах времени существования, таких как обычный
ref struct - Параметр типа нельзя использовать в полях
static, элементах массива и т. д. - Экземпляры можно пометить
scoped
Примеры этих правил в действии:
interface I1 { }
I1 M1<T>(T p)
where T : I1, allows ref struct
{
// Error: cannot box potential ref struct
return p;
}
T M2<T>(T p)
where T : allows ref struct
{
Span<int> span = stackalloc int[42];
// The safe-to-escape of the return is current method because one of the inputs is
// current method
T t = M3<int, T>(span);
// Error: the safe-to-escape is current method.
return t;
// Okay
return default;
return p;
}
R M3<T, R>(Span<T> span)
where R : allows ref struct
{
return default;
}
Антиограничение не наследуется из ограничения типа параметра.
Например, S в приведенном ниже коде нельзя заменить структурой ссылки.
class C<T, S>
where T : allows ref struct
where S : T
{}
Подробные заметки:
- Универсальный параметр
where T : allows ref structне может- Есть
where T : U, гдеUявляется известным ссылочным типом - Имеется ограничение
where T : class - Невозможно использовать в качестве универсального аргумента, если соответствующий параметр не является
where T: allows ref struct.
- Есть
-
allows ref structдолжно быть последним ограничением в предложенииwhere - Параметр типа,
Tкоторый имеетallows ref structимеет все те же ограничения, что и типref struct.
Представление в метаданных
Параметры типа, разрешающие использование структур ссылок, будут закодированы в метаданных, как описано в документации byref-like generics. В частности, с использованием значения флага CorGenericParamAttr.gpAllowByRefLike(0x0020) или System.Reflection.GenericParameterAttributes.AllowByRefLike(0x0020).
Поддержка этой функции средой выполнения может быть определена путем проверки наличия поля System.Runtime.CompilerServices.RuntimeFeature.ByRefLikeGenerics.
API были добавлены в https://github.com/dotnet/runtime/pull/98070.
инструкция using
Оператор using распознает и использует реализацию интерфейса IDisposable, если ресурс является структурой ref.
ref struct S2 : System.IDisposable
{
void System.IDisposable.Dispose()
{
}
}
class C
{
static void Main()
{
using (new S2())
{
} // S2.System.IDisposable.Dispose is called
}
}
Обратите внимание, что предпочтение отдается методу Dispose, реализующему шаблон, и только если он не найден, используется реализация IDisposable.
Оператор using распознает и использует реализацию интерфейса IDisposable, если ресурс является параметром типа, в наборе эффективных интерфейсов которого находятся allows ref struct и IDisposable.
class C
{
static void Test<T>(T t) where T : System.IDisposable, allows ref struct
{
using (t)
{
}
}
}
Обратите внимание, что шаблонный метод Dispose не будет распознан в параметре типа allows ref struct, поскольку интерфейс (и это единственное место, где мы могли бы искать шаблонный метод) не является структурой ссылок.
interface IMyDisposable
{
void Dispose();
}
class C
{
static void Test<T>(T t, IMyDisposable s) where T : IMyDisposable, allows ref struct
{
using (t) // Error, the pattern is not recognized
{
}
using (s) // Error, the pattern is not recognized
{
}
}
}
инструкция await using
В настоящее время язык запрещает использование ссылочных структур в качестве ресурсов в инструкции await using. Это же ограничение будет применяться к параметру типа, который allows ref struct.
Существует предложение отменить общие ограничения по использованию структур ссылок в асинхронных методах - https://github.com/dotnet/csharplang/pull/7994.
Оставшаяся часть раздела описывает поведение после того, как будет отменено общее ограничение для инструкции await using, если или когда это произойдет.
Оператор await using распознает и использует реализацию интерфейса IAsyncDisposable, если ресурс является ссылочной структурой.
ref struct S2 : IAsyncDisposable
{
ValueTask IAsyncDisposable.DisposeAsync()
{
}
}
class C
{
static async Task Main()
{
await using (new S2())
{
} // S2.IAsyncDisposable.DisposeAsync
}
}
Обратите внимание, что предпочтение отдается методу DisposeAsync, реализующему шаблон, и только если он не найден, используется реализация IAsyncDisposable.
Метод DisposeAsync будет распознаваться в параметре типа allows ref struct так же, как он распознается в параметрах типа без этого ограничения в настоящее время.
interface IMyAsyncDisposable
{
ValueTask DisposeAsync();
}
class C
{
static async Task Test<T>() where T : IMyAsyncDisposable, new(), allows ref struct
{
await using (new T())
{
} // IMyAsyncDisposable.DisposeAsync
}
}
Оператор using распознает и использует реализацию интерфейса IAsyncDisposable, если ресурс является параметром типа, который allows ref struct, процесс поиска метода шаблона DisposeAsync завершился сбоем, и IAsyncDisposable находится в наборе эффективных интерфейсов параметра типа.
interface IMyAsyncDisposable1
{
ValueTask DisposeAsync();
}
interface IMyAsyncDisposable2
{
ValueTask DisposeAsync();
}
class C
{
static async Task Test<T>() where T : IMyAsyncDisposable1, IMyAsyncDisposable2, IAsyncDisposable, new(), allows ref struct
{
await using (new T())
{
System.Console.Write(123);
} // IAsyncDisposable.DisposeAsync
}
}
инструкция foreach
Раздел https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/statements.md#1295-the-foreach-statement следует обновить соответствующим образом, чтобы включить следующие компоненты.
Оператор foreach будет распознавать и использовать реализацию интерфейса IEnumerable<T>/IEnumerable, когда коллекция является ссылочной структурой.
ref struct S : IEnumerable<int>
{
IEnumerator<int> IEnumerable<int>.GetEnumerator() {...}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {...}
}
class C
{
static void Main()
{
foreach (var i in new S()) // IEnumerable<int>.GetEnumerator
{
}
}
}
Метод GetEnumerator будет распознаваться в параметре типа allows ref struct так же, как он распознается в параметрах типа без этого ограничения в настоящее время.
interface IMyEnumerable<T>
{
IEnumerator<T> GetEnumerator();
}
class C
{
static void Test<T>(T t) where T : IMyEnumerable<int>, allows ref struct
{
foreach (var i in t) // IMyEnumerable<int>.GetEnumerator
{
}
}
}
Оператор foreach распознает и использует реализацию интерфейса IEnumerable<T>/IEnumerable, когда коллекция является параметром типа, который allows ref struct, процесс поиска шаблонного метода GetEnumerator не увенчался успехом, и IEnumerable<T>/IEnumerable находится в наборе эффективных интерфейсов параметра типа.
interface IMyEnumerable1<T>
{
IEnumerator<int> GetEnumerator();
}
interface IMyEnumerable2<T>
{
IEnumerator<int> GetEnumerator();
}
class C
{
static void Test<T>(T t) where T : IMyEnumerable1<int>, IMyEnumerable2<int>, IEnumerable<int>, allows ref struct
{
foreach (var i in t) // IEnumerable<int>.GetEnumerator
{
}
}
}
Шаблон enumerator будет распознан для параметра типа, который allows ref struct, так же как он распознается для параметров типа без этого ограничения в настоящее время.
interface IGetEnumerator<TEnumerator> where TEnumerator : allows ref struct
{
TEnumerator GetEnumerator();
}
class C
{
static void Test1<TEnumerable, TEnumerator>(TEnumerable t)
where TEnumerable : IGetEnumerator<TEnumerator>, allows ref struct
where TEnumerator : IEnumerator, IDisposable, allows ref struct
{
foreach (var i in t) // IEnumerator.MoveNext/Current
{
}
}
static void Test2<TEnumerable, TEnumerator>(TEnumerable t)
where TEnumerable : IGetEnumerator<TEnumerator>, allows ref struct
where TEnumerator : IEnumerator<int>, allows ref struct
{
foreach (var i in t) // IEnumerator<int>.MoveNext/Current
{
}
}
static void Test3<TEnumerable, TEnumerator>(TEnumerable t)
where TEnumerable : IGetEnumerator<TEnumerator>, allows ref struct
where TEnumerator : IMyEnumerator<int>, allows ref struct
{
foreach (var i in t) // IMyEnumerator<int>.MoveNext/Current
{
}
}
}
interface IMyEnumerator<T> : System.IDisposable
{
T Current {get;}
bool MoveNext();
}
Оператор foreach распознает и использует реализацию интерфейса IDisposable, когда перечислитель является структурой ссылок.
struct S1
{
public S2 GetEnumerator()
{
return new S2();
}
}
ref struct S2 : System.IDisposable
{
public int Current {...}
public bool MoveNext() {...}
void System.IDisposable.Dispose() {...}
}
class C
{
static void Main()
{
foreach (var i in new S1())
{
} // S2.System.IDisposable.Dispose()
}
}
Обратите внимание, что предпочтение отдается методу Dispose, реализующему шаблон, и только если он не найден, используется реализация IDisposable.
Оператор foreach распознает и использует реализацию интерфейса IDisposable, если перечислитель является параметром типа, а allows ref struct и IDisposable находятся в его эффективном наборе интерфейсов.
interface ICustomEnumerator
{
int Current {get;}
bool MoveNext();
}
interface IGetEnumerator<TEnumerator> where TEnumerator : allows ref struct
{
TEnumerator GetEnumerator();
}
class C
{
static void Test<TEnumerable, TEnumerator>(TEnumerable t)
where TEnumerable : IGetEnumerator<TEnumerator>
where TEnumerator : ICustomEnumerator, System.IDisposable, allows ref struct
{
foreach (var i in t)
{
} // System.IDisposable.Dispose()
}
}
Обратите внимание, что шаблонный метод Dispose не будет распознан в параметре типа allows ref struct, поскольку интерфейс (и это единственное место, где мы могли бы искать шаблонный метод) не является структурой ссылок.
Кроме того, так как среда выполнения не предоставляет способ проверить, не будет ли параметр типа, который allows ref struct реализует интерфейс IDisposable, перечислитель параметров типа, который allows ref struct будет запрещен, если IDisposable не входит в его эффективный набор интерфейсов.
interface ICustomEnumerator
{
int Current {get;}
bool MoveNext();
}
interface IMyDisposable
{
void Dispose();
}
interface IGetEnumerator<TEnumerator> where TEnumerator : allows ref struct
{
TEnumerator GetEnumerator();
}
class C
{
static void Test<TEnumerable, TEnumerator>(TEnumerable t)
where TEnumerable : IGetEnumerator<TEnumerator>
where TEnumerator : ICustomEnumerator, IMyDisposable, allows ref struct
{
// error CS9507: foreach statement cannot operate on enumerators of type 'TEnumerator'
// because it is a type parameter that allows ref struct and
// it is not known at compile time to implement IDisposable.
foreach (var i in t)
{
}
}
}
инструкция await foreach
Раздел https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/statements.md#1295-the-foreach-statement следует обновить соответствующим образом, чтобы включить следующие компоненты.
Инструкция await foreach распознает и использует реализацию интерфейса IAsyncEnumerable<T>, если коллекция является ref struct.
ref struct S : IAsyncEnumerable<int>
{
IAsyncEnumerator<int> IAsyncEnumerable<int>.GetAsyncEnumerator(CancellationToken token) {...}
}
class C
{
static async Task Main()
{
await foreach (var i in new S()) // S.IAsyncEnumerable<int>.GetAsyncEnumerator
{
}
}
}
Метод GetAsyncEnumerator будет распознаваться в параметре типа allows ref struct так же, как он распознается в параметрах типа без этого ограничения в настоящее время.
interface IMyAsyncEnumerable<T>
{
IAsyncEnumerator<int> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}
class C
{
static async Task Test<T>() where T : IMyAsyncEnumerable<int>, allows ref struct
{
await foreach (var i in default(T)) // IMyAsyncEnumerable<int>.GetAsyncEnumerator
{
}
}
}
Оператор await foreach распознает и использует реализацию интерфейса IAsyncEnumerable<T>, если коллекция является параметром типа, который allows ref struct, процесс поиска метода шаблона GetAsyncEnumerator завершился сбоем, и IAsyncEnumerable<T> находится в наборе эффективных интерфейсов параметра типа.
interface IMyAsyncEnumerable1<T>
{
IAsyncEnumerator<int> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}
interface IMyAsyncEnumerable2<T>
{
IAsyncEnumerator<int> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}
class C
{
static async Task Test<T>() where T : IMyAsyncEnumerable1<int>, IMyAsyncEnumerable2<int>, IAsyncEnumerable<int>, allows ref struct
{
await foreach (var i in default(T)) // IAsyncEnumerable<int>.GetAsyncEnumerator
{
System.Console.Write(i);
}
}
}
Оператор await foreach будет продолжать запрещать перечислитель ref struct и перечислитель параметра типа, который allows ref struct. Причина заключается в том, что в вызовах await MoveNextAsync() перечислитель должен быть сохранен.
Тип делегата для анонимной функции или группы методов
В разделе https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/lambda-improvements.md#delegate-types указано следующее:
Компилятор может позволить большему количеству подписей связываться с типами
System.Action<>иSystem.Func<>в будущем (если, например, типыref structразрешены в качестве аргументов типов).
Action<> и Func<> типы с ограничениями allows ref struct для параметров типа будут использоваться в большем числе сценариев, связанных с типами структур с модификатором ref в сигнатуре делегата.
Если целевая среда выполнения поддерживает ограничения allows ref struct, универсальные типы анонимных делегатов будут включать allows ref struct ограничения для параметров типа. Это позволит заменить параметры типа на параметры типа структур ссылок и другие параметры типа с ограничением allows ref struct.
Встроенные массивы
В разделе https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/inline-arrays.md#detailed-design указано следующее:
Язык предоставляет типо- и ссылочно безопасный способ доступа к элементам массивов встроенных типов. Доступ будет основан на диапазоне. Это ограничивает поддержку типов массивов встраиваемых, где типы элементов могут использоваться в качестве аргумента типа.
При изменении типов диапазонов для поддержки диапазонов структур ссылок ограничение должно быть отменено для встроенных массивов ссылочных структур.
Основательность
Мы хотели бы проверить правильность как ref struct анти-ограничения, так и концепции анти-ограничений в целом. Для этого мы хотели бы воспользоваться существующими доказательствами корректности, предоставленными для системы типов C#. Эта задача упрощается путем определения нового языка, аналогичного C#, но более регулярного в структуре. Мы проверим безопасность этой модели, а затем укажите звуковой перевод на этот язык. Так как этот новый язык сосредоточен на ограничениях, мы будем называть этот язык "constraint-C#".
Основной инвариант безопасности структуры ссылок, который необходимо сохранить, заключается в том, что переменные типа структуры ссылок не должны отображаться в куче. Это ограничение можно закодировать с помощью ограничения. Поскольку ограничения разрешают подстановку, а не запрещают ее, мы технически определим обратное ограничение: heap. Ограничение heap указывает, что тип может отображаться в куче. В "constraint-C#" все типы удовлетворяют ограничению heap, за исключением ссылочных структур. Кроме того, все существующие параметры типа в C# будут снижены до параметров типа с ограничением heap в "constraint-C#".
Теперь, при условии, что существующий C# является безопасным, мы можем передать правила ref-struct в C# в "constraint-C#".
- Поля классов не могут иметь тип структуры ref-struct.
- Статические поля не могут иметь тип ref-struct.
- Переменные типа ref-структур не могут быть преобразованы в не ref-структуры.
- Переменные типа ref-struct не могут использоваться в качестве аргументов типа.
- Переменные типа структуры ссылок не могут реализовывать интерфейсы.
Новые правила применяются к ограничению heap:
- Поля классов должны иметь типы, удовлетворяющие ограничению
heap. - Статические поля должны иметь типы, удовлетворяющие ограничению
heap. - Типы с ограничением
heapимеют только тождественное преобразование. - Переменные типа ref-struct могут быть подставлены только для параметров типа без ограничения
heap. - Типы ref-struct могут реализовывать только интерфейсы без элементов интерфейсов по умолчанию.
Правила (4) и (5) немного изменены. Обратите внимание, что правило (4) не обязательно строго передавать, потому что у нас есть понятие параметров типа без ограничения heap. Правило (5) сложно. Реализация интерфейсов не всегда является ошибкой, но методы интерфейса по умолчанию предполагают получателя типа интерфейса, который не является значимым типом и нарушает правило (3). Таким образом, члены интерфейса по умолчанию запрещены.
Используя эти правила, параметр "constraint-C#" безопасен для ref-struct, поддерживает подстановку типов и реализацию интерфейса. Следующим шагом является перевод языка, определенного в этом предложении, который можно назвать allow-C#, в "constraint-C#". К счастью, это пустяковое. Снижение — это простое синтаксическое преобразование. Синтаксис where T : allows ref struct в "allow-C#" эквивалентен отсутствию ограничения в "constraint-C#", а отсутствие "предложений allow" эквивалентно ограничению heap. Поскольку абстрактная семантика и типизация эквивалентны, "allow-C#" также является корректным.
Существует одно последнее свойство, которое мы можем рассмотреть: являются ли все типизированные термины на C# также типизированными на "constraint-C#". Другими словами, мы хотим знать, будет ли для всех терминов t в C# соответствующий термин t' после понижения до "constraint-C#" хорошо типизированным. Это не ограничение корректности, то есть термины, имеющие неверный тип в нашем целевом языке, никогда не создадут угрозы безопасности — скорее, это вопрос обратной совместимости. Если мы решили использовать ввод "constraint-C#" для проверки "allow-C#", мы хотели бы подтвердить, что мы не делаем существующий код C#незаконным.
Поскольку все термины C# изначально являются допустимыми терминами "constraint-C#", мы можем проверить их сохранение, исследуя каждое из наших новых ограничений "constraint-C#". Во-первых, добавление ограничения heap. Так как все параметры типа в C# получают ограничение heap, все существующие термины должны соответствовать заданному ограничению. Это верно для всех конкретных типов, кроме ссылочных структур, что уместно, поскольку ссылочные структуры на данный момент не могут использоваться в качестве аргументов типа. Это также верно для всех параметров типа, так как они сами получили бы ограничение heap. Кроме того, поскольку ограничение heap является допустимым сочетанием всех других ограничений, это не будет представлять никаких проблем. Правила (1–5) не будут представлять никаких проблем, так как они напрямую соответствуют существующим правилам C# или являются их расслаблениями. Таким образом, все термины, которые могут быть типизированы в C#, должны быть типизированы в "constraint-C#", и мы не должны вводить изменения, нарушающие систему типов.
Открытые проблемы
Синтаксис защиты от ограничений
Решение: используйте where T: allows ref struct
Это предложение решило предоставить ref struct анти-ограничение путем расширения существующего синтаксиса where для включения allows ref struct. Это как кратко описывает функцию, так и расширяется для включения других анти-ограничений в будущем, таких как указатели. Существуют и другие решения, которые стоит обсудить.
Первое — это просто выбор другого синтаксиса для использования в предложении where. К другим предлагаемым вариантам относятся:
-
~ref struct:~служит маркером, указывающим, что следующий синтаксис является анти-ограничением. -
include ref struct: использованиеincludesвместоallows
void M<T>(T p)
where T : IDisposable, ~ref struct
{
p.Dispose();
}
Во-вторых, следует использовать новую конструкцию, чтобы было ясно, что следующее предложение расширяет набор разрешенных типов. Сторонники этого считают, что использование синтаксиса в where может привести к путанице при чтении. Первоначальное предложение использовало следующий синтаксис: allow T: ref struct:
void M<T>(T p)
where T : IDisposable
allow T : ref struct
{
p.Dispose();
}
Синтаксис where T: allows ref struct пользовался немного большей популярностью в обсуждениях LDM.
Ко и контравариантность
решение: новые проблемы не возникают
Чтобы параметры типа allows ref struct были максимально полезными, они должны быть совместимы с параметрической ковариантностью. В частности, он должен быть законным для параметра, который должен быть как co/contravariant, так и allows ref struct. Из-за их отсутствия они не могут быть использованы во многих из самых популярных типов в .NET, таких как delegate, interface, Func<T>, Action<T>, IEnumerable<T>и т. д.
После обсуждения было заключено, что это не проблема. Ограничение allows ref struct является еще одним способом использования struct как универсальных аргументов. Так же, как обычный аргумент struct устраняет дисперсию API, так же и ref struct.
Автоматическое применение к членам группы делегатов
Решение: не применять автоматически
Для многих универсальных членов delegate язык может автоматически применять allows ref struct, потому что это приносит чистую выгоду. Учитывая, что для делегатов стиля Func<> / Action<> и большинства определений интерфейса расширение до разрешения ref structне имеет недостатков. Язык может указать правила, в которых безопасно автоматически применять это анти-ограничение. Это удаляет процесс вручную и ускорит внедрение этой функции.
Это автоматическое приложение allows ref struct представляет несколько проблем. Первый — в нескольких целевых сценариях. Код компилируется в одной целевой платформе, но завершается ошибкой в другой, и нет синтаксического индикатора того, почему API должны вести себя по-разному.
// Works in net9.0 but fails in all other TF
Func<Span<char>> func;
Это, вероятно, приведет к путанице клиентов и просмотр изменений в Func<T> в источнике net9.0 не даст клиентам никакой подсказки о том, что изменилось.
Другая проблема заключается в том, что очень тонкие изменения в коде могут вызвать действие spooky на расстоянии проблем. Рассмотрим следующий код:
interface I1<T>
{
}
Этот интерфейс может быть пригоден для автоматического применения allows ref struct. Если разработчик позже добавит метод интерфейса по умолчанию, то ситуация внезапно изменится, и это сломает всех потребителей, которые уже использовали вызовы, подобные I1<Span<char>>. Это очень тонкое изменение, которое было бы трудно отследить.
Двоичное изменение, нарушающее совместимость
Добавление allows ref struct в существующий API не является изменением, нарушающим совместимость. Это чисто расширение набора разрешенных типов для API. Необходимо выяснить, является ли это нарушающим двоичным изменением или нет. Неясно, если обновление атрибутов универсального параметра представляет собой двоичное критическое изменение.
Предупреждение о вызове DIM
Должен ли компилятор предупредить о следующем вызове M, так как он создает возможность для исключения среды выполнения?
interface I1
{
// Virtual method with default implementation
void M() { }
}
// Invocation of a virtual instance method with default implementation in a generic method that has the `allows ref struct`
// anti-constraint
void M<T>(T p)
where T : allows ref struct, I1
{
p.M(); // Warn?
}
Однако это может быть шумным и не очень полезным в большинстве сценариев. Для всех виртуальных API в C# потребуется реализация с использованием ref-структур. Поэтому, предполагая, что другие игроки следуют тому же правилу, единственная ситуация, когда это может вызвать исключение, это когда метод добавляется постфактум. Автор потребляющего кода часто не знает обо всех этих деталях и часто не имеет контроля над структурами ссылок, которые будут использоваться кодом. Поэтому единственным действием, которые автор может предпринять, является подавление предупреждения.
Соображения
Поддержка среды выполнения
Для этой функции требуется несколько видов поддержки от команды, отвечающей за среду выполнения и библиотеки.
- Предотвращение применения методов интерфейса по умолчанию к
ref struct - API в
System.Reflection.Metadataдля кодирования значенияgpAcceptByRefLike - Поддержка универсальных параметров как
ref struct
Большая часть этой поддержки, скорее всего, уже существует. Общая ref struct как поддержка универсальных параметров уже реализована, как описано здесь. Возможно, реализация DIM уже учитывает ref struct. Но каждый из этих элементов необходимо отслеживать.
Управление версиями API
разрешает антиограничения для ref struct
allows ref struct анти-ограничение можно безопасно применить к большому количеству универсальных определений, которые не имеют реализаций. Это означает, что большинство делегатов, интерфейсов и abstract методов могут безопасно применять allows ref struct к своим параметрам. Это просто определения API без реализации, поэтому расширение набора разрешенных типов приведет только к ошибкам, если они используются в качестве аргументов типа, где ref struct не разрешены.
Владельцы API могут полагаться на простое правило: "если код компилируется, значит, он безопасен". Компилятор выдаст ошибку при любом небезопасном использовании allows ref struct, подобно другим использованиям ref struct.
В то же время, авторам API следует учитывать соображения о версионировании. По сути, владельцы API должны избегать добавления allows ref struct в параметры типа, где владелец или член может измениться в будущем, чтобы быть несовместимым с allows ref struct. Например:
- Метод
abstract, который может позже измениться на методvirtual - Тип
abstract, который может в будущем добавить реализации
В таких случаях автор API должен быть осторожным при добавлении allows ref struct, если он не уверен, что эволюция типа или члена не будет использовать T таким образом, чтобы нарушить правила ref struct.
Снятие анти-констрейнта allows ref struct всегда приводит к критическому изменению: как на уровне исходного кода, так и на уровне двоичного кода.
Методы интерфейса по умолчанию
Авторы API должны учитывать, что добавление DIMS будет ломать функциональность реализаций ref struct, пока они не будут перекомпилированы. Это похоже на существующее поведение DIM, где добавление DIM в интерфейс нарушает существующие реализации до тех пор, пока они не будут перекомпилированы. Это означает, что авторы API должны учитывать вероятность реализации ref struct при добавлении DIM.
Существует три компонента кода, необходимые для создания этой ситуации:
interface I1
{
// 1. The addition of a DIM method to an _existing_ interface
void M() { }
}
// 2. A ref struct implementing the interface but not explicitly defining the DIM
// method
ref struct S : I1 { }
// 3. The invocation of the DIM method in a generic method that has the `allows ref struct`
// anti-constraint
void M<T>(T p)
where T : allows ref struct, I1
{
p.M();
}
Для создания этой конкретной проблемы необходимы все три этих компонента. Кроме того, не менее (1) и (2) должны находиться в разных сборках. Если они находились в той же сборке, возникнет ошибка компиляции.
UnscopedRef
Добавление или удаление [UnscopedRef] для членов interface — это изменение, нарушающее совместимость с исходным кодом (что также может потенциально вызвать проблемы во время выполнения). Атрибут должен применяться при определении члена интерфейса, а не добавлен или удален позже.
Диапазон<Диапазон<T>>
Эта комбинация функций не позволяет создавать такие конструкции, как Span<Span<T>>. Это становится более понятным при взгляде на определение Span<T>.
readonly ref struct Span<T>
{
public readonly ref T _data;
public readonly int _length;
public Span(T[] array) { ... }
public static implicit operator Span<T>(T[]? array) { }
public static implicit operator Span<T>(ArraySegment<T> segment) { }
}
Если бы это определение типа включало allows ref struct, все экземпляры T в определении следует рассматривать как потенциальный тип ref struct. Это представляет два класса проблем.
Первое — для API, таких как Span(T[] array), и неявные операторы T не могут быть ref struct: он используется как элемент массива или в качестве универсального параметра, который не может быть allows ref struct. Существует несколько общедоступных API на Span<T>, которые используют T, которые не могут быть совместимы с ref struct. Это общедоступный API, который не может быть удален и поэтому должен быть рационализирован языком. Наиболее вероятный путь вперед заключается в том, что компилятор будет обрабатывать случай Span<T> особо и выдавать код ошибки, связанный с одним из этих API, если аргумент для T является , потенциально связанный с и ref struct.
Во-вторых, язык не поддерживает такие поля ref, которые являются ref struct. Существует проектное предложение для предоставления этой функции. Неясно, будет ли это принято в язык и будет ли это достаточно выразительным, чтобы справиться с полным набором сценариев с использованием Span<T>.
Оба этих вопроса выходят за рамки этого предложения.
Логика реализации UnscopedRef
Обоснование, лежащее в основе правил [UnscopedRef] для реализации интерфейса, проще всего понять при визуализации параметра this в виде явного, а не неявного аргумента методов. Рассмотрим, например, следующие struct, где this визуализированы как неявный параметр (аналогично тому, как Python обрабатывает его):
struct S
{
public void M(scoped ref S this) { }
}
[UnscopedRef] на члене интерфейса указывает, что this не хватает scoped для целей времени жизни в месте вызова. Разрешение [UnscopedRef] быть опущенным в элементе реализации эффективно позволяет параметру, который ref T быть реализован параметром, который является scoped ref T. Язык уже допускает следующее:
interface I1
{
void M(ref Span<char> span);
}
struct S : I1
{
public void M(scoped ref Span<char> span) { }
}
Связанные элементы
Связанные элементы:
- https://github.com/dotnet/csharplang/issues/7608
- https://github.com/dotnet/csharplang/pull/7555
- https://github.com/dotnet/runtime/blob/main/docs/design/features/byreflike-generics.md
- https://github.com/dotnet/runtime/pull/67783
- https://github.com/dotnet/runtime/issues/27229#issuecomment-1537274804
- https://github.com/dotnet/runtime/issues/68002
C# feature specifications