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.Range
mohou 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 typexpr2
jeint
, bude přeložen nareceiver.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:
-
receiver
se vyhodnocuje; -
expr
se vyhodnocuje; -
length
se vyhodnocuje, je-li třeba; - 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 typuint
. - 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átuexpr1..expr2
(kdeexpr2
lze vynechat) aexpr1
má typint
, bude emitováno jakoexpr1
. - Je-li
expr
ve tvaru^expr1..expr2
(kdeexpr2
lze vynechat), pak bude emitován jakoreceiver.Length - expr1
. - Je-li
expr
ve tvaru..expr2
(kdeexpr2
lze vynechat), pak bude emitován jako0
. - 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átuexpr1..expr2
(kdeexpr1
lze vynechat) aexpr2
má typint
, bude emitováno jakoexpr2 - start
. - Je-li
expr
ve tvaruexpr1..^expr2
(kdeexpr1
lze vynechat), pak bude emitován jako(receiver.Length - expr2) - start
. - Je-li
expr
ve tvaruexpr1..
(kdeexpr1
lze vynechat), pak bude emitován jakoreceiver.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:
-
receiver
se vyhodnocuje; -
expr
se vyhodnocuje; -
length
se vyhodnocuje, je-li třeba; - metoda
Slice
je vyvolána.
Výrazy receiver
, expr
a 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
: metodaSubstring
bude použita místoSlice
. -
array
: metodaSystem.Runtime.CompilerServices.RuntimeHelpers.GetSubArray
bude použita místoSlice
.
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í s0..^0
a 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 jakoSpan<T>
jsou ideální pro podporu indexování nebo rozsahu. -
string
: neimplementujeICollection
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 zICollection
a proto dávají přednostCount
nad délkou. - Použít
Count
: vylučujestring
, pole,Span<T>
a většinu typů založených naref 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 typexpr2
jeint
, přeloží se nareceiver.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
C# feature specifications