Områden
Notera
Den här artikeln är en funktionsspecifikation. Specifikationen fungerar som designdokument för funktionen. Den innehåller föreslagna specifikationsändringar, tillsammans med information som behövs under utformningen och utvecklingen av funktionen. Dessa artiklar publiceras tills de föreslagna specifikationsändringarna har slutförts och införlivats i den aktuella ECMA-specifikationen.
Det kan finnas vissa skillnader mellan funktionsspecifikationen och den slutförda implementeringen. Dessa skillnader finns i de relevanta anteckningarna från Language Design Meeting (LDM) .
Du kan läsa mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.
Champion-utgåva: https://github.com/dotnet/csharplang/issues/185
Sammanfattning
Den här funktionen syftar till att introducera två nya operatorer som möjliggör konstruktion av System.Index
- och System.Range
-objekt och att använda dem för att indexera eller skära samlingar vid körning.
Överblick
Välkända typer och medlemmar
Om du vill använda de nya syntaktiska formulären för System.Index
och System.Range
kan nya välkända typer och medlemmar vara nödvändiga, beroende på vilka syntaktiska formulär som används.
För att använda "hat"-operatorn (^
) krävs följande
namespace System
{
public readonly struct Index
{
public Index(int value, bool fromEnd);
}
}
Om du vill använda System.Index
typ som ett argument i en matriselementåtkomst krävs följande medlem:
int System.Index.GetOffset(int length);
Syntaxen för ..
för System.Range
kräver System.Range
typ, samt en eller flera av följande medlemmar:
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; }
}
}
Med syntaxen ..
kan båda eller inget av argumenten vara frånvarande. Oavsett antalet argument räcker Range
konstruktorn alltid för att använda Range
syntax. Men om någon av de andra medlemmarna är närvarande och ett eller flera av de ..
argumenten saknas, kan lämplig medlem ersättas.
För att ett värde av typen System.Range
ska användas i ett åtkomstuttryck för matriselement måste följande medlem finnas:
namespace System.Runtime.CompilerServices
{
public static class RuntimeHelpers
{
public static T[] GetSubArray<T>(T[] array, System.Range range);
}
}
System.Index
C# har inget sätt att indexera en samling från slutet, utan snarare använder de flesta indexerare begreppet "från början" eller gör ett "längd - i"-uttryck. Vi introducerar ett nytt indexuttryck som betyder "från slutet". Funktionen kommer att introducera en ny unär prefixoperator "hat". Dess enda operand måste konverteras till System.Int32
. Det kommer att sänkas till lämpligt System.Index
fabriksmetodanrop.
Vi utökar grammatiken för unary_expression med följande ytterligare syntaxformulär:
unary_expression
: '^' unary_expression
;
Vi kallar detta -indexet hos slutoperatorn. Det fördefinierade -indexet för de slutliga operatörerna är följande:
System.Index operator ^(int fromEnd);
Beteendet för den här operatorn definieras endast för indatavärden som är större än eller lika med noll.
Exempel:
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# har inget syntaktiskt sätt att komma åt "intervall" eller "sektorer" av samlingar. Vanligtvis tvingas användarna att implementera komplexa strukturer för att filtrera/arbeta på minnessektorer eller använda LINQ-metoder som list.Skip(5).Take(2)
. Med tillägg av System.Span<T>
och andra liknande typer blir det viktigare att ha den här typen av åtgärd som stöds på en djupare nivå i språket/körningen och få gränssnittet enhetligt.
Språket introducerar en ny intervalloperator x..y
. Det är en binär infixoperator som accepterar två uttryck. Endera operand kan utelämnas (exempel nedan) och de måste konverteras till System.Index
. Metoden kommer att sänkas till det lämpliga fabriksmetodanropet System.Range
.
Vi ersätter C#-grammatikreglerna för multiplicative_expression med följande (för att införa en ny prioritetsnivå):
range_expression
: unary_expression
| range_expression? '..' range_expression?
;
multiplicative_expression
: range_expression
| multiplicative_expression '*' range_expression
| multiplicative_expression '/' range_expression
| multiplicative_expression '%' range_expression
;
Alla former av intervalloperator ha samma prioritet. Den här nya prioritetsgruppen har lägre prioritet än de unära operatorerna och högre än de multiplikativa aritmetiska operatorerna .
Vi kallar ..
-operatorn för intervalloperator. Den inbyggda intervalloperatorn kan ungefär tolkas som att den motsvarar anropet av en inbyggd operator i det här formuläret:
System.Range operator ..(Index start = 0, Index end = ^0);
Exempel:
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]
Dessutom bör System.Index
ha en implicit konvertering från System.Int32
, för att undvika behovet av att hantera överbelastning av blandade heltal och index i flerdimensionella signaturer.
Lägga till stöd för index och intervall till befintliga bibliotekstyper
Stöd för implicit index
Språket ger en instansindexerare medlem med en enda parameter av typen Index
för typer som uppfyller följande villkor:
- Typen är Countable.
- Typen har en tillgänglig instansindexerare som tar en enda
int
som argument. - Typen har ingen tillgänglig instansindexerare som tar en
Index
som den första parametern.Index
måste vara den enda parametern, annars måste de återstående parametrarna vara valfria.
En typ är Countable om den har en egenskap som heter Length
eller Count
, med en tillgänglig getter och en returtyp av int
. Språket kan använda den här egenskapen för att konvertera ett uttryck av typen Index
till en int
vid uttryckspunkten utan att behöva använda typen Index
alls. Om både Length
och Count
finns kommer Length
att föredras. För enkelhetens skull använder förslaget namnet Length
för att representera Count
eller Length
.
För sådana typer fungerar språket som om det finns en indexerarmedlem i formen T this[Index index]
där T
är returtypen för den int
-baserade indexeraren, inklusive eventuella ref
stilanteckningar. Den nya medlemmen kommer att ha samma get
och set
medlem med matchande åtkomsträttigheter som int
indexeraren.
Den nya indexeraren implementeras genom att konvertera argumentet av typen Index
till en int
och skicka ett anrop till den int
baserade indexeraren. I diskussionssyfte använder vi exemplet med receiver[expr]
. Konverteringen av expr
till int
sker på följande sätt:
- När argumentet är av formuläret
^expr2
och typen avexpr2
ärint
översätts det tillreceiver.Length - expr2
. - Annars översätts den som
expr.GetOffset(receiver.Length)
.
Oavsett den specifika konverteringsstrategin bör utvärderingsordningen motsvara följande:
-
receiver
utvärderas. -
expr
utvärderas. -
length
utvärderas vid behov. - den
int
-baserade indexeraren som anropas.
På så sätt kan utvecklare använda funktionen Index
på befintliga typer utan att behöva ändra den. Till exempel:
List<char> list = ...;
var value = list[^1];
// Gets translated to
var value = list[list.Count - 1];
De receiver
- och Length
-uttrycken kommer att hanteras på lämpligt sätt för att säkerställa att eventuella sidoeffekter endast körs en gång. Till exempel:
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);
}
}
Den här koden skriver ut "Hämta längd 3".
Stöd för implicit intervall
Språket ger en instansindexerare medlem med en enda parameter av typen Range
för typer som uppfyller följande villkor:
- Typen är Countable.
- Typen har en tillgänglig medlem med namnet
Slice
som har två parametrar av typenint
. - Typen har ingen instansindexerare som tar en enda
Range
som den första parametern.Range
måste vara den enda parametern, annars måste de återstående parametrarna vara valfria.
För sådana typer kommer språket att binda som om det finns en indexerarmedlem av formen T this[Range range]
där T
är returtypen för metoden Slice
inklusive eventuella stilanteckningar av typen ref
. Den nya medlemmen kommer också att ha matchande tillgänglighet med Slice
.
När den Range
baserade indexeraren är bunden till ett uttryck med namnet receiver
sänks det genom att konvertera Range
-uttrycket till två värden som sedan skickas till metoden Slice
. I diskussionssyfte använder vi exemplet med receiver[expr]
.
Det första argumentet i Slice
hämtas genom att konvertera det typerade uttrycket för intervallet på följande sätt:
- När
expr
är av formuläretexpr1..expr2
(därexpr2
kan utelämnas) ochexpr1
har typenint
genereras den somexpr1
. - När
expr
är av formuläret^expr1..expr2
(därexpr2
kan utelämnas) genereras det somreceiver.Length - expr1
. - När
expr
är av formuläret..expr2
(därexpr2
kan utelämnas) genereras det som0
. - Annars genereras den som
expr.Start.GetOffset(receiver.Length)
.
Det här värdet kommer att återanvändas i beräkningen av det andra Slice
argumentet. När detta görs kommer det att kallas start
. Det andra argumentet i Slice
hämtas genom att konvertera det typerade uttrycket för intervallet på följande sätt:
- När
expr
är av formuläretexpr1..expr2
(därexpr1
kan utelämnas) ochexpr2
har typenint
genereras den somexpr2 - start
. - När
expr
är av formuläretexpr1..^expr2
(därexpr1
kan utelämnas) genereras det som(receiver.Length - expr2) - start
. - När
expr
är av formuläretexpr1..
(därexpr1
kan utelämnas) genereras det somreceiver.Length - start
. - Annars genereras den som
expr.End.GetOffset(receiver.Length) - start
.
Oavsett den specifika konverteringsstrategin bör utvärderingsordningen motsvara följande:
-
receiver
utvärderas. -
expr
utvärderas. -
length
utvärderas vid behov. - metoden
Slice
anropas.
De receiver
, expr
och length
uttryck kommer att spillas ut efter behov för att säkerställa att eventuella biverkningar endast körs en gång. Till exempel:
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);
}
}
Den här koden kommer att skriva ut "Get Length 2".
Språket kommer att behandla följande kända typer särskilt:
-
string
: metodenSubstring
används i stället förSlice
. -
array
: metodenSystem.Runtime.CompilerServices.RuntimeHelpers.GetSubArray
används i stället förSlice
.
Alternativ
De nya operatörerna (^
och ..
) är syntaktisk socker. Funktionen kan implementeras med explicita anrop till System.Index
och System.Range
fabriksmetoder, men det resulterar i mycket mer standardkod, och upplevelsen blir inte intuitiv.
IL-representation
Dessa två operatorer kommer att sänkas till vanliga indexerare/metodanrop, utan någon ändring i efterföljande kompilatorlager.
Körningsbeteende
- Kompilatorn kan optimera indexerare för inbyggda typer som matriser och strängar och sänka indexeringen till lämpliga befintliga metoder.
-
System.Index
genererar om det skapas med ett negativt värde. -
^0
kastar inte, men det översätts till längden på samlingen/enumerationen till vilken det levereras. -
Range.All
motsvarar semantiskt0..^0
och kan dekonstrueras till dessa index.
Överväganden
Identifiera indexerbara objekt baserat på ICollection
Inspirationen till det här beteendet var insamlingsinitierare. Använda strukturen för en typ för att förmedla att den hade valt en funktion. När det gäller typer av insamlingsinitierare kan du välja funktionen genom att implementera gränssnittet IEnumerable
(icke-generisk).
Det här förslaget krävde inledningsvis att typerna implementerar ICollection
för att kvalificera sig som indexerbara. Det krävde dock ett antal särskilda fall:
-
ref struct
: dessa kan inte implementera gränssnitt men typer somSpan<T>
är idealiska för index-/intervallstöd. -
string
: implementerar inteICollection
och att lägga till gränssnittet har en stor kostnad.
Det innebär att du redan behöver stöd för viktiga typer av särskilda höljen. Den speciella hanteringen av string
är mindre intressant eftersom språket gör detta i andra sammanhang (foreach
-nedsänkning, konstanter, etc ...). Den speciella hanteringen av ref struct
är mer oroande eftersom den omfattar en hel klass av typer. De får etiketten Indexerbar om de bara har en egenskap med namnet Count
med en returtyp av int
.
Efter övervägande normaliserades designen för att säga att alla typer som har en egenskap Count
/ Length
med en returtyp av int
är indexerbara. Det tar bort alla särskilda höljen, även för string
och matriser.
Identifiera bara antal
Identifiering på egenskapsnamnen Count
eller Length
komplicerar designen lite. Det räcker dock inte att bara välja en för att standardisera eftersom det slutar med att ett stort antal typer utesluts:
- Använd
Length
: exkluderar i stort sett alla samlingar i System.Collections och undernamnområden. De tenderar att härledas frånICollection
och föredrar därförCount
framför längd. - Använd
Count
: exkluderarstring
, matriser,Span<T>
och de flestaref struct
baserade typer
Den extra komplikationen vid den första identifieringen av indexerbara typer uppvägs av dess förenkling i andra aspekter.
Val av Slice som namn
Namnet Slice
valdes eftersom det är de facto-standardnamnet för segmentformatsåtgärder i .NET. Från och med netcoreapp2.1 använder alla typer av intervallformat namnet Slice
för segmenteringsåtgärder. Före netcoreapp2.1 finns det egentligen inga exempel på segmentering att titta på för ett exempel. Typer som List<T>
, ArraySegment<T>
, SortedList<T>
skulle ha varit idealiska för segmentering, men konceptet fanns inte när typer lades till.
Så, eftersom Slice
var det enda exemplet, valdes det som namn.
Konvertering av indexmåltyp
Ett annat sätt att visa Index
transformering i ett indexeraruttryck är som en måltypkonvertering. I stället för att binda som om det finns en medlem i formuläret return_type this[Index]
tilldelar språket i stället en måltypkonvertering till int
.
Det här konceptet kan generaliseras för all medlemsåtkomst för countable-typer. När ett uttryck med typen Index
används som ett argument vid anrop av en instansmedlem och om mottagaren är Countable, omvandlas uttrycket till måltypen int
. De medlemsanrop som gäller för den här konverteringen omfattar metoder, indexerare, egenskaper, tilläggsmetoder osv . Endast konstruktorer undantas eftersom de inte har någon mottagare.
Måltypkonverteringen implementeras på följande sätt för alla uttryck som har en typ av Index
. I diskussionssyfte kan du använda exemplet med receiver[expr]
:
- När
expr
är av formuläret^expr2
och typen avexpr2
ärint
översätts den tillreceiver.Length - expr2
. - Annars översätts den som
expr.GetOffset(receiver.Length)
.
De receiver
- och Length
-uttrycken kommer att hanteras på lämpligt sätt för att säkerställa att eventuella sidoeffekter endast körs en gång. Till exempel:
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);
}
}
Den här koden skriver ut "Hämta längd 3".
Den här funktionen skulle vara fördelaktig för alla medlemmar som hade en parameter som representerade ett index. Till exempel List<T>.InsertAt
. Detta kan också orsaka förvirring eftersom språket inte kan ge någon vägledning om huruvida ett uttryck är avsett för indexering eller inte. Allt det kan göra är att konvertera alla Index
-uttryck till int
när en medlem anropas på en Countable-typ.
Inskränkningar:
- Den här konverteringen gäller endast när uttrycket med typen
Index
är direkt ett argument till medlemmen. Det skulle inte gälla för kapslade uttryck.
Beslut som fattas under implementeringen
- Alla medlemmar i mönstret måste vara instansmedlemmar
- Om en length-metod hittas men den har fel returtyp fortsätter du att leta efter Antal
- Indexeraren som används för indexmönstret måste ha exakt en int-parameter
- Den
Slice
metod som används för range-mönstret måste ha exakt två int-parametrar - När vi letar efter mönstermedlemmar letar vi efter ursprungliga definitioner, inte konstruerade medlemmar
Designa möten
C# feature specifications