Condividi tramite


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 language design meeting (LDM) pertinenti.

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. Il Index 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 di expr2 è int, verrà convertito in receiver.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:

  1. receiver viene valutato;
  2. expr viene valutato;
  3. length viene valutato, se necessario;
  4. 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 tipo int.
  • Il tipo non dispone di un indicizzatore di istanza che accetta un singolo Range come primo parametro. Il Range 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 forma expr1..expr2 (dove expr2 può essere omesso) e expr1 ha tipo int, verrà generato come expr1.
  • Quando expr è della forma ^expr1..expr2 (dove expr2 può essere omesso), verrà emesso come receiver.Length - expr1.
  • Quando expr è della forma ..expr2 (dove expr2 può essere omesso), verrà emesso come 0.
  • 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 forma expr1..expr2 (dove expr1 può essere omesso) e expr2 ha tipo int, verrà generato come expr2 - start.
  • Quando expr è della forma expr1..^expr2 (dove expr1 può essere omesso), verrà emesso come (receiver.Length - expr2) - start.
  • Quando expr è della forma expr1.. (dove expr1 può essere omesso), verrà emesso come receiver.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:

  1. receiver viene valutato;
  2. expr viene valutato;
  3. length viene valutato, se necessario;
  4. viene richiamato il metodo Slice.

Le espressioni receiver, expre 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 metodo Substring verrà usato anziché Slice.
  • array: il metodo System.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 a 0..^0e 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 come Span<T> sono ideali per il supporto di indice/intervallo.
  • string: non implementa ICollection 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 da ICollection e quindi preferiscono Count piuttosto che la lunghezza.
  • Usare Count: esclude string, matrici, Span<T> e la maggior parte dei tipi basati su ref 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 di expr2 è int, verrà convertito in receiver.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