Rangos
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
Esta característica consiste en entregar dos nuevos operadores que permiten construir objetos System.Index
y System.Range
, y usarlos para indexar o segmentar colecciones en tiempo de ejecución.
Visión general
Tipos y miembros conocidos
Para usar los nuevos formularios sintácticos para System.Index
y System.Range
, pueden ser necesarios nuevos tipos y miembros conocidos, dependiendo de qué formas sintácticas se usen.
Para usar el operador "hat" (^
), se requiere lo siguiente:
namespace System
{
public readonly struct Index
{
public Index(int value, bool fromEnd);
}
}
Para usar el tipo System.Index
como argumento en un acceso de elemento de matriz, se requiere el siguiente miembro:
int System.Index.GetOffset(int length);
La sintaxis ..
para System.Range
requerirá el tipo System.Range
, así como uno o varios de los miembros siguientes:
namespace System
{
public readonly struct Range
{
public Range(System.Index start, System.Index end);
public static Range StartAt(System.Index start);
public static Range EndAt(System.Index end);
public static Range All { get; }
}
}
La sintaxis ..
permite que uno, ambos o ninguno de sus argumentos esté ausente. Independientemente del número de argumentos, el constructor Range
siempre es suficiente para usar la sintaxis Range
. Sin embargo, si cualquiera de los demás miembros está presente y faltan uno o varios de los argumentos de ..
, el miembro adecuado podría ser sustituido.
Por último, para que un valor de tipo System.Range
se use en una expresión de acceso de elemento de matriz, el siguiente miembro debe estar presente:
namespace System.Runtime.CompilerServices
{
public static class RuntimeHelpers
{
public static T[] GetSubArray<T>(T[] array, System.Range range);
}
}
System.Index
C# no tiene forma de indexar una colección desde el final, sino que la mayoría de los indizadores usan la noción "from start" o realizan una expresión "length - i". Presentamos una nueva expresión Index que significa "desde el final". La característica introducirá un nuevo operador unario de prefijo "hat". Su operando único debe ser convertible a System.Int32
. Será dirigida a la llamada adecuada del método de fábrica System.Index
.
Aumentamos la gramática para unary_expression con la siguiente forma de sintaxis adicional:
unary_expression
: '^' unary_expression
;
Lo llamamos operador de índice desde el final. Los operadores de índice desde el final predefinidos son los siguientes:
System.Index operator ^(int fromEnd);
El comportamiento de este operador solo se define para los valores de entrada mayores o iguales que cero.
Ejemplos:
var array = new int[] { 1, 2, 3, 4, 5 };
var thirdItem = array[2]; // array[2]
var lastItem = array[^1]; // array[new Index(1, fromEnd: true)]
System.Range
C# no tiene ninguna manera sintáctica de acceder a "intervalos" o "segmentos" de colecciones. Normalmente, los usuarios se ven obligados a implementar estructuras complejas para filtrar o operar en segmentos de memoria, o recurrir a métodos LINQ como list.Skip(5).Take(2)
. Con la adición de System.Span<T>
y otros tipos similares, es más importante tener este tipo de operación compatible con un nivel más profundo en el lenguaje o tiempo de ejecución, y tener la interfaz unificada.
El lenguaje introducirá un nuevo operador de intervalo x..y
. Es un operador de infijo binario que acepta dos expresiones. Se puede omitir cualquiera de los operandos (ejemplos a continuación) y deben ser convertibles a System.Index
. Será dirigida a la llamada adecuada del método de fábrica System.Range
.
Reemplazamos las reglas gramaticales de C# para multiplicative_expression por las siguientes (para introducir un nuevo nivel de precedencia):
range_expression
: unary_expression
| range_expression? '..' range_expression?
;
multiplicative_expression
: range_expression
| multiplicative_expression '*' range_expression
| multiplicative_expression '/' range_expression
| multiplicative_expression '%' range_expression
;
Todas las formas del operador de intervalo tienen la misma prioridad. Este nuevo grupo de precedencia es inferior que los operadores unarios y superior a los operadores aritméticos multiplicativos .
Denominamos al operador ..
como el operador de rango . El operador de intervalo integrado se puede entender aproximadamente para corresponder a la invocación de un operador integrado de este formato:
System.Range operator ..(Index start = 0, Index end = ^0);
Ejemplos:
var array = new int[] { 1, 2, 3, 4, 5 };
var slice1 = array[2..^3]; // array[new Range(2, new Index(3, fromEnd: true))]
var slice2 = array[..^3]; // array[Range.EndAt(new Index(3, fromEnd: true))]
var slice3 = array[2..]; // array[Range.StartAt(2)]
var slice4 = array[..]; // array[Range.All]
Además, System.Index
debe tener una conversión implícita de System.Int32
con el fin de evitar la necesidad de sobrecargar al mezclar enteros e índices sobre firmas multidimensionales.
Adición de compatibilidad con índices y intervalos a los tipos de biblioteca existentes
Compatibilidad con índices implícitos
El lenguaje proporcionará un miembro de indexador de instancia con un único parámetro de tipo Index
para los tipos que cumplen los siguientes criterios:
- El tipo es Countable.
- El tipo tiene un indexador de instancia accesible que toma un único
int
como argumento. - El tipo no tiene un indexador de instancia accesible que toma un
Index
como primer parámetro. ElIndex
debe ser el único parámetro o los parámetros restantes deben ser opcionales.
Un tipo es Countable si tiene una propiedad denominada Length
o Count
con un captador accesible y un tipo de valor devuelto de int
. El lenguaje puede hacer uso de esta propiedad para convertir una expresión de tipo Index
en un int
en el punto de la expresión sin necesidad de usar el tipo Index
en absoluto. En caso de que Length
y Count
estén presentes, se prefiere Length
. Para simplificar en el futuro, la propuesta usará el nombre Length
para representar Count
o Length
.
Para estos tipos, el lenguaje actuará como si hubiera un miembro de indexador con formato T this[Index index]
donde T
es el tipo de valor devuelto del indexador basado en int
, incluidas las anotaciones de estilo ref
. El nuevo miembro tendrá los mismos get
y set
miembros con la misma accesibilidad que el indexador int
.
El nuevo indexador se implementará convirtiendo el argumento de tipo Index
en un int
y emitiendo una llamada al indexador basado en int
. Para fines de discusión, vamos a usar el ejemplo de receiver[expr]
. La conversión de expr
a int
se producirá de la siguiente manera:
- Cuando el argumento tiene el formato
^expr2
y el tipo deexpr2
esint
, se traducirá areceiver.Length - expr2
. - De lo contrario, se traducirá como
expr.GetOffset(receiver.Length)
.
Independientemente de la estrategia de conversión específica, el orden de evaluación debe ser equivalente a lo siguiente:
-
receiver
se evalúa; -
expr
se evalúa; -
length
se evaluará, si es necesario; - se invoca el indexador basado en
int
.
Esto permite a los desarrolladores usar la característica Index
en tipos existentes sin necesidad de modificación. Por ejemplo:
List<char> list = ...;
var value = list[^1];
// Gets translated to
var value = list[list.Count - 1];
Las expresiones receiver
y Length
se guardarán en memoria según sea necesario para garantizar que los efectos secundarios solo se ejecuten una vez. Por ejemplo:
class Collection {
private int[] _array = new[] { 1, 2, 3 };
public int Length {
get {
Console.Write("Length ");
return _array.Length;
}
}
public int this[int index] => _array[index];
}
class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}
void Use() {
int i = Get()[^1];
Console.WriteLine(i);
}
}
Este código imprimirá "Get Length 3".
Compatibilidad con intervalos implícitos
El lenguaje proporcionará un miembro de indexador de instancia con un único parámetro de tipo Range
para los tipos que cumplen los siguientes criterios:
- El tipo es Countable.
- El tipo tiene un miembro accesible denominado
Slice
que tiene dos parámetros de tipoint
. - El tipo no tiene un indexador de instancia que toma un único
Range
como primer parámetro. ElRange
debe ser el único parámetro o los parámetros restantes deben ser opcionales.
Para estos tipos, el lenguaje enlazará como si hubiera un miembro de indexador con formato T this[Range range]
donde T
es el tipo de valor devuelto del método Slice
, incluidas las anotaciones de estilo ref
. El nuevo miembro también tendrá accesibilidad equivalente con Slice
.
Cuando el indexador basado en Range
está enlazado en una expresión denominada receiver
, se reducirá convirtiendo la expresión de Range
en dos valores que luego se pasan al método Slice
. Para fines de discusión, vamos a usar el ejemplo de receiver[expr]
.
El primer argumento de Slice
se obtendrá convirtiendo la expresión con tipo de intervalo de la siguiente manera:
- Cuando
expr
tiene el formatoexpr1..expr2
(donde se puede omitirexpr2
) yexpr1
tiene el tipoint
, se emitirá comoexpr1
. - Cuando
expr
es del formulario^expr1..expr2
(donde se puede omitirexpr2
), se emitirá comoreceiver.Length - expr1
. - Cuando
expr
es del formulario..expr2
(donde se puede omitirexpr2
), se emitirá como0
. - De lo contrario, se emitirá como
expr.Start.GetOffset(receiver.Length)
.
Este valor se volverá a usar en el cálculo del segundo argumento Slice
. Al hacerlo, se denominará start
. El segundo argumento de Slice
se obtendrá convirtiendo la expresión con tipo de intervalo de la siguiente manera:
- Cuando
expr
tiene el formatoexpr1..expr2
(donde se puede omitirexpr1
) yexpr2
tiene el tipoint
, se emitirá comoexpr2 - start
. - Cuando
expr
es del formularioexpr1..^expr2
(donde se puede omitirexpr1
), se emitirá como(receiver.Length - expr2) - start
. - Cuando
expr
es del formularioexpr1..
(donde se puede omitirexpr1
), se emitirá comoreceiver.Length - start
. - De lo contrario, se emitirá como
expr.End.GetOffset(receiver.Length) - start
.
Independientemente de la estrategia de conversión específica, el orden de evaluación debe ser equivalente a lo siguiente:
-
receiver
se evalúa; -
expr
se evalúa; -
length
se evaluará, si es necesario; - se invoca el método
Slice
.
Las expresiones receiver
, expr
y length
se guardarán en memoria según sea necesario para garantizar que los efectos secundarios solo se ejecuten una vez. Por ejemplo:
class Collection {
private int[] _array = new[] { 1, 2, 3 };
public int Length {
get {
Console.Write("Length ");
return _array.Length;
}
}
public int[] Slice(int start, int length) {
var slice = new int[length];
Array.Copy(_array, start, slice, 0, length);
return slice;
}
}
class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}
void Use() {
var array = Get()[0..2];
Console.WriteLine(array.Length);
}
}
Este código imprimirá "Get Length 2".
El idioma tratará específicamente los siguientes tipos conocidos:
string
: el métodoSubstring
se usará en lugar deSlice
.array
: el métodoSystem.Runtime.CompilerServices.RuntimeHelpers.GetSubArray
se usará en lugar deSlice
.
Alternativas
Los nuevos operadores (^
y ..
) son azúcar sintáctica. La funcionalidad se puede implementar mediante llamadas explícitas a los métodos de fábrica System.Index
y System.Range
, pero dará lugar a un código mucho más repetitivo, y la experiencia será poco intuitiva.
Representación de IL
Estos dos operadores se reducirán a las llamadas normales de indexador o método, sin cambios en las capas posteriores del compilador.
Comportamiento en tiempo de ejecución
- El compilador puede optimizar los indexadores para tipos integrados, como matrices y cadenas, y reducir la indexación a los métodos existentes adecuados.
-
System.Index
dará un error si se construye con un valor negativo. -
^0
no se inicia, pero se traduce a la longitud de la colección o enumerable a la que se proporciona. Range.All
es semánticamente equivalente a0..^0
y se puede descontruir en estos índices.
Consideraciones
Detectar indexable basado en ICollection
La inspiración de este comportamiento fueron los inicializadores de colección. Usar la estructura de un tipo para transmitir que ha optado por una característica. En el caso de los inicializadores de colección, los tipos pueden optar por la característica implementando la interfaz IEnumerable
(no genérica).
Esta propuesta requería inicialmente que los tipos implementaran ICollection
para ser considerados Indexable. Sin embargo, eso requería una serie de casos especiales:
ref struct
: Aún no pueden implementar interfaces; sin embargo, tipos comoSpan<T>
son ideales para la compatibilidad con índices o intervalos.string
: no implementaICollection
y agregar esa interfaz tiene un gran costo.
Esto significa que para admitir tipos clave ya es necesario un caso especial. El manejo especial de string
es menos interesante, ya que el lenguaje lo hace en otras áreas (conversión a minúsculas deforeach
, constantes, etc.). El manejo especial de ref struct
es más preocupante, ya que es un manejo especial de toda una clase de tipos. Se etiquetan como Indexable si simplemente tienen una propiedad denominada Count
con un tipo de valor devuelto de int
.
Después de considerar el diseño se normalizó para decir que cualquier tipo que tenga una propiedad Count
/ Length
con un tipo de valor devuelto de int
es Indexable. Esto elimina todo el tratamiento especial, incluso para string
y matrices.
Detección de recuento justo
La detección en los nombres de propiedad Count
o Length
sí complica un poco el diseño. Elegir solo uno para estandarizar aunque no es suficiente, ya que termina excluyendo un gran número de tipos:
- Usar
Length
: prácticamente excluye todas las colecciones en System.Collections y sus subespacios de nombres. Los que tienden a derivar deICollection
y, por tanto, prefierenCount
sobre la longitud. - Usar
Count
: excluyestring
, matrices,Span<T>
y la mayoría de los tipos basados enref struct
La complicación adicional de la detección inicial de tipos indexables es superada por su simplificación en otros aspectos.
Elección de Slice como nombre
El nombre Slice
fue elegido ya que es el nombre estándar de facto para las operaciones de estilo de corte en .NET. A partir de netcoreapp2.1, todos los tipos de estilo de 'span' usan el nombre Slice
para las operaciones de segmentación. Antes de netcoreapp2.1, realmente no hay ejemplos de segmentación para buscar un ejemplo. Los tipos como List<T>
, ArraySegment<T>
, SortedList<T>
habrían sido ideales para la segmentación, pero el concepto no existía cuando se agregaron tipos.
Por lo tanto, siendo Slice
el único ejemplo, se eligió como nombre.
Conversión de tipos de destino de índice
Otra manera de ver la transformación Index
en una expresión de indexador es como una conversión de tipo de destino. En lugar de enlazar como si hubiera un miembro con el formato return_type this[Index]
, el idioma asigna en su lugar una conversión con tipo de destino a int
.
Este concepto se podría generalizar a todos los accesos de miembros en tipos contables. Cada vez que una expresión con tipo Index
se usa como argumento para una invocación de miembro de instancia y el receptor es Countable, la expresión tendrá una conversión de tipo de destino en int
. Las invocaciones de miembro aplicables a esta conversión incluyen métodos, indexadores, propiedades, métodos de extensión, etc. Solo se excluyen los constructores, ya que no tienen receptor.
La conversión de tipos de destino se implementará de la siguiente manera para cualquier expresión que tenga un tipo de Index
. Con fines de discusión se puede usar el ejemplo de receiver[expr]
:
- Cuando
expr
es de la forma^expr2
y el tipo deexpr2
esint
, se traducirá areceiver.Length - expr2
. - De lo contrario, se traducirá como
expr.GetOffset(receiver.Length)
.
Las expresiones receiver
y Length
se guardarán en memoria según sea necesario para garantizar que los efectos secundarios solo se ejecuten una vez. Por ejemplo:
class Collection {
private int[] _array = new[] { 1, 2, 3 };
public int Length {
get {
Console.Write("Length ");
return _array.Length;
}
}
public int GetAt(int index) => _array[index];
}
class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}
void Use() {
int i = Get().GetAt(^1);
Console.WriteLine(i);
}
}
Este código imprimirá "Longitud obtenida: 3".
Esta característica sería beneficiosa para cualquier miembro que tuviera un parámetro que representase un índice. Por ejemplo, List<T>.InsertAt
. Esto también tiene la posibilidad de confusión, ya que el lenguaje no puede proporcionar instrucciones sobre si una expresión está pensada o no para la indexación. Todo lo que puede hacer es convertir cualquier expresión de Index
en int
al invocar un miembro en un tipo countable.
Restricciones:
- Esta conversión solo es aplicable cuando la expresión con tipo
Index
es directamente un argumento para el miembro. No se aplicaría a ninguna expresión anidada.
Decisiones tomadas durante la implementación
- Todos los miembros del patrón deben ser miembros de instancia
- Si se encuentra un método Length, pero tiene el tipo de valor devuelto incorrecto, continúe buscando Count.
- El indexador usado para el patrón Index debe tener exactamente un parámetro int.
- El método
Slice
usado para el patrón Range debe tener exactamente dos parámetros int. - Al buscar los miembros de patrón, buscamos definiciones originales, no miembros construidos
Reuniones de diseño
C# feature specifications