Intervalos
Observação
Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ela inclui alterações de especificação propostas, juntamente com as informações necessárias durante o design e o desenvolvimento do recurso. Esses artigos são publicados até que as alterações de especificação propostas sejam finalizadas e incorporadas na especificação ECMA atual.
Pode haver algumas divergências entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da reunião de design de idioma (LDM).
Você pode saber mais sobre o processo de adoção de speclets de recursos no padrão de linguagem C# no artigo sobre as especificações de .
Problema do especialista: https://github.com/dotnet/csharplang/issues/185
Resumo
Este recurso introduz dois novos operadores que permitem construir objetos System.Index
e System.Range
e usá-los para indexar/fatiar coleções em runtime.
Visão geral
Tipos e membros bem conhecidos
Para usar os novos formatos sintáticos para System.Index
e System.Range
, podem ser necessários novos tipos e membros bem conhecidos, dependendo dos formatos sintáticos a serem usados.
Para usar o operador "hat" (^
), o seguinte é necessário
namespace System
{
public readonly struct Index
{
public Index(int value, bool fromEnd);
}
}
Para usar o tipo System.Index
como argumento em um acesso de elemento de matriz, o seguinte membro é obrigatório:
int System.Index.GetOffset(int length);
A sintaxe ..
para System.Range
exigirá o tipo System.Range
, além de 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 de seus argumentos esteja ausente. Independentemente do número de argumentos, o construtor Range
é sempre suficiente para usar a sintaxe Range
. No entanto, se um dos outros membros estiver presente e um ou mais dos argumentos ..
estiverem ausentes, o membro apropriado poderá ser substituído.
Por fim, para que um valor do tipo System.Range
seja usado em uma expressão de acesso de elemento 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 a maioria dos indexadores usam a noção "desde o início" ou fazem uma expressão "length - i". Apresentamos uma nova expressão Index que significa "a partir do final". O recurso introduz um novo operador de prefixo unário chamado "hat". Seu único operando deve ser conversível para System.Int32
. Ele será reduzido para a chamada de método de fábrica System.Index
apropriada.
Aumentamos a gramática para unary_expression com o seguinte formulário de sintaxe adicional:
unary_expression
: '^' unary_expression
;
Damos a ele o nome de operador de índice a partir do final. Os operadores predefinidos de índice a partir do final são os seguintes:
System.Index operator ^(int fromEnd);
O comportamento desse 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 uma 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 recorrem 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 com suporte em um nível mais profundo na linguagem/no runtime e ter a interface unificada.
A linguagem introduzirá um novo operador de intervalo x..y
. É um operador infix binário que aceita duas expressões. Qualquer operando pode ser omitido (veja os exemplos abaixo) e eles precisam ser convertíveis em System.Index
. Ele será reduzido para a chamada de método de fábrica System.Range
apropriada.
Substituímos as regras da linguagem 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 têm a mesma precedência. Esse novo grupo de precedência é inferior aos operadores unários e superior aos operadores aritméticos de multiplicação.
Chamamos o operador ..
de operador de intervalo. O operador de intervalo embutido pode ser aproximadamente entendido como correspondente à invocação de um operador embutido desta forma:
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
para evitar a necessidade de sobrecarregar a combinação de inteiros e índices em assinaturas multidimensionais.
Adicionando suporte para índice e intervalo a tipos de biblioteca existentes
Suporte para índice implícito
A linguagem fornecerá um membro indexador de instância com um único parâmetro do tipo Index
para tipos que atendem aos seguintes critérios:
- O tipo é Contável.
- 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 usa um
Index
como primeiro parâmetro.Index
deve ser o único parâmetro ou os parâmetros restantes devem ser opcionais.
Um tipo será Contável se tiver uma propriedade nomeada Length
ou Count
com um getter acessível e um tipo de retorno int
. A linguagem pode usar essa 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
. Caso Length
e Count
estejam presentes, Length
será preferencial. Para simplificar, daqui para frente a proposta usará o nome Length
para representar Count
ou Length
.
Para esses tipos, a linguagem se comportará como se existisse um membro indexador na forma 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 da mesma maneira que o indexador int
.
O novo indexador será implementado convertendo o argumento do tipo Index
em 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
para int
ocorrerá da seguinte maneira:
- Quando o argumento estiver na forma
^expr2
e o tipo deexpr2
forint
, ele será convertido emreceiver.Length - expr2
. - Do contrário, será convertido como
expr.GetOffset(receiver.Length)
.
Independentemente da estratégia de conversão específica, a ordem de avaliação deve ser equivalente ao seguinte:
-
receiver
é avaliado; -
expr
é avaliado; -
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 fazer modificações. 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 despejadas conforme apropriado para 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);
}
}
Esse código imprimirá "Get Length 3".
Suporte a intervalos implícitos
A linguagem fornecerá um membro indexador de instância com um único parâmetro do tipo Range
para tipos que atendem aos seguintes critérios:
- O tipo é Contável.
- 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 usa um único
Range
como primeiro parâmetro.Range
deve ser o único parâmetro ou os parâmetros restantes devem ser opcionais.
Para esses tipos, a linguagem será vinculada como se houvesse um membro indexador na forma de T this[Range range]
, em que T
é o tipo de retorno do método Slice
, incluindo qualquer anotação de estilo ref
. O novo membro também terá acessibilidade semelhante à de Slice
.
Quando o indexador baseado em Range
estiver associado a uma expressão chamada receiver
, será reduzido convertendo a expressão Range
em dois valores que, então, serã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 de tipo de intervalo da seguinte maneira:
- Quando
expr
for da formaexpr1..expr2
(em queexpr2
pode ser omitido) eexpr1
tiver o tipoint
, ele será emitido comoexpr1
. - Quando
expr
estiver na forma^expr1..expr2
(em queexpr2
pode ser omitido), será emitido comoreceiver.Length - expr1
. - Quando
expr
estiver na forma..expr2
(em queexpr2
pode ser omitido), será emitido como0
. - Do contrário, ele será emitido como
expr.Start.GetOffset(receiver.Length)
.
Esse valor será novamente utilizado no cálculo do segundo argumento Slice
. Ao fazer isso, ele será referido como start
. O segundo argumento de Slice
será obtido ao converter a expressão de tipo de intervalo da seguinte maneira:
- Quando
expr
for da formaexpr1..expr2
(em queexpr1
pode ser omitido) eexpr2
tiver o tipoint
, ele será emitido comoexpr2 - start
. - Quando
expr
estiver na formaexpr1..^expr2
(em queexpr1
pode ser omitido), será emitido como(receiver.Length - expr2) - start
. - Quando
expr
estiver na formaexpr1..
(em queexpr1
pode ser omitido), será emitido comoreceiver.Length - start
. - Do contrário, ele 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 ao seguinte:
-
receiver
é avaliado; -
expr
é avaliado; -
length
é avaliado, se necessário; - o método
Slice
é invocado.
As expressões receiver
, expr
e length
serão despejadas conforme apropriado para 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);
}
}
Esse código imprimirá "Get Length 2".
A linguagem diferenciará os seguintes tipos conhecidos:
-
string
: o métodoSubstring
será usado no lugar deSlice
. -
array
: o métodoSystem.Runtime.CompilerServices.RuntimeHelpers.GetSubArray
será usado no lugar deSlice
.
Alternativas
Os novos operadores (^
e ..
) são simplificações sintáticas. A funcionalidade pode ser implementada por chamadas explícitas para métodos de fábrica System.Index
e System.Range
, mas resultará em uma quantidade bem maior de código clichê e a experiência não será intuitiva.
Representação IL
Esses dois operadores serão reduzidos para chamadas comuns de indexador/método, sem alterações nas camadas subsequentes do compilador.
Comportamento em tempo de execução
- O compilador pode otimizar os indexadores para tipos internos, como matrizes e cadeias de caracteres, e reduzir a indexação aos métodos existentes apropriados.
-
System.Index
será gerado se for construído com um valor negativo. -
^0
não é gerado, mas convertido em comprimento da coleção/enumerável à qual ele é fornecido. -
Range.All
é semanticamente equivalente a0..^0
e pode ser desconstruído para esses índices.
Considerações
Detecção de 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 indicar que ele ativou um recurso. No caso de inicializadores de coleção, os tipos podem optar pelo recurso implementando a interface IEnumerable
(não genérica).
Inicialmente, essa proposta exigia que os tipos implementassem ICollection
para se qualificarem como indexáveis. Mas para isso eram necessários inúmeros casos especiais:
-
ref struct
: não podem implementar interfaces, mas tipos comoSpan<T>
são ideais para suporte para índice/intervalo. -
string
: não implementaICollection
, e adicionar essa interface tem um custo alto.
Isso significa que é necessário já lidar com casos especiais para tipos de chaves. O tratamento especial de string
é menos interessante, pois a linguagem faz isso em outras áreas (redução de foreach
, constantes etc.). O tratamento especial de ref struct
é mais preocupante, pois lida de maneira especial com uma classe inteira de tipos. Eles serão rotulados como Indexáveis se tiverem simplesmente uma propriedade chamada Count
com um tipo de retorno de int
.
Após consideração, o design foi normalizado para informar que qualquer tipo que tem uma propriedade Count
/ Length
com um tipo de retorno de int
é indexável. Isso remove toda a capitalização especial, mesmo para string
e matrizes.
Detectar contagem justa
Detectar nos nomes de propriedades Count
ou Length
complica um pouco o design. No entanto, escolher apenas um para padronizar não é suficiente, pois acaba excluindo muitos tipos:
- Use
Length
: exclui praticamente todas as coleções em System.Collections e sub-namespaces. Eles tendem a derivar deICollection
e, portanto, preferemCount
em vez de comprimento. - Use
Count
: excluistring
, matrizes,Span<T>
e a maioria dos tipos baseados emref struct
A complicação extra na detecção inicial de tipos indexáveis é superada por sua simplificação em outros aspectos.
Escolha de "Slice" como nome
O nome Slice
foi escolhido por ser o nome padrão de fato para operações de estilo de fatia no .NET. A partir de netcoreapp2.1, todos os tipos de estruturas span usam o nome Slice
para operações de fatiamento. Antes do netcoreapp2.1, realmente não há exemplos de fatiamento disponíveis. Tipos como List<T>
, ArraySegment<T>
, SortedList<T>
teriam sido ideais para fatiamento, mas o conceito não existia quando os tipos foram adicionados.
Então, como Slice
era o único exemplo, ele foi escolhido como o nome.
Conversão de tipo de destino de índice
Outra maneira de ver a transformação de Index
em expressão de indexador é como uma conversão de tipo de destino. Em vez de associar como se houvesse um membro do formulário return_type this[Index]
, a linguagem atribui uma conversão de tipo de destino para int
.
Esse conceito pode ser generalizado para todo o acesso de membro em tipos Contáveis. Sempre que uma expressão com tipo Index
for usada como argumento para uma invocação de membro de instância e o receptor for Contável, a expressão terá uma conversão de tipo de destino para int
. As invocações de membros aplicáveis a essa conversão incluem métodos, indexadores, propriedades, métodos de extensão, etc... Somente os construtores são excluídos porque não possuem um receptor.
A conversão de tipo de destino será implementada conforme a seguir para qualquer expressão que tem um tipo de Index
. Para fins de discussão, vamos usar o exemplo de receiver[expr]
:
- Quando
expr
é do formato^expr2
e o tipoexpr2
éint
, será traduzido parareceiver.Length - expr2
. - Do contrário, será convertido como
expr.GetOffset(receiver.Length)
.
As expressões receiver
e Length
serão despejadas conforme apropriado para 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);
}
}
Esse código imprimirá "Get Length 3".
Esse recurso seria benéfico para qualquer membro que tivesse um parâmetro que representasse um índice. Por exemplo, List<T>.InsertAt
. Ele também tem o potencial de confundir, pois a linguagem não pode orientar sobre se uma expressão deve ou não ser usada para indexação. Tudo que ele pode fazer é converter qualquer expressão Index
em int
ao invocar um membro em um tipo contável.
Restrições:
- Essa conversão só é aplicável quando a expressão com o tipo
Index
é diretamente um argumento para o membro. Isso não se aplica a expressões aninhadas.
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 por 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 do tipo int. - Ao procurar os membros do padrão, procuramos definições originais, não membros construídos
Reuniões de design
C# feature specifications