Matrices insertadas
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 .
Resumen
Proporcionar un mecanismo general y seguro para consumir tipos struct utilizando la función InlinematrizAttribute. Proporcione un mecanismo de propósito general y seguro para declarar matrices en línea dentro de clases, estructuras e interfaces de C#.
Nota: Las versiones anteriores de esta especificación utilizaban los términos "ref-safe-to-escape" y "safe-to-escape", que se introdujeron en la especificación de la función Span safety. El comité estándar ECMA cambió los nombres a "contexto seguro de referencia" y "contexto seguro", respectivamente. Los valores del contexto seguro se han refinado para utilizar "declaration-block", "function-member" y "caller-context" de forma coherente. Los speclets habían utilizado una redacción diferente para estos términos, y también utilizaban "safe-to-return" como sinónimo de "caller-context". Esta especificación se ha actualizado para usar los términos del estándar C# 7.3.
Motivación
Esta propuesta planea abordar las numerosas limitaciones de https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/unsafe-code.md#238-fixed-size-buffers. En concreto, pretende permitir:
- el acceso a elementos de tipos struct utilizando la función InlinematrizAttribute;
- la declaración de matrices en línea para tipos administrados y no administrados en un
struct
,class
, ointerface
.
Además, ofrezca una verificación de seguridad lingüística para ellos.
Diseño detallado
Recientemente runtime ha añadido la función InlinematrizAttribute. En resumen, un usuario puede declarar un tipo de estructura como el siguiente:
[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer
{
private object _element0;
}
Runtime proporciona un diseño de tipo especial para el tipo Buffer
:
- El tamaño del tipo se extiende para ajustarse a 10 elementos (el número procede del atributo InlineArray) de tipo
object
(el tipo procede del tipo del campo de instancia única en el struct,_element0
en este ejemplo). - El primer elemento se alinea con el campo de instancia y con el principio de la estructura
- Los elementos se colocan secuencialmente en la memoria como si fueran elementos de una matriz.
Runtime proporciona un seguimiento GC regular para todos los elementos de la estructura.
Esta propuesta se referirá a tipos como este como "tipos de matriz insertada".
Se puede acceder a los elementos de un tipo de matriz insertada a través de punteros o a través de instancias span devueltas por las API System.Runtime.InteropServices.MemoryMarshal.CreateSpan/System.Runtime.InteropServices.MemoryMarshal.CreateReadOnlySpan. Sin embargo, ni el enfoque de puntero ni las API proporcionan comprobación de tipo y límites de forma inmediata.
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. Por ejemplo, un tipo de puntero no se puede usar como un tipo de elemento. Otros ejemplos de tipos span.
Obtención de instancias de tipos span para un tipo de matriz insertada
Dado que hay una garantía de que el primer elemento de un tipo de matriz insertada esté alineado al principio del tipo (sin espacio), el compilador usará el código siguiente para obtener un valor de Span
:
MemoryMarshal.CreateSpan(ref Unsafe.As<TBuffer, TElement>(ref buffer), size);
Y el siguiente código para obtener un valor ReadOnlySpan
:
MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<TBuffer, TElement>(ref Unsafe.AsRef(in buffer)), size);
Para reducir el tamaño de IL en los sitios de uso, el compilador debe poder agregar dos auxiliares genéricos reutilizables en el tipo de detalle privado de implementación y usarlos en todos los sitios de uso del mismo programa.
public static System.Span<TElement> InlineArrayAsSpan<TBuffer, TElement>(ref TBuffer buffer, int size) where TBuffer : struct
{
return MemoryMarshal.CreateSpan(ref Unsafe.As<TBuffer, TElement>(ref buffer), size);
}
public static System.ReadOnlySpan<TElement> InlineArrayAsReadOnlySpan<TBuffer, TElement>(in TBuffer buffer, int size) where TBuffer : struct
{
return MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<TBuffer, TElement>(ref Unsafe.AsRef(in buffer)), size);
}
Acceso a elementos
El Acceso a elementos se ampliará para soportar el acceso a elementos de matrices insertadas.
Un element_access consta de un primary_no_matriz_creation_expression, seguido de un token "[
", seguido de un token argument_list, seguido de un token "]
". El argument_list consta de uno o varios argumentos s, separados por comas.
element_access
: primary_no_array_creation_expression '[' argument_list ']'
;
No se permite que el argument_list de un element_access contenga argumentos ref
o out
.
Un element_access se enlaza dinámicamente (§11.3.3) si al menos una de las siguientes condiciones:
- El primary_no_matriz_creation_expression tiene un tipo en tiempo de compilación
dynamic
. - Al menos una expresión del argument_list tiene
dynamic
de tipo en tiempo de compilación y el primary_no_matriz_creation_expression no tiene un tipo de matriz, y el primary_no_matriz_creation_expression no tiene un tipo de matriz insertado o hay más de un elemento en la lista de argumentos.
En este caso, el compilador clasifica el element_access como un valor de tipo dynamic
. Las reglas siguientes para determinar el significado de la element_access son aplicadas en tiempo de ejecución, usando el tipo en tiempo de ejecución en lugar del tipo en tiempo de compilación de las expresiones de primary_no_matriz_creation_expression y argument_list que tienen el tipo en tiempo de compilación dynamic
. Si el primary_no_matriz_creation_expression no tiene un tipo de tiempo de compilación dynamic
, entonces el acceso al elemento se somete a una comprobación de tiempo de compilación limitada, tal como se describe en §11.6.5.
Si el primary_no_matriz_creation_expression de un element_access es un valor de un matriz_type, el element_access es un acceso a matriz (§12.8.12.2). Si el primary_no_matriz_creation_expression de un element_access es una variable o un valor de un tipo de matriz insertada y la argument_list consta de un único argumento, el element_access es un acceso a elemento de matriz insertada. De lo contrario, el primary_no_array_creation_expression será una variable o un valor de una clase, estructura o tipo de interfaz que tenga uno o varios miembros del indizador, en cuyo caso el element_access es un acceso de indexador (§12.8.12.3).
Acceso a elementos de matriz insertada
Para el acceso a un elemento de matriz insertada, el primary_no_matriz_creation_expression del element_access debe ser una variable o un valor de un tipo de matriz insertada. Además, no se permite que la argument_list de acceso a un elemento de matriz insertada contenga argumentos con nombre. El argument_list debe contener una sola expresión y la expresión debe ser
- de tipo
int
, o - convertible implícitamente a
int
, o - convertible implícitamente a
System.Index
, o - convertible implícitamente a
System.Range
.
Cuando el tipo de expresión es int
Si primary_no_matriz_creation_expression es una variable editable, el resultado de evaluar el acceso a un elemento de matriz insertada es una variable editable equivalente a invocar public ref T this[int index] { get; }
con ese valor entero sobre una instancia de System.Span<T>
devuelto por el método System.Span<T> InlineArrayAsSpan
sobre primary_no_matriz_creation_expression. Para el análisis de ref-safety, el ref-safe-context/contexto seguro del acceso es equivalente al contexto de una invocación de un método con la firma static ref T GetItem(ref InlineArrayType array)
.
La variable resultante se considera extraíble si y solo si primary_no_array_creation_expression es extraíble.
Si primary_no_array_creation_expression es una variable de solo lectura, el resultado de evaluar el acceso a un elemento de matriz en línea es una variable de solo lectura equivalente a invocar public ref readonly T this[int index] { get; }
con ese valor entero en una instancia de System.ReadOnlySpan<T>
devuelta por el método System.ReadOnlySpan<T> InlineArrayAsReadOnlySpan
en primary_no_array_creation_expression. Para el análisis de ref-safety, el ref-safe-context/contexto seguro del acceso es equivalente al contexto de una invocación de un método con la firma static ref readonly T GetItem(in InlineArrayType array)
.
La variable resultante se considera extraíble si y solo si primary_no_array_creation_expression es extraíble.
Si primary_no_matriz_creation_expression es un valor, el resultado de evaluar el acceso a un elemento de matriz insertada es un valor editable equivalente a invocar public ref readonly T this[int index] { get; }
con ese valor entero sobre una instancia de System.ReadOnlySpan<T>
devuelto por el método System.ReadOnlySpan<T> InlineArrayAsReadOnlySpan
sobre primary_no_matriz_creation_expression. Para el análisis de ref-safety, el ref-safe-context/contexto seguro del acceso es equivalente al contexto de una invocación de un método con la firma static T GetItem(InlineArrayType array)
.
Por ejemplo:
[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer10<T>
{
private T _element0;
}
void M1(Buffer10<int> x)
{
ref int a = ref x[0]; // Ok, equivalent to `ref int a = ref InlineArrayAsSpan<Buffer10<int>, int>(ref x, 10)[0]`
}
void M2(in Buffer10<int> x)
{
ref readonly int a = ref x[0]; // Ok, equivalent to `ref readonly int a = ref InlineArrayAsReadOnlySpan<Buffer10<int>, int>(in x, 10)[0]`
ref int b = ref x[0]; // An error, `x` is a readonly variable => `x[0]` is a readonly variable
}
Buffer10<int> GetBuffer() => default;
void M3()
{
int a = GetBuffer()[0]; // Ok, equivalent to `int a = InlineArrayAsReadOnlySpan<Buffer10<int>, int>(GetBuffer(), 10)[0]`
ref readonly int b = ref GetBuffer()[0]; // An error, `GetBuffer()[0]` is a value
ref int c = ref GetBuffer()[0]; // An error, `GetBuffer()[0]` is a value
}
La indexación en una matriz en línea con una expresión constante fuera de los límites declarados de la matriz en línea es un error en tiempo de compilación.
Cuando la expresión se convierte implícitamente en int
La expresión se convierte a int y luego el acceso al elemento se interpreta como se describe en la sección Cuando el tipo de expresión es int.
Cuando la expresión se puede convertir implícitamente en System.Index
La expresión se convierte en System.Index
, que después se transforma en un valor de índice basado en entero, tal como se describe en https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#implicit-index-support, suponiendo que la longitud de la colección se conoce en el momento de compilación y es igual a la cantidad de elementos del tipo de arreglo en línea del primary_no_array_creation_expression. A continuación, el acceso al elemento se interpreta como se describe en la sección Cuando el tipo de expresión es int.
Cuando la expresión se puede convertir implícitamente en System.Range
Si primary_no_matriz_creation_expression es una variable con capacidad de escritura, el resultado de evaluar un acceso a un elemento de matriz insertada es un valor equivalente a invocar public Span<T> Slice (int start, int length)
a una instancia de System.Span<T>
devuelto por el método System.Span<T> InlineArrayAsSpan
en primary_no_matriz_creation_expression.
Para el análisis de ref-safety, el ref-safe-context/contexto seguro del acceso es equivalente al contexto de una invocación de un método con la firma static System.Span<T> GetSlice(ref InlineArrayType array)
.
Si primary_no_matriz_creation_expression es una variable de solo lectura, el resultado de evaluar un acceso a un elemento de matriz insertada es un valor equivalente a invocar public ReadOnlySpan<T> Slice (int start, int length)
a una instancia de System.ReadOnlySpan<T>
devuelto por el método System.ReadOnlySpan<T> InlineArrayAsReadOnlySpan
en primary_no_matriz_creation_expression.
Para el análisis de ref-safety, el ref-safe-context/contexto seguro del acceso es equivalente al contexto de una invocación de un método con la firma static System.ReadOnlySpan<T> GetSlice(in InlineArrayType array)
.
Si primary_no_matriz_creation_expression es un valor, se reporta un error.
Los argumentos de la invocación del método Slice
se calculan a partir de la expresión de índice convertida a System.Range
, tal como se describe en https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#implicit-range-support, suponiendo que la longitud de la colección se conoce en el momento de la compilación y es igual a la cantidad de elementos del tipo de matriz en línea de la expresión primaria_sin_creación_de_matriz .
El compilador puede omitir la llamada Slice
si se conoce en tiempo de compilación que start
es 0 y length
es menor o igual que la cantidad de elementos del tipo de matriz insertada. El compilador también puede informar de un error si se sabe en tiempo de compilación que el corte sale de los límites de la matriz insertada.
Por ejemplo:
void M1(Buffer10<int> x)
{
System.Span<int> a = x[..]; // Ok, equivalent to `System.Span<int> a = InlineArrayAsSpan<Buffer10<int>, int>(ref x, 10).Slice(0, 10)`
}
void M2(in Buffer10<int> x)
{
System.ReadOnlySpan<int> a = x[..]; // Ok, equivalent to `System.ReadOnlySpan<int> a = InlineArrayAsReadOnlySpan<Buffer10<int>, int>(in x, 10).Slice(0, 10)`
System.Span<int> b = x[..]; // An error, System.ReadOnlySpan<int> cannot be converted to System.Span<int>
}
Buffer10<int> GetBuffer() => default;
void M3()
{
_ = GetBuffer()[..]; // An error, `GetBuffer()` is a value
}
Conversiones
Se agregará una nueva conversión: una conversión de matriz en línea derivada de una expresión. La conversión de matriz insertada es una conversión estándar.
Hay una conversión implícita de la expresión de un tipo de matriz en línea a los siguientes tipos:
System.Span<T>
System.ReadOnlySpan<T>
Sin embargo, convertir una variable de solo lectura en System.Span<T>
o convertir un valor en cualquier tipo es un error.
Por ejemplo:
void M1(Buffer10<int> x)
{
System.ReadOnlySpan<int> a = x; // Ok, equivalent to `System.ReadOnlySpan<int> a = InlineArrayAsReadOnlySpan<Buffer10<int>, int>(in x, 10)`
System.Span<int> b = x; // Ok, equivalent to `System.Span<int> b = InlineArrayAsSpan<Buffer10<int>, int>(ref x, 10)`
}
void M2(in Buffer10<int> x)
{
System.ReadOnlySpan<int> a = x; // Ok, equivalent to `System.ReadOnlySpan<int> a = InlineArrayAsReadOnlySpan<Buffer10<int>, int>(in x, 10)`
System.Span<int> b = x; // An error, readonly mismatch
}
Buffer10<int> GetBuffer() => default;
void M3()
{
System.ReadOnlySpan<int> a = GetBuffer(); // An error, ref-safety
System.Span<int> b = GetBuffer(); // An error, ref-safety
}
A efectos del análisis de seguridad de referencia, el contexto seguro de la conversión es equivalente al contexto seguro de una invocación de un método con la firma static System.Span<T> Convert(ref InlineArrayType array)
o static System.ReadOnlySpan<T> Convert(in InlineArrayType array)
.
Patrones de lista
Los patrones de lista no se admitirán para instancias de tipos de matrices en línea.
Comprobación de asignaciones definitivas
Las reglas de asignación definitiva normales son aplicables a las variables que tienen un tipo de matriz insertada.
Literales de colección
Una instancia de un tipo de matriz insertada es una expresión válida en un spread_element.
La característica siguiente no se distribuyó en C# 12. Sigue siendo una propuesta abierta. El código de este ejemplo genera CS9174:
Un tipo de matriz insertada es un tipo de destino de colección construible válido para una expresión de colección. Por ejemplo:
Buffer10<int> b = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // initializes user-defined inline array
La longitud del literal de colección debe coincidir con la longitud del tipo de matriz insertada de destino. Si la longitud del literal se conoce en tiempo de compilación y no coincide con la longitud de destino, se notifica un error. De lo contrario, se lanzará una excepción durante el tiempo de ejecución cuando se detecte la falta de coincidencia. El tipo de excepción exacto es TBD. Algunos candidatos son: System.NotSupportedException, System.InvalidOperationException.
Validación de las aplicaciones InlineArrayAttribute
El compilador validará los siguientes aspectos de las aplicaciones InlineArrayAttribute:
- El tipo de destino no es una estructura de registro
- El tipo de destino solo tiene un campo.
- Longitud especificada > 0
- La estructura de destino no tiene un diseño explícito especificado.
Elementos de matriz en línea en un inicializador de objeto
De forma predeterminada, no se admitirá la inicialización de elementos a través de initializer_target de formulario '[' argument_list ']'
(consulte https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#128173-object-initializers):
static C M2() => new C() { F = {[0] = 111} }; // error CS1913: Member '[0]' cannot be initialized. It is not a field or property.
class C
{
public Buffer10<int> F;
}
Sin embargo, si el tipo de matriz insertada define explícitamente el indexador adecuado, el inicializador de objetos lo usará:
static C M2() => new C() { F = {[0] = 111} }; // Ok, indexer is invoked
class C
{
public Buffer10<int> F;
}
[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer10<T>
{
private T _element0;
public T this[int i]
{
get => this[i];
set => this[i] = value;
}
}
La instrucción foreach
La instrucción foreach se ajustará para permitir el uso de un tipo de matriz insertada como colección en una sentencia foreach.
Por ejemplo:
foreach (var a in getBufferAsValue())
{
WriteLine(a);
}
foreach (var b in getBufferAsWritableVariable())
{
WriteLine(b);
}
foreach (var c in getBufferAsReadonlyVariable())
{
WriteLine(c);
}
Buffer10<int> getBufferAsValue() => default;
ref Buffer10<int> getBufferAsWritableVariable() => default;
ref readonly Buffer10<int> getBufferAsReadonlyVariable() => default;
es equivalente a:
Buffer10<int> temp = getBufferAsValue();
foreach (var a in (System.ReadOnlySpan<int>)temp)
{
WriteLine(a);
}
foreach (var b in (System.Span<int>)getBufferAsWritableVariable())
{
WriteLine(b);
}
foreach (var c in (System.ReadOnlySpan<int>)getBufferAsReadonlyVariable())
{
WriteLine(c);
}
Se admitirá foreach
en matrices insertadas, incluso si comienza siendo restringido en los métodos async
debido a la participación de los tipos span en la traducción.
Preguntas de diseño abiertas
Alternativas
Sintaxis de tipos de matriz insertada
La gramática en https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/types.md#821-general se ajustará de la siguiente manera:
array_type
: non_array_type rank_specifier+
;
rank_specifier
: '[' ','* ']'
+ | '[' constant_expression ']'
;
El tipo del constant_expression debe convertirse implícitamente en el tipo int
y el valor debe ser un entero positivo distinto de cero.
La parte pertinente de la sección https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/arrays.md#1721-general se ajustará de la manera siguiente.
Las producciones gramaticales para tipos matriz se proporcionan en https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/types.md#821-general.
Un tipo de matriz se escribe como un non_matriz_type seguido de uno o varios rank_specifiers.
Un non_matriz_type es cualquier tipo de que no es un matriz_type.
El rango de un tipo de matriz lo proporciona el rank_specifier situado más a la izquierda en el matriz_type: un rank_specifier indica que la matriz es una matriz con un rango de uno más el número de tokens ",
" en el rank_specifier.
El tipo de elemento de un tipo de matriz es el tipo que resulta de eliminar el especificador_de_rango más a la izquierda:
- Un tipo de matriz de la forma
T[ constant_expression ]
es un tipo de matriz en línea anónimo con longitud indicada por constant_expression y un tipo de elemento que no es una matrizT
. - Un tipo de matriz del formulario
T[ constant_expression ][R₁]...[Rₓ]
es un tipo de matriz insertado anónimo con longitud indicada por constant_expression y un tipo de elementoT[R₁]...[Rₓ]
. - Un tipo de matriz del formulario
T[R]
(donde R no es un constant_expression) es un tipo de matriz normal con rangoR
y un tipo de elemento que no es de matrizT
. - Un tipo de matriz del formulario
T[R][R₁]...[Rₓ]
(donde R no es un constant_expression) es un tipo de matriz normal con rankR
y un tipo de elementoT[R₁]...[Rₓ]
.
En efecto, los rank_specifierse leen de izquierda a derecha antes de el tipo de elemento no matriz final.
Ejemplo: el tipo de
int[][,,][,]
es una matriz unidimensional de matrices tridimensionales de matrices bidimensionales deint
. ejemplo final
En tiempo de ejecución, un valor de un tipo de matriz normal puede ser null
o una referencia a una instancia de ese tipo de matriz.
Nota: siguiendo las reglas de https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/arrays.md#176-array-covariance, el valor también puede ser una referencia a un tipo de matriz covariante. nota final
Un tipo de matriz insertada anónima es un tipo de matriz insertada sintetizado por el compilador con accesibilidad interna. El tipo de elemento debe ser un tipo que se puede usar como argumento de tipo. A diferencia de un tipo de matriz insertado declarado explícitamente, no se puede hacer referencia a un tipo de matriz insertado anónimo por nombre, solo se puede hacer referencia a él mediante array_type sintaxis. En el contexto del mismo programa, dos tipos de matriz insertada matriz_type que denotan el mismo tipo de elemento y de la misma longitud, se refieren al mismo tipo de matriz insertada anónima.
Además de la accesibilidad interna, el compilador impedirá el uso de APIs que utilizan tipos de matriz en línea anónimos a través de las fronteras del ensamblado mediante un modificador personalizado obligatorio (tipo exacto por determinar) aplicado a una referencia de tipo de matriz en línea anónima en la firma.
Expresiones de creación de matrices
Expresiones de creación de matrices
array_creation_expression
: 'new' non_array_type '[' expression_list ']' rank_specifier*
array_initializer?
| 'new' array_type array_initializer
| 'new' rank_specifier array_initializer
;
Dada la gramática actual, el uso de un constant_expression en lugar de la expression_list significa asignar un tipo de matriz unidimensional normal de la longitud especificada. Por lo tanto, matriz_creation_expression seguirá representando la asignación de una matriz regular.
Sin embargo, la nueva forma de la rank_specifier podría usarse para incorporar un tipo de matriz anónima en línea en el tipo de elemento de la matriz asignada.
Por ejemplo, las expresiones siguientes crean una matriz regular de longitud 2 con un tipo de elemento de un tipo de matriz insertada anónima con tipo de elemento int y longitud 5:
new int[2][5];
new int[][5] {default, default};
new [] {default(int[5]), default(int[5])};
Inicializadores de matriz
Los inicializadores de matriz no se implementaron en C# 12. Esta sección sigue siendo una propuesta activa.
La sección Inicializadores de matriz se ajustará para permitir el uso de array_initializer para inicializar tipos de matriz en línea (sin necesidad de cambios en la sintaxis).
array_initializer
: '{' variable_initializer_list? '}'
| '{' variable_initializer_list ',' '}'
;
variable_initializer_list
: variable_initializer (',' variable_initializer)*
;
variable_initializer
: expression
| array_initializer
;
El tipo de destino debe proporcionar explícitamente la longitud de la matriz en línea.
Por ejemplo:
int[5] a = {1, 2, 3, 4, 5}; // initializes anonymous inline array of length 5
Buffer10<int> b = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // initializes user-defined inline array
var c = new int[][] {{11, 12}, {21, 22}, {31, 32}}; // An error for the nested array initializer
var d = new int[][2] {{11, 12}, {21, 22}, {31, 32}}; // An error for the nested array initializer
Diseño detallado (opción 2)
Tenga en cuenta que, a efectos de esta propuesta, un término "búfer de tamaño fijo" hace referencia a una característica propuesta de "búfer de tamaño fijo seguro" en lugar de a un búfer descrito en https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/unsafe-code.md#238-fixed-size-buffers.
En este diseño, los tipos de buffer de tamaño fijo no reciben un tratamiento especial general por parte del lenguaje. Hay una sintaxis especial para declarar miembros que representan buffers de tamaño fijo y nuevas reglas sobre el consumo de esos miembros. No son campos desde el punto de vista del idioma.
La gramática de variable_declarator en https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/classes.md#155-fields se extenderá para permitir especificar el tamaño del búfer:
field_declaration
: attributes? field_modifier* type variable_declarators ';'
;
field_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'readonly'
| 'volatile'
| unsafe_modifier // unsafe code support
;
variable_declarators
: variable_declarator (',' variable_declarator)*
;
variable_declarator
: identifier ('=' variable_initializer)?
+ | fixed_size_buffer_declarator
;
fixed_size_buffer_declarator
: identifier '[' constant_expression ']'
;
Un fixed_size_buffer_declarator introduce un búfer de tamaño fijo de un tipo de elemento determinado.
El tipo de elemento de búfer es el tipo especificado en field_declaration
. Un declarador de búfer de tamaño fijo introduce un nuevo miembro y consta de un identificador que asigna un nombre al miembro, seguido de una expresión constante encerrada en los tokens [
y ]
. La expresión constante denota el número de elementos en el miembro introducido por ese declarador de búfer de tamaño fijo. El tipo de la expresión constante debe convertirse implícitamente en el tipo int
y el valor debe ser un entero positivo distinto de cero.
Los elementos de un búfer de tamaño fijo se colocarán secuencialmente en la memoria como si fueran elementos de una matriz.
Un field_declaration con un fixed_size_buffer_declarator en una interfaz debe tener un modificador static
.
Dependiendo de la situación (los detalles se especifican a continuación), un acceso a un miembro de búfer de tamaño fijo se clasifica como un valor (nunca una variable) de System.ReadOnlySpan<S>
o System.Span<S>
, donde S es el tipo de elemento del búfer de tamaño fijo. Ambos tipos proporcionan indexadores que devuelven una referencia a un elemento específico con la "condición de solo lectura" adecuada, lo que impide la asignación directa a los elementos cuando las reglas del lenguaje no lo permiten.
Esto limita el conjunto de tipos que se pueden usar como un tipo de elemento de búfer de tamaño fijo a tipos que se pueden usar como argumentos de tipo. Por ejemplo, un tipo de puntero no se puede usar como un tipo de elemento.
La instancia de intervalo resultante tendrá una longitud igual al tamaño declarado en el búfer de tamaño fijo. Indexar en el span con una expresión constante fuera de los límites declarados del buffer de tamaño fijo es un error en tiempo de compilación.
El contexto seguro del valor será igual al contexto seguro del contenedor, al igual que ocurriría si se accediera a los datos de respaldo como un campo.
Búferes de tamaño fijo en expresiones
La búsqueda de un miembro de una memoria intermedia de tamaño fijo se realiza exactamente igual que la búsqueda de un campo.
Se puede hacer referencia a un búfer de tamaño fijo en una expresión mediante un simple_name o un member_access.
Cuando se hace referencia a un miembro del búfer de tamaño fijo de instancia como un nombre simple, el efecto es el mismo que un acceso a un miembro de la forma this.I
, donde I
es el miembro del búfer de tamaño fijo. Cuando se hace referencia a un miembro estático de búfer de tamaño fijo como un nombre simple, el efecto es el mismo que un acceso de miembro del formulario E.I
, donde I
es el miembro de búfer de tamaño fijo y E
es el tipo declarativo.
Búferes de tamaño fijo no legibles
En un acceso de miembro del formulario E.I
, si E
es de un tipo struct y una búsqueda de miembros de I
en ese tipo de estructura identifica un miembro de tamaño fijo de instancia no legible, E.I
se evalúa y clasifica de la siguiente manera:
- Si
E
se clasifica como un valor,E.I
solo se puede usar como un primary_no_matriz_creation_expression de un acceso a elementos con índice de tipoSystem.Index
o de un tipo convertible implícitamente a int. El resultado del acceso al elemento es un miembro de tamaño fijo en la posición especificada, clasificado como un valor. - De lo contrario, si
E
se clasifica como una variable readonly y el resultado de la expresión se clasifica como un valor de tipoSystem.ReadOnlySpan<S>
, donde S es el tipo de elemento deI
. El valor se puede usar para tener acceso a los elementos del miembro. - De lo contrario,
E
se clasifica como una variable grabable y el resultado de la expresión se clasifica como un valor de tipoSystem.Span<S>
, donde S es el tipo de elemento deI
. El valor se puede usar para tener acceso a los elementos del miembro.
En un acceso de miembro de la forma E.I
, si E
es de un tipo de clase y una búsqueda de miembros de I
en ese tipo de clase identifica un miembro de instancia de tamaño fijo no modificable, entonces E.I
se evalúa y clasifica como un valor de tipo System.Span<S>
, donde S es el tipo de elemento de I
.
En un acceso de miembro del formulario E.I
, si la búsqueda de miembros de I
identifica un miembro de tamaño fijo estático no legible, E.I
se evalúa y clasifica como un valor de tipo System.Span<S>
, donde S es el tipo de elemento de I
.
Búferes de tamaño fijo de solo lectura
Cuando un field_declaration incluye el modificador readonly
, el miembro introducido por el fixed_size_buffer_declarator es un búfer de tamaño fijo de solo lectura.
Las asignaciones directas a elementos de un búfer de tamaño fijo de solo lectura solo pueden ocurrir en un constructor de instancia, un miembro de inicialización o un constructor estático del mismo tipo.
En concreto, las asignaciones directas a un elemento del búfer de tamaño fijo de solo lectura solo se permiten en los contextos siguientes:
- Para un miembro de instancia, en los constructores de instancia o miembro init del tipo que contiene la declaración de miembro; para un miembro estático, en el constructor estático del tipo que contiene la declaración de miembro. Estos también son los únicos contextos en los que es válido pasar un elemento de búfer de tamaño fijo de solo lectura como un parámetro
out
oref
.
Intentar asignar a un elemento de un búfer de tamaño fijo de solo lectura o pasarlo como un parámetro out
o ref
en cualquier otro contexto es un error en tiempo de compilación.
Esto se logra mediante lo siguiente.
Un acceso de miembro para un búfer de tamaño fijo de solo lectura se evalúa y clasifica de la siguiente manera:
- En un acceso de miembro del formulario
E.I
, siE
es de tipo struct yE
está clasificado como valor,E.I
solo puede usarse como una primary_no_matriz_creation_expression de un acceso a elemento con índice de tipoSystem.Index
, o de un tipo que se pueda convertir implícitamente a int. El resultado del acceso al elemento es un elemento de tamaño fijo en la posición especificada, clasificado como valor. - Si se produce acceso en un contexto en el que se permiten asignaciones directas a un elemento de búfer de tamaño fijo de solo lectura, el resultado de la expresión se clasifica como un valor de tipo
System.Span<S>
, donde S es el tipo de elemento del búfer de tamaño fijo. El valor se puede usar para tener acceso a los elementos del miembro. - De lo contrario, la expresión se clasifica como un valor de tipo
System.ReadOnlySpan<S>
, donde S es el tipo de elemento del búfer de tamaño fijo. El valor se puede usar para tener acceso a los elementos del miembro.
Comprobación de asignaciones definitivas
Los búferes de tamaño fijo no están sujetos a la comprobación de asignaciones definitivas y los miembros del búfer de tamaño fijo se omiten con fines de comprobación de asignación definitiva de variables de tipo struct.
Cuando un miembro de búfer de tamaño fijo es estático o la variable de estructura más externa de un miembro de búfer de tamaño fijo es una variable estática, una variable de instancia de una instancia de clase o un elemento de matriz, los elementos del búfer de tamaño fijo se inicializan automáticamente a sus valores predeterminados. En todos los demás casos, el contenido inicial de un búfer de tamaño fijo no está definido.
Metadatos
Emisión de metadatos y generación de código
El compilador para codificación de metadatos se basará en el System.Runtime.CompilerServices.InlineArrayAttribute
añadido recientemente.
Búferes de tamaño fijo como el pseudocódigo siguiente:
// Not valid C#
public partial class C
{
public int buffer1[10];
public readonly int buffer2[10];
}
se emitirán como campos de un tipo struct especialmente decorado.
El código de C# equivalente será:
public partial class C
{
public Buffer10<int> buffer1;
public readonly Buffer10<int> buffer2;
}
[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer10<T>
{
private T _element0;
[UnscopedRef]
public System.Span<T> AsSpan()
{
return System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref _element0, 10);
}
[UnscopedRef]
public readonly System.ReadOnlySpan<T> AsReadOnlySpan()
{
return System.Runtime.InteropServices.MemoryMarshal.CreateReadOnlySpan(
ref System.Runtime.CompilerServices.Unsafe.AsRef(in _element0), 10);
}
}
Las convenciones de nomenclatura reales para el tipo y sus miembros están por determinar. Es probable que el marco incluya un conjunto de tipos predefinidos de "búfer" que cubran un conjunto limitado de tamaños de búfer. Cuando no existe un tipo predefinido, el compilador lo sintetizará en el módulo que se va a compilar. Los nombres de los tipos generados serán fáciles de pronunciar para ser utilizados en otros idiomas.
Código generado para un acceso como:
public partial class C
{
void M1(int val)
{
buffer1[1] = val;
}
int M2()
{
return buffer2[1];
}
}
será equivalente a:
public partial class C
{
void M1(int val)
{
buffer.AsSpan()[1] = val;
}
int M2()
{
return buffer2.AsReadOnlySpan()[1];
}
}
Importación de metadatos
Cuando el compilador importa una declaración de campo de tipo T y se cumplen todas las condiciones siguientes:
- T es un tipo de estructura decorado con el atributo
InlineArray
y - El primer campo de instancia declarado en T tiene el tipo Fy
- Hay un
public System.Span<F> AsSpan()
dentro de T y - Hay un
public readonly System.ReadOnlySpan<T> AsReadOnlySpan()
opublic System.ReadOnlySpan<T> AsReadOnlySpan()
dentro de T.
El campo se tratará como un búfer de tamaño fijo de C# con tipo de elemento F. De lo contrario, el campo se considerará como un campo normal de tipo T.
Método o grupo de propiedades similar a un enfoque en el lenguaje
Una idea es tratar a estos elementos más como grupos de métodos, ya que no son automáticamente un valor por sí mismos, sino que se pueden convertir en uno si es necesario. Así es como funcionaría esto:
- Los accesos seguros a búferes de tamaño fijo tienen su propia clasificación (igual que, por ejemplo, los grupos de métodos y las lambdas)
- Se pueden indexar directamente como una operación lingüística (no a través de tipos de rango) para generar una variable (que es de solo lectura si el búfer está en un contexto de solo lectura, al igual que los campos de una estructura).
- Tienen conversiones implícitas de expresión a
Span<T>
yReadOnlySpan<T>
, pero el uso de la primera es un error si están en un contexto de solo lectura. - Su tipo natural es
ReadOnlySpan<T>
, por lo que eso es lo que aportan si participan en inferencias de tipos (por ejemplo, var, tipo más común o genérico).
Búferes de tamaño fijo de C/C++
C/C++ tiene una noción diferente de búferes de tamaño fijo. Por ejemplo, hay una noción de "búferes de tamaño fijo de longitud cero", que a menudo se usa como una manera de indicar que los datos son "longitud variable". No es un objetivo de esta propuesta poder interoperar con eso.
Reuniones LDM
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-04-03.md#fixed-size-buffers
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-04-10.md#fixed-size-buffers
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-01.md#fixed-size-buffers
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#inline-arrays
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-17.md#inline-arrays
- https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-06-17.md#inline-arrays-as-record-structs
C# feature specifications