Delen via


Eersteklas spantypen

Opmerking

Dit artikel is een functiespecificatie. De specificatie fungeert als het ontwerpdocument voor de functie. Het bevat voorgestelde specificatiewijzigingen, samen met informatie die nodig is tijdens het ontwerp en de ontwikkeling van de functie. Deze artikelen worden gepubliceerd totdat de voorgestelde specificaties zijn voltooid en opgenomen in de huidige ECMA-specificatie.

Er kunnen enkele verschillen zijn tussen de functiespecificatie en de voltooide implementatie. Deze verschillen worden vastgelegd in de relevante LDM-notities (Language Design Meeting).

Meer informatie over het proces voor het aannemen van functiespeclets in de C#-taalstandaard vindt u in het artikel over de specificaties.

Kampioensprobleem: https://github.com/dotnet/csharplang/issues/8714

Samenvatting

We introduceren eersteklas ondersteuning voor Span<T> en ReadOnlySpan<T> in de taal, waaronder nieuwe impliciete conversietypen en overwegen ze op meer plaatsen, waardoor meer natuurlijke programmering met deze integrale typen mogelijk is.

Motivatie

Sinds hun introductie in versie C# 7.2 hebben Span<T> en ReadOnlySpan<T> zich op veel belangrijke manieren in de taal en in de basisklassebibliotheek (BCL) ingeburgerd. Dit is ideaal voor ontwikkelaars, omdat hun introductie de prestaties verbetert zonder kosten voor de veiligheid van ontwikkelaars. De taal heeft deze typen echter op een aantal belangrijke manieren op afstand gehouden, waardoor het moeilijk is om de intentie van API's uit te drukken en tot een aanzienlijke hoeveelheid duplicatie van oppervlak leidt voor nieuwe API's. De BCL heeft bijvoorbeeld een aantal nieuwe tensor primitieve API's toegevoegd in .NET 9, maar deze API's worden allemaal aangeboden op ReadOnlySpan<T>. C# herkent de relatie tussen ReadOnlySpan<T>, Span<T>en T[]niet, dus ook al zijn er door de gebruiker gedefinieerde conversies tussen deze typen, kunnen ze niet worden gebruikt voor ontvangers van extensiemethoden, kunnen niet worden samengesteld met andere door de gebruiker gedefinieerde conversies en helpen niet bij alle algemene typedeductiescenario's. Gebruikers moeten expliciete conversies of typeargumenten gebruiken, wat betekent dat IDE-hulpprogramma's gebruikers niet begeleiden bij het gebruik van deze API's, omdat niets aangeeft aan de IDE dat het geldig is om deze typen door te geven na de conversie. Om maximale bruikbaarheid voor deze API-stijl te bieden, moet de BCL een volledige reeks Span<T> en T[] overloads definiëren, wat betekent dat er veel overbodige onderdelen moeten worden onderhouden zonder echt voordeel. Dit voorstel probeert het probleem op te lossen door de taal meer direct deze typen en conversies te erkennen.

De BCL kan bijvoorbeeld slechts één overload van een MemoryExtensions helper toevoegen, zoals:

int[] arr = [1, 2, 3];
Console.WriteLine(
    arr.StartsWith(1) // CS8773 in C# 13, permitted with this proposal
    );

public static class MemoryExtensions
{
    public static bool StartsWith<T>(this ReadOnlySpan<T> span, T value) where T : IEquatable<T> => span.Length != 0 && EqualityComparer<T>.Default.Equals(span[0], value);
}

Voorheen zouden span- en matrixoverbelastingen nodig zijn om de extensiemethode bruikbaar te maken voor variabelen met span-/matrixtypen, omdat door de gebruiker gedefinieerde conversies (die bestaan tussen Span/array/ReadOnlySpan) niet worden beschouwd voor extensieontvangers.

Gedetailleerd ontwerp

De wijzigingen in dit voorstel zijn gekoppeld aan LangVersion >= 14.

Omzettingen van tijdsperioden

We voegen een nieuw type impliciete conversie toe aan de lijst in §10.2.1, een impliciete spanconversie. Deze conversie is een conversie van een type en wordt als volgt gedefinieerd:


Met een impliciete spanconversie kunnen array_types, System.Span<T>, System.ReadOnlySpan<T>en string als volgt tussen elkaar worden geconverteerd:

  • Van een enkeldimensionale array_type met elementtype Ei tot System.Span<Ei>
  • Van een enkeldimensionale array_type met elementtype Ei tot System.ReadOnlySpan<Ui>, mits Ei covariantie-converteerbaar is (§18.2.3.3) tot Ui
  • Van System.Span<Ti> tot System.ReadOnlySpan<Ui>, mits Ti covariantie-converteerbaar is (§18.2.3.3) tot Ui
  • Van System.ReadOnlySpan<Ti> tot System.ReadOnlySpan<Ui>, mits Ti covariantie-converteerbaar is (§18.2.3.3) tot Ui
  • Van string tot System.ReadOnlySpan<char>

Alle Span-/ReadOnlySpan-typen worden als van toepassing beschouwd voor de conversie als ze ref structzijn en ze overeenkomen met hun volledig gekwalificeerde naam (LDM 2024-06-24).

We voegen ook impliciete spanconversie toe aan de lijst met standaard impliciete conversies (§10.4.2). Hierdoor kan de overbelastingsresolutie rekening houden met de argumentresolutie, zoals in het eerder genoemde API-voorstel.

De expliciete spanningsconversies zijn als volgt:

  • Alle impliciete spanconversies.
  • Van een array_type met elementtype Ti naar System.Span<Ui> of System.ReadOnlySpan<Ui>, op voorwaarde dat er een expliciete verwijzingsconversie bestaat van Ti naar Ui.

Er is geen standaard expliciete spanconversie in tegenstelling tot andere standaard expliciete conversies (§10.4.3) die altijd bestaan op basis van de tegenovergestelde impliciete standaardconversie.

Door de gebruiker gedefinieerde conversies

Door de gebruiker gedefinieerde conversies worden niet overwogen bij het converteren tussen typen waarvoor een impliciete of expliciete spanconversie bestaat.

De impliciete spanconversies zijn uitgesloten van de regel dat het niet mogelijk is om een door de gebruiker gedefinieerde operator te definiëren tussen typen waarvoor een niet-door de gebruiker gedefinieerde conversie bestaat (§10.5.2 Toegestane door de gebruiker gedefinieerde conversies). Dit is nodig zodat de BCL de bestaande Span-conversieoperators kan blijven definiëren, zelfs wanneer ze overschakelen naar C# 14 (ze zijn nog steeds nodig voor lagere LangVersions en ook omdat deze operators worden gebruikt in codegen van de nieuwe standaard spanconversies). Maar het kan worden gezien als een implementatiedetail (codegen en lagere LangVersions maken geen deel uit van de specificatie) en Roslyn schendt dit deel van de specificatie toch (deze specifieke regel over door de gebruiker gedefinieerde conversies wordt niet afgedwongen).

Extensieontvanger

We voegen ook impliciete bereikconversie toe aan de lijst met acceptabele impliciete conversies voor de eerste parameter van een extensiemethode bij het bepalen van de toepasbaarheid (12.8.9.3) (vetgedrukte wijziging):

Een extensiemethode Cᵢ.Mₑ komt in aanmerking als:

  • Cᵢ is een klasse die niet generiek en niet genest is
  • De naam van Mₑ is identificator
  • Mₑ is toegankelijk en van toepassing wanneer deze wordt toegepast op de argumenten als een statische methode, zoals hierboven wordt weergegeven
  • Een impliciete identiteit, verwijzing of boksen, boksen of conversie bestaat van expr tot het type van de eerste parameter van Mₑ. Spanconversie wordt niet in overweging genomen wanneer overbelastingsoplossing wordt uitgevoerd voor een conversie van een methodegroep.

Houd er rekening mee dat impliciete spanconversie niet wordt overwogen voor extensieontvanger in methodegroepconversies (LDM 2024-07-15) waardoor de volgende code blijft werken in plaats van dat dit resulteert in een compilatiefout CS1113: Extension method 'E.M<int>(Span<int>, int)' defined on value type 'Span<int>' cannot be used to create delegates:

using System;
using System.Collections.Generic;
Action<int> a = new int[0].M; // binds to M<int>(IEnumerable<int>, int)
static class E
{
    public static void M<T>(this Span<T> s, T x) => Console.Write(1);
    public static void M<T>(this IEnumerable<T> e, T x) => Console.Write(2);
}

Als toekomstige mogelijke taak zouden we kunnen overwegen om de voorwaarde te verwijderen waarbij de bereikconversie niet in aanmerking wordt genomen voor een extensieontvanger bij methodegroepconversies. In plaats daarvan zouden we wijzigingen kunnen implementeren, zodat een scenario zoals het bovenstaande succesvol de Span-overbelasting oproept.

  • De compiler kan een thunk genereren die de array als ontvanger neemt en de span-conversie binnenin uitvoert (vergelijkbaar met de gebruiker die handmatig de delegate creëert, zoals x => new int[0].M(x)).
  • Als ze zijn geïmplementeerd, kunnen waardedelegaten de Span rechtstreeks als ontvanger nemen.

Verschil

Het doel van de variantiesectie in impliciete bereikconversie is om een bepaalde hoeveelheid covariantie voor System.ReadOnlySpan<T>te repliceren. Runtimewijzigingen zijn vereist om de variantie volledig te implementeren via generics (zie .. /csharp-13.0/ref-struct-interfaces.md voor het gebruik van ref struct typen in generics), maar we kunnen een beperkte hoeveelheid covariantie toestaan door gebruik te maken van een voorgestelde .NET 9-API: https://github.com/dotnet/runtime/issues/96952. Hierdoor kan de taal System.ReadOnlySpan<T> behandelen alsof de T in sommige scenario's als out T is gedeclareerd. We pluizen deze variantconversie echter niet door alle variantiescenario's en voegen deze niet toe aan de definitie van variantie-converteerbaar in §18.2.3.3. Als we in de toekomst de runtime wijzigen om beter de variatie hier te begrijpen, kunnen we de kleine wijziging die lichte verstoringen veroorzaakt gebruiken om deze volledig in de taal te herkennen.

Patronen

Houd er rekening mee dat wanneer ref structs worden gebruikt als een type in een patroon, alleen identiteitsconversies zijn toegestaan:

class C<T> where T : allows ref struct
{
    void M1(T t) { if (t is T x) { } } // ok (T is T)
    void M2(R r) { if (r is R x) { } } // ok (R is R)
    void M3(T t) { if (t is R x) { } } // error (T is R)
    void M4(R r) { if (r is T x) { } } // error (R is T)
}
ref struct R { }

Uit de specificatie van volgt de is-type-operator (§12.12.12.1):

Het resultaat van de bewerking E is T [...] is een booleaanse waarde die aangeeft of E niet-null is en met succes kan worden geconverteerd naar het type T door een verwijzingsconversie, een boxing conversie, een unboxing conversie, een wrapping conversie of een unwrapping conversie.

[...]

Als T een niet-null-waardetype is, wordt het resultaat true als D en T hetzelfde type zijn.

Dit gedrag verandert niet met deze functie, daarom is het niet mogelijk om patronen te schrijven voor Span/ReadOnlySpan, hoewel vergelijkbare patronen mogelijk zijn voor matrices (inclusief variantie):

using System;

M1<object[]>(["0"]); // prints
M1<string[]>(["1"]); // prints

void M1<T>(T t)
{
    if (t is object[] r) Console.WriteLine(r[0]); // ok
}

void M2<T>(T t) where T : allows ref struct
{
    if (t is ReadOnlySpan<object> r) Console.WriteLine(r[0]); // error
}

Code genereren

De conversies bestaan altijd, ongeacht of er runtime-helpers aanwezig zijn (LDM 2024-05-13). Als de helpers niet aanwezig zijn, zal een poging tot conversie leiden tot een compilatiefout die meldt dat een vereist lid voor de compiler ontbreekt.

De compiler verwacht de volgende helpers of equivalenten te gebruiken om de conversies te implementeren:

Conversie Hulpverleners
array naar span static implicit operator Span<T>(T[]) (gedefinieerd in Span<T>)
matrix naar ReadOnlySpan static implicit operator ReadOnlySpan<T>(T[]) (gedefinieerd in ReadOnlySpan<T>)
Van Span naar ReadOnlySpan static implicit operator ReadOnlySpan<T>(Span<T>) (gedefinieerd in Span<T>) en static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>)
ReadOnlySpan naar ReadOnlySpan static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>)
string naar ReadOnlySpan static ReadOnlySpan<char> MemoryExtensions.AsSpan(string)

Houd er rekening mee dat MemoryExtensions.AsSpan wordt gebruikt in plaats van de equivalente impliciete operator die is gedefinieerd op string. Dit betekent dat het codegen verschilt tussen LangVersions (de impliciete operator wordt gebruikt in C# 13; de statische methode AsSpan wordt gebruikt in C# 14). Aan de andere kant kan de conversie worden verzonden op .NET Framework (de AsSpan methode bestaat daar, terwijl de operator string niet).

De expliciete matrix naar (ReadOnly)Span-conversie converteert eerst expliciet van de bronmatrix naar een matrix met het doelelementtype en vervolgens naar (ReadOnly)Span via dezelfde helper als een impliciete conversie zou worden gebruikt, bijvoorbeeld de bijbehorende op_Implicit(T[]).

Betere conversie van uitdrukking

Betere conversie van uitdrukking (§12.6.4.5) wordt bijgewerkt om voorkeur te geven aan impliciete spanconversies. Dit is gebaseerd op wijzigingen in overbelastingsresolutie van verzamelingsexpressies.

Gezien een impliciete conversie C₁ die wordt geconverteerd van een expressie E naar een type T₁en een impliciete conversie C₂ die wordt geconverteerd van een expressie E naar een type T₂, is C₁ een betere conversie dan C₂ als een van de volgende bewaringen geldt:

  • E is een verzamelingsexpressieen C₁ is een betere verzamelingsconversie van expressies dan C₂
  • E is geen verzamelingsexpressie en een van de volgende is waar:
    • E komt precies overeen met T₁ en E komt niet precies overeen met T₂
    • E komt exact overeen met geen van T₁ en T₂, en C₁ is een impliciete spanconversie en C₂ geen impliciete spanconversie
    • E komt exact overeen met zowel T₁ als T₂, komt zowel overeen met geen van beiden C₁ en C₂, die een implicieteconversie zijn, en T₁ is een beter conversiedoelwit dan T₂
  • E is een methodegroep, T₁ compatibel is met de beste methode uit de methodegroep voor conversie C₁en T₂ niet compatibel is met de beste methode uit de methodegroep voor conversie C₂

Beter conversiedoelstelling

Beter conversiedoel (§12.6.4.7) wordt bijgewerkt om de voorkeur te geven aan ReadOnlySpan<T> boven Span<T>.

Voor de twee typen T₁ en T₂is T₁ een beter conversiedoel dan T₂ als een van de volgende voorwaarden geldt:

  • T₁ is System.ReadOnlySpan<E₁>, T₂ is System.Span<E₂>en bestaat er een identiteitsconversie van E₁ naar E₂
  • T₁ is System.ReadOnlySpan<E₁>, T₂ is System.ReadOnlySpan<E₂>en er bestaat een impliciete conversie van T₁ naar T₂ en er bestaat geen impliciete conversie van T₂ naar T₁
  • dat ten minste één van T₁ of T₂ niet System.ReadOnlySpan<Eᵢ> is en niet System.Span<Eᵢ>is, en er een impliciete conversie van T₁ naar T₂ bestaat en geen impliciete conversie van T₂ naar T₁ bestaat
  • ...

Designvergaderingen:

Opmerkingen over betere prestaties

De betere conversie van expressies regel moet ervoor zorgen dat wanneer een overbelasting van toepassing wordt vanwege de nieuwe spanconversies, elke mogelijke dubbelzinnigheid met een andere overbelasting wordt vermeden omdat de nieuwe overbelasting de voorkeur heeft.

Zonder deze regel zou de volgende code die is gecompileerd in C# 13 leiden tot een dubbelzinnigheidsfout in C# 14 vanwege de nieuwe impliciete standaardconversie van matrix naar ReadOnlySpan die van toepassing is op een ontvanger van de extensiemethode:

using System;
using System.Collections.Generic;

var a = new int[] { 1, 2, 3 };
a.M();

static class E
{
    public static void M(this IEnumerable<int> x) { }
    public static void M(this ReadOnlySpan<int> x) { }
}

Met de regel kunt u ook nieuwe API's introduceren die eerder leiden tot dubbelzinnigheid, bijvoorbeeld:

using System;
using System.Collections.Generic;

C.M(new int[] { 1, 2, 3 }); // would be ambiguous before

static class C
{
    public static void M(IEnumerable<int> x) { }
    public static void M(ReadOnlySpan<int> x) { } // can be added now
}

Waarschuwing

Omdat de beterheidsregel is gedefinieerd voor de spanconversies die alleen in LangVersion >= 14bestaan, kunnen API-auteurs dergelijke nieuwe overbelastingen niet toevoegen als ze ondersteunende gebruikers op LangVersion <= 13willen houden. Als .NET 9 BCL bijvoorbeeld dergelijke overbelastingen introduceert, zullen gebruikers die upgraden naar net9.0 TFM maar op een lagere LangVersion blijven, dubbelzinnigheidsfouten tegenkomen in de bestaande code. Zie ook een open vraag hieronder.

Type-inferentie

We werken de sectie type-inferenties van de specificatie als volgt bij (wijzigingen in vet).

12.6.3.9 Exacte deducties

Een exacte inferentievan een type Unaar een type V wordt als volgt gemaakt:

  • Als V een van de niet-vastgesteldeXᵢ is, wordt U toegevoegd aan de set van exacte limieten voor Xᵢ.
  • Anders worden sets V₁...Vₑ en U₁...Uₑ bepaald door te controleren of een van de volgende gevallen van toepassing is:
    • V is een matrixtype V₁[...] en U is een matrixtype U₁[...] van dezelfde rang
    • V is een Span<V₁> en U is een matrixtype U₁[] of een Span<U₁>
    • V is een ReadOnlySpan<V₁> en U is een matrixtype U₁[] of een Span<U₁> of ReadOnlySpan<U₁>
    • V is het type V₁? en U het type U₁
    • V is een samengesteld type C<V₁...Vₑ> en U een samengesteld type C<U₁...Uₑ>
      Als een van deze gevallen van toepassing is, wordt er een exacte inferentie gemaakt van elke Uᵢ naar de bijbehorende Vᵢ.
  • Anders worden er geen deducties gemaakt.

12.6.3.10 Ondergrensdeducties

Een ondergrensdeductie van een type U naar een type V wordt als volgt gemaakt:

  • Als V een van de niet-vastgesteldeXᵢ is, wordt U toegevoegd aan de set van ondergrenzen voor Xᵢ.
  • Als V van het type V₁? is en U van het type U₁?, wordt er een ondergrensinferentie gemaakt van U₁ naar V₁.
  • Anders worden sets U₁...Uₑ en V₁...Vₑ bepaald door te controleren of een van de volgende gevallen van toepassing is:
    • V is een matrixtype V₁[...] en U is een matrixtype U₁[...]van dezelfde rang
    • V is een Span<V₁> en U is een matrixtype U₁[] of een Span<U₁>
    • V is een ReadOnlySpan<V₁> en U is een matrixtype U₁[] of een Span<U₁> of ReadOnlySpan<U₁>
    • V is een van IEnumerable<V₁>, ICollection<V₁>, IReadOnlyList<V₁>>, IReadOnlyCollection<V₁> of IList<V₁> en U een enkeldimensionaal matrixtype U₁[]
    • V is een samengesteld class, struct, interface of delegate type C<V₁...Vₑ> en er is een uniek type C<U₁...Uₑ> zodanig dat U (of, als U een type parameter is, de effectieve basisklasse of een lid van de effectieve interfaceset) identiek is aan, inherits van (direct of indirect) of implementeert (direct of indirect) C<U₁...Uₑ>.
    • (De beperking 'uniekheid' betekent dat in het geval van interface C<T>{} class U: C<X>, C<Y>{}er geen afleiding wordt gemaakt bij het afleiden van U naar C<T>, omdat U₁ zou kunnen zijn X of Y.)
      Als een van deze gevallen van toepassing is, wordt van elke Uᵢ een deductie gemaakt op de bijbehorende Vᵢ als volgt:
    • Als het niet bekend is dat Uᵢ een verwijzingstype is, wordt er een exacte inferentie gemaakt
    • Als U een arraytype is, dan wordt een ondergrensdeductie gemaaktwaarbij de deductie afhankelijk is van het type V:
      • Als V een Span<Vᵢ>is, wordt een exacte deductie gemaakt
      • Als V een matrixtype of een ReadOnlySpan<Vᵢ>is, wordt er een ondergrensinferentie gemaakt
    • Anders, als U een Span<Uᵢ> is, hangt de afleiding af van het type V:
      • Als V een Span<Vᵢ>is, wordt een exacte deductie gemaakt
      • Als V een ReadOnlySpan<Vᵢ>is, wordt er een ondergrensinferentie gemaakt
    • Anders, als U een ReadOnlySpan<Uᵢ> is en V een ReadOnlySpan<Vᵢ> is, wordt een ondergrensdeductie gemaakt:
    • Als VC<V₁...Vₑ> is, is de inferentie afhankelijk van de i-th typeparameter van C:
      • Als deze covariant is, wordt er een ondergrensinferentie gemaakt.
      • Als het contravariant is, wordt er een bovengrensdeductie gemaakt.
      • Als deze invariant is, wordt er een exacte deductie gemaakt.
  • Anders worden er geen deducties gemaakt.

Er zijn geen regels voor bovengrensafleiding omdat het niet mogelijk zou zijn om ze te bereiken. Type-inferentie begint nooit als bovengrens; het moet eerst door een ondergrens-inferentie en een contravariante typeparameter gaan. Vanwege de regel 'als Uᵢ niet bekend is als een verwijzingstype, kan een exacte deductie worden gemaakt', kan het brontypeargument niet worden Span/ReadOnlySpan (deze kunnen geen verwijzingstypen zijn). De inferentie van de bovengrens is echter alleen van toepassing als het brontype een Span/ReadOnlySpanis, aangezien het regels zou hebben zoals:

  • U is een Span<U₁> en V is een matrixtype V₁[] of een Span<V₁>
  • U is een ReadOnlySpan<U₁> en V is een matrixtype V₁[] of een Span<V₁> of ReadOnlySpan<V₁>

Brekende wijzigingen

Zoals elk voorstel dat de conversies van bestaande scenario's verandert, introduceert dit voorstel enkele nieuwe ingrijpende veranderingen. Hier volgen enkele voorbeelden:

Reverse aanroepen op een matrix

Het aanroepen van x.Reverse() waarin x een instantie is van type T[], zou eerder binden aan IEnumerable<T> Enumerable.Reverse<T>(this IEnumerable<T>), maar wordt nu gekoppeld aan void MemoryExtensions.Reverse<T>(this Span<T>). Helaas zijn deze API's niet compatibel (de laatste voert de omkering ter plekke uit en retourneert void).

.NET 10 beperkt dit door een matrixspecifieke overbelasting toe te voegen IEnumerable<T> Reverse<T>(this T[]), zie https://github.com/dotnet/runtime/issues/107723.

void M(int[] a)
{
    foreach (var x in a.Reverse()) { } // fine previously, an error now (`Reverse` returns `void`)
    foreach (var x in Enumerable.Reverse(a)) { } // workaround
}

Zie ook:

Ontwerpvergadering: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#reverse

Dubbelzinnigheden

De volgende voorbeelden, waarvoor de typedeductie eerder mislukte bij de overload van Span, slagen nu omdat de typedeductie van matrix naar Span is gelukt; daarom zijn ze nu dubbelzinnig. Om dit te omzeilen, kunnen gebruikers .AsSpan() of API-auteurs OverloadResolutionPriorityAttributegebruiken.

var x = new long[] { 1 };
Assert.Equal([2], x); // previously Assert.Equal<T>(T[], T[]), now ambiguous with Assert.Equal<T>(ReadOnlySpan<T>, Span<T>)
Assert.Equal([2], x.AsSpan()); // workaround
var x = new int[] { 1, 2 };
var s = new ArraySegment<int>(x, 1, 1);
Assert.Equal(x, s); // previously Assert.Equal<T>(T, T), now ambiguous with Assert.Equal<T>(Span<T>, Span<T>)
Assert.Equal(x.AsSpan(), s); // workaround

xUnit voegt meer overbelastingen toe om dit te verhelpen: https://github.com/xunit/xunit/discussions/3021.

Ontwerpvergadering: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#new-ambiguities

Covariantmatrices

Overbelastingen die IEnumerable<T> voor covariante arrays werkten, maar overbelastingen die Span<T> (die we nu liever gebruiken) niet, omdat de spanconversie een ArrayTypeMismatchException genereert voor covariante arrays. Je zou kunnen beweren dat de Span<T> overbelasting niet zou moeten bestaan en dat in plaats daarvan ReadOnlySpan<T> zou moeten worden gebruikt. Om dit te omzeilen, kunnen gebruikers .AsEnumerable()gebruiken of API-auteurs OverloadResolutionPriorityAttribute gebruiken of ReadOnlySpan<T> overbelasting toevoegen die de voorkeur heeft vanwege de betere regel.

string[] s = new[] { "a" };
object[] o = s;

C.R(o); // wrote 1 previously, now crashes in Span<T> constructor with ArrayTypeMismatchException
C.R(o.AsEnumerable()); // workaround

static class C
{
    public static void R<T>(IEnumerable<T> e) => Console.Write(1);
    public static void R<T>(Span<T> s) => Console.Write(2);
    // another workaround:
    public static void R<T>(ReadOnlySpan<T> s) => Console.Write(3);
}

Ontwerpvergadering: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#covariant-arrays

Voorkeur voor ReadOnlySpan boven Span

De beterheidsregel veroorzaakt de voorkeur van ReadOnlySpan-overbelastingen over Span-overbelastingen om ArrayTypeMismatchExceptions in covariante matrixscenario'ste voorkomen. Dit kan leiden tot compilatiefouten in sommige scenario's, bijvoorbeeld wanneer de overbelastingen verschillen door hun retourtype.

double[] x = new double[0];
Span<ulong> y = MemoryMarshal.Cast<double, ulong>(x); // previously worked, now a compilation error (returns ReadOnlySpan, not Span)
Span<ulong> z = MemoryMarshal.Cast<double, ulong>(x.AsSpan()); // workaround

static class MemoryMarshal
{
    public static ReadOnlySpan<TTo> Cast<TFrom, TTo>(ReadOnlySpan<TFrom> span) => default;
    public static Span<TTo> Cast<TFrom, TTo>(Span<TFrom> span) => default;
}

Zie https://github.com/dotnet/roslyn/issues/76443.

Expressiebomen

Overbelastingen die spans gebruiken zoals MemoryExtensions.Contains hebben de voorkeur boven klassieke overbelastingen zoals Enumerable.Contains, zelfs binnen expressiebomen, maar refstructs worden niet ondersteund door de interpreterengine.

Expression<Func<int[], int, bool>> exp = (array, num) => array.Contains(num);
exp.Compile(preferInterpretation: true); // fails at runtime in C# 14

Expression<Func<int[], int, bool>> exp2 = (array, num) => Enumerable.Contains(array, num); // workaround
exp2.Compile(preferInterpretation: true); // ok

Op dezelfde manier moeten vertaalmachines zoals LINQ-to-SQL hierop reageren als hun boomtraversers Enumerable.Contains verwachten, omdat ze in plaats daarvan MemoryExtensions.Contains tegenkomen.

Zie ook:

Designvergaderingen:

Door de gebruiker gedefinieerde conversies via overname

Door impliciete spanconversies toe te voegen aan de lijst met standaard impliciete conversies, kunnen we mogelijk gedrag wijzigen wanneer door de gebruiker gedefinieerde conversies betrokken zijn bij een typehiërarchie. In dit voorbeeld ziet u dat wijziging, in vergelijking met een scenario met een geheel getal dat zich al gedraagt als het nieuwe C# 14-gedrag dat doet.

Span<string> span = [];
var d = new Derived();
d.M(span); // Base today, Derived tomorrow
int i = 1;
d.M(i); // Derived today, demonstrates new behavior

class Base
{
    public void M(Span<string> s)
    {
        Console.WriteLine("Base");
    }

    public void M(int i)
    {
        Console.WriteLine("Base");
    }
}

class Derived : Base
{
    public static implicit operator Derived(ReadOnlySpan<string> r) => new Derived();
    public static implicit operator Derived(long l) => new Derived();

    public void M(Derived s)
    {
        Console.WriteLine("Derived");
    }
}

(Zie ook: https://github.com/dotnet/roslyn/issues/78314

Zoekactie voor extensiemethode

Door impliciete spanconversies toe te staan bij het opzoeken van extensiemethoden, kunnen we mogelijk wijzigen welke extensiemethode wordt opgelost door overbelastingsresolutie.

namespace N1
{
    using N2;

    public class C
    {
        public static void M()
        {
            Span<string> span = new string[0];
            span.Test(); // Prints N2 today, N1 tomorrow
        }
    }

    public static class N1Ext
    {
        public static void Test(this ReadOnlySpan<string> span)
        {
            Console.WriteLine("N1");
        }
    }
}

namespace N2
{
    public static class N2Ext
    {
        public static void Test(this Span<string> span)
        {
            Console.WriteLine("N2");
        }
    }
}

Open vragen

Onbeperkte betereheidsregel

Moeten we de beterheidsregel onvoorwaardelijk voor LangVersion maken? Hierdoor kunnen API-auteurs nieuwe Span-API's toevoegen waarbij IEnumerable-equivalenten bestaan zonder gebruikers te breken in oudere LangVersions of andere compilers of talen (bijvoorbeeld VB). Dit betekent echter dat gebruikers anders gedrag kunnen krijgen na het bijwerken van de toolset (zonder LangVersion of TargetFramework te wijzigen):

  • Compiler kan verschillende overbelastingen kiezen (technisch een belangrijke wijziging, maar hopelijk hebben deze overbelastingen gelijkwaardig gedrag).
  • Andere onderbrekingen kunnen zich voordoen, onbekend op dit moment.

Houd er rekening mee dat OverloadResolutionPriorityAttribute dit niet volledig kan oplossen omdat het ook wordt genegeerd op oudere LangVersions. Het moet echter mogelijk zijn om deze te gebruiken om dubbelzinnigheden van VB te voorkomen waar het kenmerk moet worden herkend.

Meer door de gebruiker gedefinieerde conversies negeren

We hebben een set typeparen gedefinieerd waarvoor taalgedefinieerde impliciete en expliciete spanconversies zijn. Wanneer een taalgedefinieerde spanconversie bestaat van T1 tot T2, wordt elke door de gebruiker gedefinieerde conversie van T1 naar T2genegeerd (ongeacht de door de gebruiker gedefinieerde conversie die impliciet of expliciet is).

Houd er rekening mee dat dit alle voorwaarden omvat, dus er is geen spanconversie van Span<object> naar ReadOnlySpan<string> (er is een overspanningsconversie van Span<T> naar ReadOnlySpan<U>, maar het moet bevatten dat T : U), vandaar dat een door de gebruiker gedefinieerde conversie wordt overwogen tussen deze typen als deze bestaat (dat zou een gespecialiseerde conversie moeten zijn zoals Span<T> naar ReadOnlySpan<string> omdat conversieoperators geen algemene parameters hebben).

Moeten we door de gebruiker gedefinieerde conversies ook negeren tussen andere combinaties van matrix-/Span-/ReadOnlySpan-/tekenreekstypen waar geen overeenkomende taalomzetting bestaat? Als er bijvoorbeeld een door de gebruiker gedefinieerde conversie is van ReadOnlySpan<T> naar Span<T>, moeten we deze negeren?

Specifieke mogelijkheden om rekening mee te houden:

  1. Wanneer er een spanconversie bestaat van T1 tot T2, negeert u een door de gebruiker gedefinieerde conversie van T1 naar T2of van T2 naar T1.

  2. Door de gebruiker gedefinieerde conversies worden niet overwogen bij het converteren tussen

    • een enkeldimensionale array_type en System.Span<T>/System.ReadOnlySpan<T>,
    • elke combinatie van System.Span<T>/System.ReadOnlySpan<T>,
    • string en System.ReadOnlySpan<char>.
  3. Zoals hierboven, maar het laatste opsommingsteken vervangt door:
    • string en System.Span<char>/System.ReadOnlySpan<char>.
  4. Zoals hierboven, maar het laatste opsommingsteken vervangt door:
    • string en System.Span<T>/System.ReadOnlySpan<T>.

Technisch gezien is het niet mogelijk om een aantal van deze door de gebruiker gedefinieerde conversies te definiëren: het is niet mogelijk om een door de gebruiker gedefinieerde operator te definiëren tussen typen waarvoor een niet-door de gebruiker gedefinieerde conversie bestaat (§10.5.2). Maar Roslyn schendt dit deel van de specificatie opzettelijk. En sommige conversies, zoals tussen Span en string, zijn toch toegestaan (er bestaat geen taalomzetting tussen deze typen).

Als alternatief voor het simpelweg negeren van de conversies, zouden we kunnen overwegen om ze helemaal niet te definiëren en wellicht de specificatieoverschrijding te vermijden, in ieder geval voor deze nieuwe spanconversies. Dat wil zeggen, Roslyn aanpassen om een compilatiefout te melden als deze conversies worden gedefinieerd (waarschijnlijk behalve die al door de BCL gedefinieerd zijn).

Alternatieven

Houd dingen zoals ze zijn.