Sdílet prostřednictvím


Rozsahy

Poznámka

Tento článek je specifikace funkce. Specifikace slouží jako návrhový dokument pro funkci. Zahrnuje navrhované změny specifikace spolu s informacemi potřebnými při návrhu a vývoji funkce. Tyto články se publikují, dokud nebudou navrhované změny specifikace finalizovány a začleněny do aktuální specifikace ECMA.

Mezi specifikací funkce a dokončenou implementací může docházet k nějakým nesrovnalostem. Tyto rozdíly jsou zachyceny v poznámkách z jednání o návrhu jazyka (LDM) .

Další informace o procesu přijetí specifikací funkcí do jazyka C# najdete v článku o specifikacích .

Problém šampiona: https://github.com/dotnet/csharplang/issues/185

Shrnutí

Tato funkce se týká zavádění dvou nových operátorů, které umožňují vytváření System.Index a System.Range objektů a jejich použití k indexování a dělení kolekcí během běhu programu.

Přehled

Známé typy a členy

K použití nových syntaktických formulářů pro System.Index a System.Rangemohou být nezbytné nové dobře známé typy a členy v závislosti na tom, které syntaktické formuláře se používají.

K použití operátoru "hat" (^), je vyžadováno následující:

namespace System
{
    public readonly struct Index
    {
        public Index(int value, bool fromEnd);
    }
}

Chcete-li použít typ System.Index jako argument v přístupu k prvku pole, je vyžadován následující člen:

int System.Index.GetOffset(int length);

Syntaxe .. pro System.Range bude vyžadovat typ System.Range a také jeden nebo více následujících členů:

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; }
    }
}

Syntaxe .. umožňuje, aby chyběl buď jeden, oba, nebo žádný z argumentů. Bez ohledu na počet argumentů je konstruktor Range vždy dostatečný pro použití syntaxe Range. Pokud však některý z ostatních členů existuje a chybí jeden nebo více .. argumentů, může být příslušný člen nahrazen.

Pro použití hodnoty typu System.Range ve výrazu přístupu k prvku pole musí být dostupný následující atribut:

namespace System.Runtime.CompilerServices
{
    public static class RuntimeHelpers
    {
        public static T[] GetSubArray<T>(T[] array, System.Range range);
    }
}

System.Index

Jazyk C# nemá žádný způsob indexování kolekce od konce, ale většina indexerů používá pojem "od začátku" nebo výraz "length - i". Zavádíme nový výraz indexu, který znamená "od konce". Funkce představí nový unární operátor předpony "hat". Jeho jeden operand musí být konvertibilní na System.Int32. Bude vloženo do příslušného volání tovární metody System.Index.

Gramatiku pro unary_expression rozšiřujeme o následující další syntaxi:

unary_expression
    : '^' unary_expression
    ;

Nazýváme to index z koncového operátoru. Předdefinované indexy z koncových operátorů jsou následující:

System.Index operator ^(int fromEnd);

Chování tohoto operátoru je definováno pouze pro vstupní hodnoty větší nebo rovno nule.

Příklady:

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

Jazyk C# nemá žádný syntaktický způsob přístupu k rozsahům nebo výsekům kolekcí. Uživatelé jsou obvykle nuceni implementovat složité struktury pro filtrování nebo práci s řezy paměti, nebo se uchylují k metodám LINQ, jako je list.Skip(5).Take(2). S přidáním System.Span<T> a dalších podobných typů je důležitější mít tento druh operace podporovaný na hlubší úrovni jazyka/modulu runtime a mít sjednocené rozhraní.

Jazyk zavede nový operátor rozsahu x..y. Jedná se o binární infixní operátor, který přijímá dva výrazy. Oba operandy je možné vynechat (příklady níže) a musí být konvertibilní na System.Index. Bude sníženo na příslušné volání factory metody System.Range.

Gramatická pravidla jazyka C# pro multiplicative_expression nahradíme následujícím kódem (aby se zavedla nová úroveň priority):

range_expression
    : unary_expression
    | range_expression? '..' range_expression?
    ;

multiplicative_expression
    : range_expression
    | multiplicative_expression '*' range_expression
    | multiplicative_expression '/' range_expression
    | multiplicative_expression '%' range_expression
    ;

Všechny formy operátoru rozsahu mají stejnou prioritu. Tato nová skupina priorit je nižší než unární operátory a vyšší než multiplikativní aritmetické operátory.

Operátor .. nazýváme operátorem rozsahu . Předdefinovaný operátor rozsahu lze přibližně pochopit jako volání předdefinovaného operátoru v této podobě:

System.Range operator ..(Index start = 0, Index end = ^0);

Příklady:

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]

Kromě toho by System.Index měl mít implicitní převod z System.Int32, aby se zabránilo potřebě přetížení při kombinaci celých čísel a indexů v rámci vícerozměrných signatur.

Přidání podpory indexu a rozsahu do existujících typů knihoven

Podpora implicitního indexu

Jazyk poskytne členu indexeru instance s jedním parametrem typu Index pro typy, které splňují následující kritéria:

  • Typ je počitatelný.
  • Typ má indexer instance s podporou přístupnosti, který jako argument přebírá jeden int.
  • Typ nemá přístupný instanční indexer, který jako první parametr přijímá Index. Index musí být jediný parametr nebo zbývající parametry musí být volitelné.

Typ je počitelný, pokud má vlastnost nazvanou Length nebo Count s přístupným getterem a návratovým typem int. Jazyk může tuto vlastnost použít k převodu výrazu typu Index na int v okamžiku výrazu, aniž by bylo nutné použít typ Index vůbec. V případě, že jsou k dispozici Length i Count, bude upřednostňovat Length. Pro zjednodušení bude návrh používat název Length k reprezentaci Count nebo Length.

U takových typů bude jazyk fungovat, jako by byl členem indexeru formuláře T this[Index index], kde T je návratový typ indexeru založeného na int včetně všech poznámek stylu ref. Nový člen bude mít stejné členy get a set s odpovídající přístupností jako indexer int.

Nový indexer bude implementován převodem argumentu typu Index na int a vygenerováním volání indexeru založeného na int. Pro účely diskuze použijeme příklad receiver[expr]. Převod expr na int proběhne takto:

  • Pokud je argument ve formátu ^expr2 a typ expr2 je int, bude přeložen na receiver.Length - expr2.
  • Jinak se přeloží jako expr.GetOffset(receiver.Length).

Pořadí vyhodnocení by bez ohledu na konkrétní konverzní strategii mělo být ekvivalentní následujícímu:

  1. receiver se vyhodnocuje;
  2. expr se vyhodnocuje;
  3. length se vyhodnocuje, je-li třeba;
  4. vyvolá se indexer založený na int.

To umožňuje vývojářům používat funkci Index u existujících typů, aniž by museli upravovat. Například:

List<char> list = ...;
var value = list[^1];

// Gets translated to
var value = list[list.Count - 1];

Výrazy receiver a Length se podle potřeby přelijí, aby se zajistilo, že se všechny vedlejší účinky spustí jenom jednou. Například:

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);
    }
}

Tento kód vytiskne "Délka je 3".

Podpora implicitního rozsahu

Jazyk poskytne členu indexeru instance s jedním parametrem typu Range pro typy, které splňují následující kritéria:

  • Typ je počitatelný.
  • Typ má přístupný člen s názvem Slice, který má dva parametry typu int.
  • Typ nemá indexer instance, který jako první parametr přebírá jeden Range. Range musí být jediný parametr nebo zbývající parametry musí být volitelné.

U takových typů jazyk vytvoří vazbu, jako kdyby byl členem indexeru formuláře T this[Range range], kde T je návratový typ metody Slice včetně všech poznámek stylu ref. Nový člen bude mít také shodnou přístupnost s Slice.

Pokud je indexer založený na Range vázán na výraz s názvem receiver, bude nižší převodem výrazu Range na dvě hodnoty, které se pak předají metodě Slice. Pro účely diskuze použijeme příklad receiver[expr].

První argument Slice se získá převedením výrazu zadaného jako rozsah následujícím způsobem:

  • Je-li expr ve formátu expr1..expr2 (kde expr2 lze vynechat) a expr1 má typ int, bude emitováno jako expr1.
  • Je-li expr ve tvaru ^expr1..expr2 (kde expr2 lze vynechat), pak bude emitován jako receiver.Length - expr1.
  • Je-li expr ve tvaru ..expr2 (kde expr2 lze vynechat), pak bude emitován jako 0.
  • Jinak bude emitován jako expr.Start.GetOffset(receiver.Length).

Tato hodnota se znovu použije při výpočtu druhého argumentu Slice. Při tom se bude označovat jako start. Druhý argument Slice se získá tak, že se rozsahový výraz převede následujícím způsobem:

  • Je-li expr ve formátu expr1..expr2 (kde expr1 lze vynechat) a expr2 má typ int, bude emitováno jako expr2 - start.
  • Je-li expr ve tvaru expr1..^expr2 (kde expr1 lze vynechat), pak bude emitován jako (receiver.Length - expr2) - start.
  • Je-li expr ve tvaru expr1.. (kde expr1 lze vynechat), pak bude emitován jako receiver.Length - start.
  • Jinak bude emitován jako expr.End.GetOffset(receiver.Length) - start.

Pořadí vyhodnocení by bez ohledu na konkrétní konverzní strategii mělo být ekvivalentní následujícímu:

  1. receiver se vyhodnocuje;
  2. expr se vyhodnocuje;
  3. length se vyhodnocuje, je-li třeba;
  4. metoda Slice je vyvolána.

Výrazy receiver, expra length budou podle potřeby alokovány, aby se zajistilo, že se všechny vedlejší účinky spustí pouze jednou. Například:

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);
    }
}

Tento kód vytiskne "Zjistit délku 2".

Jazyk bude speciální pro následující známé typy:

  • string: metoda Substring bude použita místo Slice.
  • array: metoda System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray bude použita místo Slice.

Alternativy

Nové operátory (^ a ..) jsou syntaktický cukr. Funkcionalita může být implementována pomocí explicitních volání továrních metod System.Index a System.Range, ale výsledkem bude mnohem více šablonového kódu a zkušenost bude neintuitivní.

Reprezentace IL

Tyto dva operátory se sníží na běžná volání indexeru nebo metody beze změny v následných vrstvách kompilátoru.

Chování za běhu

  • Kompilátor může optimalizovat indexery pro předdefinované typy, jako jsou pole a řetězce, a snížit indexování na odpovídající existující metody.
  • System.Index vyvolá výjimku, pokud bude vytvořen se zápornou hodnotou.
  • ^0 se nevyvolá, ale překládá se na délku kolekce/výčtu, do které se dodává.
  • Range.All je sémanticky ekvivalentní s 0..^0a lze jej rozložit na tyto indexy.

Úvahy

Zjištění indexovatelného na základě ICollection

Inspiraci pro toto chování byly inicializátory kolekcí. Pomocí struktury typu vyjadřuje, že se přihlásil k nějaké funkci. V případě inicializátorů kolekcí se typy mohou zapojit do funkce implementací rozhraní IEnumerable (ne generické).

Tento návrh původně vyžadoval, aby typy implementovaly ICollection, aby se kvalifikovaly jako indexovatelné. To však vyžadovalo řadu zvláštních případů:

  • ref struct: Tyto zatím nemohou implementovat rozhraní, ale typy jako Span<T> jsou ideální pro podporu indexování nebo rozsahu.
  • string: neimplementuje ICollection a přidání tohoto rozhraní by znamenalo velké náklady.

To znamená, že k podpoře typů klíčů už je potřeba speciální pouzdro. Speciální případy pro string jsou méně zajímavé, protože jazyk tak činí v jiných oblastech (foreach změna velikosti písmen, konstanty atd.). Speciální případy pro ref struct jsou více znepokojující, protože se týkají celé třídy typů. Pokud mají jednoduše vlastnost s názvem Count s návratovým typem int, zobrazí se jim označení Indexable.

Po zvážení návrh byl normalizován, aby řekl, že jakýkoli typ, který má vlastnost Count / Length s návratovým typem int je indexovatelný. Tím se odeberou všechna speciální pouzdra, a to i pro string a pole.

Zjistit pouze počet

Detekce názvů vlastností Count nebo Length trochu komplikuje návrh. Výběr pouze jednoho, který se má standardizovat, ale nestačí, protože končí s výjimkou velkého počtu typů:

  • Použijte Length: vyloučí prakticky všechny kolekce v System.Collections a dílčích oborech názvů. Ty mají tendenci odvozovat z ICollection a proto dávají přednost Count nad délkou.
  • Použít Count: vylučuje string, pole, Span<T> a většinu typů založených na ref struct

Další komplikace při počáteční detekci indexovatelných typů převáží zjednodušením v jiných aspektech.

Volba názvu 'Slice'

Název Slice byl zvolen, protože se jedná o standardní název operací ve stylu řezu v .NET. Počínaje verzí netcoreapp2.1 používají všechny typy stylů span název Slice pro operace rozdělení. Před verzí netcoreapp2.1 skutečně neexistují žádné příklady slicingu, na které byste mohli odkázat. Typy jako List<T>, ArraySegment<T>, SortedList<T> by byly ideální pro segmentování, ale tento koncept při přidání typů neexistoval.

Proto Slice jako jediný příklad byl zvolen jako název.

Převod cílového typu indexu

Dalším způsobem, jak zobrazit transformaci Index ve výrazu indexeru, je jako převod cílového typu. Místo vazby, jako by byl členem formuláře return_type this[Index], jazyk místo toho přiřadí cílový typ převodu int.

Tento koncept může být zobecněn pro přístup všech členů k počítaným typům. Kdykoli je výraz s typem Index použit jako argument při vyvolání člena instance a příjemce je Countable, výraz projde cílovým převodem typu na int. Volání členů pro tento převod zahrnují metody, indexery, vlastnosti, rozšiřující metody atd. Pouze konstruktory jsou vyloučeny, protože nemají žádný přijímač.

Převod cílového typu bude implementován následujícím způsobem pro libovolný výraz, který má typ Index. Pro účely diskuse použijme příklad receiver[expr]:

  • Když má expr tvar ^expr2 a typ expr2 je int, přeloží se na receiver.Length - expr2.
  • Jinak se přeloží jako expr.GetOffset(receiver.Length).

Výrazy receiver a Length se podle potřeby přelijí, aby se zajistilo, že se všechny vedlejší účinky spustí jenom jednou. Například:

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);
    }
}

Tento kód vytiskne "Délka je 3".

Tato funkce by byla přínosná pro všechny členy, kteří měli parametr, který představoval index. Například List<T>.InsertAt. To může také vést k nejasnostem, protože jazyk nemůže poskytnout žádné pokyny k tomu, jestli je výraz určený pro indexování. Stačí převést libovolný výraz Index na int při vyvolání člena typu Početný.

Omezení:

  • Tento převod platí pouze v případě, že výraz s typem Index je přímo argumentem člena. Nevztahuje se na žádné vnořené výrazy.

Rozhodnutí přijatá během provádění

  • Všichni členové v modelu musí být členy instance.
  • Pokud se najde metoda Length, ale má nesprávný návratový typ, pokračujte hledáním count.
  • Indexer použitý pro vzor indexu musí mít přesně jeden int parametr.
  • Metoda Slice použitá pro vzor Range musí mít přesně dva int parametry.
  • Při hledání členů vzorů vyhledáme původní definice, ne vytvořené členy.

Schůzky k návrhu