Gamas
Observação
Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui mudanças de especificação propostas, juntamente com as informações necessárias durante o design e desenvolvimento do recurso. Estes artigos são publicados até que as alterações de especificações propostas sejam finalizadas e incorporadas na especificação ECMA atual.
Pode haver algumas discrepâncias entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da Language Design Meeting (LDM).
Você pode saber mais sobre o processo de adoção de especificações de recursos no padrão de linguagem C# no artigo sobre as especificações .
Questão campeã: https://github.com/dotnet/csharplang/issues/185
Resumo
Esta funcionalidade consiste em apresentar dois novos operadores para construir objetos System.Index
e System.Range
e utilizá-los para indexar/cortar coleções durante a execução.
Visão geral
Tipos e membros bem conhecidos
Para usar as novas formas sintáticas para System.Index
e System.Range
, novos tipos e membros bem conhecidos podem ser necessários, dependendo de quais formas sintáticas são usadas.
Para usar o operador "chapéu" (^
), o seguinte é necessário
namespace System
{
public readonly struct Index
{
public Index(int value, bool fromEnd);
}
}
Para usar o tipo System.Index
como um argumento em um acesso a elementos de matriz, é necessário o seguinte membro:
int System.Index.GetOffset(int length);
A sintaxe ..
para System.Range
exigirá o tipo System.Range
, bem como um ou mais dos seguintes membros:
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; }
}
}
A sintaxe ..
permite que um, ambos, ou nenhum dos seus argumentos esteja ausente. Independentemente do número de argumentos, o construtor Range
é sempre suficiente para usar a sintaxe Range
. No entanto, se algum dos outros membros estiver presente e faltar um ou mais dos ..
argumentos, o membro adequado pode ser substituído.
Finalmente, para que um valor do tipo System.Range
seja usado em uma expressão de acesso a elementos de matriz, o seguinte membro deve estar presente:
namespace System.Runtime.CompilerServices
{
public static class RuntimeHelpers
{
public static T[] GetSubArray<T>(T[] array, System.Range range);
}
}
System.Index
C# não tem como indexar uma coleção a partir do final, mas sim a maioria dos indexadores usam a noção "desde o início", ou fazem uma expressão "length - i". Introduzimos uma nova expressão de índice que significa "a partir do final". O recurso introduzirá um novo operador de prefixo unário "chapéu". Seu operando único deve ser conversível para System.Int32
. Ele será baixado para a chamada de método de fábrica System.Index
apropriada.
Aumentamos a gramática para unary_expression com a seguinte forma de sintaxe adicional:
unary_expression
: '^' unary_expression
;
Chamamos isto de índice do operador final. O índice predefinido dos operadores finais é o seguinte:
System.Index operator ^(int fromEnd);
O comportamento deste operador só é definido para valores de entrada maiores ou iguais a zero.
Exemplos:
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# não tem nenhuma maneira sintática de acessar "intervalos" ou "fatias" de coleções. Normalmente, os usuários são forçados a implementar estruturas complexas para filtrar/operar em fatias de memória, ou recorrer a métodos LINQ como list.Skip(5).Take(2)
. Com a adição de System.Span<T>
e outros tipos semelhantes, torna-se mais importante ter esse tipo de operação suportada em um nível mais profundo na linguagem/tempo de execução, e ter a interface unificada.
A linguagem introduzirá um novo operador de gama x..y
. É um operador de infix binário que aceita duas expressões. Cada operando pode ser omitido (veja os exemplos abaixo), e têm que ser convertíveis para System.Index
. Ele será reduzido para a chamada de método de fábrica System.Range
apropriada.
Substituímos as regras gramaticais do C# para multiplicative_expression pelo seguinte (para introduzir um novo nível de precedência):
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 as formas do operador de intervalo de têm a mesma precedência. Este novo grupo de precedência é menor do que os operadores unários e maior do que os operadores aritméticos multiplicativos .
Chamamos o operador ..
de operador de faixa . O operador de intervalo embutido pode ser entendido aproximadamente como correspondendo à invocação de um operador interno deste formulário:
System.Range operator ..(Index start = 0, Index end = ^0);
Exemplos:
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]
Além disso, System.Index
deve ter uma conversão implícita de System.Int32
, de modo a evitar a necessidade de sobrecarregar a combinação de inteiros e índices em assinaturas multidimensionais.
Adicionando suporte a índice e intervalo a tipos de biblioteca existentes
Suporte a índice implícito
O idioma fornecerá a um membro do indexador de instância um único parâmetro do tipo Index
para tipos que atendam aos seguintes critérios:
- O tipo é Countable.
- O tipo tem um indexador de instância acessível que usa um único
int
como argumento. - O tipo não tem um indexador de instância acessível que toma um
Index
como o primeiro parâmetro. OIndex
deve ser o único parâmetro ou os parâmetros restantes devem ser opcionais.
Um tipo é Countable se tiver uma propriedade chamada Length
ou Count
com um método get acessível e um tipo de retorno de int
. A linguagem pode fazer uso dessa propriedade para converter uma expressão do tipo Index
em um int
no ponto da expressão sem a necessidade de usar o tipo Index
de todo. Caso Length
e Count
estejam presentes, Length
será preferível. Para simplificar, no futuro, a proposta utilizará o nome Length
para representar Count
ou Length
.
Para esses tipos, a linguagem agirá como se houvesse um membro indexador do formulário T this[Index index]
onde T
é o tipo de retorno do indexador baseado em int
, incluindo quaisquer anotações de estilo ref
. O novo membro terá os mesmos membros get
e set
com acessibilidade correspondente ao indexador int
.
O novo indexador será implementado convertendo o argumento do tipo Index
em um int
e emitindo uma chamada para o indexador baseado em int
. Para fins de discussão, vamos usar o exemplo de receiver[expr]
. A conversão de expr
em int
ocorrerá da seguinte forma:
- Quando o argumento é da forma
^expr2
e o tipo deexpr2
éint
, será traduzido parareceiver.Length - expr2
. - Caso contrário, será traduzido como
expr.GetOffset(receiver.Length)
.
Independentemente da estratégia de conversão específica, a ordem de avaliação deve ser equivalente à seguinte:
-
receiver
é avaliada; -
expr
é avaliada; -
length
é avaliado, se necessário; - O indexador baseado em
int
é invocado.
Isso permite que os desenvolvedores usem o recurso Index
em tipos existentes sem a necessidade de modificação. Por exemplo:
List<char> list = ...;
var value = list[^1];
// Gets translated to
var value = list[list.Count - 1];
As expressões receiver
e Length
serão derramadas conforme apropriado para garantir que quaisquer efeitos colaterais sejam executados apenas uma vez. Por exemplo:
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".
Suporte implícito a intervalos
O idioma fornecerá a um membro do indexador de instância um único parâmetro do tipo Range
para tipos que atendam aos seguintes critérios:
- O tipo é Countable.
- O tipo tem um membro acessível chamado
Slice
que tem dois parâmetros do tipoint
. - O tipo não tem um indexador de instância que toma um único
Range
como o primeiro parâmetro. ORange
deve ser o único parâmetro ou os parâmetros restantes devem ser opcionais.
Para esses tipos, o idioma será vinculado como se houvesse um membro indexador do formulário T this[Range range]
onde T
é o tipo de retorno do método Slice
, incluindo quaisquer anotações de estilo ref
. O novo membro também terá acessibilidade equivalente a Slice
.
Quando o indexador baseado em Range
é vinculado em uma expressão chamada receiver
, ele será reduzido convertendo a expressão Range
em dois valores que são passados para o método Slice
. Para fins de discussão, vamos usar o exemplo de receiver[expr]
.
O primeiro argumento de Slice
será obtido ao converter a expressão do intervalo digitada da seguinte maneira:
- Quando
expr
é da formaexpr1..expr2
(ondeexpr2
pode ser omitido) eexpr1
tem tipoint
, então ele será emitido comoexpr1
. - Quando
expr
é da forma^expr1..expr2
(ondeexpr2
pode ser omitido), então ele será emitido comoreceiver.Length - expr1
. - Quando
expr
é da forma..expr2
(ondeexpr2
pode ser omitido), então ele será emitido como0
. - Caso contrário, será emitido como
expr.Start.GetOffset(receiver.Length)
.
Este valor será reutilizado no cálculo do segundo argumento Slice
. Ao fazê-lo, será referido como start
. O segundo argumento de Slice
será obtido convertendo a expressão digitada do intervalo da seguinte maneira:
- Quando
expr
é da formaexpr1..expr2
(ondeexpr1
pode ser omitido) eexpr2
tem tipoint
, então ele será emitido comoexpr2 - start
. - Quando
expr
é da formaexpr1..^expr2
(ondeexpr1
pode ser omitido), então ele será emitido como(receiver.Length - expr2) - start
. - Quando
expr
é da formaexpr1..
(ondeexpr1
pode ser omitido), então ele será emitido comoreceiver.Length - start
. - Caso contrário, será emitido como
expr.End.GetOffset(receiver.Length) - start
.
Independentemente da estratégia de conversão específica, a ordem de avaliação deve ser equivalente à seguinte:
-
receiver
é avaliada; -
expr
é avaliada; -
length
é avaliado, se necessário; - O método
Slice
é invocado.
As expressões receiver
, expr
e length
serão derramadas conforme apropriado para garantir que quaisquer efeitos colaterais sejam executados apenas uma vez. Por exemplo:
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".
A língua tratará de forma especial os seguintes tipos conhecidos:
-
string
: o métodoSubstring
será usado em vez deSlice
. -
array
: o métodoSystem.Runtime.CompilerServices.RuntimeHelpers.GetSubArray
será usado em vez deSlice
.
Alternativas
Os novos operadores (^
e ..
) são açúcar sintático. A funcionalidade pode ser implementada por chamadas explícitas para System.Index
e System.Range
métodos de fábrica, mas resultará em muito mais código clichê e a experiência será pouco intuitiva.
Representação da IL
Esses dois operadores serão reduzidos para chamadas regulares de indexador/método, sem alteração nas camadas subsequentes do compilador.
Comportamento de tempo de execução
- O compilador pode otimizar indexadores para tipos internos, como matrizes e cadeias de caracteres, e reduzir a indexação para os métodos existentes apropriados.
-
System.Index
lançará se construído com um valor negativo. -
^0
não lança, mas traduz-se no comprimento da coleção/enumerável para o qual é fornecido. -
Range.All
é semanticamente equivalente a0..^0
, e pode ser desconstruída para esses índices.
Considerações
Detetar indexável com base em ICollection
A inspiração para esse comportamento foram os inicializadores de coleção. Usando a estrutura de um tipo para transmitir que ele optou por um recurso. No caso de inicializadores de coleção, os tipos podem optar pelo recurso implementando a interface IEnumerable
(não genérica).
Inicialmente, esta proposta exigia que os tipos implementassem ICollection
para poderem ser considerados indexáveis. No entanto, isso exigiu uma série de casos especiais:
-
ref struct
: estes não podem implementar interfaces, mas tipos comoSpan<T>
são ideais para suporte de índice / intervalo. -
string
: não implementaICollection
e adicionar essa interface tem um grande custo.
Isto significa que, para suportar os principais tipos, já é necessário um invólucro especial. O tratamento especial de string
é menos interessante, pois a linguagem faz isso noutras áreas (rebaixamento deforeach
, constantes, etc...). O tratamento especial de ref struct
é mais preocupante, pois é um tratamento especial para uma classe inteira de tipos. Eles são rotulados como indexáveis se simplesmente tiverem uma propriedade chamada Count
com um tipo de retorno de int
.
Após consideração, o design foi normalizado para dizer que qualquer tipo que tenha uma propriedade Count
/ Length
com um tipo de retorno de int
é indexável. Isso remove todo o invólucro especial, mesmo para string
e matrizes.
Detetar apenas a contagem
Detetar nos nomes de propriedade Count
ou Length
complica um pouco o design. Escolher apenas um para padronizar, no entanto, não é suficiente, pois acaba excluindo um grande número de tipos:
- Use
Length
: exclui praticamente todas as coleções em System.Collections e sub-namespaces. Estes tendem a derivar deICollection
e, portanto, preferemCount
em detrimento do comprimento. - Use
Count
: excluistring
, matrizes,Span<T>
e a maioria dos tipos baseados emref struct
A complicação extra na deteção inicial de tipos indexáveis é superada por sua simplificação em outros aspetos.
Escolha de Slice como nome
O nome Slice
foi escolhido porque é o nome padrão tácitamente para operações de corte no .NET. A partir de netcoreapp2.1, todos os tipos de estilo span usam o nome Slice
para operações de fatiamento. Antes do netcoreapp2.1 realmente não há exemplos de fatiamento para procurar um exemplo. Tipos como List<T>
, ArraySegment<T>
, SortedList<T>
teriam sido ideais para o fatiamento, mas o conceito não existia quando os tipos foram adicionados.
Assim, Slice
sendo o único exemplo, foi escolhido como nome.
Conversão do tipo de destino do índice
Outra maneira de exibir a transformação Index
em uma expressão de indexador é como uma conversão de tipo de destino. Em vez de vincular como se houvesse um membro do formulário return_type this[Index]
, a linguagem atribui uma conversão orientada ao tipo de destino para int
.
Este conceito pode ser generalizado para todos os membros que acessam os tipos Countable. Sempre que uma expressão com o tipo Index
for usada como argumento para uma invocação de membro da instância e o recetor for Countable, a expressão terá uma conversão de tipo de destino para int
. As invocações de membro aplicáveis para esta conversão incluem métodos, indexadores, propriedades, métodos de extensão, etc ... Apenas os construtores são excluídos, pois não têm recetor.
A conversão de tipo de destino será implementada da seguinte forma para qualquer expressão que tenha um tipo de Index
. Para fins de discussão, vamos usar o exemplo de receiver[expr]
:
- Quando
expr
for da forma^expr2
e o tipo deexpr2
forint
, será traduzido parareceiver.Length - expr2
. - Caso contrário, será traduzido como
expr.GetOffset(receiver.Length)
.
As expressões receiver
e Length
serão derramadas conforme apropriado para garantir que quaisquer efeitos colaterais sejam executados apenas uma vez. Por exemplo:
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á "Get Length 3".
Este recurso seria benéfico para qualquer membro que tivesse um parâmetro que representasse um índice. Por exemplo, List<T>.InsertAt
. Isso também tem o potencial de confusão, já que a linguagem não pode dar nenhuma orientação sobre se uma expressão deve ou não ser indexada. Tudo o que ele pode fazer é converter qualquer expressão Index
em int
ao invocar um membro em um tipo Countable.
Restrições:
- Esta conversão só é aplicável quando a expressão com o tipo
Index
é diretamente um argumento para o membro. Não se aplicaria a nenhuma expressão aninhada.
Decisões tomadas durante a implementação
- Todos os membros no padrão devem ser membros da instância
- Se um método Length for encontrado, mas tiver o tipo de retorno errado, continue procurando pelo Count
- O indexador usado para o padrão Index deve ter exatamente um parâmetro int
- O método
Slice
usado para o padrão Range deve ter exatamente dois parâmetros inteiros - Ao procurar os membros do padrão, procuramos definições originais, não membros construídos
Reuniões de design
C# feature specifications