Dela via


Samlingsuttryck

Anteckning

Den här artikeln är en funktionsspecifikation. Specifikationen fungerar som designdokument för funktionen. Den innehåller föreslagna specifikationsändringar, tillsammans med information som behövs under utformningen och utvecklingen av funktionen. Dessa artiklar publiceras tills de föreslagna specifikationsändringarna har slutförts och införlivats i den aktuella ECMA-specifikationen.

Det kan finnas vissa skillnader mellan funktionsspecifikationen och den slutförda implementeringen. Dessa skillnader samlas in i de relevanta LDM-anteckningar (Language Design Meeting).

Du kan läsa mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.

Champion-fråga: https://github.com/dotnet/csharplang/issues/8652

Sammanfattning

Samlingsuttryck introducerar en ny terse-syntax, [e1, e2, e3, etc], för att skapa gemensamma samlingsvärden. Det går att infoga andra samlingar i dessa värden med hjälp av ett spridningselement ..e så här: [e1, ..c2, e2, ..c2].

Flera samlingsliknande typer kan skapas utan att det krävs externT BCL-stöd. Följande typer är:

Ytterligare stöd finns för samlingsliknande typer som inte omfattas av ovanstående via ett nytt attribut och API-mönster som kan användas direkt på själva typen.

Motivation

  • Samlingsliknande värden finns enormt i programmering, algoritmer och särskilt i C#/.NET-ekosystemet. Nästan alla program använder dessa värden för att lagra data och skicka eller ta emot data från andra komponenter. För närvarande måste nästan alla C#-program använda många olika och tyvärr utförliga metoder för att skapa instanser av sådana värden. Vissa metoder har också prestanda nackdelar. Här är några vanliga exempel:

    • Matriser, som kräver antingen new Type[] eller new[] före { ... } värden.
    • Spännvidder, som kan använda stackalloc och andra besvärliga konstruktioner.
    • Insamlingsinitiatorer, som kräver syntax som new List<T> (saknar slutsatsdragning av en möjligen utförlig T) före deras värden, och som kan orsaka flera omfördelningar av minne eftersom de använder N .Add anrop utan att tillhandahålla en initial kapacitet.
    • Oföränderliga samlingar, som kräver syntax som ImmutableArray.Create(...) för att initiera värdena, och som kan orsaka mellanliggande allokeringar och datakopiering. Mer effektiva konstruktionsformer (som ImmutableArray.CreateBuilder) är otympliga och producerar fortfarande oundvikligt skräp.
  • När vi tittar på det omgivande ekosystemet hittar vi också exempel överallt där listskapande är bekvämare och trevligare att använda. TypeScript, Dart, Swift, Elm, Python och mer väljer en kortfattad syntax för detta ändamål, med utbredd användning och till stor effekt. Ytliga undersökningar har inte avslöjat några väsentliga problem som uppstår i dessa ekosystem genom att dessa literaler är inbyggda.

  • C# har också lagt till listmönster i C# 11. Det här mönstret tillåter matchning och dekonstruktion av listliknande värden med hjälp av en ren och intuitiv syntax. Men till skillnad från nästan alla andra mönsterkonstruktioner saknar den här matchande/dekonstruktionssyntaxen motsvarande konstruktionssyntax.

  • Det kan vara svårt att få bästa möjliga prestanda för att konstruera varje samlingstyp. Enkla lösningar slösar ofta både CPU och minne. Att ha ett literalt formulär ger maximal flexibilitet från kompilatorimplementeringen för att optimera literalen så att den ger minst ett så bra resultat som en användare kan ge, men med enkel kod. Mycket ofta kommer kompilatorn att kunna göra bättre, och specifikationen syftar till att ge genomförandet stora mängder spelrum när det gäller genomförandestrategi för att säkerställa detta.

En inkluderande lösning krävs för C#. Det bör uppfylla de allra flesta fall för kunder i fråga om de typer av samlingar och värden som de redan har. Det bör också kännas naturligt i språket och spegla det arbete som utförs i mönstermatchning.

Detta leder till en naturlig slutsats att syntaxen ska vara som [e1, e2, e3, e-etc] eller [e1, ..c2, e2], vilket motsvarar mönsterekvivalenterna för [p1, p2, p3, p-etc] och [p1, ..p2, p3].

Detaljerad design

Följande grammatik produktioner läggs till:

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
  ;

Samlingsliteraler är typstyrda.

Förtydliganden av specifikationer

  • För att förenkla kommer collection_expression hädanefter att kallas "literal".

  • expression_element instanser kallas ofta e1, e_nosv.

  • spread_element instanser kallas ofta ..s1, ..s_nosv.

  • spantyp innebär antingen Span<T> eller ReadOnlySpan<T>.

  • Literaler visas ofta som [e1, ..s1, e2, ..s2, etc] för att förmedla valfritt antal element i valfri ordning. Det är viktigt att det här formuläret används för att representera alla fall, till exempel:

    • Tomma literaler []
    • Literaler utan expression_element i dem.
    • Literaler utan spread_element i dem.
    • Literaler med godtycklig ordning av valfri elementtyp.
  • Den iterationstypen av ..s_n är typen av iterationsvariabel bestäms som om s_n användes som uttrycket som itererades över i en foreach_statement.

  • Variabler som börjar med __name används för att representera resultatet av utvärderingen av name, som lagras på en plats så att den bara utvärderas en gång. Till exempel __e1 är utvärderingen av e1.

  • List<T>, IEnumerable<T>osv. refererar till respektive typer i System.Collections.Generic namnområdet.

  • Specifikationen definierar en översättning av literal till befintliga C#-konstruktioner. Precis som översättningen av frågeuttrycketär själva litteralen endast giltig om översättningen skulle resultera i giltig kod. Syftet med den här regeln är att undvika att behöva upprepa andra regler för språket som är underförstådda (till exempel om uttryckskonverterbarhet när de tilldelas lagringsplatser).

  • En implementering krävs inte för att översätta literaler exakt som anges nedan. Alla översättningar är lagliga om samma resultat skapas och det inte finns några observerbara skillnader i resultatets produktion.

    • En implementering kan till exempel översätta literaler som [1, 2, 3] direkt till ett new int[] { 1, 2, 3 }-uttryck som i sig bakar in rådata i sammansättningen, utan att behöva __index eller en sekvens instruktioner för att tilldela varje värde. Det är viktigt att detta innebär att om något steg i översättningen kan leda till ett undantag under körning, så förblir programtillståndet oförändrat enligt det tillstånd som översättningen anger.
  • Referenser till "stackallokering" refererar till alla strategier som ska allokeras på stacken och inte heapen. Det är viktigt att det inte innebär eller kräver att strategin sker via den faktiska stackalloc mekanismen. Till exempel är användningen av infogade matriser också en tillåten och önskvärd metod för att genomföra stackallokering där detta är möjligt. Observera att inline-arrayer i C# 12 inte kan initieras med ett samlingsuttryck. Det är fortfarande ett öppet förslag.

  • Samlingar antas vara välfungerande. Till exempel:

    • Det antas att värdet för Count i en samling ger samma värde som antalet element när de räknas upp.
    • De typer som används i den här specifikationen som definieras i System.Collections.Generic-namnområdet antas vara sidoeffektfria. Kompilatorn kan därför optimera scenarier där sådana typer kan användas som mellanliggande värden, men annars inte exponeras.
    • Det antas att ett anrop till en tillämplig medlem av .AddRange(x) i en samling resulterar i samma slutliga värde som att iterera över x och lägga till varje värde för sig i samlingen med .Add.
    • Beteendet för samlingsliteraler med samlingar som inte är välfungerande är odefinierat.

Omvandlingar

En konvertering av samlingsuttryck gör att ett samlingsuttryck kan konverteras till en typ.

En implicit konvertering av samlingsuttryck finns från ett samlingsuttryck till följande typer:

  • En endimensionell matristypT[], i vilket fall elementtypen är T
  • En spann av typ:
    • System.Span<T>
    • System.ReadOnlySpan<T>
      i vilka fall elementtypen är T
  • En typ med en lämplig skaparmetod, i det fallet är elementtyp den iterationstyp som bestäms från en GetEnumerator instansmetod eller ett uppräkningsbart gränssnitt, inte från en tilläggsmetod.
  • En struct eller klasstyp som implementerar System.Collections.IEnumerable där:
    • Den typen har en tillämplig konstruktor som kan anropas utan argument, och konstruktorn är tillgänglig på platsen för samlingsuttrycket.

    • Om samlingsuttrycket har några element har typ en instans- eller tilläggsmetod Add där:

      • Metoden kan anropas med ett argument med ett enda värde.
      • Om metoden är generisk kan typargumenten härledas från samlingen och argumentet.
      • Metoden är tillgänglig på platsen för samlingsuttrycket.

      I det fall då -elementtypen är iterationstyp av typ .

  • En gränssnittstyp:
    • 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>
      i vilka fall elementtypen är T

En implicit konvertering finns om typen har en elementtypU där för varje elementEᵢ i samlingsuttryck:

  • Om Eᵢ är ett uttryckselementfinns det en implicit konvertering från Eᵢ till U.
  • Om Eᵢ är ett spridningselement..Sᵢfinns det en implicit konvertering från iterationstypen av Sᵢ till U.

Det finns ingen konvertering av samlingsuttryck från ett samlingsuttryck till en flerdimensionell matristyp.

Typer som har en implicit samlingsuttryckskonvertering från ett samlingsuttryck är de giltiga måltyperna för det samlingsuttrycket.

Följande ytterligare implicita konverteringar finns från ett samlingsuttryck:

  • Till en nullbar värdetypT? där det finns en konvertering av samlingsuttryck från samlingsuttrycket till en värdetyp T. Konverteringen är en konvertering av samlingsuttryck till T, följt av en implicit nullbar konvertering från T till T?.

  • Till en referenstyp T där det finns en skapa metod associerad med T som returnerar en typ U och en implicit referenskonvertering från U till T. Konverteringen är en konvertering av samlingsuttryck till U följt av en implicit referenskonvertering från U till T.

  • Till en gränssnittstyp I där det finns en skapa metod associerad med I som returnerar en typ V och en implicit boxningskonvertering från V till I. Konverteringen är en samlingsuttryckskonvertering till V följt av en implicit boxningskonvertering från V till I.

Skapa metoder

En skapandemetod anges med attributet [CollectionBuilder(...)]samlingstypen. Attributet anger builder-typ och metodnamn för en metod som ska anropas för att skapa en instans av samlingstypen.

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

Attributet kan tillämpas på en class, struct, ref structeller interface. Attributet ärvs inte även om attributet kan tillämpas på en bas class eller en abstract class.

builder-typen måste vara en icke-generisk class eller struct.

Först bestäms uppsättningen av tillämpliga skapandemetoder CM.
Den består av metoder som uppfyller följande krav:

  • Metoden måste ha det namn som anges i attributet [CollectionBuilder(...)].
  • Metoden måste definieras på builder-typen direkt.
  • Metoden måste vara static.
  • Metoden måste vara tillgänglig där samlingsuttrycket används.
  • Metodens aritet måste överensstämma med samlingstypens aritet .
  • Metoden måste ha en enda parameter av typen System.ReadOnlySpan<E>, som passerar som värde.
  • Det finns en identitetskonvertering, implicit referenskonverteringeller boxningskonvertering från metodens returtyp till samlingstyp.

Metoder som deklareras för bastyper eller gränssnitt ignoreras och ingår inte i den CM uppsättningen.

Om CM-uppsättningen är tom har samlingstypen inte någon elementtyp och har ingen skapa-metod. Inget av följande steg gäller.

Om endast en metod i CM har en identitetskonvertering från E till -elementtypen av -samlingstypen, är det den skapa-metoden för samlingstypen. Annars har samlingstypen inte någon skapa-metod.

Ett fel rapporteras om attributet [CollectionBuilder] inte refererar till en anropande metod med den förväntade signaturen.

För ett samlingsuttryck med en måltyp C<S0, S1, …> där -typdeklarationenC<T0, T1, …> har en associerad builder-metodB.M<U0, U1, …>()tillämpas de allmänna typargumenten från måltypen i ordning – och från den yttersta innehållstypen till innersta – till builder-metoden.

Span-parametern för skapa-metoden kan uttryckligen markeras scoped eller [UnscopedRef]. Om parametern är implicit eller explicit scopedkan kompilatorn allokera lagringen för intervallet på stacken i stället för heapen.

Till exempel kan en möjlig skapa metod för ImmutableArray<T>:

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

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

Med skapa-metoden ovan kan ImmutableArray<int> ia = [1, 2, 3]; genereras som:

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

Byggnation

Element i ett samlingsuttryck värderas i ordning, från vänster till höger. Varje element utvärderas exakt en gång, och eventuella ytterligare referenser till elementen refererar till resultatet av den första utvärderingen.

Ett spridningselement kan itereras före eller efter att efterföljande element i samlingsuttrycket utvärderas.

Ett ohanterat undantag som utlöses från någon av de metoder som används under konstruktionen kommer att vara ouppfångat och förhindra vidare steg i konstruktionen.

Length, Countoch GetEnumerator antas inte ha några biverkningar.


Om måltypen är en struct eller klasstyp som implementerar System.Collections.IEnumerableoch måltypen inte har någon skapa-metod, är uppbyggnaden av samlingsinstansen följande:

  • Elementen utvärderas i ordning. Vissa eller alla element kan utvärderas under stegen nedan i stället för tidigare.

  • Kompilatorn kan fastställa den kända längden för samlingsuttrycket genom att anropa räknelika egenskaper – eller motsvarande egenskaper från välkända gränssnitt eller typer – på varje spridningselementuttryck.

  • Konstruktorn som är tillämplig utan argument anropas.

  • För varje element i ordning:

    • Om elementet är ett uttryckselementanropas den tillämpliga Add-instansen eller tilläggsmetoden med elementet uttryck som argument. (Till skillnad från det klassiska initieringsbeteendet för samlingenär elementutvärdering och Add-anrop inte nödvändigtvis interfolierade.)
    • Om elementet är ett spridningselement används något av följande:
      • En tillämplig GetEnumerator-instans- eller tilläggsmetod anropas på utspridningselementuttryck och för varje objekt från uppräknaren anropas den tillämpliga Add-instansen eller tilläggsmetoden på samlingsinstansen med objektet som argument. Om uppräknaren implementerar IDisposableanropas Dispose efter uppräkning, oavsett undantag.
      • En tillämplig AddRange-instans eller tilläggsmetod anropas på samlingsinstansen med spreadelementuttrycket som argument.
      • En tillämplig CopyTo instans- eller tilläggsmetod anropas på spridningselementuttrycket med samlingsinstansen och int index som argument.
  • Under byggstegen ovan kan en tillämplig EnsureCapacity instans- eller tilläggsmetod anropas en eller flera gånger på insamlingsinstans med ett int kapacitetsargument.


Om måltypen är en matris, ett span, en typ med en skapa-metodeller ett -gränssnittär uppbyggnaden av samlingsinstansen följande:

  • Elementen utvärderas i ordning. Vissa eller alla element kan utvärderas under stegen nedan i stället för tidigare.

  • Kompilatorn kan fastställa den kända längden för samlingsuttrycket genom att anropa räkbara egenskaper – eller motsvarande egenskaper från välkända gränssnitt eller typer – på varje spridningselementuttryck.

  • En initieringsinstans skapas på följande sätt:

    • Om måltypen är en matris och samlingsuttrycket har en känd längdallokeras en matris med förväntad längd.
    • Om måltypen är ett spann eller en typ med en skaparmetodoch samlingen har en känd längdskapas ett spann med förväntad längd som refererar till sammanhängande lagring.
    • Annars allokeras mellanliggande lagring.
  • För varje element i ordning:

    • Om elementet är ett uttryckselementanropas initieringsinstansen indexeraren för att lägga till det utvärderade uttrycket i det aktuella indexet.
    • Om elementet är ett spridningselement används något av följande:
      • En medlem i ett välkänt gränssnitt eller en typ anropas för att kopiera objekt från uttrycket för spread-element till initieringsinstansen.
      • En tillämplig GetEnumerator instans- eller tilläggsmetod anropas på uttryck för spridningselement och för varje objekt från uppräknaren anropas initieringsinstansen indexeraren för att lägga till objektet i det aktuella indexet. Om uppräknaren implementerar IDisposableanropas Dispose efter uppräkning, oavsett undantag.
      • En tillämplig CopyTo instans- eller tilläggsmetod anropas på spridningselementuttrycket med initieringsinstansen och int index som argument.
  • Om mellanliggande lagring allokerades för samlingen allokeras en samlingsinstans med den faktiska samlingslängden och värdena från instansen av initieringen kopieras till samlingsinstansen, eller om ett intervall krävs kan kompilatorn använda ett intervall av den faktiska insamlingslängden från mellanlagringen. Annars är initieringsinstansen samlingsinstansen.

  • Om måltypen har en metod för skapande, anropas metoden med span-instansen.


Obs! Kompilatorn kan fördröja läggande till av element i samlingen – eller fördröja genom att iterera över spridningselement – tills efterföljande element har utvärderats. (När efterföljande spridningselement har räknebara egenskaper som gör det möjligt att beräkna samlingens förväntade längd innan samlingen allokeras.) Omvänt kan kompilatorn ivrigt lägga till element i samlingen – och ivrigt iterera genom spridningselement – när det inte finns någon fördel med fördröjning.

Överväg följande samlingsuttryck:

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

Om spridningselement b och c är räknbara, kan kompilatorn fördröja tillägg av objekt från a och b tills efter att c har utvärderats, så att den resulterande matrisen kan allokeras till den förväntade längden. Därefter kan kompilatorn ivrigt lägga till objekt från cinnan dutvärderas.

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;

Tom samlingsliteral

  • Den tomma literalen [] har ingen typ. Precis som null-literalkan dock denna literal implicit konverteras till valfri konstruerbar samlingstyp.

    Följande är till exempel inte lagligt eftersom det inte finns någon måltyp och inga andra konverteringar är inblandade.

    var v = []; // illegal
    
  • Det är tillåtet att utelämna spridning av en tom literal. Till exempel:

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

    Här, om b är falskt, krävs det inte att något värde faktiskt skapas för det tomma samlingsuttrycket eftersom det omedelbart skulle spridas till nollvärden i den slutliga literalen.

  • Det tomma samlingsuttrycket tillåts vara en singleton om det används för att konstruera ett slutligt samlingsvärde som är känt för att inte vara föränderligt. Till exempel:

    // 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 = [];
    

Referenssäkerhet

Se säker kontextbegränsning för definitioner av värden för säker kontext: declaration-block, function-memberoch caller-context.

Det säker-kontext för ett samlingsuttryck är:

  • Den säkra kontexten för ett tomt samlingsuttryck [] är anroparkontexten.

  • Om måltypen är en span-typSystem.ReadOnlySpan<T>och T är en av de primitiva typernabool, sbyte, byte, short, ushort, char, int, uint, long, ulong, float, eller double, och samlingsuttrycket innehåller endast konstanta värden. Samlingsuttryckets säkra kontext är anroparkontext.

  • Om måltypen är en spänntypSystem.Span<T> eller System.ReadOnlySpan<T>är samlingsuttryckets safe-context declaration-block.

  • Om måltypen är en referensstrukturtyp med en skapa-metod, så är den säkra kontexten för samlingsuttrycket den säkra kontexten för ett anrop av skapningsmetoden där samlingsuttrycket är det spannargument som skickas till metoden.

  • Annars är samlingsuttryckets safe-context den anroparkontexten.

Ett samlingsuttryck med en säker kontext i deklarationsblocket kan inte lämna det omgivande omfånget, och kompilatorn kan lagra samlingen på stacken istället för i heapen.

För att tillåta att ett samlingsuttryck för en ref struct-typ undfly deklarationsblocketkan det vara nödvändigt att omvandla uttrycket till en annan 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
}

Typinferens

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;

De typinferenreglerna uppdateras på följande sätt.

Befintliga regler för första fasen extraheras till ett nytt avsnitt för indatatypsinferens, och en regel läggs till i indatatypsinferens och utdatatypinferens för samlingsuttryck.

11.6.3.2 Den första fasen

För vart och ett av metodargumenten Eᵢ:

  • En indatatypsinferens görs frånEᵢtill motsvarande parametertypTᵢ.

En indatatypsinferens görs från ett uttryck Etill en typ T på följande sätt:

  • Om E är ett samlingsuttryck med element Eᵢ, och T är en typ med en elementtypTₑ eller T är en nullbar värdetypT0? och T0 har en elementtypTₑ, då för varje Eᵢ:
    • Om Eᵢ är ett uttryckselementgörs en indatatypsinferensfrånEᵢtillTₑ.
    • Om Eᵢ är ett spridningselement med en iterationstypSᵢ, görs en lägre slutsatsdragningfrånSᵢtillTₑ.
  • [befintliga regler från första fasen] ...

11.6.3.7 Slutsatsdragningar av utdatatyp

En typinferens för utdata görs från ett uttryck Etill en typ T på följande sätt:

  • Om E är ett samlingsuttryck med element Eᵢ, och T är en typ med en elementtypTₑ, eller T är en nullbar värdetypT0?, och T0 har en elementtypTₑ, då gäller för varje Eᵢ:
    • Om Eᵢ är ett uttryckselementgörs en inferens för utdatatypenfrånEᵢtillTₑ.
    • Om Eᵢ är ett spridningselementgörs ingen slutsatsdragning från Eᵢ.
  • [befintliga regler från slutsatsdragningar av utdatatyp] ...

Tilläggsmetoder

Inga ändringar i regler för tilläggsmetodanrop .

12.8.10.3 Tilläggsmetodanrop

En tilläggsmetod Cᵢ.Mₑ är berättigad om:

  • ...
  • Det finns en implicit identitets-, referens- eller boxningskonvertering från uttryck till typen av den första parametern i Mₑ.

Ett samlingsuttryck har ingen naturlig typ, så de befintliga konverteringarna från typ är inte tillämpliga. Därför kan ett samlingsuttryck inte användas direkt som den första parametern för anrop av tilläggsmetod.

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

Överbelastningsupplösning

Bättre konvertering från uttryck uppdateras för att föredra vissa måltyper i konverteringar av samlingsuttryck.

I de uppdaterade reglerna:

  • En span_type är en av:
    • System.Span<T>
    • System.ReadOnlySpan<T>.
  • En array_or_array_interface är en av:
    • en matristyp
    • någon av följande gränssnittstyper implementeras av en matristyp:
      • 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>

Med en implicit konvertering C₁ som konverterar från ett uttryck E till en typ T₁och en implicit konvertering C₂ som konverterar från ett uttryck E till en typ T₂är C₁ en bättre konvertering än C₂ om något av följande gäller:

  • E är ett samlingsuttryck och något av följande gäller:
    • T₁ är System.ReadOnlySpan<E₁>och T₂ är System.Span<E₂>och det finns en implicit konvertering från E₁ till E₂
    • T₁ är System.ReadOnlySpan<E₁> eller System.Span<E₁>, och T₂ är en array eller arraygränssnitt med elementtypenE₂, och en implicit konvertering finns från E₁ till E₂
    • T₁ är inte en span_typeoch T₂ är inte en span_typeoch det finns en implicit konvertering från T₁ till T₂
  • E är inte ett samlingsuttryck och något av följande gäller:
    • E stämmer exakt med T₁ och E stämmer inte exakt med T₂
    • E matchar exakt både eller inget av T₁ och T₂, och T₁ är ett bättre konverteringsmål än T₂
  • E är en metodgrupp, ...

Exempel på skillnader med överbelastningsupplösning mellan matrisinitierare och samlingsuttryck:

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

Intervalltyper

Intervalltyperna ReadOnlySpan<T> och Span<T> är båda konstrukbla samlingstyper. Stödet för dessa följer designen för params Span<T>. Mer specifikt, att konstruera något av dessa spann kommer att resultera i en matris T[] skapad på -stacken om parameterarrayen är inom de gränser (om några) som fastställts av kompilatorn. Annars allokeras matrisen på heapen.

Om kompilatorn väljer att allokera på stacken behöver du inte översätta en literal direkt till en stackalloc vid den specifika tidpunkten. Till exempel givet:

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

Kompilatorn kan översätta det med hjälp av stackalloc så länge betydelsen av Span förblir densamma och att behåller för intervallsäkerhet. Det kan till exempel översätta ovanstående till:

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
}

Kompilatorn kan också använda inlinje-arrayer, om de är tillgängliga, när den väljer att allokera på stacken. Observera att inline-arrayer i C# 12 inte kan initieras med ett samlingsuttryck. Den funktionen är ett öppet förslag.

Om kompilatorn bestämmer sig för att allokera på heap är översättningen för Span<T> helt enkelt:

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

Samlingsliteral översättning

Ett samlingsuttryck har en känd längd om kompileringstidstypen för varje spridningselement i samlingsuttrycket är räkningsbar.

Gränssnittsöversättning

Icke-föränderlig gränssnittsöversättning

Med tanke på en måltyp som inte innehåller muterande medlemmar, nämligen IEnumerable<T>, IReadOnlyCollection<T>och IReadOnlyList<T>, krävs en kompatibel implementering för att skapa ett värde som implementerar gränssnittet. Om en typ syntetiseras rekommenderar vi att den syntetiserade typen implementerar alla dessa gränssnitt, samt ICollection<T> och IList<T>, oavsett vilken gränssnittstyp som var mål. Detta säkerställer maximal kompatibilitet med befintliga bibliotek, inklusive de som introspekterar de gränssnitt som implementeras av ett värde för att möjliggöra prestandaoptimeringar.

Dessutom måste värdet implementera de icke-generiska ICollection- och IList-gränssnitten. På så sätt kan samlingsuttryck stödja dynamisk introspektion i scenarier som databindning.

En kompatibel implementering är fri att:

  1. Använd en befintlig typ som implementerar de gränssnitt som krävs.
  2. Syntetisera en typ som implementerar de gränssnitt som krävs.

I båda fallen tillåts den typ som används att implementera en större uppsättning gränssnitt än de som krävs strikt.

Syntetiserade typer är fria att använda vilken strategi de vill för att implementera de nödvändiga gränssnitten på ett korrekt sätt. Till exempel kan en syntetiserad typ infoga elementen direkt inom sig själv, vilket undviker behovet av ytterligare interna samlingsallokeringar. En syntetiserad typ kunde inte heller använda någon lagring alls och valde att beräkna värdena direkt. Du kan till exempel returnera index + 1 för [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].

  1. Värdet måste returnera true vid frågor om ICollection<T>.IsReadOnly (om det implementeras) och icke-generiska IList.IsReadOnly och IList.IsFixedSize. Detta säkerställer att konsumenterna på lämpligt sätt kan se att samlingen inte är föränderlig, trots implementeringen av de föränderliga vyerna.
  2. Värdet måste utlösa alla anrop till en mutationsmetod (som IList<T>.Add). Detta säkerställer säkerheten, vilket förhindrar att en icke-föränderlig samling av misstag muteras.

Översättning av föränderligt gränssnitt

Angiven måltyp som innehåller muterande medlemmar, nämligen ICollection<T> eller IList<T>:

  1. Värdet måste vara en instans av List<T>.

Känd längdöversättning

Att ha en känd längd möjliggör effektiv konstruktion av ett resultat med potential för ingen kopiering av data och inget onödigt slackutrymme i ett resultat.

Att inte ha en känd längd förhindrar inte att något resultat skapas. Det kan dock leda till extra processor- och minneskostnader för att producera data och sedan flytta till slutmålet.

  • För en literal med känd längd[e1, ..s1, etc]börjar först översättningen med följande:

    int __len = count_of_expression_elements +
                __s1.Count;
                ...
                __s_n.Count;
    
  • Angiven måltyp T för den literalen:

    • Om T är något T1[]översätts literalen som:

      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
      

      Implementeringen tillåts använda andra metoder för att fylla i matrisen. Du kan till exempel använda effektiva masskopieringsmetoder som .CopyTo().

    • Om T är något Span<T1>översätts literalen som ovan, förutom att __result-initieringen översätts som:

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

      Översättningen kan använda stackalloc T1[] eller en inline-arrayer i stället för new T1[] om span-säkerhet bibehålls.

    • Om T är något ReadOnlySpan<T1>översätts literalen på samma sätt som för Span<T1>-fallet, förutom att slutresultatet blir att Span<T1>implicit konverteras till en ReadOnlySpan<T1>.

      En ReadOnlySpan<T1> där T1 är någon primitiv typ, och alla samlingselement är konstanta behöver inte dess data vara på heapen eller på stacken. En implementering kan till exempel konstruera det här intervallet direkt som en referens till en del av programmets datasegment.

      Ovanstående formulär (för matriser och intervall) är basrepresentationerna av samlingsuttrycket och används för följande översättningsregler:

      • Om T är en C<S0, S1, …> som har motsvarande create-methodB.M<U0, U1, …>()översätts literalen som:

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

        Eftersom skapa-metoden måste ha en argumenttyp av någon instansierad ReadOnlySpan<T>gäller översättningsregeln för intervall när samlingsuttrycket skickas till metoden create.

      • Om T har stöd för samlingsinitierare, så:

        • om typen T innehåller en tillgänglig konstruktor med en enda parameter int capacityöversätts literalen som:

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

          Obs! Namnet på parametern måste vara capacity.

          Det här formuläret gör det möjligt för en literal att informera den nyligen konstruerade typen av antalet element för att möjliggöra effektiv allokering av intern lagring. Detta undviker slösaktiga omfördelningar när elementen läggs till.

        • annars översätts literalen som:

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

          Detta gör det möjligt att skapa måltypen, men utan kapacitetsoptimering för att förhindra intern omfördelning av lagring.

Översättning av okänd längd

  • Givet en måltyp T för en okänd längd literal:

    • Om T stöder insamlingsinitierareöversätts literalen som:

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

      Detta möjliggör spridning av alla itererbara typer, om än med minsta möjliga optimering.

    • Om T är något T1[]har literalen samma semantik som:

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

      Ovanstående är dock ineffektivt; den skapar mellanhandslistan och skapar sedan en kopia av den slutliga matrisen från den. Implementeringar kan optimera bort detta, till exempel genom att skapa kod så här:

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

      Detta möjliggör minimalt avfall och kopiering, utan ytterligare omkostnader som bibliotekssamlingar kan medföra.

      Antalet som skickas till CreateArray används för att ge ett tips om startstorlek för att förhindra slösaktiga storleksändringar.

    • Om T är någon spännviddstypkan en implementering följa ovanstående T[] strategi, eller någon annan strategi med samma semantik, men bättre prestanda. I stället för att till exempel allokera matrisen som en kopia av listelementen kan CollectionsMarshal.AsSpan(__list) användas för att hämta ett spannvärde direkt.

Scenarier som inte stöds

Även om samlingsliteraler kan användas för många scenarier finns det några som de inte kan ersätta. Dessa inkluderar:

  • Flerdimensionella matriser (t.ex. new int[5, 10] { ... }). Det finns ingen möjlighet eller funktion att inkludera dimensionerna, och alla samlingsliteraler är antingen linjära strukturer eller endast kartstrukturer.
  • Kollektioner som överför specialvärden till sina konstruktorer. Det finns ingen möjlighet att komma åt konstruktorn som används.
  • Initialiseringar för kapslade samlingar, t.ex. new Widget { Children = { w1, w2, w3 } }. Det här formuläret måste stanna eftersom det har mycket olika semantik från Children = [w1, w2, w3]. Den förra anropar .Add upprepade gånger på .Children medan den senare skulle tilldela en ny samling över .Children. Vi kan överväga att låta det senare formuläret återgå till att lägga till en befintlig samling om .Children inte kan tilldelas, men det verkar som om det kan vara mycket förvirrande.

Syntaxambiguiteter

  • Det finns två "sanna" syntaktiska tvetydigheter där det finns flera juridiska syntaktiska tolkningar av kod som använder en collection_literal_expression.

    • spread_element är tvetydig i jämförelse med en range_expression. Man kan tekniskt sett ha:

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

      För att lösa detta kan vi antingen:

      • Kräv att användare parenteserar (..e) eller inkluderar ett startindex 0..e om de vill ha ett intervall.
      • Välj en annan syntax (till exempel ...) för spridning. Detta skulle vara olyckligt för bristen på konsekvens med segmentmönster.
  • Det finns två fall där det inte finns en sann tvetydighet, men där syntaxen avsevärt ökar parsningskomplexiteten. Även om det inte är ett problem med tanke på tekniktiden ökar detta fortfarande kognitiva omkostnader för användare när de tittar på kod.

    • Tvetydighet mellan collection_literal_expression och attributes för uttryck eller lokala funktioner. Överväga:

      [X(), Y, Z()]
      

      Detta kan vara något av följande:

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

      Utan avancerad framåtkik skulle det vara omöjligt att avgöra utan att analysera hela den bokstavliga sekvensen.

      Alternativ för att åtgärda detta är:

      • Tillåt detta genom att utföra parseringsarbetet för att avgöra vilket av dessa fall det är.
      • Tillåt inte detta och kräva att användaren omsluter literalen i parenteser som ([X(), Y, Z()]).ForEach(...).
      • Tvetydighet mellan en collection_literal_expression i en conditional_expression och en null_conditional_operations. Överväga:
      M(x ? [a, b, c]
      

      Detta kan vara något av följande:

      // 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]);
      

      Utan en avancerad förhandsanalys skulle det vara omöjligt att avgöra utan att läsa hela literalen.

      Obs! Detta är ett problem även utan en naturlig typ eftersom målskrivning gäller via conditional_expressions.

      Precis som med de andra kan vi behöva använda parenteser för att klargöra. Anta med andra ord tolkningen null_conditional_operation om det inte är skrivet så här: x ? ([1, 2, 3]) :. Det verkar dock ganska olyckligt. Den här typen av kod verkar inte orimlig att skriva och kan sannolikt förvirra människor.

Nackdelar

  • Detta introducerar ännu ett formulär för samlingsuttryck utöver de otaliga sätt som vi redan har. Det här är extra komplext för språket. Med detta sagt gör detta också det möjligt att förena sig i en ring syntax för att styra över dem alla, vilket innebär att befintliga kodbaser kan förenklas och flyttas mot ett enhetligt utseende överallt.
  • Att använda [...] i stället för {...} flyttas bort från den syntax som vi vanligtvis har använt för matriser och insamlingsinitiatorer redan. Specifikt att den använder [...] i stället för {...}. Men det här löstes redan av språkteamet när vi gjorde listmönster. Vi försökte få {...} arbeta med listmönster och stötte på oöverstigliga problem. På grund av detta flyttade vi till [...] som, även om det är nytt för C#, känns naturligt i många programmeringsspråk och tillät oss att börja om utan tvetydighet. Att använda [...] som motsvarande literalform kompletterar våra senaste beslut och ger oss en ren plats att arbeta utan problem.

Detta introducerar vårtor i språket. Följande är till exempel både juridiska och (lyckligtvis) betyder exakt samma sak:

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

Men med tanke på den bredd och konsekvens som den nya literalsyntaxen medför bör vi överväga att rekommendera att personer flyttar till det nya formuläret. IDE-förslag och korrigeringar kan vara till hjälp i det avseendet.

Alternativ

  • Vilka andra konstruktioner har övervägts? Vad är effekten av att inte göra detta?

Lösta frågor

  • Ska kompilatorn använda stackalloc för stackallokering när infogade matriser inte är tillgängliga och iterationstyp är en primitiv typ?

    Lösning: Nej. Att hantera en stackalloc buffert kräver ytterligare arbete jämfört med en infogad matris för att säkerställa att bufferten inte allokeras upprepade gånger när samlingen befinner sig i en loop. Den ytterligare komplexiteten i kompilatorn och i den genererade koden överväger fördelarna med stackallokering på äldre plattformar.

  • I vilken ordning ska vi utvärdera literalelement jämfört med egenskapsutvärdering för längd/antal? Ska vi utvärdera alla element först, sedan alla längder? Eller ska vi utvärdera ett element, sedan dess längd, sedan nästa element och så vidare?

    Lösning: Vi utvärderar alla element först, sedan följer allt annat det.

  • Kan en okänd längd literal skapa en samlingstyp som behöver en känd längd, till exempel en matris, ett spann eller en samling av typen Construct(array/span)? Detta skulle vara svårare att göra effektivt, men det kan vara möjligt genom smart användning av sammanställda arrayer och/eller konstruktörer.

    Lösning: Ja, vi tillåter skapandet av en samling med fast längd från en okänd längd literal. Kompilatorn får implementera detta på ett så effektivt sätt som möjligt.

    Följande text finns för att registrera den ursprungliga diskussionen om det här ämnet.

    Användare kan alltid göra en okänd längd literal till en känd längd med kod som denna:

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

    Detta är dock olyckligt på grund av behovet av att tvinga fram allokeringar av tillfällig lagring. Vi skulle potentiellt kunna vara mer effektiva om vi kontrollerade hur detta släpptes ut.

  • Kan en collection_expression måltypas till ett IEnumerable<T> eller andra samlingsgränssnitt?

    Till exempel:

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

    Lösning: Ja, en literal kan måltypas till vilken gränssnittstyp som helst I<T> som List<T> implementerar. Till exempel IEnumerable<long>. Detta är samma sak som målskrivning för List<long> och sedan tilldela det resultatet till den angivna gränssnittstypen. Följande text finns för att registrera den ursprungliga diskussionen om det här ämnet.

    Den öppna frågan här är att avgöra vilken underliggande typ som faktiskt ska skapas. Ett alternativ är att titta på förslaget för params IEnumerable<T>. Där skulle vi generera en matris för att skicka värdena vidare, ungefär som med params T[].

  • Kan/bör kompilatorn generera Array.Empty<T>() för []? Bör vi kräva att den gör detta för att undvika allokeringar när det är möjligt?

    Ja. Kompilatorn bör avge Array.Empty<T>() i alla fall där detta är lagligt och slutresultatet inte kan ändras. Du kan till exempel rikta in dig på T[], IEnumerable<T>, IReadOnlyCollection<T> eller IReadOnlyList<T>. Den bör inte använda Array.Empty<T> när målet är föränderligt (ICollection<T> eller IList<T>).

  • Ska vi utöka samlingsinitieringar för att söka efter den mycket vanliga metoden AddRange? Den kan användas av den underliggande konstruerade typen för att lägga till spridningselement på ett potentiellt mer effektivt sätt. Vi kanske också vill leta efter saker som .CopyTo. Det kan finnas nackdelar här eftersom dessa metoder kan leda till överskott av allokeringar/sändningar jämfört med direkt uppräkning i den översatta koden.

    Ja. En implementering tillåts använda andra metoder för att initiera ett samlingsvärde, under antagandet att dessa metoder har väldefinierad semantik, och att samlingstyper bör vara "välfungerande". I praktiken bör dock en implementering vara försiktig eftersom fördelar på ett sätt (masskopiering) också kan få negativa konsekvenser (till exempel boxning av en struct-samling).

    En implementering bör dra nytta av de fall där det inte finns några nackdelar. Till exempel med en .AddRange(ReadOnlySpan<T>)-metod.

Olösta frågor

  • Ska vi tillåta att härleda -elementtypen om -iterationstypen är "tvetydig" efter en viss definition? Till exempel:
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;
}
  • Bör det vara lagligt att skapa och omedelbart indexera i en samlingsliteral? Obs! Detta kräver ett svar på den olösta frågan nedan om samlingsliteraler har en naturlig typ.

  • Stackallokeringar för enorma samlingar kan överbelasta stacken. Ska kompilatorn ha en heuristik för att placera dessa data på högen? Bör språket vara ospecificerat för att möjliggöra den här flexibiliteten? Vi bör följa specifikationen för params Span<T>.

  • Behöver vi rikta in oss på måltyp spread_element? Överväg till exempel:

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

    Obs! Detta kan ofta komma upp i följande formulär för att tillåta villkorsstyrd inkludering av vissa uppsättningar element, eller ingenting om villkoret är falskt:

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

    För att kunna utvärdera det här fullständiga literalet måste vi utvärdera elementuttrycken inom den. Det innebär att kunna utvärdera b ? [c] : [d, e]. Utan en måltyp för att utvärdera det här uttrycket i kontexten för och utan någon typ av naturlig typ, skulle detta inte kunna avgöra vad vi ska göra med antingen [c] eller [d, e] här.

    För att lösa detta kan vi säga att när vi utvärderar en literals spread_element-uttryck, fanns det en implicit måltyp som motsvarade måltypen för själva literalen. Så i ovanstående skulle det skrivas om som:

    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;
    

Specifikation av en konstruerad samlingstyp som använder en skapa-metod är känslig för den kontext där konvertering klassificeras

Förekomsten av konverteringen i det här fallet beror på begreppet en iterationstyp av samlingstypen . Om det finns en skapandemetod som tar en ReadOnlySpan<T> där T är av iterationstypen finns konverteringen. Annars gör den inte det.

En iterationstyp är dock känslig för den kontext där foreach utförs. För samma samlingstyp kan det variera beroende på vilka tilläggsmetoder som finns i kontexten, och det kan också vara odefinierat.

Det känns bra för ändamålet med foreach när typen inte är tänkt att vara foreach-kapabel i sig. Om så är fallet kan tilläggsmetoder inte ändra hur typen itereras över, oavsett sammanhang.

Men det känns lite konstigt för en konvertering att vara sammanhangskänslig så. Konverteringen är i själva verket "instabil". En samlingstyp som är uttryckligen utformad för att kunna konstrueras kan utelämna en definition av en mycket viktig detalj – dess itereringstyp. Lämnar typen "okonverterbar" som den är.

Här är ett exempel:

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

Givet den aktuella designen, om typen inte definierar iterationstyp själv, kan kompilatorn inte tillförlitligt verifiera en tillämpning av ett CollectionBuilder-attribut. Om vi inte vet iterationstypvet vi inte vad signaturen för skapa-metoden ska vara. Om den iterationstypen kommer från kontexten finns det ingen garanti för att typen alltid kommer att användas i en liknande kontext.

Params Collections-funktionen påverkas också av detta. Det känns konstigt att inte kunna förutsäga elementtypen för en params parameter på deklarationspunkten på ett tillförlitligt sätt. Det aktuella förslaget kräver också att metoden för att skapa är minst lika tillgänglig som paramssamlingstypen. Det är omöjligt att utföra den här kontrollen på ett tillförlitligt sätt, om inte samlingstyp själv definierar sin iterationstyp.

Observera att vi också har https://github.com/dotnet/roslyn/issues/69676 öppnat för kompilatorn, som i princip hanterar samma problem, men diskuterar det ur perspektivet av optimering.

Förslag

Kräv en typ som använder attributet CollectionBuilder för att definiera sin egen iterationstyp med. Det innebär med andra ord att typen antingen ska implementera IEnumarable/IEnumerable<T>, eller att den ska ha en offentlig GetEnumerator-metod med rätt signatur (detta utesluter alla tilläggsmetoder).

Just nu krävs också för att skapa metod och för att "vara tillgänglig där samlingsuttrycket används". Detta är en annan punkt i kontextberoende baserat på tillgänglighet. Syftet med den här metoden är mycket likt syftet med en användardefinierad konverteringsmetod, och den måste vara offentlig. Därför bör vi överväga att kräva att metoden skapa också ska vara offentlig.

Slutsats

Godkänd med ändringar LDM-2024-01-08

Begreppet iterationstyp tillämpas inte konsekvent under konverteringar

  • Till en struct eller klass av typen som implementerar System.Collections.Generic.IEnumerable<T> där:
    • För varje elementEi det finns en implicit konvertering till T.

Det verkar som om ett antagande görs att T nödvändigtvis är iterationstyp av struct eller klasstyp i det här fallet. Det antagandet är dock felaktigt. Vilket kan leda till ett mycket konstigt beteende. Till exempel:

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" };
    }
}
  • Till en struct eller klasstyp som implementerar System.Collections.IEnumerable och inte implementerarSystem.Collections.Generic.IEnumerable<T>.

Implementeringen förutsätter att iterationstyp är object, men specifikationen lämnar det här faktumet ospecificerat och kräver helt enkelt inte att varje element konverteras till någonting. I allmänhet är dock iterationstyp inte nödvändigtvis den object typ. Vilket kan observeras i följande exempel:

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

Begreppet iterationstyp är grundläggande för funktionen Params Collections. Och det här problemet leder till en konstig avvikelse mellan de två funktionerna. Till exempel:

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

Det kommer förmodligen att vara bra att justera på ett eller annat sätt.

Förslag

Ange konvertibilitet för struct eller klasstyp som implementerar System.Collections.Generic.IEnumerable<T> eller System.Collections.IEnumerable när det gäller iterationstyp och kräver en implicit konvertering för varje elementEi till iterationstyp.

Slutsats

Godkänd LDM-2024-01-08

Ska konvertering av samlingsuttryck kräva tillgänglighet för en minimal uppsättning API:er för konstruktion?

En konstruerbar insamlingstyp enligt konverteringar kan faktiskt vara inte konstruerbar, vilket troligen leder till ett oväntat överbelastningslösningsbeteende. Till exempel:

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[])'
    }
}

Men "C1. M1(sträng)' är inte en kandidat som kan användas eftersom:

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?)

Här är ett annat exempel med en användardefinierad typ och ett starkare fel som inte ens nämner en giltig kandidat:

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

Det verkar som om situationen är mycket lik den vi brukade ha med metodgrupp-till-delegeringskonverteringar. Dvs. det fanns scenarier där konverteringen fanns, men var felaktig. Vi bestämde oss för att förbättra det genom att säkerställa att om konverteringen är felaktig, då finns den inte.

Observera att med funktionen "Params Collections" kommer vi att stöta på ett liknande problem. Det kan vara bra att inte tillåta användning av params modifierare för icke-konstruktionsbara samlingar. Men i det aktuella förslaget baseras kontrollen på konverteringar i avsnittet. Här är ett exempel:

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

Det verkar som om problemet har diskuterats något tidigare, se https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-02.md#collection-expressions. Då framfördes ett argument om att reglerna, som anges just nu, överensstämmer med hur interpolerade stränghanterare anges. Här är ett citat:

I synnerhet angavs interpolerade stränghanterare ursprungligen på det här sättet, men vi ändrade specifikationen efter att ha övervägt det här problemet.

Även om det finns vissa likheter finns det också en viktig skillnad som är värd att överväga. Här är ett citat från https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/improved-interpolated-strings.md#interpolated-string-handler-conversion:

Typ T sägs vara en applicable_interpolated_string_handler_type om den är tilldelad System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute. Det finns en implicit interpolerad_stränghanterarkonvertering till T från ett interpolerad_stränguttryck, eller ett additivt uttryck som helt består av interpolerade_stränguttryck och som endast använder + operatorer.

Måltypen måste ha ett särskilt attribut som är en stark indikator på författarens avsikt att typen ska vara en interpolerad stränghanterare. Det är rättvist att anta att förekomsten av attributet inte är en tillfällighet. I motsats innebär det faktum att en typ är "uppräkningsbar" inte nödvändigtvis att det var författarens avsikt att typen skulle vara konstruerbar. En förekomst av en create-metod, vilket dock anges med ett [CollectionBuilder(...)]-attribut på samlingstyp, indikerar starkt författarens avsikt att typen ska vara konstruerbar.

Förslag

För en -strukt eller -klasstyp som implementerar System.Collections.IEnumerable och som inte har någon skapa-metod-konverteringar bör avsnittet kräva förekomst av minst följande API:er:

  • En tillgänglig konstruktor som är tillämplig utan argument.
  • En tillgänglig Add instans- eller tilläggsmetod som kan anropas med värdet iterationstyp som argument.

För Params Collections-funktionen är sådana typer giltiga params-typer när dessa API:er deklareras som offentliga och används som instansmetoder (i motsats till tilläggsmetoder).

Slutsats

Godkänd med ändringar LDM-2024-01-10

Designa möten

https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-11-01.md#collection-literals https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-09.md#ambiguity-of--in-collection-expressions https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-28.md#collection-literals https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-01-08.md https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-01-10.md

Arbetsgruppsmöten

https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-06.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-14.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-21.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-04-05.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-04-28.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-05-26.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-06-12.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-06-26.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-08-03.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-08-10.md

Kommande punkter på dagordningen

  • Stackallokeringar för stora samlingar kan överbelasta stacken. Ska kompilatorn ha en heuristik för att placera denna data på heapen? Bör språket vara ospecificerat för att möjliggöra den här flexibiliteten? Vi bör följa vad specifikationen/impl gör för params Span<T>. Alternativen är:

    • Alltid stackalloc. Lär folk att vara försiktiga med Span. På så sätt kan saker som Span<T> span = [1, 2, ..s] fungera bra, förutsatt att s är liten. Om detta kan orsaka stacköverskridning kan användarna alltid skapa en matris istället och sedan få ett "span" runt detta. Detta verkar vara det mest i linje med vad människor kanske vill, men med extrem fara.
    • Stackalloc endast när literalen har en fast antal element (dvs. inga spridningselement). Detta gör förmodligen alltid saker säkra, med fast stackanvändning, och kompilatorn (förhoppningsvis) kan återanvända den fasta bufferten. Det innebär dock att saker som [1, 2, ..s] aldrig skulle vara möjliga, även om användaren vet att det är helt säkert vid körning.
  • Hur fungerar överbelastningsupplösning? Om ett API har:

    public void M(T[] values);
    public void M(List<T> values);
    

    Vad händer med M([1, 2, 3])? Vi behöver förmodligen definiera "bättre" för dessa konverteringar.

  • Ska vi utöka insamlingsinitierare för att leta efter den mycket vanliga metoden AddRange? Den kan användas av den underliggande konstruerade typen för att utföra tillägg av spridningselement potentiellt mer effektivt. Vi kanske också vill leta efter saker som .CopyTo. Det kan finnas nackdelar här eftersom dessa metoder kan leda till överskott av allokeringar/sändningar jämfört med direkt uppräkning i den översatta koden.

  • Typinferens för generella typer bör uppdateras för att överföra typinformation till och från insamlingsliteraler. Till exempel:

    void M<T>(T[] values);
    M([1, 2, 3]);
    

    Det verkar naturligt att detta bör vara något som slutsatsningsalgoritmen kan göras medveten om. När detta stöds för "grundläggande" konstruktionsbara samlingstypfall (T[], I<T>, Span<T>new T()), bör det också falla ur Collect(constructible_type) fallet. Till exempel:

    void M<T>(ImmutableArray<T> values);
    M([1, 2, 3]);
    

    Här kan Immutable<T> konstrueras via en init void Construct(T[] values)-metod. Därför används T[] values typ med slutsatsdragning mot [1, 2, 3] vilket leder till en slutsatsdragning av int för T.

  • Tvetydighet för cast/index.

    I dag är följande ett uttryck som indexeras till

    var v = (Expr)[1, 2, 3];
    

    Men det skulle vara trevligt att kunna göra saker som:

    var v = (ImmutableArray<int>)[1, 2, 3];
    

    Kan/ska vi ta en paus här?

  • Syntaktiska tvetydigheter med ?[.

    Det kan vara värt att ändra reglerna för nullable index access för att ange att inget utrymme kan uppstå mellan ? och [. Det skulle vara en brytande ändring (men troligen obetydlig eftersom VS redan tvingar ihop dem om du skriver dem med ett mellanslag). Om vi gör detta kan vi få x?[y] att parsas på ett annat sätt än x ? [y].

    En liknande sak inträffar om vi vill gå med https://github.com/dotnet/csharplang/issues/2926. I den världen är x?.y tvetydig med x ? .y. Om vi kräver ?. att abut, kan vi syntaktiskt skilja de två fallen trivialt.