Bereiken
Notitie
Dit artikel is een functiespecificatie. De specificatie fungeert als het ontwerpdocument voor de functie. Het bevat voorgestelde specificatiewijzigingen, samen met informatie die nodig is tijdens het ontwerp en de ontwikkeling van de functie. Deze artikelen worden gepubliceerd totdat de voorgestelde specificaties zijn voltooid en opgenomen in de huidige ECMA-specificatie.
Er kunnen enkele verschillen zijn tussen de functiespecificatie en de voltooide implementatie. Deze verschillen worden vastgelegd in de relevante LDM-notities (Language Design Meeting).
Meer informatie over het proces voor het aannemen van functiespeclets in de C#-taalstandaard vindt u in het artikel over de specificaties.
Kampioenprobleem: https://github.com/dotnet/csharplang/issues/185
Samenvatting
Deze functie gaat over het leveren van twee nieuwe operators waarmee System.Index
- en System.Range
-objecten kunnen worden gemaakt en waarmee verzamelingen tijdens runtime kunnen worden geïndexeerd/gesegmenteerd.
Overzicht
Bekende typen en leden
Als u de nieuwe syntactische formulieren wilt gebruiken voor System.Index
en System.Range
, kunnen er nieuwe bekende typen en leden nodig zijn, afhankelijk van welke syntactische formulieren worden gebruikt.
Als u de operator 'hat' (^
) wilt gebruiken, is het volgende vereist
namespace System
{
public readonly struct Index
{
public Index(int value, bool fromEnd);
}
}
Als u het type System.Index
als argument wilt gebruiken bij het benaderen van een array-element, is het volgende lid vereist:
int System.Index.GetOffset(int length);
De ..
syntaxis voor System.Range
vereist het System.Range
type, evenals een of meer van de volgende leden:
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; }
}
}
Met de syntaxis van ..
kunnen beide of geen van de argumenten ontbreken. Ongeacht het aantal argumenten is de Range
constructor altijd voldoende voor het gebruik van de Range
syntaxis. Als echter een van de andere leden aanwezig is en een of meer van de argumenten ..
ontbreken, kan het juiste lid worden vervangen.
Ten slotte moet het volgende lid aanwezig zijn om een waarde van het type System.Range
te gebruiken in een expressie voor toegang tot matrixelementen:
namespace System.Runtime.CompilerServices
{
public static class RuntimeHelpers
{
public static T[] GetSubArray<T>(T[] array, System.Range range);
}
}
System.Index
C# heeft geen manier om een verzameling vanaf het einde te indexeren, maar de meeste indexeerfuncties gebruiken de notie 'van begin' of een 'lengte - i'-expressie. We introduceren een nieuwe indexexpressie die 'vanaf het einde' betekent. De functie introduceert een nieuwe unaire voorvoegsel 'hat'-operator. De single operand moet converteerbaar zijn naar System.Int32
. Deze wordt verlaagd in de juiste aanroep van de System.Index
factory-methode.
We verbeteren de grammatica voor unary_expression met de volgende aanvullende syntaxisvorm:
unary_expression
: '^' unary_expression
;
We noemen deze de -index vanaf de end--operator. De vooraf gedefinieerde index van end operators zijn als volgt:
System.Index operator ^(int fromEnd);
Het gedrag van deze operator wordt alleen gedefinieerd voor invoerwaarden die groter zijn dan of gelijk zijn aan nul.
Voorbeelden:
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# heeft geen syntactische manier om toegang te krijgen tot 'bereiken' of 'segmenten' van verzamelingen. Meestal worden gebruikers gedwongen om complexe structuren te implementeren om te filteren of te werken op geheugensegmenten, of hun toevlucht te nemen tot LINQ-methoden zoals list.Skip(5).Take(2)
. Met de toevoeging van System.Span<T>
en andere vergelijkbare typen wordt het belangrijker om dit soort bewerkingen op een dieper niveau in de taal/runtime te laten ondersteunen en de interface geïntegreerd te hebben.
De taal zal een nieuwe bereikoperator x..y
introduceren. Het is een binaire infix-operator die twee expressies accepteert. Beide operanden kunnen worden weggelaten (voorbeelden hieronder) en ze moeten converteerbaar zijn naar System.Index
. Deze wordt verlaagd naar de juiste System.Range
factory-methode-aanroep.
We vervangen de C#-grammaticaregels voor multiplicative_expression door het volgende (om een nieuw prioriteitsniveau te introduceren):
range_expression
: unary_expression
| range_expression? '..' range_expression?
;
multiplicative_expression
: range_expression
| multiplicative_expression '*' range_expression
| multiplicative_expression '/' range_expression
| multiplicative_expression '%' range_expression
;
Alle vormen van de bereikoperator hebben dezelfde prioriteit als. Deze nieuwe prioriteitsgroep is lager dan de unaire operatoren en hoger dan de multiplicatieve rekenkundige operatoren.
We noemen de ..
-operator de bereikoperator . De ingebouwde bereikoperator kan ruwweg worden begrepen als overeenkomend met de aanroep van een ingebouwde operator van deze vorm:
System.Range operator ..(Index start = 0, Index end = ^0);
Voorbeelden:
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]
Bovendien moet System.Index
een impliciete conversie van System.Int32
hebben om te voorkomen dat het combineren van gehele getallen en indexen over multidimensionale handtekeningen overbelast raakt.
Ondersteuning voor index en bereik toevoegen aan bestaande bibliotheektypen
Ondersteuning voor impliciete index
De taal biedt een exemplaarindexeerlid met één parameter van het type Index
voor typen die voldoen aan de volgende criteria:
- Het type kan worden geteld.
- Het type heeft een toegankelijke exemplaarindexeerfunctie die één
int
als argument gebruikt. - Het type heeft geen toegankelijke indexeerfunctie voor exemplaren die een
Index
als de eerste parameter gebruikt. DeIndex
moet de enige parameter zijn of de resterende parameters moeten optioneel zijn.
Een type is Telbare als het een eigenschap heeft met de naam Length
of Count
, met een toegankelijke getter en een retourtype van int
. De taal kan deze eigenschap gebruiken om een expressie van het type Index
te converteren naar een int
op het punt van de expressie zonder dat u het type Index
helemaal hoeft te gebruiken. Als zowel Length
als Count
aanwezig zijn, heeft Length
de voorkeur. Voor het gemak gebruikt het voorstel de naam Length
om Count
of Length
weer te geven.
Voor dergelijke typen fungeert de taal alsof er een indexeerfunctielid is van het formulier T this[Index index]
waarbij T
het retourtype is van de int
gebaseerde indexeerfunctie, inclusief eventuele ref
stijlaantekeningen. Het nieuwe lid heeft dezelfde get
en set
leden met overeenkomende toegankelijkheid als de int
indexeerfunctie.
De nieuwe indexeerfunctie wordt geïmplementeerd door het argument van het type Index
te converteren naar een int
en een aanroep naar de int
gebaseerde indexeerfunctie te verzenden. Voor discussiedoeleinden gaan we het voorbeeld van receiver[expr]
gebruiken. De conversie van expr
naar int
vindt als volgt plaats:
- Wanneer het argument van het formulier
^expr2
is en het typeexpr2
isint
, wordt het omgezet inreceiver.Length - expr2
. - Anders wordt het vertaald als
expr.GetOffset(receiver.Length)
.
Ongeacht de specifieke conversiestrategie moet de volgorde van de evaluatie gelijk zijn aan het volgende:
-
receiver
wordt geëvalueerd; -
expr
wordt geëvalueerd; - Indien nodig wordt
length
geëvalueerd; - de
int
gebaseerde indexeerfunctie wordt aangeroepen.
Hierdoor kunnen ontwikkelaars de functie Index
op bestaande typen gebruiken zonder dat ze hoeven te worden gewijzigd. Bijvoorbeeld:
List<char> list = ...;
var value = list[^1];
// Gets translated to
var value = list[list.Count - 1];
De receiver
- en Length
expressies worden zo nodig overlopen om ervoor te zorgen dat eventuele bijwerkingen slechts eenmaal worden uitgevoerd. Bijvoorbeeld:
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);
}
}
Met deze code wordt "Geef lengte 3" afgedrukt.
Ondersteuning voor impliciet bereik
De taal biedt een exemplaarindexeerlid met één parameter van het type Range
voor typen die voldoen aan de volgende criteria:
- Het type kan worden geteld.
- Het type heeft een toegankelijk lid met de naam
Slice
met twee parameters van het typeint
. - Het type heeft geen exemplaarindexeerfunctie die één
Range
als eerste parameter gebruikt. DeRange
moet de enige parameter zijn of de resterende parameters moeten optioneel zijn.
Voor dergelijke typen wordt de taal gebonden alsof er een indexeerfunctielid is in de vorm van T this[Range range]
, waarbij T
het retourtype van de Slice
-methode is, inclusief eventuele ref
-stijlaantekeningen. Het nieuwe lid zal ook gelijke toegankelijkheid hebben met Slice
.
Wanneer de Range
gebaseerde indexeerfunctie afhankelijk is van een expressie met de naam receiver
, wordt deze verlaagd door de Range
-expressie te converteren naar twee waarden die vervolgens worden doorgegeven aan de methode Slice
. Voor discussiedoeleinden gaan we het voorbeeld van receiver[expr]
gebruiken.
Het eerste argument van Slice
wordt verkregen door de expressie met het getypte bereik op de volgende manier te converteren:
- Wanneer
expr
de vormexpr1..expr2
heeft (waarbijexpr2
kan worden weggelaten) enexpr1
typeint
heeft, wordt het uitgevoerd alsexpr1
. - Wanneer
expr
van de vorm^expr1..expr2
is (waarbijexpr2
kan worden weggelaten), wordt het verzonden alsreceiver.Length - expr1
. - Wanneer
expr
van de vorm..expr2
is (waarbijexpr2
kan worden weggelaten), wordt het verzonden als0
. - Anders wordt deze verzonden als
expr.Start.GetOffset(receiver.Length)
.
Deze waarde wordt opnieuw gebruikt in de berekening van het tweede Slice
argument. Als u dit doet, wordt het aangeduid als start
. Het tweede argument van Slice
wordt verkregen door de getypte bereikexpressie op de volgende manier te converteren:
- Wanneer
expr
de vormexpr1..expr2
heeft (waarbijexpr1
kan worden weggelaten) enexpr2
typeint
heeft, wordt het uitgevoerd alsexpr2 - start
. - Wanneer
expr
van de vormexpr1..^expr2
is (waarbijexpr1
kan worden weggelaten), wordt het verzonden als(receiver.Length - expr2) - start
. - Wanneer
expr
van de vormexpr1..
is (waarbijexpr1
kan worden weggelaten), wordt het verzonden alsreceiver.Length - start
. - Anders wordt deze verzonden als
expr.End.GetOffset(receiver.Length) - start
.
Ongeacht de specifieke conversiestrategie moet de volgorde van de evaluatie gelijk zijn aan het volgende:
-
receiver
wordt geëvalueerd; -
expr
wordt geëvalueerd; - Indien nodig wordt
length
geëvalueerd; - de methode
Slice
wordt aangeroepen.
De receiver
, expr
en length
expressies worden zo nodig overlopen om ervoor te zorgen dat eventuele bijwerkingen slechts eenmaal worden uitgevoerd. Bijvoorbeeld:
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);
}
}
Met deze code wordt "Bereken Lengte 2" afgedrukt.
De taal zal de volgende bekende typen speciaal behandelen:
-
string
: de methodeSubstring
wordt gebruikt in plaats vanSlice
. -
array
: de methodeSystem.Runtime.CompilerServices.RuntimeHelpers.GetSubArray
wordt gebruikt in plaats vanSlice
.
Alternatieven
De nieuwe operators (^
en ..
) zijn syntactische suiker. De functionaliteit kan worden geïmplementeerd door expliciete aanroepen van de System.Index
- en System.Range
-factory-methoden, maar dit resulteert in veel meer standaardcode en de ervaring zal niet intuïtief zijn.
IL-weergave
Deze twee operators worden verlaagd tot reguliere indexeerfunctie-/methode-aanroepen, zonder wijzigingen in volgende compilerlagen.
Runtime gedrag
- Compiler kan indexeerfuncties optimaliseren voor ingebouwde typen, zoals matrices en tekenreeksen, en de indexering verlagen naar de juiste bestaande methoden.
-
System.Index
wordt gegenereerd als deze is samengesteld met een negatieve waarde. -
^0
gooit niet, maar wordt omgezet in de lengte van de verzameling/opsomming waaraan het wordt geleverd. -
Range.All
is semantisch gelijk aan0..^0
en kan worden gedeconstrueerd aan deze indexen.
Overwegingen
Indexeerbaar detecteren op basis van ICollection
De inspiratie voor dit gedrag was initialisatie van verzamelingen. De structuur van een type gebruiken om aan te geven dat het heeft gekozen voor een functie. In het geval van verzamelingsinitialisatortypen kan voor de functie worden gekozen door de interface IEnumerable
(niet generiek) te implementeren.
Dit voorstel vereist in eerste instantie dat typen ICollection
implementeren om in aanmerking te komen als indexeerbaar. Hiervoor zijn echter een aantal speciale gevallen vereist:
-
ref struct
: deze kunnen geen interfaces implementeren, maar typen zoalsSpan<T>
zijn ideaal voor index- en bereikondersteuning. -
string
implementeertICollection
niet en het toevoegen van die interface brengt hoge kosten met zich mee.
Dit betekent dat er al speciale behuizingen nodig zijn om belangrijke typen te ondersteunen. De speciale behandeling van string
is minder interessant omdat de taal dit ook op andere gebieden doet (zoalsforeach
met kleine letters, constanten, enz.). De speciale behandeling van ref struct
is zorgwekkender omdat het een hele klasse van typen speciaal behandelt. Ze worden gelabeld als Indexeerbaar als ze gewoon een eigenschap hebben met de naam Count
met een retourtype van int
.
Na overweging werd het ontwerp genormaliseerd om te zeggen dat elk type dat een eigenschap heeft Count
/ Length
met een retourtype van int
indexeerbaar is. Hiermee verwijdert u alle speciale behuizingen, zelfs voor string
en matrices.
Alleen het aantal detecteren
Het detecteren van de eigenschapsnamen Count
of Length
maakt het ontwerp enigszins ingewikkeld. Het kiezen van slechts één om te standaardiseren is echter niet voldoende omdat het uiteindelijk een groot aantal typen uitsluit:
- Gebruik
Length
: sluit vrijwel elke verzameling in System.Collections en subnaamruimten uit. Deze hebben de neiging om af te leiden vanICollection
en geven daarom de voorkeur aanCount
over lengte. - Gebruik
Count
: sluitstring
, matrices,Span<T>
en de meesteref struct
gebaseerde typen uit
De extra complicatie bij de aanvankelijke detectie van indexeerbare typen wordt gecompenseerd door de vereenvoudiging ervan in andere aspecten.
Keuze van Slice als naam
De naam Slice
is gekozen omdat dit de feitelijke standaardnaam is voor segmentstijlbewerkingen in .NET. Vanaf netcoreapp2.1 gebruiken alle typen spanstijlen de naam Slice
voor segmentbewerkingen. Voorafgaand aan netcoreapp2.1 zijn er echt geen voorbeelden van segmentering om naar een voorbeeld te zoeken. Typen zoals List<T>
, ArraySegment<T>
, SortedList<T>
zouden ideaal zijn geweest voor segmentering, maar het concept bestond niet toen er typen werden toegevoegd.
Dus Slice
het enige voorbeeld was, werd het gekozen als de naam.
Conversie van indextype doel
Een andere manier om de Index
transformatie in een indexeerexpressie weer te geven, is als doeltypeconversie. In plaats van te binden alsof er een lid van formulier return_type this[Index]
is, wijst de taal een doelgetypte conversie toe aan int
.
Dit concept kan worden gegeneraliseerd voor alle toegang tot leden binnen telbare typen. Wanneer een expressie met het type Index
wordt gebruikt als argument voor het aanroepen van een exemplaarelement en de ontvanger telbaar is, zal de expressie een doeltypeconversie naar int
ondergaan. De lid-aanroepen die van toepassing zijn op deze conversie zijn methoden, indexeerfuncties, eigenschappen, uitbreidingsmethoden, enzovoort... Alleen constructors worden uitgesloten omdat ze geen ontvanger hebben.
De conversie van het doeltype wordt als volgt geïmplementeerd voor elke expressie met een type Index
. Voor discussiedoeleinden kunt u het voorbeeld van receiver[expr]
gebruiken:
- Wanneer
expr
van het formulier^expr2
is en het typeexpr2
int
is, wordt deze omgezet inreceiver.Length - expr2
. - Anders wordt het vertaald als
expr.GetOffset(receiver.Length)
.
De receiver
- en Length
expressies worden zo nodig overlopen om ervoor te zorgen dat eventuele bijwerkingen slechts eenmaal worden uitgevoerd. Bijvoorbeeld:
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);
}
}
Met deze code wordt "Geef lengte 3" afgedrukt.
Deze functie zou nuttig zijn voor elk lid dat een parameter had die een index vertegenwoordigde. Bijvoorbeeld List<T>.InsertAt
. Dit heeft ook de kans op verwarring omdat de taal geen richtlijnen kan geven over of een expressie al dan niet bedoeld is voor indexering. Alles wat het kan doen, is elke Index
-expressie omzetten naar int
wanneer een lid wordt aangeroepen op een telbaar type.
Beperkingen:
- Deze conversie is alleen van toepassing wanneer de expressie met het type
Index
rechtstreeks een argument voor het lid is. Dit zou niet van toepassing zijn op geneste expressies.
Beslissingen genomen tijdens de implementatie
- Alle leden binnen het patroon moeten instantieleden zijn
- Als een lengtemethode wordt gevonden, maar het verkeerde retourtype heeft, gaat u verder met het zoeken naar Aantal
- De indexeerfunctie die wordt gebruikt voor het indexpatroon, moet precies één int-parameter hebben
- De
Slice
methode die wordt gebruikt voor het bereikpatroon moet exact twee int-parameters hebben - Bij het zoeken naar de patroonleden zoeken we naar oorspronkelijke definities, niet samengestelde leden
Ontwerpvergaderingen
C# feature specifications