Delen via


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..yintroduceren. 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.Int32hebben 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. De Index 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 Lengthweer 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 type expr2 is int, wordt het omgezet in receiver.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:

  1. receiver wordt geëvalueerd;
  2. expr wordt geëvalueerd;
  3. Indien nodig wordt length geëvalueerd;
  4. 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 type int.
  • Het type heeft geen exemplaarindexeerfunctie die één Range als eerste parameter gebruikt. De Range 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 vorm expr1..expr2 heeft (waarbij expr2 kan worden weggelaten) en expr1 type intheeft, wordt het uitgevoerd als expr1.
  • Wanneer expr van de vorm ^expr1..expr2 is (waarbij expr2 kan worden weggelaten), wordt het verzonden als receiver.Length - expr1.
  • Wanneer expr van de vorm ..expr2 is (waarbij expr2 kan worden weggelaten), wordt het verzonden als 0.
  • 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 vorm expr1..expr2 heeft (waarbij expr1 kan worden weggelaten) en expr2 type intheeft, wordt het uitgevoerd als expr2 - start.
  • Wanneer expr van de vorm expr1..^expr2 is (waarbij expr1 kan worden weggelaten), wordt het verzonden als (receiver.Length - expr2) - start.
  • Wanneer expr van de vorm expr1.. is (waarbij expr1 kan worden weggelaten), wordt het verzonden als receiver.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:

  1. receiver wordt geëvalueerd;
  2. expr wordt geëvalueerd;
  3. Indien nodig wordt length geëvalueerd;
  4. de methode Slice wordt aangeroepen.

De receiver, expren 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 methode Substring wordt gebruikt in plaats van Slice.
  • array: de methode System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray wordt gebruikt in plaats van Slice.

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 aan 0..^0en 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 zoals Span<T> zijn ideaal voor index- en bereikondersteuning.
  • stringimplementeert ICollection 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 van ICollection en geven daarom de voorkeur aan Count over lengte.
  • Gebruik Count: sluit string, matrices, Span<T> en de meeste ref 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 intondergaan. 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 type expr2intis, wordt deze omgezet in receiver.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