Dela via


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.Rangekan 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 av expr2 är intöversätts det till receiver.Length - expr2.
  • Annars översätts den som expr.GetOffset(receiver.Length).

Oavsett den specifika konverteringsstrategin bör utvärderingsordningen motsvara följande:

  1. receiver utvärderas.
  2. expr utvärderas.
  3. length utvärderas vid behov.
  4. 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 typen int.
  • 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 receiversä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äret expr1..expr2 (där expr2 kan utelämnas) och expr1 har typen intgenereras den som expr1.
  • När expr är av formuläret ^expr1..expr2 (där expr2 kan utelämnas) genereras det som receiver.Length - expr1.
  • När expr är av formuläret ..expr2 (där expr2 kan utelämnas) genereras det som 0.
  • 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äret expr1..expr2 (där expr1 kan utelämnas) och expr2 har typen intgenereras den som expr2 - start.
  • När expr är av formuläret expr1..^expr2 (där expr1 kan utelämnas) genereras det som (receiver.Length - expr2) - start.
  • När expr är av formuläret expr1.. (där expr1 kan utelämnas) genereras det som receiver.Length - start.
  • Annars genereras den som expr.End.GetOffset(receiver.Length) - start.

Oavsett den specifika konverteringsstrategin bör utvärderingsordningen motsvara följande:

  1. receiver utvärderas.
  2. expr utvärderas.
  3. length utvärderas vid behov.
  4. metoden Slice anropas.

De receiver, exproch 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: metoden Substring används i stället för Slice.
  • array: metoden System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray används i stället för Slice.

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 semantiskt 0..^0och 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 som Span<T> är idealiska för index-/intervallstöd.
  • string: implementerar inte ICollection 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ån ICollection och föredrar därför Count framför längd.
  • Använd Count: exkluderar string, matriser, Span<T> och de flesta ref 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 av expr2 är intöversätts den till receiver.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