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:
-
fälttyper, till exempel
int[]
. -
Span<T>
ochReadOnlySpan<T>
. - Typer som stöder insamlingsinitierare, till exempel
List<T>
.
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[]
ellernew[]
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örligT
) 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 (somImmutableArray.CreateBuilder
) är otympliga och producerar fortfarande oundvikligt skräp.
- Matriser, som kräver antingen
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 oftae1
,e_n
osv.spread_element
instanser kallas ofta..s1
,..s_n
osv.spantyp innebär antingen
Span<T>
ellerReadOnlySpan<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.
- Tomma literaler
Den iterationstypen av
..s_n
är typen av iterationsvariabel bestäms som oms_n
användes som uttrycket som itererades över i enforeach_statement
.Variabler som börjar med
__name
används för att representera resultatet av utvärderingen avname
, som lagras på en plats så att den bara utvärderas en gång. Till exempel__e1
är utvärderingen ave1
.List<T>
,IEnumerable<T>
osv. refererar till respektive typer iSystem.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 ettnew 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.
- En implementering kan till exempel översätta literaler som
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 överx
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.
- Det antas att värdet för
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 matristyp
T[]
, i vilket fall elementtypen ärT
- En spann av typ:
System.Span<T>
System.ReadOnlySpan<T>
i vilka fall elementtypen ärT
- 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.
- 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 ärT
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ånEᵢ
tillU
. - Om
Eᵢ
är ett spridningselement..Sᵢ
finns det en implicit konvertering från iterationstypen avSᵢ
tillU
.
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ärdetyp
T?
där det finns en konvertering av samlingsuttryck från samlingsuttrycket till en värdetypT
. Konverteringen är en konvertering av samlingsuttryck tillT
, följt av en implicit nullbar konvertering frånT
tillT?
.Till en referenstyp
T
där det finns en skapa metod associerad medT
som returnerar en typU
och en implicit referenskonvertering frånU
tillT
. Konverteringen är en konvertering av samlingsuttryck tillU
följt av en implicit referenskonvertering frånU
tillT
.Till en gränssnittstyp
I
där det finns en skapa metod associerad medI
som returnerar en typV
och en implicit boxningskonvertering frånV
tillI
. Konverteringen är en samlingsuttryckskonvertering tillV
följt av en implicit boxningskonvertering frånV
tillI
.
Skapa metoder
En skapandemetod anges med attributet [CollectionBuilder(...)]
på 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 struct
eller 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 scoped
kan 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
, Count
och GetEnumerator
antas inte ha några biverkningar.
Om måltypen är en struct eller klasstyp som implementerar System.Collections.IEnumerable
och 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 ochAdd
-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ämpligaAdd
-instansen eller tilläggsmetoden på samlingsinstansen med objektet som argument. Om uppräknaren implementerarIDisposable
anropasDispose
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 ochint
index som argument.
- En tillämplig
- Om elementet är ett uttryckselementanropas den tillämpliga
Under byggstegen ovan kan en tillämplig
EnsureCapacity
instans- eller tilläggsmetod anropas en eller flera gånger på insamlingsinstans med ettint
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 implementerarIDisposable
anropasDispose
efter uppräkning, oavsett undantag. - En tillämplig
CopyTo
instans- eller tilläggsmetod anropas på spridningselementuttrycket med initieringsinstansen ochint
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
ochc
är räknbara, kan kompilatorn fördröja tillägg av objekt fråna
ochb
tills efter attc
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ånc
innand
utvä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-typ
System.ReadOnlySpan<T>
ochT
är en av de primitiva typernabool
,sbyte
,byte
,short
,ushort
,char
,int
,uint
,long
,ulong
,float
, ellerdouble
, och samlingsuttrycket innehåller endast konstanta värden. Samlingsuttryckets säkra kontext är anroparkontext.Om måltypen är en spänntyp
System.Span<T>
ellerSystem.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ån
Eᵢ
till motsvarande parametertypTᵢ
.En indatatypsinferens görs från ett uttryck
E
till en typT
på följande sätt:
- Om
E
är ett samlingsuttryck med elementEᵢ
, ochT
är en typ med en elementtypTₑ
ellerT
är en nullbar värdetypT0?
ochT0
har en elementtypTₑ
, då för varjeEᵢ
:
- 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
E
till en typT
på följande sätt:
- Om
E
är ett samlingsuttryck med elementEᵢ
, ochT
är en typ med en elementtypTₑ
, ellerT
är en nullbar värdetypT0?
, ochT0
har en elementtypTₑ
, då gäller för varjeEᵢ
:
- Om
Eᵢ
är ett uttryckselementgörs en inferens för utdatatypenfrånEᵢ
tillTₑ
.- Om
Eᵢ
är ett spridningselementgörs ingen slutsatsdragning frånEᵢ
.- [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 uttryckE
till en typT₁
och en implicit konverteringC₂
som konverterar från ett uttryckE
till en typT₂
ärC₁
en bättre konvertering änC₂
om något av följande gäller:
E
är ett samlingsuttryck och något av följande gäller:
T₁
ärSystem.ReadOnlySpan<E₁>
ochT₂
ärSystem.Span<E₂>
och det finns en implicit konvertering frånE₁
tillE₂
T₁
ärSystem.ReadOnlySpan<E₁>
ellerSystem.Span<E₁>
, ochT₂
är en array eller arraygränssnitt med elementtypenE₂
, och en implicit konvertering finns frånE₁
tillE₂
T₁
är inte en span_typeochT₂
är inte en span_typeoch det finns en implicit konvertering frånT₁
tillT₂
E
är inte ett samlingsuttryck och något av följande gäller:
E
stämmer exakt medT₁
ochE
stämmer inte exakt medT₂
E
matchar exakt både eller inget avT₁
ochT₂
, ochT₁
är ett bättre konverteringsmål änT₂
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:
- Använd en befintlig typ som implementerar de gränssnitt som krävs.
- 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]
.
- Värdet måste returnera
true
vid frågor omICollection<T>.IsReadOnly
(om det implementeras) och icke-generiskaIList.IsReadOnly
ochIList.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. - 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>
:
- 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ågotT1[]
ö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ågotSpan<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örnew T1[]
om span-säkerhet bibehålls.Om
T
är någotReadOnlySpan<T1>
översätts literalen på samma sätt som förSpan<T1>
-fallet, förutom att slutresultatet blir attSpan<T1>
implicit konverteras till enReadOnlySpan<T1>
.En
ReadOnlySpan<T1>
därT1
ä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 enC<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 parameterint 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ågotT1[]
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åendeT[]
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 kanCollectionsMarshal.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ånChildren = [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 enrange_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 startindex0..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.
- Kräv att användare parenteserar
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
ochattributes
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 enconditional_expression
och ennull_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
ringsyntax 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 ettIEnumerable<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>
somList<T>
implementerar. Till exempelIEnumerable<long>
. Detta är samma sak som målskrivning förList<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 medparams 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>
ellerIReadOnlyList<T>
. Den bör inte användaArray.Empty<T>
när målet är föränderligt (ICollection<T>
ellerIList<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 params
samlingstypen. 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 element
Ei
det finns en implicit konvertering tillT
.
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 tilldeladSystem.Runtime.CompilerServices.InterpolatedStringHandlerAttribute
. Det finns en implicit interpolerad_stränghanterarkonvertering tillT
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 atts
ä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.
- Alltid stackalloc. Lär folk att vara försiktiga med Span. På så sätt kan saker som
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 urCollect(constructible_type)
fallet. Till exempel:void M<T>(ImmutableArray<T> values); M([1, 2, 3]);
Här kan
Immutable<T>
konstrueras via eninit void Construct(T[] values)
-metod. Därför användsT[] values
typ med slutsatsdragning mot[1, 2, 3]
vilket leder till en slutsatsdragning avint
förT
.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 änx ? [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 medx ? .y
. Om vi kräver?.
att abut, kan vi syntaktiskt skilja de två fallen trivialt.
C# feature specifications