Sdílet prostřednictvím


Výrazy kolekce

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 příslušných poznámkách ze schůzky jazykového návrhu (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/8652

Shrnutí

Výrazy kolekce představují novou syntaxi terse, [e1, e2, e3, etc]která vytvoří společné hodnoty kolekce. Vložení dalších kolekcí do těchto hodnot je možné pomocí rozprostřeného prvku ..e , jako je například: [e1, ..c2, e2, ..c2].

Můžete vytvořit několik typů podobných kolekcím bez nutnosti externí podpory seznamu BCL. Mezi tyto typy patří:

Další podpora je k dispozici pro typy podobné kolekci, které nejsou popsané výše prostřednictvím nového atributu a vzoru rozhraní API, které lze přijmout přímo na samotný typ.

Motivation

  • Hodnoty podobné kolekci se výrazně vyskytují v programování, algoritmech a zejména v ekosystému C#/.NET. Téměř všechny programy budou tyto hodnoty využívat k ukládání dat a odesílání nebo přijímání dat z jiných komponent. V současné době musí téměř všechny programy v jazyce C# používat mnoho různých a bohužel podrobných přístupů k vytváření instancí těchto hodnot. Některé přístupy mají také nevýhody výkonu. Tady je několik běžných příkladů:

    • Matice, které vyžadují hodnoty new Type[] nebo new[] před hodnotami { ... } .
    • Rozsahy, které mohou používat stackalloc a další těžkopádné konstrukce.
    • Inicializátory kolekcí, které vyžadují syntaxi jako new List<T> (chybí odvození pravděpodobně podrobného T) před jejich hodnotami a které mohou způsobit více relokací paměti, protože používají volání N .Add bez poskytnutí počáteční kapacity.
    • Neměnné kolekce, které vyžadují syntaxi, jako ImmutableArray.Create(...) je inicializace hodnot, a které můžou způsobit zprostředkující přidělení a kopírování dat. Efektivnější stavební formy (jako ImmutableArray.CreateBuilder) jsou nepraktické a stále produkují neuložené odpadky.
  • Při pohledu na okolní ekosystém také najdeme příklady všude, kde vytváření seznamů je pohodlnější a příjemnější pro použití. TypeScript, Dart, Swift, Elm, Python a další možnosti se pro tento účel rozhodnou pro stručnou syntaxi s rozšířeným využitím a pro velký efekt. Šetření kurzoru odhalila žádné podstatné problémy vzniklé v těchto ekosystémech s tím, že tyto literály byly sestaveny.

  • Jazyk C# také přidal vzory seznamů v jazyce C# 11. Tento vzor umožňuje porovnávání a dekonstrukci hodnot podobných seznamu pomocí čisté a intuitivní syntaxe. Na rozdíl od téměř všech ostatních konstruktorů vzorů však tato syntaxe porovnávání/dekonstrukce chybí odpovídající syntaxi konstrukce.

  • Získání nejlepšího výkonu pro vytváření jednotlivých typů kolekce může být složité. Jednoduchá řešení často ztrácí využití procesoru i paměti. Použití literálového formuláře umožňuje maximální flexibilitu od implementace kompilátoru k optimalizaci literálu tak, aby vznikl alespoň tak dobrý výsledek, jak by mohl uživatel poskytnout, ale s jednoduchým kódem. Velmi často bude kompilátor schopen lépe provádět a cílem specifikace je umožnit implementaci velkých objemů volnosti, pokud jde o strategii implementace, aby to zajistilo.

Pro jazyk C# je potřeba inkluzivní řešení. Měla by splňovat velkou většinu casse pro zákazníky z hlediska typů podobných kolekcí a hodnot, které už mají. Měl by také mít přirozený pocit v jazyce a zrcadlit práci provedenou ve vzorovém párování.

To vede k přirozenému závěru, že syntaxe by měla být podobná [e1, e2, e3, e-etc] nebo , která odpovídá vzorovým ekvivalentům a [p1, ..p2, p3][p1, p2, p3, p-etc] .[e1, ..c2, e2]

Podrobný návrh

Přidají se následující gramatické produkce:

primary_no_array_creation_expression
  ...
+ | collection_expression
  ;

+ collection_expression
  : '[' ']'
  | '[' collection_element ( ',' collection_element )* ']'
  ;

+ collection_element
  : expression_element
  | spread_element
  ;

+ expression_element
  : expression
  ;

+ spread_element
  : '..' expression
  ;

Literály kolekce jsou typu cíle.

Vysvětlení specifikací

  • Pro stručnost collection_expression se bude v následujících částech označovat jako literál.

  • expression_element instance se obvykle označují jako e1, e_natd.

  • spread_element instance se obvykle označují jako ..s1, ..s_natd.

  • typ rozpětí znamená buď Span<T> nebo ReadOnlySpan<T>.

  • Literály se obvykle zobrazí tak, aby [e1, ..s1, e2, ..s2, etc] vyjadřovaly libovolný počet prvků v libovolném pořadí. Důležité je, že tento formulář bude sloužit k reprezentaci všech případů, například:

    • Prázdné literály []
    • Literály bez nich expression_element .
    • Literály bez nich spread_element .
    • Literály s libovolným pořadím libovolného typu elementu.
  • Typ iterace je typ..s_nproměnné iterace určené jako kdyby s_n byl použit jako výraz, který se iteruje v objektu foreach_statement.

  • Proměnné začínající __name se používají k vyjádření výsledků vyhodnocení namehodnoty , uložené v umístění tak, aby se vyhodnocovala pouze jednou. Příkladem __e1 je vyhodnocení .e1

  • List<T>, IEnumerable<T>atd. odkazují na příslušné typy v System.Collections.Generic oboru názvů.

  • Specifikace definuje překlad literálu do existujících konstruktorů jazyka C#. Podobně jako překlad výrazu dotazu je literál sám sám o sobě legální pouze v případě, že by překlad měl za následek právní kód. Účelem tohoto pravidla je vyhnout se opakování dalších pravidel jazyka, která jsou implicitní (například o převodu výrazů při přiřazení k umístěním úložiště).

  • Implementace není nutná k překladu literálů přesně tak, jak je uvedeno níže. Jakýkoli překlad je legální, pokud se vytvoří stejný výsledek a neexistují žádné pozorovatelné rozdíly v produkci výsledku.

    • Implementace by například mohla překládat literály jako [1, 2, 3] přímo do new int[] { 1, 2, 3 } výrazu, který sám upeče nezpracovaná data do sestavení, což posune potřebu __index nebo posloupnost instrukcí pro přiřazení každé hodnoty. Důležité je, že to znamená, že jakýkoli krok překladu může způsobit výjimku za běhu, že stav programu je stále ve stavu označeném překladem.
  • Odkazy na "přidělení zásobníku" odkazují na jakoukoli strategii přidělení zásobníku, nikoli haldy. Důležité je, že neznamená ani nevyžaduje, aby tato strategie byla prostřednictvím skutečného stackalloc mechanismu. Například použití vložených polí je také povolený a žádoucí přístup k dosažení přidělení zásobníku tam, kde je k dispozici. Všimněte si, že v jazyce C# 12 nelze inicializovat vložená pole pomocí výrazu kolekce. To zůstává otevřeným návrhem.

  • U kolekcí se předpokládá, že se dobře chovají. Například:

    • Předpokládá se, že hodnota Count kolekce vytvoří stejnou hodnotu jako počet prvků při výčtu.
    • U typů použitých v této specifikaci definované v System.Collections.Generic oboru názvů se předpokládá, že jsou bez vedlejšího efektu. Kompilátor může například optimalizovat scénáře, ve kterých se takové typy můžou používat jako zprostředkující hodnoty, ale jinak nebudou vystaveny.
    • Předpokládá se, že volání některého příslušného .AddRange(x) člena v kolekci bude mít za následek stejnou konečnou hodnotu jako iterace x a přidání všech jeho výčtových hodnot jednotlivě do kolekce s .Add.
    • Chování literálů kolekce s kolekcemi, které nejsou dobře se chovají, není definováno.

Conversions

Převod výrazu kolekce umožňuje převod výrazu kolekce na typ.

Převod implicitního výrazu kolekce existuje z výrazu kolekce na následující typy:

  • Jednorozměrný typT[] pole, v takovém případě typ prvkuT
  • Typ rozsahu:
    • System.Span<T>
    • System.ReadOnlySpan<T>
      V takovém případě je typ prvkuT
  • Typ s příslušnou metodou create, v takovém případě typ prvku je typ iterace určený z GetEnumerator metody instance nebo výčtového rozhraní, nikoli z rozšiřující metody.
  • Struktura nebotyp třídy, který implementujeSystem.Collections.IEnumerable, kde:
    • Typ mápoužitelný konstruktor, který lze vyvolat bez argumentů a konstruktor je přístupný v umístění výrazu kolekce.

    • Pokud výraz kolekce obsahuje nějaké prvky, typ má instanci nebo rozšiřující metodu Add , kde:

      • Metodu lze vyvolat pomocí argumentu s jednou hodnotou.
      • Pokud je metoda obecná, argumenty typu lze odvodit z kolekce a argumentu.
      • Metoda je přístupná v umístění výrazu kolekce.

      V takovém případě je typ prvkutyp iteracetypu.

  • Typ rozhraní:
    • System.Collections.Generic.IEnumerable<T>
    • System.Collections.Generic.IReadOnlyCollection<T>
    • System.Collections.Generic.IReadOnlyList<T>
    • System.Collections.Generic.ICollection<T>
    • System.Collections.Generic.IList<T>
      V takovém případě je typ prvkuT

Implicitní převod existuje, pokud typ má typT elementu, kde pro každý prvekEᵢ ve výrazu kolekce:

  • Pokud Eᵢ je prvek výrazu, existuje implicitní převod z Eᵢ na T.
  • Pokud Eᵢ je rozprostřený prvek..Sᵢ, existuje implicitní převod z typuSᵢ iterace na T.

Neexistuje žádný převod výrazu kolekce z výrazu kolekce na multidimenzionální typ pole.

Typy, pro které existuje implicitní převod výrazu kolekce z výrazu kolekce jsou platné cílové typy pro tento výraz kolekce.

Z výrazu kolekce existují následující další implicitní převody:

  • Na typ T?hodnoty s možnou hodnotou null, kde existuje převod výrazu kolekce z výrazu kolekce na typ Thodnoty . Převod je převod výrazu kolekce , za T nímž následuje implicitní převod nullable z T do T?.

  • Do typu T odkazu, kde je k dispozici metoda create , T která vrací typ U a implicitní převod odkazu z U na T. Převod je převod výrazu kolekce , za U nímž následuje implicitní odkaz převod z U na T.

  • Do typu I rozhraní, kde je k dispozici metoda create , I která vrací typ V a implicitní boxing převodu z V do I. Převod je převod výrazu kolekce , za V nímž následuje implicitní boxing převod z V do I.

Vytvoření metod

Metoda create je označena atributem [CollectionBuilder(...)] pro typ kolekce. Atribut určuje typ tvůrce a název metody , která má být vyvolána k vytvoření instance typu kolekce.

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(
        AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface,
        Inherited = false,
        AllowMultiple = false)]
    public sealed class CollectionBuilderAttribute : System.Attribute
    {
        public CollectionBuilderAttribute(Type builderType, string methodName);
        public Type BuilderType { get; }
        public string MethodName { get; }
    }
}

Atribut lze použít na , classstruct, ref structnebo interface. Atribut není zděděný, i když lze atribut použít na základ class nebo na abstract class.

Typ tvůrce musí být ne generický class nebo struct.

Nejprve je určena sada použitelných metodCM vytvoření.
Skládá se z metod, které splňují následující požadavky:

  • Metoda musí mít název zadaný v atributu [CollectionBuilder(...)] .
  • Metoda musí být definována přímo u typu tvůrce .
  • Metoda musí být static.
  • Metoda musí být přístupná tam, kde se používá výraz kolekce.
  • Arity metody musí odpovídat arity typu kolekce.
  • Metoda musí mít jeden parametr typu System.ReadOnlySpan<E>, předaný hodnotou.
  • Existuje převod identity, implicitní převod odkazu nebo boxing převodu z metody návratový typ na typ kolekce.

Metody deklarované na základních typech nebo rozhraních se ignorují a nejsou součástí CM sady.

CM Pokud je sada prázdná, typ kolekce nemá typ elementu a nemá metodu create. Žádný z následujících kroků se nevztahuje.

Pokud pouze jedna metoda z těch v CM sadě má převod identity z E na typ elementutypu kolekce, to je create metoda pro typ kolekce. V opačném případě typ kolekce nemá metodu vytvoření.

Pokud atribut neodkazuje na vyvolání metody s očekávaným podpisem, zobrazí [CollectionBuilder] se chyba.

Pro výraz kolekce s cílovým typemC<S0, S1, …>, kde deklaraceC<T0, T1, …> typu má přidruženou metoduB.M<U0, U1, …>() tvůrce, se na metodu tvůrce použijí argumenty obecného typu z cílového typu v pořadí – a od nejkrajnějšího typu obsahujícího typ nejvíce dovnitř – do metody tvůrce.

Parametr span pro metodu create lze explicitně označit scoped nebo [UnscopedRef]. Pokud je parametr implicitně nebo explicitně scoped, kompilátor může přidělit úložiště pro rozsah v zásobníku, nikoli haldu.

Například možné vytvoření metody pro ImmutableArray<T>:

[CollectionBuilder(typeof(ImmutableArray), "Create")]
public struct ImmutableArray<T> { ... }

public static class ImmutableArray
{
    public static ImmutableArray<T> Create<T>(ReadOnlySpan<T> items) { ... }
}

Výše uvedenou ImmutableArray<int> ia = [1, 2, 3];metodu create je možné vygenerovat takto:

[InlineArray(3)] struct __InlineArray3<T> { private T _element0; }

Span<int> __tmp = new __InlineArray3<int>();
__tmp[0] = 1;
__tmp[1] = 2;
__tmp[2] = 3;
ImmutableArray<int> ia =
    ImmutableArray.Create((ReadOnlySpan<int>)__tmp);

Konstrukce

Prvky výrazu kolekce se vyhodnocují v pořadí, zleva doprava. Každý prvek se vyhodnotí přesně jednou a všechny další odkazy na prvky odkazují na výsledky tohoto počátečního vyhodnocení.

Prvek šíření může být iterated před nebo za následující prvky ve výrazu kolekce jsou vyhodnoceny.

Neošetřená výjimka vyvolaná některou z metod použitých během výstavby se nezachytí a zabrání dalším krokům v konstrukci.

Length, Counta GetEnumerator předpokládá se, že nemají žádné vedlejší účinky.


Pokud cílový typ je struktura nebo typ třídy , který implementuje System.Collections.IEnumerable, a cílový typ nemá metodu create, konstrukce instance kolekce je následující:

  • Prvky se vyhodnocují v pořadí. Některé nebo všechny prvky mohou být vyhodnoceny v následujících krocích, nikoli dříve.

  • Kompilátor může určit známou délku výrazu kolekce vyvoláním počítaných vlastností – nebo ekvivalentních vlastností z dobře známých rozhraní nebo typů – u každého výrazu elementu šíření.

  • Konstruktor, který je použitelný bez argumentů, je vyvolán.

  • Pro každý prvek v pořadí:

    • Pokud je elementem výrazu, je příslušná Add instance nebo rozšiřující metoda vyvolána s výrazem elementu jako argument. (Na rozdíl od chování inicializátoru klasické kolekce nemusí být vyhodnocení prvků a Add volání nutně prokládání.)
    • Pokud je prvek rozprostřený prvek , použije se jedna z následujících možností:
      • Příslušná GetEnumerator instance nebo rozšiřující metoda je vyvolána na výrazu elementu spread a pro každou položku z enumerátoru je Add příslušná instance nebo rozšiřující metoda vyvolána v instanci kolekce s položkou jako argument. Pokud enumerátor implementuje IDisposable, bude volána Dispose po výčtu bez ohledu na výjimky.
      • V instanci kolekce se jako argument vyvolá příslušná AddRange instance nebo rozšiřující metoda s výrazem elementu spread.
      • Příslušná CopyTo instance nebo rozšiřující metoda se vyvolá na výraz elementu spread s instancí kolekce a int indexem jako argumenty.
  • Během výše uvedených kroků výstavby může být v instanci kolekce jednou nebo vícekrát vyvolána příslušná EnsureCapacity instance nebo metoda rozšíření s argumentem int kapacity.


Pokud je cílovým typem pole, rozsah, typ s metodou create nebo rozhraní, je konstrukce instance kolekce následující:

  • Prvky se vyhodnocují v pořadí. Některé nebo všechny prvky mohou být vyhodnoceny v následujících krocích, nikoli dříve.

  • Kompilátor může určit známou délku výrazu kolekce vyvoláním počítaných vlastností – nebo ekvivalentních vlastností z dobře známých rozhraní nebo typů – u každého výrazu elementu šíření.

  • Inicializační instance se vytvoří takto:

    • Pokud je cílovým typem pole a výraz kolekce má známou délku, je pole přiděleno s očekávanou délkou.
    • Pokud je cílovým typem rozsah nebo typ s metodou vytvoření a kolekce má známou délku, vytvoří se rozsah s očekávanou délkou odkazující na souvislé úložiště.
    • V opačném případě se přidělí přechodné úložiště.
  • Pro každý prvek v pořadí:

    • Pokud je elementem výrazu, inicializační instance indexer je vyvolán pro přidání vyhodnoceného výrazu do aktuálního indexu.
    • Pokud je prvek rozprostřený prvek , použije se jedna z následujících možností:
      • Člen dobře známého rozhraní nebo typu je vyvolán ke kopírování položek z výrazu elementu spread do inicializační instance.
      • Příslušná GetEnumerator instance nebo rozšiřující metoda je vyvolána na výraz elementu spread a pro každou položku z enumerátoru je vyvolána inicializační instance indexer pro přidání položky do aktuálního indexu. Pokud enumerátor implementuje IDisposable, bude volána Dispose po výčtu bez ohledu na výjimky.
      • Příslušná CopyTo instance nebo rozšiřující metoda je vyvolána na výraz elementu spread s inicializační instance a int index jako argumenty.
  • Pokud bylo pro kolekci přiděleno přechodné úložiště, je instance kolekce přidělena se skutečnou délkou kolekce a hodnoty z inicializační instance se zkopírují do instance kolekce nebo pokud je vyžadováno rozpětí, může kompilátor použít rozsah skutečné délky kolekce z přechodného úložiště. Jinak inicializační instance představuje instanci kolekce.

  • Pokud má cílový typ metodu create, metoda create se vyvolá s instancí span.


Poznámka: Kompilátor může zpozdit přidávání prvků do kolekce nebo zpožďovat iterace prostřednictvím rozprostřených prvků až po vyhodnocení následných prvků. (Pokud následující rozprostřené elementy mají počítané vlastnosti, které by umožňovaly vypočítat očekávanou délku kolekce před přidělením kolekce.) Kompilátor naopak může do kolekce dychtivě přidávat prvky – a dychtivě iterovat prostřednictvím rozprostřených prvků – pokud není možné zpozdit žádné výhody.

Představte si následující výraz kolekce:

int[] x = [a, ..b, ..c, d];

Pokud jsou rozprostřené prvky b a cjsou počítáné, kompilátor by mohl zpozdit přidávání položek z a a b až po c vyhodnocení, aby umožnil přidělení výsledného pole s očekávanou délkou. Potom by kompilátor mohl dychtivě přidávat položky z c, před vyhodnocením d.

var __tmp1 = a;
var __tmp2 = b;
var __tmp3 = c;
var __result = new int[2 + __tmp2.Length + __tmp3.Length];
int __index = 0;
__result[__index++] = __tmp1;
foreach (var __i in __tmp2) __result[__index++] = __i;
foreach (var __i in __tmp3) __result[__index++] = __i;
__result[__index++] = d;
x = __result;

Prázdný literál kolekce

  • Prázdný literál [] nemá žádný typ. Podobně jako u literálu s hodnotou null lze tento literál implicitně převést na jakýkoli typ konstruktovatelné kolekce.

    Například následující není legální, protože neexistuje žádný cílový typ a nejsou zahrnuty žádné další převody:

    var v = []; // illegal
    
  • Šíření prázdného literálu je povoleno elidovat. Například:

    bool b = ...
    List<int> l = [x, y, .. b ? [1, 2, 3] : []];
    

    Pokud b je hodnota false, není nutné, aby byla pro prázdný výraz kolekce vytvořena žádná hodnota, protože by se okamžitě rozložila do nulových hodnot v posledním literálu.

  • Prázdný výraz kolekce může být singleton, pokud se používá k vytvoření konečné hodnoty kolekce, která je známo, že není proměnlivá. Například:

    // Can be a singleton, like Array.Empty<int>()
    int[] x = []; 
    
    // Can be a singleton. Allowed to use Array.Empty<int>(), Enumerable.Empty<int>(),
    // or any other implementation that can not be mutated.
    IEnumerable<int> y = [];
    
    // Must not be a singleton.  Value must be allowed to mutate, and should not mutate
    // other references elsewhere.
    List<int> z = [];
    

Bezpečnost ref

Projděte si bezpečné omezení kontextu pro definice hodnot bezpečného kontextu : deklarací blok, člen funkce a kontext volajícího.

Bezpečný kontext výrazu kolekce je:

  • Bezpečný kontext prázdného výrazu [] kolekce je kontext volajícího.

  • Pokud je cílovým typem typSystem.ReadOnlySpan<T> rozsahu a T je jedním z primitivních typůbool, sbyte, , shortbyte, ushort, char, int, , longulongfloatuintnebo doublea výraz kolekce obsahuje pouze konstantní hodnoty, bezpečný kontext výrazu kolekce je kontext volajícího.

  • Pokud je cílovým typem typSystem.Span<T> rozsahu nebo System.ReadOnlySpan<T>je bezpečný kontext výrazu kolekce blok deklarace.

  • Pokud je cílovým typem ref typ struktury s metodou create, bezpečný kontext výrazu kolekce je bezpečný kontext vyvolání metody create, kde výraz kolekce je argument span metody.

  • V opačném případě je bezpečným kontextem výrazu kolekce kontext volajícího.

Výraz kolekce s bezpečným kontextem bloku deklarací nemůže utéct uzavírací obor a kompilátor může uložit kolekci do zásobníku místo haldy.

Chcete-li povolit výraz kolekce pro typ ref struktury řídicí blok deklarace, může být nutné přetypovat výraz na jiný typ.

static ReadOnlySpan<int> AsSpanConstants()
{
    return [1, 2, 3]; // ok: span refers to assembly data section
}

static ReadOnlySpan<T> AsSpan2<T>(T x, T y)
{
    return [x, y];    // error: span may refer to stack data
}

static ReadOnlySpan<T> AsSpan3<T>(T x, T y, T z)
{
    return (T[])[x, y, z]; // ok: span refers to T[] on heap
}

Odvození typu

var a = AsArray([1, 2, 3]);          // AsArray<int>(int[])
var b = AsListOfArray([[4, 5], []]); // AsListOfArray<int>(List<int[]>)

static T[] AsArray<T>(T[] arg) => arg;
static List<T[]> AsListOfArray<T>(List<T[]> arg) => arg;

Pravidla odvození typu se aktualizují následujícím způsobem.

Existující pravidla pro první fázi se extrahují do nového oddílu odvozování vstupního typu a pravidlo se přidá do odvozování vstupního typu a odvození výstupního typu pro výrazy výrazů kolekce.

11.6.3.2 První fáze

Pro každý argument metody Eᵢ:

  • Odvození vstupního typu je provedeno zEᵢodpovídajícíhotypuTᵢ parametru.

Odvození vstupního typu je provedeno z výrazu E na typ T následujícím způsobem:

  • Pokud E je výraz kolekce s elementy Eᵢa T je typem s typemTₑ elementu nebo T je typT0? hodnoty null a T0typTₑ elementu, pak pro každý Eᵢ:
    • Pokud Eᵢ je element výrazu, je odvození vstupního typu provedeno zEᵢdoTₑ.
    • Pokud Eᵢ je rozprostřený prvek s typemSᵢ iterace, je odvození z dolní hranice provedeno zSᵢTₑ.
  • [existující pravidla z první fáze] ...

11.6.3.7 Odvození výstupního typu

Odvození výstupního

  • Pokud E je výraz kolekce s elementy Eᵢa T je typem s typemTₑ elementu nebo T je typT0? hodnoty null a T0typTₑ elementu, pak pro každý Eᵢ:
    • Pokud Eᵢ je element výrazu, je odvození výstupního typu provedeno zEᵢdoTₑ.
    • Je-li Eᵢrozprostřený prvek, není odvozována žádná odvození z Eᵢ.
  • [existující pravidla z odvození výstupního typu] ...

Rozšiřující metody

Žádné změny pravidel vyvolání metody rozšíření .

12.8.10.3 Vyvolání metody rozšíření

Metoda Cᵢ.Mₑ rozšíření má nárok , pokud:

  • ...
  • Implicitní identita, odkaz nebo převod boxování existuje z výrazu na typ prvního parametru .Mₑ

Výraz kolekce nemá přirozený typ, takže existující převody z typu nelze použít. V důsledku toho nelze výraz kolekce použít přímo jako první parametr pro vyvolání metody rozšíření.

static class Extensions
{
    public static ImmutableArray<T> AsImmutableArray<T>(this ImmutableArray<T> arg) => arg;
}

var x = [1].AsImmutableArray();           // error: collection expression has no target type
var y = [2].AsImmutableArray<int>();      // error: ...
var z = Extensions.AsImmutableArray([3]); // ok

Rozlišení přetěžování

Lepší převod z výrazu je aktualizován tak, aby upřednostňoval určité cílové typy v převodech výrazů kolekce.

V aktualizovaných pravidlech:

  • Span_type je jedna z těchto možností:
    • System.Span<T>
    • System.ReadOnlySpan<T>.
  • Array_or_array_interface je jedna z těchto možností:
    • typ pole
    • jeden z následujících typů rozhraní implementovaných typem pole:
      • System.Collections.Generic.IEnumerable<T>
      • System.Collections.Generic.IReadOnlyCollection<T>
      • System.Collections.Generic.IReadOnlyList<T>
      • System.Collections.Generic.ICollection<T>
      • System.Collections.Generic.IList<T>

Vzhledem k implicitnímu převodu C₁, který převádí z výrazu E na typ T₁, a implicitnímu převodu C₂, který převádí z výrazu E na typ T₂, C₁ je lepším převodem než C₂, pokud platí jedna z následujících možností:

  • E je výraz kolekce a jeden z následujících blokování:
    • T₁ is System.ReadOnlySpan<E₁>, and T₂ is System.Span<E₂>, and an implicitní převod is from E₁ to to E₂
    • T₁ je System.ReadOnlySpan<E₁> nebo System.Span<E₁>, a T₂ je array_or_array_interface s typemE₂ elementu a implicitní převod existuje z E₁ do E₂
    • T₁ není span_type a T₂ nejedná se o span_type a implicitní převod existuje z T₁ do T₂
  • E není výraz kolekce a jeden z následujících blokování:
    • E přesně odpovídá T₁ a E přesně neodpovídá T₂
    • E přesně odpovídá oběma nebo ani jednomu z T₁ nich T₂a T₁ je lepším cílem převodu než T₂
  • E je skupina metod, ...

Příklady rozdílů s překladem přetížení mezi inicializátory polí a výrazy kolekce:

static void Generic<T>(Span<T> value) { }
static void Generic<T>(T[] value) { }

static void SpanDerived(Span<string> value) { }
static void SpanDerived(object[] value) { }

static void ArrayDerived(Span<object> value) { }
static void ArrayDerived(string[] value) { }

// Array initializers
Generic(new[] { "" });      // string[]
SpanDerived(new[] { "" });  // ambiguous
ArrayDerived(new[] { "" }); // string[]

// Collection expressions
Generic([""]);              // Span<string>
SpanDerived([""]);          // Span<string>
ArrayDerived([""]);         // ambiguous

Typy rozpětí

Typy rozpětí ReadOnlySpan<T> a Span<T> oba typy kolekcí jsou konstruktovatelné. Podpora pro ně následuje návrh pro params Span<T>. Konkrétně vytvoření některého z těchto rozsahů způsobí, že pole T[] vytvořené v zásobníku v případě, že pole parametrů spadá do limitů (pokud existuje) nastavených kompilátorem. V opačném případě bude pole přiděleno na haldě.

Pokud se kompilátor rozhodne přidělit v zásobníku, není nutné přeložit literál přímo do konkrétního stackalloc bodu. Například:

foreach (var x in y)
{
    Span<int> span = [a, b, c];
    // do things with span
}

Kompilátor může přeložit, že pokud stackallocSpan význam zůstane stejný a zachová se bezpečnost rozsahu. Může například přeložit výše uvedené:

Span<int> __buffer = stackalloc int[3];
foreach (var x in y)
{
    __buffer[0] = a
    __buffer[1] = b
    __buffer[2] = c;
    Span<int> span = __buffer;
    // do things with span
}

Kompilátor může také použít vložená pole, pokud je k dispozici, když se rozhodnete přidělit v zásobníku. Všimněte si, že v jazyce C# 12 nelze inicializovat vložená pole pomocí výrazu kolekce. Tato funkce je otevřený návrh.

Pokud se kompilátor rozhodne přidělit haldu, překlad Span<T> jednoduše:

T[] __array = [...]; // using existing rules
Span<T> __result = __array;

Překlad literálů kolekce

Výraz kolekce má známou délku, pokud je možné spočítat typ kompilace každého rozprostřeného prvku ve výrazu kolekce.

Překlad rozhraní

Překlad nesměnitelného rozhraní

Vzhledem k cílovému typu, který neobsahuje mutační členy, konkrétně IEnumerable<T>IReadOnlyCollection<T>, a IReadOnlyList<T>, je vyžadována vyhovující implementace k vytvoření hodnoty, která implementuje toto rozhraní. Pokud je typ syntetizován, doporučuje se syntetizovaný typ implementovat všechna tato rozhraní, stejně jako ICollection<T> a , bez IList<T>ohledu na typ rozhraní byl cílem. Tím se zajistí maximální kompatibilita se stávajícími knihovnami, včetně těch, které introspektují rozhraní implementovaná hodnotou, aby bylo možné optimalizovat výkon.

Kromě toho musí hodnota implementovat negenerické ICollection a IList rozhraní. Díky tomu mohou výrazy kolekce podporovat dynamické introspekce ve scénářích, jako jsou datové vazby.

Kompatibilní implementace je bezplatná pro:

  1. Použijte existující typ, který implementuje požadovaná rozhraní.
  2. Syntetizuje typ, který implementuje požadovaná rozhraní.

V obou případech může použitý typ implementovat větší sadu rozhraní než ty, které jsou přísně požadovány.

Syntetizované typy mohou využívat jakoukoli strategii, kterou chtějí správně implementovat požadovaná rozhraní. Syntetizovaný typ může například vložit prvky přímo uvnitř sebe a vyhnout se nutnosti dalších interních přidělení kolekce. Syntetizovaný typ také nemohl použít žádné úložiště, které by se rozhodlo vypočítat hodnoty přímo. Například vrácení index + 1 pro [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].

  1. Hodnota se musí vrátit true při dotazování ( ICollection<T>.IsReadOnly pokud je implementováno) a negenerické IList.IsReadOnly a IList.IsFixedSize. Tím se zajistí, že uživatelé můžou odpovídajícím způsobem informovat, že kolekce není proměnlivá, i když implementuje proměnlivá zobrazení.
  2. Hodnota musí vyvolat jakékoli volání metody mutaci (například IList<T>.Add). Tím zajistíte bezpečnost a zabráníte náhodnému ztlumení nesměnitelné kolekce.

Proměnlivý překlad rozhraní

Daný cílový typ, který obsahuje ztlumené členy, konkrétně ICollection<T>IList<T>:

  1. Hodnota musí být instancí List<T>.

Překlad známé délky

Díky známé délce lze efektivně vytvořit výsledek s potenciálem pro žádné kopírování dat a bez zbytečného místa pro časovou rezervu ve výsledku.

Pokud nemáte známou délku , nezabrání vytvoření žádného výsledku. Může však vést k dodatečným nákladům na procesor a paměť, které vytvářejí data, a pak přejít do konečného cíle.

  • U známého literálu [e1, ..s1, etc]délky začíná překlad nejprve následujícím:

    int __len = count_of_expression_elements +
                __s1.Count;
                ...
                __s_n.Count;
    
  • Vzhledem k cílovému typu T pro tento literál:

    • Pokud T je nějaký T1[], literál se přeloží takto:

      T1[] __result = new T1[__len];
      int __index = 0;
      
      __result[__index++] = __e1;
      foreach (T1 __t in __s1)
          __result[__index++] = __t;
      
      // further assignments of the remaining elements
      

      Implementace může využívat další prostředky k naplnění pole. Například pomocí efektivních metod hromadného kopírování, jako .CopyTo()je .

    • Pokud T je nějaký Span<T1>, literál se přeloží stejně jako výše, s tím rozdílem, že __result inicializace se přeloží takto:

      Span<T1> __result = new T1[__len];
      
      // same assignments as the array translation
      

      Překlad může místo zachování bezpečnosti rozsahu použít stackalloc T1[] vložené pole nebo vložené polenew T1[].

    • Pokud T je nějaký ReadOnlySpan<T1>, pak literál je přeložen stejně jako pro Span<T1> případ s tím rozdílem, že konečný výsledek bude Span<T1>implicitně převeden na ReadOnlySpan<T1>.

      T1 Kde ReadOnlySpan<T1> je nějaký primitivní typ a všechny prvky kolekce jsou konstantní, nepotřebuje, aby data byla na haldě nebo v zásobníku. Například implementace by mohla vytvořit tento rozsah přímo jako odkaz na část datového segmentu programu.

      Výše uvedené formuláře (pro pole a rozsahy) jsou základní reprezentací výrazu kolekce a používají se pro následující pravidla překladu:

      • Pokud T je některá C<S0, S1, …> , která má odpovídající metodu create-methodB.M<U0, U1, …>(), literál se přeloží takto:

        // Collection literal is passed as is as the single B.M<...>(...) argument
        C<S0, S1, …> __result = B.M<S0, S1, …>([...])
        

        Vzhledem k tomu, že metoda create musí mít typ argumentu některé instance ReadOnlySpan<T>, pravidlo překladu spans platí při předání výrazu kolekce do metody create.

      • Pokud T podporuje inicializátory kolekcí, pak:

        • pokud typ T obsahuje přístupný konstruktor s jedním parametrem int capacity, literál se přeloží takto:

          T __result = new T(capacity: __len);
          __result.Add(__e1);
          foreach (var __t in __s1)
              __result.Add(__t);
          
          // further additions of the remaining elements
          

          Poznámka: Název parametru musí být capacity.

          Tento formulář umožňuje literálu informovat nově vytvořený typ počtu prvků, aby bylo možné efektivně přidělit interní úložiště. Tím se zabrání plýtvání relokacemi při přidání prvků.

        • jinak se literál přeloží takto:

          T __result = new T();
          
          __result.Add(__e1);
          foreach (var __t in __s1)
              __result.Add(__t);
          
          // further additions of the remaining elements
          

          To umožňuje vytvořit cílový typ, i když bez optimalizace kapacity, aby se zabránilo interní relokaci úložiště.

Neznámý překlad délky

  • Zadání cílového typu T pro literál neznámé délky :

    • Pokud T podporuje inicializátory kolekcí, literál se přeloží takto:

      T __result = new T();
      
      __result.Add(__e1);
      foreach (var __t in __s1)
          __result.Add(__t);
      
      // further additions of the remaining elements
      

      To umožňuje šíření libovolného iterovatelného typu, i když s nejmenší možnou optimalizací.

    • Pokud T je nějaký T1[], pak literál má stejnou sémantiku jako:

      List<T1> __list = [...]; /* initialized using predefined rules */
      T1[] __result = __list.ToArray();
      

      Výše uvedené je však neefektivní; vytvoří zprostředkující seznam a pak z něj vytvoří kopii konečného pole. Implementace jsou zdarma pro optimalizaci, například vytváření kódu, například takto:

      T1[] __result = <private_details>.CreateArray<T1>(
          count_of_expression_elements);
      int __index = 0;
      
      <private_details>.Add(ref __result, __index++, __e1);
      foreach (var __t in __s1)
          <private_details>.Add(ref __result, __index++, __t);
      
      // further additions of the remaining elements
      
      <private_details>.Resize(ref __result, __index);
      

      To umožňuje minimální plýtvání a kopírování bez dalších režijních nákladů, ke kterým může dojít u kolekcí knihoven.

      Počet předaných CreateArray souborů slouží k poskytnutí nápovědy počáteční velikosti, aby se zabránilo plýtvání změnou velikosti.

    • Pokud T je nějaký typ rozsahu, implementace může dodržovat výše uvedenou T[] strategii nebo jakoukoli jinou strategii se stejnou sémantikou, ale lepším výkonem. Například místo přidělení pole jako kopie prvků CollectionsMarshal.AsSpan(__list) seznamu lze použít k přímému získání hodnoty rozsahu.

Nepodporované scénáře

I když literály kolekce lze použít pro mnoho scénářů, existuje několik, které nejsou schopné nahradit. Patří mezi ně:

  • Multidimenzionální pole (např. new int[5, 10] { ... }). Není k dispozici žádná možnost zahrnout rozměry a všechny literály kolekce jsou pouze lineární nebo mapové struktury.
  • Kolekce, které předávají svým konstruktorům speciální hodnoty. Není k dispozici žádné zařízení pro přístup k použitému konstruktoru.
  • Inicializátory vnořených kolekcí, např. new Widget { Children = { w1, w2, w3 } } Tato forma musí zůstat, protože má velmi odlišnou sémantiku od Children = [w1, w2, w3]. Dřívější volání opakovaně, .Add.Children zatímco druhá funkce by přiřadil novou kolekci ..Children Mohli bychom zvážit, že druhá forma se vrátí k přidání do existující kolekce, pokud .Children není možné ji přiřadit, ale zdá se, že by to mohlo být velmi matoucí.

Nejednoznačnosti syntaxe

  • Existují dvě syntaktické syntaktické nejednoznačnosti, kdy existuje více právních syntaktických interpretací kódu, které používají collection_literal_expression.

    • Nejednoznačný spread_elementrange_expressionje s . Technicky vzato může mít:

      Range[] ranges = [range1, ..e, range2];
      

      Pokud chcete tento problém vyřešit, můžeme provést následující:

      • Vyžadovat, aby uživatelé závorky (..e) nebo zahrnuli počáteční index 0..e , pokud chtějí rozsah.
      • Zvolte jinou syntaxi (například ...) pro rozložení. To by bylo nešťastné kvůli nedostatku konzistence se vzory řezů.
  • Existují dva případy, kdy neexistuje pravdivá nejednoznačnost, ale kde syntaxe výrazně zvyšuje parsování složitosti. I když to není problém s daným časem přípravy, zvyšuje se tím kognitivní režie uživatelů při prohlížení kódu.

    • Nejednoznačnost mezi collection_literal_expression příkazy a attributes místními funkcemi a příkazy. Uvažovat:

      [X(), Y, Z()]
      

      Může to být jedna z těchto možností:

      // A list literal inside some expression statement
      [X(), Y, Z()].ForEach(() => ...);
      
      // The attributes for a statement or local function
      [X(), Y, Z()] void LocalFunc() { }
      

      Bez složitých pohledů by nebylo možné bez použití celého literálu zjistit.

      Mezi možnosti, které je potřeba vyřešit, patří:

      • Povolte to tím, že provedete analýzu, abyste zjistili, o které z těchto případů se jedná.
      • Nepovolte to a vyžadovat, aby uživatel zabalil literál do závorek jako ([X(), Y, Z()]).ForEach(...).
      • Nejednoznačnost mezi in collection_literal_expression a conditional_expressionnull_conditional_operationsa . Uvažovat:
      M(x ? [a, b, c]
      

      Může to být jedna z těchto možností:

      // A ternary conditional picking between two collections
      M(x ? [a, b, c] : [d, e, f]);
      
      // A null conditional safely indexing into 'x':
      M(x ? [a, b, c]);
      

      Bez složitých pohledů by nebylo možné bez použití celého literálu zjistit.

      Poznámka: Jedná se o problém i bez přirozeného typu , protože cílový typ se vztahuje na conditional_expressions.

      Stejně jako u ostatních bychom mohli vyžadovat, aby závorky byly nejednoznačné. Jinými slovy, předpokládá se null_conditional_operation , že interpretace není-li napsána takto: x ? ([1, 2, 3]) :. To se ale zdá být docela nešťastné. Tento druh kódu se zdá být nerozumný napsat a bude pravděpodobně cestovat lidi nahoru.

Nevýhody

  • To představuje ještě další formu pro výrazy kolekce nad řadu způsobů, jak už máme. Jedná se o větší složitost jazyka. To znamená, že to také umožňuje sjednocení jedné syntaxe kruhu , aby je všechny pravidlo, což znamená, že stávající základ kódu lze zjednodušit a přesunout na jednotný vzhled všude.
  • Použití [... místo {...]} se od syntaxe, která jsme obecně použili pro pole a inicializátory kolekcí, už používáme. Konkrétně používá [...] místo {...}. Nicméně, to bylo již vyřešeno týmem jazyků, když jsme vytvořili seznam vzorů. Pokusili jsme se pracovat {} se vzory seznamů a narazili jsme na nepřesné problémy. Z tohoto důvodu jsme se přestěhovali do [...] , což, zatímco nový pro C#, cítí přirozený v mnoha programovacích jazycích a umožnili nám začít znovu bez nejednoznačnosti. Použití [...] jako odpovídající literálová forma doplňuje naše nejnovější rozhodnutí a poskytuje nám čisté místo pro práci bez problému.

To do jazyka zavádí warty. Například následující jsou právní i (naštěstí) znamenají naprosto stejnou věc:

int[] x = { 1, 2, 3 };
int[] x = [ 1, 2, 3 ];

Vzhledem k šířce a konzistenci, kterou přináší nová syntaxe literálu, bychom ale měli zvážit doporučení, aby se lidé přesunuli do nového formuláře. Návrhy a opravy integrovaného vývojového prostředí (IDE) by v tomto ohledu mohly pomoct.

Alternatives

  • O jakých dalších návrzích se uvažuje? Jaký je dopad toho, že to neuděláte?

Vyřešené otázky

  • Má kompilátor použít stackalloc pro přidělení zásobníku, pokud vložená pole nejsou k dispozici a typ iterace je primitivní typ?

    Řešení: Ne. stackalloc Správa vyrovnávací paměti vyžaduje větší úsilí nad vloženým polem, aby se zajistilo, že vyrovnávací paměť není přidělena opakovaně, když je výraz kolekce ve smyčce. Další složitost kompilátoru a vygenerovaného kódu převáží výhodu přidělování zásobníku na starších platformách.

  • V jakém pořadí bychom měli vyhodnotit literální prvky ve srovnání s vyhodnocením vlastnosti Length/Count? Měli bychom nejprve vyhodnotit všechny prvky, pak všechny délky? Nebo bychom měli vyhodnotit prvek, pak jeho délku, další prvek atd.?

    Řešení: Nejprve vyhodnotíme všechny prvky, pak vše ostatní následuje.

  • Může literál s neznámou délkou vytvořit typ kolekce, který potřebuje známou délku, například matici, rozsah nebo kolekci Construct(array/span)? To by bylo obtížnější provádět efektivně, ale může to být možné pomocí chytrých použití polí ve fondu a/nebo tvůrce.

    Řešení: Ano, umožňujeme vytvořit kolekci oprav a délky z neznámého literálu délky. Kompilátor je povolen tak, aby ho co nejefektivněji implementoval.

    Následující text existuje k zaznamenání původní diskuze o tomto tématu.

    Uživatelé můžou vždy vytvořit literál neznámé délky do známé délky s kódem takto:

    ImmutableArray<int> x = [a, ..unknownLength.ToArray(), b];
    

    Je to však nešťastné kvůli nutnosti vynutit přidělení dočasného skladování. Pokud bychom mohli řídit, jak se to vygenerovalo, mohli bychom být efektivnější.

  • collection_expression Může být typ cílený na IEnumerable<T> rozhraní kolekce nebo jiné rozhraní kolekce?

    Například:

    void DoWork(IEnumerable<long> values) { ... }
    // Needs to produce `longs` not `ints` for this to work.
    DoWork([1, 2, 3]);
    

    Řešení: Ano, literál lze cílit na libovolný typ I<T> rozhraní, který List<T> implementuje. Například: IEnumerable<long>. Jedná se o stejný typ jako typ cílového List<long> rozhraní a následné přiřazení tohoto výsledku k zadanému typu rozhraní. Následující text existuje k zaznamenání původní diskuze o tomto tématu.

    Otevřená otázka tady určuje, jaký základní typ se má skutečně vytvořit. Jednou z možností je podívat se na návrh .params IEnumerable<T> Tam bychom vygenerovali pole, které předává hodnoty, podobně jako to, co se stane s params T[].

  • Může/má kompilátor vygenerovat Array.Empty<T>()[]? Měli bychom, aby to bylo možné, abychom se vyhnuli přidělení, kdykoli je to možné?

    Ano. Kompilátor by měl generovat Array.Empty<T>() pro případ, kdy je to legální a konečný výsledek není proměnlivý. Například cílení T[], IEnumerable<T>nebo IReadOnlyCollection<T>IReadOnlyList<T>. Neměl by se používat Array.Empty<T> , pokud je cíl proměnlivý (ICollection<T> nebo IList<T>).

  • Měli bychom rozšířit inicializátory kolekcí, abychom hledali velmi běžnou AddRange metodu? Mohl by ho použít základní vytvořený typ k provádění přidávání rozprostřených prvků potenciálně efektivněji. Možná bychom také chtěli hledat věci jako .CopyTo . Mohou zde být nevýhody, protože tyto metody můžou nakonec způsobit nadbytečné přidělení a odesílání a přímé výčet v přeloženého kódu.

    Ano. Implementace může využívat jiné metody k inicializaci hodnoty kolekce podle předpokladu, že tyto metody mají dobře definovanou sémantiku a že typy kolekcí by se měly "dobře chovat". V praxi by však implementace měla být opatrná, protože výhody jedním způsobem (hromadné kopírování) můžou mít negativní důsledky i (například boxování kolekce struktur).

    Implementace by měla využít výhod v případech, kdy neexistují žádné nevýhody. Například s metodou .AddRange(ReadOnlySpan<T>) .

Nevyřešené otázky

  • Měli bychom povolit odvození typu elementu , když je typ iterace "nejednoznačný" (podle určité definice)? Například:
Collection x = [1L, 2L];

// error CS1640: foreach statement cannot operate on variables of type 'Collection' because it implements multiple instantiations of 'IEnumerable<T>'; try casting to a specific interface instantiation
foreach (var x in new Collection) { }

static class Builder
{
    public Collection Create(ReadOnlySpan<long> items) => throw null;
}

[CollectionBuilder(...)]
class Collection : IEnumerable<int>, IEnumerable<string>
{
    IEnumerator<int> IEnumerable<int>.GetEnumerator() => throw null;
    IEnumerator<string> IEnumerable<string>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}
  • Je vhodné vytvořit a okamžitě indexovat do literálu kolekce? Poznámka: To vyžaduje odpověď na nevyřešenou otázku níže, zda literály kolekce mají přirozený typ.

  • Přidělení zásobníku pro obrovské kolekce může vyhodit zásobník. Má mít kompilátor heuristiku pro umístění těchto dat do haldy? Měl by být jazyk nezadaný tak, aby umožňoval tuto flexibilitu? Měli bychom postupovat podle specifikace pro params Span<T>.

  • Potřebujeme cílový typ spread_element? Představte si například:

    Span<int> span = [a, ..b ? [c] : [d, e], f];
    

    Poznámka: To se může běžně objevit v následující podobě, aby bylo možné podmíněné zahrnutí některých prvků nebo nic, pokud je podmínka nepravda:

    Span<int> span = [a, ..b ? [c, d, e] : [], f];
    

    Abychom mohli vyhodnotit celý literál, musíme vyhodnotit výrazy prvků uvnitř. To znamená schopnost vyhodnotit b ? [c] : [d, e]. Chybí však cílový typ pro vyhodnocení tohoto výrazu v kontextu a chybí jakýkoli druh přirozeného typu, nebudeme moct určit, co dělat s tímto [c] nebo [d, e] tímto typem.

    Abychom to vyřešili, můžeme říci, že při vyhodnocování výrazu literálu spread_element byl implicitní cílový typ ekvivalentní cílovému typu samotného literálu. Ve výše uvedeném příkladu by se tedy přepsal takto:

    int __e1 = a;
    Span<int> __s1 = b ? [c] : [d, e];
    int __e2 = f;
    
    Span<int> __result = stackalloc int[2 + __s1.Length];
    int __index = 0;
    
    __result[__index++] = a;
    foreach (int __t in __s1)
      __result[index++] = __t;
    __result[__index++] = f;
    
    Span<int> span = __result;
    

Specifikace konstruktovatelného typu kolekce využívající metodu create je citlivá na kontext, při kterém je převod klasifikovaný.

Existence převodu v tomto případě závisí na myšlence typu iteracetypu kolekce. Pokud existuje metoda create , která přebírá místo ReadOnlySpan<T> , kde T je typ iterace, převod existuje. Jinak to nepomůže.

Typ iterace je však citlivý na kontext, při kterém foreach se provádí. Pro stejný typ kolekce se může lišit podle toho, jaké metody rozšíření jsou v oboru, a může být také nedefinováno.

To je v pořádku pro účely foreach , kdy typ není navržen tak, aby byl schopen sám o sobě. Pokud ano, rozšiřující metody nemůžou změnit způsob, jakým je typ přetěžován, bez ohledu na to, co je kontext.

To ale vypadá trochu divně, že převod bude citlivý na kontext, jako je to. Převod je v podstatě "nestabilní". Typ kolekce explicitně navržený tak, aby byl konstruktovatelný, může vynechat definici velmi důležitého detailu – jeho iteračního typu. Ponechte typ "nekonvertible" na sobě.

Zde je příklad:

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

[CollectionBuilder(typeof(MyCollectionBuilder), nameof(MyCollectionBuilder.Create))]
class MyCollection
{
}
class MyCollectionBuilder
{
    public static MyCollection Create(ReadOnlySpan<long> items) => throw null;
    public static MyCollection Create(ReadOnlySpan<string> items) => throw null;
}

namespace Ns1
{
    static class Ext
    {
        public static IEnumerator<long> GetEnumerator(this MyCollection x) => throw null;
    }
    
    class Program
    {
        static void Main()
        {
            foreach (var l in new MyCollection())
            {
                long s = l;
            }
        
            MyCollection x1 = ["a", // error CS0029: Cannot implicitly convert type 'string' to 'long'
                               2];
        }
    }
}

namespace Ns2
{
    static class Ext
    {
        public static IEnumerator<string> GetEnumerator(this MyCollection x) => throw null;
    }
    
    class Program
    {
        static void Main()
        {
            foreach (var l in new MyCollection())
            {
                string s = l;
            }
        
            MyCollection x1 = ["a",
                               2]; // error CS0029: Cannot implicitly convert type 'int' to 'string'
        }
    }
}

namespace Ns3
{
    class Program
    {
        static void Main()
        {
            // error CS1579: foreach statement cannot operate on variables of type 'MyCollection' because 'MyCollection' does not contain a public instance or extension definition for 'GetEnumerator'
            foreach (var l in new MyCollection())
            {
            }
        
            MyCollection x1 = ["a", 2]; // error CS9188: 'MyCollection' has a CollectionBuilderAttribute but no element type.
        }
    }
}

Vzhledem k aktuálnímu návrhu, pokud typ nedefinuje samotný typ iterace , kompilátor nemůže spolehlivě ověřit aplikaci atributu CollectionBuilder . Pokud typ iterace neznáme, nevíme, jaký má být podpis metody create . Pokud typ iterace pochází z kontextu, neexistuje žádná záruka, že typ bude vždy použit v podobném kontextu.

Tato funkce kolekce parametrů je také ovlivněna. Zdá se divné, že nelze spolehlivě předpovědět typ params prvku parametru v bodě deklarace. Aktuální návrh také vyžaduje, aby metoda vytvoření byla alespoň tak přístupná jako paramstyp kolekce. Tuto kontrolu nelze provést spolehlivým způsobem, pokud typ kolekce nedefinuje samotný typ iterace .

Všimněte si, že jsme také https://github.com/dotnet/roslyn/issues/69676 otevřeli pro kompilátor, který v podstatě sleduje stejný problém, ale hovoří o něm z hlediska optimalizace.

Návrh

Vyžadovat typ využívající CollectionBuilder atribut definovat jeho iterační typ sám. Jinými slovy to znamená, že typ by měl buď implementovat IEnumarable/IEnumerable<T>, nebo by měl mít veřejnou GetEnumerator metodu se správným podpisem (to vylučuje všechny rozšiřující metody).

Teď je také nutné , aby byla metoda create přístupná tam, kde se používá výraz kolekce. Toto je další bod závislosti kontextu na základě přístupnosti. Účel této metody je velmi podobný účelu uživatelem definované metody převodu a ten musí být veřejný. Proto bychom měli zvážit, že metoda vytvoření bude také veřejná.

Conclusion

Schváleno s úpravami LDM-2024-01-08

Pojem typu iterace se v průběhu převodů nepoužívá konzistentně.

  • Do struktury nebo typu třídy , která implementuje System.Collections.Generic.IEnumerable<T> :
    • Pro každý prvekEi existuje implicitní převod na T.

Zdá se, že se předpokládá, že T je nutné iterační typstruktury nebo typu třídy v tomto případě. Tento předpoklad je však nesprávný. Což může vést k velmi neobvyklému chování. Například:

using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable<long>
{
    IEnumerator<long> IEnumerable<long>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;

    public void Add(string l) => throw null;
    
    public IEnumerator<string> GetEnumerator() => throw null; 
}

class Program
{
    static void Main()
    {
        foreach (var l in new MyCollection())
        {
            string s = l; // Iteration type is string
        }
        
        MyCollection x1 = ["a", // error CS0029: Cannot implicitly convert type 'string' to 'long'
                           2];
        MyCollection x2 = new MyCollection() { "b" };
    }
}
  • Do struktury nebo typu třídy , který implementuje System.Collections.IEnumerable a neimplementujeSystem.Collections.Generic.IEnumerable<T>.

Zdá se, že implementace předpokládá, že typ iterace je object, ale specifikace ponechá tento fakt nezadanou a jednoduše nevyžaduje, aby každý prvek převést na nic. Obecně však typ iterace není nutný object . Které lze pozorovat v následujícím příkladu:

using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable
{
    public IEnumerator<string> GetEnumerator() => throw null; 
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}

class Program
{
    static void Main()
    {
        foreach (var l in new MyCollection())
        {
            string s = l; // Iteration type is string
        }
    }
}

Pojem typu iterace je zásadní pro funkci Params Collections . A tento problém vede k podivné nesrovnalosti mezi těmito dvěma funkcemi. Například:

using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable<long>
{
    IEnumerator<long> IEnumerable<long>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;

    public IEnumerator<string> GetEnumerator() => throw null; 

    public void Add(long l) => throw null; 
    public void Add(string l) => throw null; 
}

class Program
{
    static void Main()
    {
        Test("2"); // error CS0029: Cannot implicitly convert type 'string' to 'long'
        Test(["2"]); // error CS1503: Argument 1: cannot convert from 'collection expressions' to 'string'
        Test(3); // error CS1503: Argument 1: cannot convert from 'int' to 'string'
        Test([3]); // Ok

        MyCollection x1 = ["2"]; // error CS0029: Cannot implicitly convert type 'string' to 'long'
        MyCollection x2 = [3];
    }

    static void Test(params MyCollection a)
    {
    }
}
using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable
{
    IEnumerator IEnumerable.GetEnumerator() => throw null;

    public IEnumerator<string> GetEnumerator() => throw null; 
    public void Add(object l) => throw null;
}

class Program
{
    static void Main()
    {
        Test("2", 3); // error CS1503: Argument 2: cannot convert from 'int' to 'string'
        Test(["2", 3]); // Ok
    }

    static void Test(params MyCollection a)
    {
    }
}

Pravděpodobně bude dobré zarovnat jeden nebo druhý.

Návrh

Určete převodnost struktury nebo typu třídy, která implementuje System.Collections.Generic.IEnumerable<T>typ iterace nebo System.Collections.IEnumerable z hlediska typu iterace a vyžaduje implicitní převod pro každý prvekEi na typ iterace.

Conclusion

Schváleno LDM-2024-01-08

převod výrazů kolekce vyžadovat dostupnost minimální sady rozhraní API pro výstavbu?

Konstruktible typ kolekce podle převodů může být ve skutečnosti nekonstruovatelný, což je pravděpodobné, že vede k nějaké neočekávanému chování řešení přetížení. Například:

class C1
{
    public static void M1(string x)
    {
    }
    public static void M1(char[] x)
    {
    }
    
    void Test()
    {
        M1(['a', 'b']); // error CS0121: The call is ambiguous between the following methods or properties: 'C1.M1(string)' and 'C1.M1(char[])'
    }
}

Nicméně, "C1. M1(řetězec) není kandidátem, který lze použít, protože:

error CS1729: 'string' does not contain a constructor that takes 0 arguments
error CS1061: 'string' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'string' could be found (are you missing a using directive or an assembly reference?)

Tady je další příklad s uživatelem definovaným typem a silnější chybou, která ani nezmíní platného kandidáta:

using System.Collections;
using System.Collections.Generic;

class C1 : IEnumerable<char>
{
    public static void M1(C1 x)
    {
    }
    public static void M1(char[] x)
    {
    }

    void Test()
    {
        M1(['a', 'b']); // error CS1061: 'C1' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'C1' could be found (are you missing a using directive or an assembly reference?)
    }

    public static implicit operator char[](C1 x) => throw null;
    IEnumerator<char> IEnumerable<char>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}

Vypadá to, že situace je velmi podobná tomu, co jsme použili pro skupinu metod delegování převodů. Tj. existují scénáře, ve kterých existoval převod, ale byl chybný. Rozhodli jsme se to zlepšit tím, že zajistíme, že pokud je převod chybný, pak neexistuje.

Všimněte si, že u funkce "Kolekce parametrů" narazíme na podobný problém. Může být vhodné zakázat použití modifikátoru params pro nekonstruovatelné kolekce. V aktuálním návrhu je však kontrola založená na části převodů . Zde je příklad:

using System.Collections;
using System.Collections.Generic;

class C1 : IEnumerable<char>
{
    public static void M1(params C1 x) // It is probably better to report an error about an invalid `params` modifier
    {
    }
    public static void M1(params ushort[] x)
    {
    }

    void Test()
    {
        M1('a', 'b'); // error CS1061: 'C1' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'C1' could be found (are you missing a using directive or an assembly reference?)
        M2('a', 'b'); // Ok
    }

    public static void M2(params ushort[] x)
    {
    }

    IEnumerator<char> IEnumerable<char>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}

Zdá se, že problém byl poněkud popsán dříve, viz https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-02.md#collection-expressions. V té době byl proveden argument, že pravidla, jak je uvedeno právě teď, jsou konzistentní s tím, jak jsou specifikovány interpolované obslužné rutiny řetězců. Tady je citát:

Konkrétně byly interpolované obslužné rutiny řetězců původně zadány tímto způsobem, ale po zvážení tohoto problému jsme upravili specifikaci.

I když existuje nějaká podobnost, je tu také důležitý rozdíl, který stojí za zvážení. Tady je citace z https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/improved-interpolated-strings.md#interpolated-string-handler-conversion:

Typ T je řečeno, že je applicable_interpolated_string_handler_type, pokud je přiřazen .System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute Existuje implicitní interpolated_string_handler_conversion z Tinterpolated_string_expression nebo additive_expression složené zcela z _interpolated_string_expression_s a použití pouze + operátorů.

Cílový typ musí mít speciální atribut, který je silným ukazatelem záměru autora, aby byl typ interpolovanou obslužnou rutinou řetězce. Je spravedlivé předpokládat, že přítomnost atributu není náhoda. Naproti tomu skutečnost, že typ je "enumerable", neznamená, že existuje záměr autora pro typ, který má být konstruktovatelný. Přítomnost metody vytvoření, která je však označena atributem [CollectionBuilder(...)] u typu kolekce, se podobá silnému indikátoru záměru autora pro typ, který má být konstruktovatelný.

Návrh

Pro strukturu nebo typ třídy, který implementuje System.Collections.IEnumerable a který nemá oddíl převodymetody create by měl vyžadovat přítomnost alespoň následujících rozhraní API:

  • Přístupný konstruktor, který je použitelný bez argumentů.
  • Přístupná Add instance nebo metoda rozšíření, kterou lze vyvolat s hodnotou typu iterace jako argumentu.

Pro účely funkce Params Collectons jsou takové typy platné params typy, pokud jsou tato rozhraní API deklarována jako veřejná a jsou to metody instance (vs. rozšíření).

Conclusion

Schváleno s úpravami LDM-2024-01-10