Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Nota
Este artículo es una especificación de características. La especificación actúa como documento de diseño de la característica. Incluye cambios de especificación propuestos, junto con la información necesaria durante el diseño y el desarrollo de la característica. Estos artículos se publican hasta que se finalizan los cambios de especificación propuestos e se incorporan en la especificación ECMA actual.
Puede haber algunas discrepancias entre la especificación de características y la implementación completada. Esas diferencias se recogen en las notas de la reunión de diseño de lenguaje (LDM) correspondientes.
Puede obtener más información sobre el proceso de adopción de especificaciones de características en el estándar del lenguaje C# en el artículo sobre las especificaciones de .
Problema del campeón: https://github.com/dotnet/csharplang/issues/7608
Resumen
Esta propuesta ampliará las funcionalidades de ref struct de modo que puedan implementar interfaces y participar como argumentos de tipo genéricos.
Motivación
La incapacidad de ref struct implementar interfaces significa que no pueden participar en técnicas de abstracción bastante fundamentales de .NET. Un Span<T>, aunque tenga todos los atributos de una lista secuencial no puede participar en métodos que toman IReadOnlyList<T>, IEnumerable<T>, etc. En su lugar, los métodos específicos deben codificarse para Span<T> que tengan prácticamente la misma implementación. Permitir que ref struct implemente interfaces permitirá que las operaciones se abstraigan sobre ellas, como ocurre con otros tipos.
Diseño detallado
interfaces ref struct
El lenguaje permitirá que los tipos de ref struct implementen interfaces. La sintaxis y las reglas son las mismas que para los struct normales con algunas excepciones para tener en cuenta las limitaciones de los tipos de ref struct.
La capacidad de implementar interfaces no afecta a las limitaciones existentes contra el empaquetado de instancias ref struct. Esto significa que incluso si un ref struct implementa una interfaz en particular, no puede ser convertido directamente a ella, ya que esto representa una acción de boxeo.
ref struct File : IDisposable
{
private SafeHandle _handle;
public void Dispose()
{
_handle.Dispose();
}
}
File f = ...;
// Error: cannot box `ref struct` type `File`
IDisposable d = f;
La capacidad de implementar interfaces solo es útil cuando se combina con la capacidad de ref struct de participar en argumentos genéricos (como se explica más adelante).
Para permitir que las interfaces cubran toda la expresividad de un ref struct y los problemas de tiempo de vida que pueden presentar, el lenguaje permitirá que [UnscopedRef] aparezca en los métodos y propiedades de las interfaces. Esto es necesario, ya que permite que las interfaces que se abstraen mediante struct tengan la misma flexibilidad que usar directamente un struct. Tenga en cuenta el ejemplo siguiente:
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;
}
Cuando un miembro de struct / ref struct implementa un miembro de interfaz con un atributo [UnscopedRef], el miembro que lo implementa también puede decorarse con [UnscopedRef], aunque no es obligatorio. Sin embargo, un miembro con [UnscopedRef] no podrá utilizarse para implementar un miembro que carezca del atributo (detalles).
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 {...} }
}
Los métodos de interfaz por defecto plantean un problema para ref struct, ya que no existen protecciones contra la implementación por defecto que encasilla al miembro 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 { }
Para gestionar esto, un ref struct se verá obligado a implementar todos los miembros de una interfaz, incluso si tienen implementaciones predeterminadas.
El tiempo de ejecución también se actualizará para lanzar una excepción si se llama a un miembro de interfaz por defecto en un tipo ref struct.
Para evitar una excepción en tiempo de ejecución, el compilador notificará un error al invocar un método de instancia o propiedad no virtual en un parámetro de tipo que admita ref struct. Este es un ejemplo:
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.
}
}
También hay una cuestión de diseño abierta sobre la notificación de una advertencia para una invocación de un método de instancia virtual (no abstracto) (o propiedad) en un parámetro de tipo que permite ref struct.
Notas detalladas:
- Un
ref structpuede implementar una interfaz - Un
ref structno puede participar en miembros predeterminados de interfaz - Un
ref structno se puede asignar a las interfaces que implementa, ya que se trata de una operación de boxeo.
Parámetros genéricos 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'
El lenguaje permitirá que los parámetros genéricos opten por admitir ref struct como argumentos mediante la sintaxis de allows ref struct dentro de una cláusula where:
T Identity<T>(T p)
where T : allows ref struct
=> p;
// Okay
Span<int> local = Identity(new Span<int>(new int[10]));
Esto es similar a otros elementos de una cláusula where en que especifica las funcionalidades del parámetro genérico. La diferencia es que otros elementos de sintaxis limitan el conjunto de tipos que pueden cumplir un parámetro genérico mientras allows ref struct expande el conjunto de tipos. Esta es efectivamente una anti-restricción, ya que elimina la restricción implícita de que ref struct no puede satisfacer un parámetro genérico. Como tal, se le da un nuevo prefijo de sintaxis, allows, para que sea más claro.
Un parámetro de tipo enlazado por allows ref struct tiene todas las funciones de un tipo ref struct.
- Las instancias no se pueden encasillar
- Las instancias participan en las reglas de vida como una
ref structnormal - El parámetro type no se puede usar en campos
static, elementos de una matriz, etc. - Las instancias pueden marcarse con
scoped
Ejemplos de estas reglas en acción:
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;
}
La antirrestricción no se "hereda" de una restricción de tipo de parámetro de tipo.
Por ejemplo, S en el código siguiente no se puede sustituir por una estructura ref:
class C<T, S>
where T : allows ref struct
where S : T
{}
Notas detalladas:
- Un parámetro genérico
where T : allows ref structno puede- Tener
where T : UdondeUes un tipo de referencia conocido - Tener restricción
where T : class - No se puede usar como argumento genérico a menos que el parámetro correspondiente también sea
where T: allows ref struct
- Tener
- El
allows ref structdebe ser la última restricción de la cláusulawhere. - Un parámetro de tipo
Tque tieneallows ref structtiene todas las mismas limitaciones que un tipo deref struct.
Representación en metadatos
Los parámetros de tipo que permiten ref structs se codificarán en metadatos tal y como se describe en el documento byref-like generics. Específicamente utilizando el valor de la bandera CorGenericParamAttr.gpAllowByRefLike(0x0020) o System.Reflection.GenericParameterAttributes.AllowByRefLike(0x0020).
Si el tiempo de ejecución admite la función, se puede determinar comprobando la presencia del campo System.Runtime.CompilerServices.RuntimeFeature.ByRefLikeGenerics.
Las API se agregaron en https://github.com/dotnet/runtime/pull/98070.
declaración using
Una instrucción using reconocerá y utilizará la implementación de la interfaz IDisposable cuando el recurso sea una ref struct.
ref struct S2 : System.IDisposable
{
void System.IDisposable.Dispose()
{
}
}
class C
{
static void Main()
{
using (new S2())
{
} // S2.System.IDisposable.Dispose is called
}
}
Tenga en cuenta que se da preferencia a un método Dispose que implementa el patrón y solo si no se encuentra uno, se usa IDisposable implementación.
Una instrucción using reconocerá y usará la implementación de la interfaz IDisposable cuando el recurso sea un parámetro de tipo y allows ref struct y IDisposable se encuentren en su conjunto de interfaces efectivas.
class C
{
static void Test<T>(T t) where T : System.IDisposable, allows ref struct
{
using (t)
{
}
}
}
Tenga en cuenta que un método de patrón Dispose no se reconocerá en un parámetro de tipo que allows ref struct porque una interfaz (y este es el único lugar donde posiblemente podríamos buscar un patrón) no es una 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
{
}
}
}
declaración await using
Actualmente, el lenguaje no permite usar ref structs como recursos en la instrucción await using. La misma limitación se aplicará a un parámetro de tipo que allows ref struct.
Hay una propuesta para elevar las restricciones generales en torno al uso de estructuras ref en métodos asincrónicos: https://github.com/dotnet/csharplang/pull/7994.
El resto de la sección describe el comportamiento después de que se levante la limitación general de la instrucción await using, si/cuándo sucederá.
Una instrucción await using reconocerá y usará la implementación de la interfaz IAsyncDisposable cuando el recurso sea una estructura de referencia.
ref struct S2 : IAsyncDisposable
{
ValueTask IAsyncDisposable.DisposeAsync()
{
}
}
class C
{
static async Task Main()
{
await using (new S2())
{
} // S2.IAsyncDisposable.DisposeAsync
}
}
Tenga en cuenta que se da preferencia a un método DisposeAsync que implementa el patrón y solo si no se encuentra uno, se usa IAsyncDisposable implementación.
Un método DisposeAsync de patrón se reconocerá en un parámetro de tipo que allows ref struct como se reconoce en parámetros de tipo sin esa limitación hoy en día.
interface IMyAsyncDisposable
{
ValueTask DisposeAsync();
}
class C
{
static async Task Test<T>() where T : IMyAsyncDisposable, new(), allows ref struct
{
await using (new T())
{
} // IMyAsyncDisposable.DisposeAsync
}
}
Una instrucción using reconocerá y usará la implementación de la interfaz IAsyncDisposable cuando el recurso sea un parámetro de tipo que allows ref struct, el proceso de búsqueda del método de patrón DisposeAsync haya fallado, y IAsyncDisposable esté en el conjunto de interfaces efectivas del parámetro de tipo.del parámetro de tipo.
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
}
}
declaración foreach
La sección https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/statements.md#1295-the-foreach-statement debe actualizarse según corresponda para incorporar lo siguiente.
Una instrucción foreach reconocerá y utilizará la implementación de la interfaz IEnumerable<T>/IEnumerable cuando la colección sea una ref struct.
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
{
}
}
}
Un método GetEnumerator de patrón se reconocerá en un parámetro de tipo que allows ref struct como se reconoce en parámetros de tipo sin esa limitación hoy en día.
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
{
}
}
}
Una instrucción foreach reconocerá y usará la implementación de la interfaz IEnumerable<T>/IEnumerable cuando la colección sea un parámetro de tipo que allows ref struct, el proceso de búsqueda del método patrón GetEnumerator falló, y IEnumerable<T>/IEnumerable esté en el conjunto de interfaces efectivas del parámetro de tipo.
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
{
}
}
}
Un patrón enumerator será reconocido en un parámetro de tipo que allows ref struct como se reconoce en los parámetros de tipo sin que la restricción de hoy.
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();
}
Una instrucción foreach reconocerá y usará la implementación de la interfaz IDisposable cuando el enumerador es una estructura de referencia.
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()
}
}
Tenga en cuenta que se da preferencia a un método Dispose que implementa el patrón y solo si no se encuentra uno, se usa IDisposable implementación.
Una instrucción foreach reconocerá y usará una implementación de la interfaz IDisposable, cuando el enumerador sea un parámetro de tipo que tenga allows ref struct y IDisposable en su conjunto de interfaces efectivas.
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()
}
}
Tenga en cuenta que un método de patrón Dispose no se reconocerá en un parámetro de tipo que allows ref struct porque una interfaz (y este es el único lugar donde posiblemente podríamos buscar un patrón) no es una ref struct.
Además, puesto que el tiempo de ejecución no proporciona una manera de comprobar si en tiempo de ejecución un parámetro de tipo que allows ref struct implementa la interfaz IDisposable, un enumerador de parámetro de tipo que allows ref struct será desautorizado, a menos que IDisposable esté en su conjunto de interfaces efectivas.
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)
{
}
}
}
declaración await foreach
La sección https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/statements.md#1295-the-foreach-statement debe actualizarse según corresponda para incorporar lo siguiente.
Una instrucción await foreach reconocerá y utilizará la implementación de la interfaz IAsyncEnumerable<T> cuando la colección sea una 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
{
}
}
}
Un método GetAsyncEnumerator de patrón se reconocerá en un parámetro de tipo que allows ref struct como se reconoce en parámetros de tipo sin esa limitación hoy en día.
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
{
}
}
}
Una instrucción await foreach reconocerá y usará la implementación de la interfaz IAsyncEnumerable<T> cuando la colección sea un parámetro de tipo que allows ref struct, el proceso de búsqueda del método patrón GetAsyncEnumerator falló, y IAsyncEnumerable<T> esté en el conjunto de interfaces efectivas del parámetro de tipo.
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);
}
}
}
Una instrucción await foreach continuará rechazando un enumerador ref struct y un enumerador de parámetro de tipo que allows ref struct. La razón es el hecho de que el enumerador debe ser preservado a través de llamadas a await MoveNextAsync().
Tipo de delegado para la función anónima o el grupo de métodos
La sección https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/lambda-improvements.md#delegate-types indica:
El compilador puede permitir que más firmas se vinculen con los tipos
System.Action<>ySystem.Func<>en el futuro (si los tiposref structestán permitidos como argumentos de tipo, por ejemplo).
Los tipos Action<> y Func<> con restricciones allows ref struct en sus parámetros de tipo se utilizarán en más escenarios que impliquen tipos ref struct en la firma del delegado.
Si el entorno de ejecución de destino admite restricciones allows ref struct, los tipos de delegado anónimo genérico incluirán allows ref struct restricción para sus parámetros de tipo. Esto habilitará la sustitución de esos parámetros de tipo con tipos de estructura ref y otros parámetros de tipo con restricción allows ref struct.
Matrices insertadas
La sección https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/inline-arrays.md#detailed-design indica:
El lenguaje proporcionará una forma segura de tipo/ref para acceder a los elementos de los tipos de matriz insertada. El acceso se basará en span. Esto limita el soporte a los tipos de matrices en línea con tipos de elementos que pueden utilizarse como argumento de tipo.
Cuando se cambien los tipos span para soportar spans de ref structs, la limitación debería eliminarse para los arrays inline de ref structs.
Solidez
Nos gustaría verificar la solidez tanto de la antirrestricción de seguridad ref struct en particular como del concepto de antirrestricción de seguridad en general. Para ello, nos gustaría aprovechar las pruebas de solidez existentes proporcionadas para el sistema de tipos de C#. Esta tarea se facilita mediante la definición de un nuevo lenguaje similar a C#, pero más normal en la construcción. Comprobaremos la seguridad de ese modelo y, a continuación, especificaremos una traducción de sonido a este idioma. Dado que este nuevo lenguaje se centra en las restricciones, llamaremos a este lenguaje "constraint-C#".
La principal invariante de seguridad de ref struct que debe preservarse es que las variables de tipo ref struct no deben aparecer en el montón. Podemos codificar esta restricción a través de una restricción. Dado que las restricciones permiten la sustitución, no lo prohíben, definiremos técnicamente la restricción inversa: heap. La restricción heap especifica que un tipo puede aparecer en el montón. En "constraint-C#" todos los tipos cumplen la restricción heap excepto las estructuras 'ref'. Además, todos los parámetros de tipo existentes de C# se reducirán a los parámetros de tipo con la restricción heap en "constraint-C#".
Ahora, suponiendo que el C# existente es seguro, podemos transferir las reglas ref-struct de C# a "constraint-C#".
- Los campos de las clases no pueden tener un tipo ref struct.
- Los campos estáticos no pueden tener un tipo ref-struct.
- Las variables de tipo ref-struct no se pueden convertir en estructuras que no sean ref.
- Las variables de tipo ref-struct no se pueden sustituir como argumentos de tipo.
- Las variables de tipo ref-struct no pueden implementar interfaces.
Las nuevas reglas se aplican a la restricción heap:
- Los campos de las clases deben tener tipos que cumplan la restricción
heap. - Los campos estáticos deben tener tipos que cumplan la restricción
heap. - Los tipos con la restricción
heapsolo tienen la conversión de identidad. - Las variables de tipo ref-struct solo se pueden sustituir por parámetros de tipo sin la restricción
heap. - Los tipos ref-struct solo pueden implementar interfaces sin miembros de interfaz predeterminados.
Las reglas (4) y (5) se modifican ligeramente. Tenga en cuenta que la regla (4) no necesita transferirse exactamente porque tenemos una noción de parámetros de tipo sin el heap contraint. La regla (5) es complicada. Implementar interfaces no es universalmente insano, pero los métodos de interfaz por defecto implican un receptor de tipo interfaz, que es un tipo no-valor y viola la regla (3). Por lo tanto, los miembros de interfaz por defecto no están permitidos.
Con estas reglas, "constraint-C#" es seguro para la estructura ref, admite la sustitución de tipos y admite la implementación de la interfaz. El siguiente paso consiste en traducir el lenguaje definido en esta propuesta, que podemos llamar "allow-C#" a "constraint-C#". Afortunadamente, esto es trivial. La reducción es una transformación sintáctica sencilla. La sintaxis where T : allows ref struct en "allow-C#" es equivalente en "constraint-C#" a ninguna restricción y la ausencia de "cláusulas allow" es equivalente a la restricción heap. Dado que la semántica abstracta y la tipificación son equivalentes, "allow-C#" también es correcto.
Hay una última propiedad que podríamos tener en cuenta: si todos los términos tipados en C# también están tipados en "constraint-C#". En otras palabras, queremos saber si, para todos los términos t en C#, si el término correspondiente t' después de bajar a "constraint-C#" está bien tipado. Esto no es una restricción de solidez; hacer que los términos estén mal tipados en nuestro lenguaje objetivo nunca permitiría inseguridad; más bien, se refiere a la compatibilidad hacia atrás. Si decidimos usar la escritura de "constraint-C#" para validar "allow-C#", nos gustaría confirmar que no estamos haciendo que ningún código de C# existente sea ilegal.
Puesto que todos los términos de C# comienzan como términos "constraint-C#" válidos, podemos validar la conservación examinando cada una de nuestras nuevas restricciones "constraint-C#". En primer lugar, la adición de la restricción heap. Dado que todos los parámetros de tipo de C# adquirirían la restricción heap, todos los términos existentes deben satisfacer dicha restricción. Esto es cierto para todos los tipos concretos excepto los ref structs, lo cual es apropiado, ya que es posible que los ref structs no aparezcan como argumentos de tipo hoy en día. También es cierto para todos los parámetros de tipo, ya que todos ellos mismos adquirirían la restricción heap. Además, dado que la restricción heap es una combinación válida con todas las demás restricciones, esto no presentaría ningún problema. Las reglas (1-5) no presentarían ningún problema, ya que se corresponden directamente con las reglas existentes de C#, o son relajaciones. Por lo tanto, todos los términos escribibles en C# deberían ser tipables en "constraint-C#" y no deberíamos introducir ningún cambio que rompa la escritura.
Problemas abiertos
Sintaxis antirrestricción
Decision: usar where T: allows ref struct
Esta propuesta opta por exponer la antirrestricción ref struct aumentando la sintaxis existente where para incluir allows ref struct. Esto describe la característica de forma sucinta y también se puede ampliar para incluir otras restricciones en el futuro, como los punteros. Hay otras soluciones consideradas que merecen la pena discutir.
La primera consiste simplemente en seleccionar otra sintaxis que se va a usar dentro de la cláusula where. Otras opciones propuestas incluyen:
-
~ref struct:~sirve como marcador de que la sintaxis que sigue es una antirrestricción. -
include ref struct: usarincludesen lugar deallows
void M<T>(T p)
where T : IDisposable, ~ref struct
{
p.Dispose();
}
La segunda consiste en usar una nueva cláusula completamente para dejar claro que lo siguiente es expandir el conjunto de tipos permitidos. Los defensores de esto sienten que el uso de la sintaxis dentro de where puede provocar confusión para el lector. La propuesta inicial usó la siguiente sintaxis: allow T: ref struct:
void M<T>(T p)
where T : IDisposable
allow T : ref struct
{
p.Dispose();
}
La sintaxis where T: allows ref struct gozaba de una ligera preferencia en los debates de LDM.
Covariante y contravariante
decisión: no hay problemas nuevos
Para que los parámetros de tipo sean lo más útiles posible y que son allows ref struct, deben ser compatibles con la varianza genérica. En concreto, debe ser legal que un parámetro sea co/contravariante y también allows ref struct. Sin ello, no se podrían usar en muchos de los tipos de delegate y interface más populares en .NET como Func<T>, Action<T>, IEnumerable<T>, etc.
Después de la discusión se concluyó, esto no es un problema. La restricción allows ref struct es simplemente otra manera de que struct se pueda usar como argumentos genéricos. Igual que un argumento struct normal elimina la varianza de una API, también lo hará un ref struct.
Aplicación automática a los miembros delegados
Decisión: no aplicar automáticamente
Para muchos miembros genéricos delegate el lenguaje podría aplicarse automáticamente ya que allows ref struct es un cambio puramente al alza. Tenga en cuenta que para los delegados de estilo Func<> / Action<> y la mayoría de las definiciones de interfaz, no hay ninguna desventaja en ampliar para permitir ref struct. El lenguaje puede describir las reglas en las que es seguro aplicar automáticamente esta anti-condición. Esto elimina el proceso manual y aceleraría la adopción de esta característica.
Sin embargo, esta aplicación automática de allows ref struct plantea algunos problemas. El primero se da en escenarios multiobjetivo. El código se compilaría en un marco de trabajo de destino, pero fallaría en otro, y no hay ningún indicador sintáctico de por qué las APIs deberían comportarse de forma diferente.
// Works in net9.0 but fails in all other TF
Func<Span<char>> func;
Es probable que esto lleve a la confusión del cliente y mirar los cambios en Func<T> en la fuente net9.0 no daría a los clientes ninguna pista sobre lo que cambió.
La otra cuestión es que cambios muy sutiles en el código pueden causar problemas de acción tétrica a distancia. Tenga en cuenta el código siguiente:
interface I1<T>
{
}
Esta interfaz sería apta para la aplicación automática de allows ref struct. Sin embargo, si un desarrollador viene más tarde y añade un método de interfaz por defecto, de repente no lo sería y rompería cualquier consumidor que ya hubiera creado invocaciones como I1<Span<char>>. Este es un cambio muy sutil que sería difícil de rastrear.
Cambios importantes de archivo binario
Añadir allows ref struct a una API existente no es un cambio que rompa la fuente. Se expande exclusivamente el conjunto de tipos permitidos para una API. Es necesario averiguar si se trata de un cambio que rompe el binario o no. No está claro si la actualización de los atributos de un parámetro genérico constituye un cambio de ruptura binaria.
Advertencia sobre la invocación de DIM
¿Debe advertir el compilador de la siguiente invocación de M a medida que crea la oportunidad de una excepción en tiempo de ejecución?
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?
}
Sin embargo, esto podría ser ruidoso y no muy útil en la mayoría de los escenarios. C# requerirá structs ref para implementar todas las API virtuales. Por lo tanto, suponiendo que otros jugadores sigan la misma regla, la única situación en la que esto podría provocar una excepción es cuando se agrega el método después del hecho. El autor del código que consume a menudo no tiene conocimiento de todos estos detalles y, a menudo, no tiene control sobre estructuras ref que el código usará. Por lo tanto, la única acción que puede realizar el autor es suprimir la advertencia.
Consideraciones
Compatibilidad con el tiempo de ejecución
Esta característica requiere varios tipos de soporte del equipo de entorno de ejecución y bibliotecas.
- Impedir que los métodos de interfaz predeterminados se apliquen a
ref struct - API en
System.Reflection.Metadatapara codificar el valorgpAcceptByRefLike - Soporte para parámetros genéricos siendo un
ref struct
Es probable que la mayoría de este apoyo ya esté implementado. El soporte general ref struct como parámetro genérico ya está implementado como se describe aquí. Es posible que la implementación de DIM ya tenga en cuenta ref struct. Pero cada uno de estos elementos debe rastrearse.
Control de versiones de API
permite ref struct antirrestricción
La allows ref struct anti-constraint se puede aplicar de forma segura a un gran número de definiciones genéricas que no tienen implementaciones. Esto significa que la mayoría de los delegados, interfaces y métodos abstract pueden aplicar allows ref struct de forma segura a sus parámetros. Estas son solo definiciones de API sin implementaciones y, por lo tanto, la expansión del conjunto de tipos permitidos solo producirá errores si se usan como argumentos de tipo en los que no se permite ref struct.
Los propietarios de API pueden confiar en una regla sencilla de "si se compila, es segura". El compilador producirá un error en los usos no seguros de allows ref struct, igual que en otros usos de ref struct.
Al mismo tiempo, hay consideraciones de control de versiones que los autores de API deben tener en cuenta. Básicamente, los propietarios de API deben evitar agregar allows ref struct a parámetros de tipo donde el tipo o miembro propietario puede cambiar en el futuro para que sea incompatible con allows ref struct. Por ejemplo:
- Un método
abstractque puede cambiar posteriormente a un método devirtual - Un tipo
abstractque puede añadir implementaciones posteriormente
En tales casos, un autor de API debe tener cuidado de agregar allows ref struct a menos que estén seguros de que la evolución del tipo o miembro no usará T de una manera que interrumpe las reglas de ref struct.
Eliminar la antirrestricción allows ref struct es siempre un cambio de ruptura: fuente y binario.
Métodos de interfaz predeterminados
Los autores de API deben tener en cuenta que la adición de DIMS interrumpirá a los implementadores de ref struct hasta que se vuelvan a compilar. Esto es similar al comportamiento actual de los DIM, en el que añadir un DIM a una interfaz romperá las implementaciones existentes hasta que sean recompiladas. Esto significa que los autores de APIs necesitan considerar la probabilidad de implementaciones ref struct cuando añaden DIM.
Hay tres componentes de código necesarios para crear esta situación:
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();
}
Estos tres componentes son necesarios para crear este problema en particular. Además, al menos (1) y (2) deben estar en ensamblados diferentes. Si estuvieran en el mismo ensamblado, se produciría un error de compilación.
UnscopedRef
Añadir o eliminar [UnscopedRef] de los miembros interface es un cambio que rompe la fuente (y potencialmente crea problemas en tiempo de ejecución). El atributo debe aplicarse al definir un miembro de interfaz y no se agregará ni quitará más adelante.
Span Span<<T>>
Esta combinación de características no permite construcciones como Span<Span<T>>. Esto se hace un poco más claro examinando la definición de 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) { }
}
Si esta definición de tipo fuera a incluir allows ref struct, todas las instancias de T de la definición se tratarían como si fueran potencialmente un tipo de ref struct. Esto presenta dos clases de problemas.
La primera es para las API como Span(T[] array) y los operadores implícitos que el T no puede ser un ref struct: se usa como elemento de matriz o como parámetro genérico que no puede ser allows ref struct. Hay una serie de API públicas en Span<T> que tienen usos de T que no pueden ser compatibles con una ref struct. Se trata de una API pública que no se puede eliminar y, por tanto, debe racionalizarse por el idioma. Lo más probable es que el compilador aplique un caso especial Span<T> y emita un código de error cuando el argumento for esté vinculado a una de estas API y el argumento para T sea potencialmente un ref struct.
La segunda es que el lenguaje no admite campos ref que sean ref struct. Existe una propuesta de diseño para permitir esta característica. No está claro si se aceptará en el idioma o si es lo suficientemente expresivo como para abordar el conjunto completo de escenarios relacionados con Span<T>.
Ambas cuestiones están fuera del ámbito de esta propuesta.
Lógica de implementación de UnscopedRef
La justificación de las reglas de [UnscopedRef] para la implementación de interfaz es más fácil de entender al visualizar el parámetro this como un argumento explícito, en lugar de implícito, para los métodos. Considere, por ejemplo, el siguiente struct en el que this se visualiza como un parámetro implícito (similar a cómo lo controla Python):
struct S
{
public void M(scoped ref S this) { }
}
El [UnscopedRef] en un miembro de la interfaz es especificar que this carece de scoped a efectos de tiempo de vida en el sitio de llamada. Permitir que se omita [UnscopedRef] en el miembro de implementación permite efectivamente que un parámetro ref T se implemente mediante un parámetro que sea scoped ref T. El idioma ya permite lo siguiente:
interface I1
{
void M(ref Span<char> span);
}
struct S : I1
{
public void M(scoped ref Span<char> span) { }
}
Elementos relacionados
Elementos relacionados:
- 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