Intervalli
Nota
Questo articolo è una specifica di funzionalità. La specifica funge da documento di progettazione per la funzionalità. Include le modifiche specifiche proposte, insieme alle informazioni necessarie durante la progettazione e lo sviluppo della funzionalità. Questi articoli vengono pubblicati fino a quando le modifiche specifiche proposte non vengono completate e incorporate nella specifica ECMA corrente.
Potrebbero verificarsi alcune discrepanze tra la specifica di funzionalità e l'implementazione completata. Tali differenze vengono acquisite nelle note
Altre informazioni sul processo per l'adozione di speclet di funzionalità nello standard del linguaggio C# sono disponibili nell'articolo sulle specifiche di .
Problema del campione: https://github.com/dotnet/csharplang/issues/185
Sommario
Questa funzionalità riguarda l'introduzione di due nuovi operatori che consentono di costruire oggetti System.Index
e System.Range
e di usarli per indicizzare o suddividere raccolte in fase di esecuzione.
Panoramica
Tipi e membri noti
Per utilizzare le nuove forme sintattiche per System.Index
e System.Range
, potrebbero essere necessari nuovi tipi e membri noti, a seconda delle forme sintattiche utilizzate.
Per usare l'operatore "cappello" (^
), è necessario quanto segue
namespace System
{
public readonly struct Index
{
public Index(int value, bool fromEnd);
}
}
Per usare il tipo System.Index
come argomento in un accesso a un elemento di matrice, è necessario il membro seguente:
int System.Index.GetOffset(int length);
La sintassi ..
per System.Range
richiederà il tipo System.Range
, nonché uno o più dei seguenti membri.
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 sintassi ..
consente che uno, entrambi o nessuno dei suoi argomenti siano assenti. Indipendentemente dal numero di argomenti, il costruttore Range
è sempre sufficiente per usare la sintassi Range
. Tuttavia, se uno degli altri membri è presente e uno o più degli argomenti ..
sono mancanti, il membro appropriato può essere sostituito.
Infine, affinché un valore di tipo System.Range
sia utilizzato in un'espressione di accesso a un elemento di matrice, deve essere presente il seguente membro:
namespace System.Runtime.CompilerServices
{
public static class RuntimeHelpers
{
public static T[] GetSubArray<T>(T[] array, System.Range range);
}
}
System.Index
C# non ha modo di indicizzare una raccolta dalla fine, ma la maggior parte degli indicizzatori usa la nozione "from start" o esegue un'espressione "length - i". Introduciamo una nuova espressione di indice che significa "a partire dalla fine". La funzionalità introdurrà un nuovo operatore di prefisso unario denominato "hat". Il singolo operando deve essere convertibile in System.Int32
. Verrà ridotta nella chiamata al metodo factory System.Index
appropriata.
La grammatica per unary_expression viene ampliata con il seguente formato di sintassi aggiuntiva:
unary_expression
: '^' unary_expression
;
Chiamiamo questo l'indice dall'operatore finale. Il valore predefinito dell'indice dagli operatori di fine è il seguente:
System.Index operator ^(int fromEnd);
Il comportamento di questo operatore è definito solo per i valori di input maggiori o uguali a zero.
Esempi:
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# non ha modo sintattico di accedere a "intervalli" o "sezioni" di raccolte. In genere gli utenti sono costretti a implementare strutture complesse per filtrare/operare su sezioni di memoria o ricorrere a metodi LINQ come list.Skip(5).Take(2)
. Con l'aggiunta di System.Span<T>
e altri tipi simili, diventa più importante disporre di questo tipo di operazione supportata su un livello più approfondito nel linguaggio/runtime e avere l'interfaccia unificata.
Il linguaggio introdurrà un nuovo operatore di intervallo x..y
. Si tratta di un operatore infix binario che accetta due espressioni. Entrambi gli operandi possono essere omessi (esempi seguenti) e devono essere convertibili in System.Index
. Verrà ridotta alla chiamata al metodo factory System.Range
appropriata.
Le regole di grammatica C# per multiplicative_expression vengono sostituite con quanto segue (per introdurre un nuovo livello di precedenza):
range_expression
: unary_expression
| range_expression? '..' range_expression?
;
multiplicative_expression
: range_expression
| multiplicative_expression '*' range_expression
| multiplicative_expression '/' range_expression
| multiplicative_expression '%' range_expression
;
Tutte le forme dell'operatore di intervallo hanno la stessa precedenza. Questo nuovo gruppo di precedenza è inferiore agli operatori unari e superiore agli operatori aritmetici moltiplicativi .
Chiamiamo l'operatore ..
l'operatore di intervallo . L'operatore di intervallo predefinito può essere compreso approssimativamente per corrispondere alla chiamata di un operatore predefinito di questo formato:
System.Range operator ..(Index start = 0, Index end = ^0);
Esempi:
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]
Inoltre, System.Index
deve avere una conversione implicita da System.Int32
, per evitare la necessità di sovraccaricare la combinazione di numeri interi e indici nelle firme multidimensionali.
Aggiunta del supporto per le funzionalità di Indice e Intervallo ai tipi di libreria esistenti
Supporto dell'indice implicito
Il linguaggio fornirà un membro dell'indicizzatore di istanza con un singolo parametro di tipo Index
per i tipi che soddisfano i criteri seguenti:
- Il tipo è Countable.
- Il tipo dispone di un indicizzatore di istanza accessibile che accetta un singolo
int
come parametro. - Il tipo non dispone di un indicizzatore di istanze accessibile che accetta un
Index
come primo parametro. IlIndex
deve essere l'unico parametro o i parametri rimanenti devono essere facoltativi.
Un tipo è Countable se ha una proprietà denominata Length
o Count
con un getter accessibile e un tipo restituito di int
. Il linguaggio può utilizzare questa proprietà per convertire un'espressione di tipo Index
in un int
al punto dell'espressione senza la necessità di usare il tipo Index
. Se sono presenti sia Length
che Count
, è preferibile Length
. Per semplicità, la proposta userà il nome Length
per rappresentare Count
o Length
.
Per questi tipi, il linguaggio si comporterà come se esistesse un membro indicizzatore della forma T this[Index index]
, in cui T
è il tipo di ritorno dell'indicizzatore basato su int
, incluse eventuali annotazioni di stile ref
. Il nuovo membro avrà gli stessi membri get
e set
, la cui accessibilità corrisponderà a quella dell'indicizzatore int
.
Il nuovo indicizzatore verrà implementato convertendo l'argomento di tipo Index
in un int
e generando una chiamata all'indicizzatore basato su int
. A scopo di discussione, si userà l'esempio di receiver[expr]
. La conversione di expr
in int
verrà eseguita nel modo seguente:
- Quando l'argomento è del formato
^expr2
e il tipo diexpr2
èint
, verrà convertito inreceiver.Length - expr2
. - In caso contrario, verrà convertito come
expr.GetOffset(receiver.Length)
.
Indipendentemente dalla strategia di conversione specifica, l'ordine di valutazione deve essere equivalente al seguente:
-
receiver
viene valutato; -
expr
viene valutato; -
length
viene valutato, se necessario; - viene attivato l'indicizzatore basato su
int
.
Ciò consente agli sviluppatori di usare la funzionalità Index
sui tipi esistenti senza la necessità di apportare modifiche. Per esempio:
List<char> list = ...;
var value = list[^1];
// Gets translated to
var value = list[list.Count - 1];
Le espressioni receiver
e Length
verranno distribuite in base alle esigenze per garantire che eventuali effetti collaterali vengano eseguiti una sola volta. Per esempio:
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);
}
}
Questo codice stamperà "Ottieni Lunghezza 3".
Supporto dell'intervallo implicito
Il linguaggio fornirà un membro dell'indicizzatore di istanza con un singolo parametro di tipo Range
per i tipi che soddisfano i criteri seguenti:
- Il tipo è Countable.
- Il tipo ha un membro accessibile denominato
Slice
che ha due parametri di tipoint
. - Il tipo non dispone di un indicizzatore di istanza che accetta un singolo
Range
come primo parametro. IlRange
deve essere l'unico parametro o i parametri rimanenti devono essere facoltativi.
Per tali tipi, il linguaggio verrà associato come se ci fosse un membro indicizzatore nella forma di T this[Range range]
, in cui T
è il tipo restituito del metodo Slice
, incluse eventuali annotazioni nello stile di ref
. Il nuovo membro avrà anche accessibilità simile a quella di Slice
.
Quando l'indicizzatore basato su Range
è associato a un'espressione denominata receiver
, verrà ridotta convertendo l'espressione Range
in due valori che vengono quindi passati al metodo Slice
. A scopo di discussione, si userà l'esempio di receiver[expr]
.
Il primo argomento di Slice
verrà ottenuto convertendo l'espressione di intervallo tipizzata nel seguente modo:
- Quando
expr
è della formaexpr1..expr2
(doveexpr2
può essere omesso) eexpr1
ha tipoint
, verrà generato comeexpr1
. - Quando
expr
è della forma^expr1..expr2
(doveexpr2
può essere omesso), verrà emesso comereceiver.Length - expr1
. - Quando
expr
è della forma..expr2
(doveexpr2
può essere omesso), verrà emesso come0
. - In caso contrario, verrà generato come
expr.Start.GetOffset(receiver.Length)
.
Questo valore verrà riutilizzato nel calcolo del secondo argomento Slice
. In questo caso, verrà definito start
. Il secondo argomento di Slice
verrà ottenuto convertendo l'espressione di intervallo tipizzata nel modo che segue:
- Quando
expr
è della formaexpr1..expr2
(doveexpr1
può essere omesso) eexpr2
ha tipoint
, verrà generato comeexpr2 - start
. - Quando
expr
è della formaexpr1..^expr2
(doveexpr1
può essere omesso), verrà emesso come(receiver.Length - expr2) - start
. - Quando
expr
è della formaexpr1..
(doveexpr1
può essere omesso), verrà emesso comereceiver.Length - start
. - In caso contrario, verrà generato come
expr.End.GetOffset(receiver.Length) - start
.
Indipendentemente dalla strategia di conversione specifica, l'ordine di valutazione deve essere equivalente al seguente:
-
receiver
viene valutato; -
expr
viene valutato; -
length
viene valutato, se necessario; - viene richiamato il metodo
Slice
.
Le espressioni receiver
, expr
e length
verranno distribuite in base alle esigenze per garantire l'esecuzione di eventuali effetti collaterali una sola volta. Per esempio:
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);
}
}
Questo codice stamperà "Get Length 2".
La lingua gestirà in modo speciale i seguenti tipi conosciuti:
-
string
: il metodoSubstring
verrà usato anzichéSlice
. -
array
: il metodoSystem.Runtime.CompilerServices.RuntimeHelpers.GetSubArray
verrà usato anzichéSlice
.
Alternative
I nuovi operatori (^
e ..
) sono zucchero sintattico. La funzionalità può essere implementata tramite chiamate esplicite ai metodi factory System.Index
e System.Range
, ma comporterà un numero molto più grande di codice boilerplate e l'esperienza risulterà poco intuitiva.
Rappresentazione IL
Questi due operatori verranno ridotti alle normali chiamate di indicizzatore/metodo, senza alcuna modifica nei livelli del compilatore successivi.
Comportamento di runtime
- Il compilatore può ottimizzare gli indicizzatori per tipi predefiniti come matrici e stringhe e abbassare l'indicizzazione ai metodi esistenti appropriati.
-
System.Index
genererà se costruito con un valore negativo. -
^0
non genera un'eccezione, ma si riferisce alla lunghezza della raccolta/enumerabile a cui è applicato. -
Range.All
è semanticamente equivalente a0..^0
e può essere scomposto nei seguenti indici.
Considerazioni
Rilevare elementi indicizzabili basandosi su ICollection
L'ispirazione per questo comportamento sono stati gli inizializzatori di raccolta. Uso della struttura di un tipo per indicare che aveva scelto una funzionalità. Nel caso degli inizializzatori di raccolta, i tipi possono optare per la funzionalità implementando l'interfaccia IEnumerable
(non generica).
Questa proposta inizialmente richiedeva che i tipi implementassero ICollection
per qualificarsi come indicizzabile. Ciò richiedeva, tuttavia, una serie di casi speciali.
-
ref struct
: questi non possono implementare interfacce ma tipi comeSpan<T>
sono ideali per il supporto di indice/intervallo. -
string
: non implementaICollection
e l'aggiunta di tale interfaccia ha un costo elevato.
Ciò significa che è già necessario supportare la gestione speciale dei casi per i tipi di chiave. Il trattamento speciale di string
è meno interessante perché il linguaggio lo gestisce in altre aree (riduzione diforeach
, costanti, ecc ...). Il trattamento speciale di ref struct
è più preoccupante perché riguarda un'intera classe di tipi. Vengono etichettati come Indicizzati se hanno semplicemente una proprietà denominata Count
con un tipo restituito di int
.
Dopo aver valutato, il design è stato normalizzato per indicare che qualsiasi tipo che possiede una proprietà Count
/ Length
con un tipo di ritorno di int
è indicizzabile. In questo modo vengono rimosse tutte le casistiche speciali, anche per string
e array.
Rileva solo il conteggio
Il rilevamento dei nomi delle proprietà Count
o Length
complica leggermente la progettazione. Scegliere solo uno per standardizzare, tuttavia, non è sufficiente perché finisce per escludere un numero elevato di tipi.
- Usare
Length
: esclude praticamente ogni raccolta in System.Collections e negli spazi dei nomi secondari. Questi tendono a derivare daICollection
e quindi preferisconoCount
piuttosto che la lunghezza. - Usare
Count
: escludestring
, matrici,Span<T>
e la maggior parte dei tipi basati suref struct
La complicazione aggiuntiva per il rilevamento iniziale dei tipi indicizzati è superata dalla sua semplificazione in altri aspetti.
Scelta di "Slice" come nome
Il nome Slice
è stato scelto in quanto è il nome standard de facto per le operazioni di slicing in .NET. A partire da netcoreapp2.1 tutti i tipi di stile span usano il nome Slice
per le operazioni di sezionamento. Prima di netcoreapp2.1 non esistono esempi di sezionamento a cui fare riferimento. I tipi come List<T>
, ArraySegment<T>
, SortedList<T>
sarebbero stati ideali per il sezionamento, ma il concetto non esisteva quando sono stati aggiunti tipi.
Pertanto, essendo l'unico esempio, Slice
è stato scelto come nome.
Conversione del tipo di destinazione dell'indice
Un altro modo per visualizzare la trasformazione Index
in un'espressione dell'indicizzatore è come una conversione del tipo di destinazione. Anziché eseguire l'associazione come se fosse presente un membro del modulo return_type this[Index]
, la lingua assegna invece una conversione tipizzata di destinazione a int
.
Questo concetto può essere generalizzato a tutti gli accessi ai membri nei tipi enumerabili. Ogni volta che un'espressione con tipo Index
viene usata come argomento per una chiamata a un membro dell'istanza e il ricevitore è Countable, l'espressione subirà una conversione nel tipo di destinazione int
. Le chiamate ai membri applicabili per questa conversione includono metodi, indicizzatori, proprietà, metodi di estensione e così via... Solo i costruttori vengono esclusi perché non hanno ricevitore.
La conversione del tipo di destinazione verrà implementata come segue per qualsiasi espressione con un tipo di Index
. Ai fini della discussione, è possibile usare l'esempio di receiver[expr]
:
- Quando
expr
è del formato^expr2
e il tipo diexpr2
èint
, verrà convertito inreceiver.Length - expr2
. - In caso contrario, verrà convertito come
expr.GetOffset(receiver.Length)
.
Le espressioni receiver
e Length
verranno distribuite in base alle esigenze per garantire che eventuali effetti collaterali vengano eseguiti una sola volta. Per esempio:
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);
}
}
Questo codice stamperà "Ottieni Lunghezza 3".
Questa funzionalità sarebbe utile per tutti i membri che avevano un parametro che rappresentava un indice. Ad esempio, List<T>.InsertAt
. Ciò ha anche il potenziale di confusione perché il linguaggio non può fornire indicazioni sul fatto che un'espressione sia destinata o meno all'indicizzazione. Tutto ciò che può fare è convertire qualsiasi espressione Index
in int
quando si richiama un membro in un tipo countable.
Restrizioni:
- Questa conversione è applicabile solo quando l'espressione con tipo
Index
è direttamente utilizzata come argomento del membro. Non si applica a nessuna espressione nidificata.
Decisioni prese durante l'implementazione
- Tutti i membri nel modello devono essere membri dell'istanza
- Se viene trovato un metodo Length ma ha un tipo di ritorno errato, si deve continuare a cercare Count.
- L'indicizzatore usato per lo schema di indice deve avere esattamente un parametro int
- Il metodo
Slice
utilizzato per il modello Range deve avere esattamente due parametri int - Quando si cercano i membri del modello, si cercano le definizioni originali, non i membri costruiti
Riunioni di progettazione
C# feature specifications