Sdílet prostřednictvím


Prvotřídní typy rozsahů

Poznámka:

Tento článek je specifikace funkce. Specifikace slouží jako návrhový dokument pro funkci. Zahrnuje navrhované změny specifikace spolu s informacemi potřebnými při návrhu a vývoji funkce. Tyto články se publikují, dokud nebudou navrhované změny specifikace finalizovány a začleněny do aktuální specifikace ECMA.

Mezi specifikací funkce a dokončenou implementací může docházet k nějakým nesrovnalostem. Tyto rozdíly jsou zachyceny v příslušných poznámkách ze schůzky jazykového návrhu (LDM).

Další informace o procesu přijetí specifikací funkcí do jazyka C# najdete v článku o specifikacích .

Problém šampiona: https://github.com/dotnet/csharplang/issues/8714

Shrnutí

Představujeme prvotřídní podporu pro Span<T> a ReadOnlySpan<T> v jazyce, včetně nových typů implicitních konverzí a jejich zohlednění na více místech, což umožňuje přirozenější programování s těmito celočíselnými typy.

Motivation

Od svého zavedení v C# 7.2 se Span<T> a ReadOnlySpan<T> dostaly do jazyka a základní knihovny tříd (BCL) v mnoha klíčových ohledech. Toto je skvělé pro vývojáře, protože jejich zavedení zlepšuje výkon, aniž by to ohrožovalo bezpečnost vývojářů. Jazyk však tyto typy držel na distanc některými klíčovými způsoby, což ztěžuje vyjádření záměru rozhraní API a vede k výraznému množství duplicitní povrchové plochy pro nové rozhraní API. Například BCL přidala v .NET 9 řadu nových primitivních rozhraní API pro tensory, ale tato rozhraní API jsou všechna nabízena na ReadOnlySpan<T>. C# nerozpoznává vztah mezi ReadOnlySpan<T>, Span<T>, a T[], takže i když existují uživatelsky definované konverze mezi těmito typy, nemohou být použity jako přijímače rozšíření metod, nemohou se skládat s jinými uživatelsky definovanými konverzemi a nepomáhají u všech scénářů odvozování typů generik. Uživatelé budou muset použít explicitní konverze nebo argumenty typů, což znamená, že nástroje IDE nebudou uživatele vést k používání těchto API, protože nic nenaznačí IDE, že je platné předávat tyto typy po konverzi. Aby byl maximálně zajištěn komfort používání tohoto typu API, bude muset BCL definovat celou sadu přetížení Span<T> a T[], což je velké množství duplicitní zátěže k udržení bez reálného přínosu. Tento návrh si klade za cíl řešit problém tím, že jazyk přímoji rozpozná tyto typy a konverze.

Například BCL může přidat pouze jednu přetížení jakéhokoli MemoryExtensions pomocného nástroje jako:

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

Dříve bylo potřeba použití přetížení pro Span a pole, aby bylo možné rozšířit metodu použitelnou pro proměnné typu Span/pole, protože uživatelsky definované konverze (které existují mezi Span/pole/ReadOnlySpan) nejsou zohledňovány pro příjemce rozšíření.

Podrobný návrh

Změny v tomto návrhu budou vázány na LangVersion >= 14.

Konverze rozsahu

Přidáváme nový typ implicitní konverze do seznamu v §10.2.1, což je implicitní konverze rozsahu. Tato konverze je konverzí z typu a je definována následovně:


Implicitní převod rozsahu umožňuje převádět array_types, System.Span<T>, System.ReadOnlySpan<T> a string mezi sebou následujícím způsobem:

  • Z jakéhokoli jednorozměrného array_type s prvkovým typem Ei na System.Span<Ei>
  • Z jakéhokoli jednorozměrného array_type s typem prvku Ei do System.ReadOnlySpan<Ui>, za předpokladu, že Ei je kovariančně konvertovatelný (§18.2.3.3) do Ui
  • Od System.Span<Ti> do System.ReadOnlySpan<Ui>, za předpokladu, že Ti je kovarianti-konvertovatelné (§18.2.3.3) na Ui
  • Od System.ReadOnlySpan<Ti> do System.ReadOnlySpan<Ui>, za předpokladu, že Ti je kovarianti-konvertovatelné (§18.2.3.3) na Ui
  • Od string do System.ReadOnlySpan<char>

Jakýkoli typ Span/ReadOnlySpan se považuje za vhodný pro konverzi, pokud jsou ref structs a odpovídají podle svého plně kvalifikovaného jména (LDM 2024-06-24).

Také přidáváme implicitní konverzi rozpětí do seznamu standardních implicitních konverzí (§10.4.2). Toto umožňuje rozlišení přetížení zohlednit je při provádění rozlišení argumentů, stejně jako v dříve uvedeném návrhu API.

Výslovné konverze rozsahu jsou následující:

  • Všechny implicitní konverze rozsahu.
  • Z array_type s typem prvku Ti na System.Span<Ui> nebo System.ReadOnlySpan<Ui> za předpokladu, že existuje explicitní převod referencia z Ti na Ui.

Neexistuje žádná standardní explicitní konverze intervalů na rozdíl od jiných standardních explicitních konverzí (§10.4.3), které vždy existují, pokud existuje opačná standardní implicitní konverze.

Uživatelsky definované konverze

U uživatelsky definovaných konverzí se nebere v úvahu při převodu mezi typy, pro které existuje implicitní nebo explicitní rozsahová konverze.

Implicitní převody rozsahů jsou výjimkou z pravidla, že není možné definovat uživatelsky definovaný operátor mezi typy, pro které existuje konverze bez definice uživatelem (§10.5.2 Povolené uživatelsky definované konverze). Toto je potřeba, aby BCL mohlo nadále definovat stávající operátory konverze Span, i když přejdou na C# 14 (jsou stále potřebné pro nižší verze jazyků a také proto, že tyto operátory jsou použity v generování kódu nových standardních konverzí span). Lze to však považovat za implementační detail (codegen a nižší verze jazyka nejsou součástí specifikace) a Roslyn tuto část specifikace stejně porušuje (tento konkrétní pravidlo o uživatelských konverzích není vynucováno).

Přijímač rozšíření

Také přidáváme implicitní konverzi rozpětí do seznamu přijatelných implicitních konverzí u prvního parametru rozšiřující metody při určování použitelnosti (12.8.9.3) (změna tučně):

Metoda rozšíření Cᵢ.Mₑ je způsobilá, pokud:

  • Cᵢ je negenerická třída, která není vnořená.
  • Název Mₑ je identifikátor
  • Mₑ je přístupná a použitelná při použití na argumenty jako statická metoda, jak je znázorněno výše.
  • Existuje implicitní identita, referenční konverze nebo konverze typu krabicování, krabicování nebo rozprostření z expr na typ prvního parametru Mₑ. Převod rozsahu není zohledněn, když se provádí rozlišení přetížení pro převod skupiny metod.

Všimněte si, že implicitní převod rozsahu není zohledněn pro příjemce rozšíření v převodech skupiny metod (LDM 2024-07-15), což znamená, že následující kód bude dál fungovat místo toho, aby vedl k chybě při překladu 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);
}

Jako možnou budoucí práci bychom mohli zvážit odstranění této podmínky, že při převodu metodických skupin se nepředpokládá převod rozpětí pro příjemce rozšíření, a místo toho zavést změny, aby scénář jako ten výše úspěšně zavolal přetížení Span.

  • Kompilátor by mohl vygenerovat thunk, který by přijal pole jako příjemce a provedl konverzi rozsahu uvnitř (podobně jako uživatel ručně vytváří delegáta, jako x => new int[0].M(x)).
  • Hodnotoví delegáti, pokud by byli implementováni, by mohli být schopni přijmout Span přímo jako příjemce.

Odchylka

Cílem sekce variance v implicitní konverzi rozsahu je replikovat určité množství kovariance pro System.ReadOnlySpan<T>. Změny za běhu by se vyžadovaly k úplné implementaci odchylek prostřednictvím obecných typů (viz .. /csharp-13.0/ref-struct-interfaces.md pro použití ref struct typů v obecných typech), ale můžeme povolit omezené množství kovariance prostřednictvím navrhovaného rozhraní API .NET 9: https://github.com/dotnet/runtime/issues/96952. To umožní jazyku zacházet s System.ReadOnlySpan<T>, jako by T bylo deklarováno jako out T v některých scénářích. Neprovádíme však tento převod varianty skrze všechny scénáře odchylek a nedoplňujeme jej do definice změnitelnosti odchylek v §18.2.3.3. Pokud v budoucnu změníme runtime, abychom hlouběji pochopili variabilitu zde, můžeme přijmout menší narušení kompatibility, abychom ji plně rozpoznali v jazyce.

Vzory

Všimněte si, že když se ref structs používají jako typ v jakémkoli vzoru, jsou povoleny pouze identitní konverze.

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

Z specifikace operátoru "is-type" (§12.12.12.1):

Výsledek operace E is T [...] je booleovská hodnota naznačující, zda je E nenulový a zda jej lze úspěšně převést na typ T pomocí konverze odkazu, konverze při balení, konverze při rozbalování, konverze obalení nebo konverze rozbalení.

[...]

Pokud je T hodnotový typ, který nepřipouští hodnotu null, výsledek je true, pokud jsou D a T stejného typu.

Toto chování se s touto funkcí nemění, takže nebude možné psát vzory pro Span/ReadOnlySpan, i když podobné vzory jsou možné pro pole (včetně variance).

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
}

Generování kódu

Konverze budou vždy existovat, bez ohledu na to, zda jsou přítomny nějaké pomocné programy runtime, které jsou použity k jejich implementaci (LDM 2024-05-13). Pokud pomocníci nejsou přítomni, pokus o použití konverze povede k chybě při překladu, která znamená, že chybí člen vyžadovaný překladačem.

Kompilátor očekává, že použije následující pomocné funkce nebo jejich ekvivalenty pro implementaci konverzí.

Přeměna Pomocníci
pole na rozpětí static implicit operator Span<T>(T[]) (definováno v Span<T>)
array to ReadOnlySpan static implicit operator ReadOnlySpan<T>(T[]) (definováno v ReadOnlySpan<T>)
Span na ReadOnlySpan static implicit operator ReadOnlySpan<T>(Span<T>) (definováno v Span<T>) a static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>)
ReadOnlySpan na ReadOnlySpan static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>)
string to ReadOnlySpan static ReadOnlySpan<char> MemoryExtensions.AsSpan(string)

Všimněte si, že místo ekvivalentního implicitního operátoru definovaného na MemoryExtensions.AsSpan je použito string. To znamená, že generování kódu je odlišné mezi verzemi jazyka (implicitní operátor je použit v C# 13; statická metoda AsSpan je použita v C# 14). Na druhou stranu, převod lze provést na .NET Framework (metoda AsSpan tam existuje, zatímco operátor string nikoliv).

Explicitní převod pole na (ReadOnly)Span nejprve explicitně převádí ze zdrojového pole na pole s typem cílového prvku a poté na (ReadOnly)Span prostřednictvím stejného pomocníka, jaký by použil implicitní převod, tj. odpovídající op_Implicit(T[]).

Lepší převod z výrazu

Lepší převod z výrazu (§12.6.4.5) je aktualizován tak, aby upřednostňoval implicitní převody rozsahů. Toto je založeno na změnách při přetížení rozlišení výrazů kolekce.

Je daná implicitní konverze C₁, která převádí z výrazu E na typ T₁, a implicitní konverze C₂, která převádí z výrazu E na typ T₂, C₁ je lepší konverze než C₂, pokud platí jedno z následujícího:

  • E je výraz kolekce a C₁ je lepší konverze kolekce z výrazu než C₂
  • E není výraz kolekce a platí jedna z následujících možností:
    • E přesně odpovídá T₁ a E přesně neodpovídá T₂
    • E přesně neodpovídá ani T₁ a T₂, a C₁ je implicitní konverze rozsahu a C₂ není implicitní konverze rozsahu
    • E přesně odpovídá buď oběma, nebo žádnému z T₁ a T₂, buď obě, nebo žádné z C₁ a C₂ jsou implicitní konverze rozsahu, a T₁ je lepší cílová konverze než T₂.
  • E je skupina metod, T₁ je kompatibilní s nejlepší metodou ze skupiny metod pro převod C₁, a T₂ není kompatibilní s nejlepší metodou ze skupiny metod pro převod C₂.

Lepší konverzní cíl

Lepší cíl konverze (§12.6.4.7) je aktualizován, aby upřednostňoval ReadOnlySpan<T> před Span<T>.

Vzhledem ke dvěma typům T₁ a T₂je T₁lepším cílem převodu než T₂, pokud platí některá z následujících možností:

  • T₁ je System.ReadOnlySpan<E₁>, T₂ je System.Span<E₂>, a existuje identická konverze z E₁ do E₂
  • T₁ je System.ReadOnlySpan<E₁>, T₂ je System.ReadOnlySpan<E₂>, implicitní konverze z T₁ na T₂ existuje a žádná implicitní konverze z T₂ na T₁ neexistuje
  • Alespoň jeden z T₁ nebo T₂ není System.ReadOnlySpan<Eᵢ> a není System.Span<Eᵢ>, a existuje implicitní konverze z T₁ na T₂ a neexistuje žádná implicitní konverze z T₂ na T₁
  • ...

Návrhářské schůzky:

Poznámky o zlepšení

Pravidlo lepší konverze z výrazu by mělo zajistit, že kdykoliv se přetížení stane použitelným díky novým konverzím rozsahu, je jakákoliv potenciální nejednoznačnost s jiným přetížením vyloučena, protože je preferováno nově použitelná přetížení.

Bez tohoto pravidla by následující kód, který se úspěšně zkompiloval v C# 13, vedl k chybě nejednoznačnosti v C# 14 kvůli novému standardu implicitní konverze z pole na ReadOnlySpan, která je použitelná pro příjemce rozšiřující metody.

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

Pravidlo také umožňuje zavádět nové API, které by dříve vedly k nejasnostem, například:

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
}

Výstraha

Protože pravidlo lepšího je definováno pro převody rozsahu, které existují pouze v LangVersion >= 14, autoři API nemohou přidat taková nová přetížení, pokud chtějí nadále podporovat uživatele na LangVersion <= 13. Například pokud .NET 9 BCL zavede taková přetížení, uživatelé, kteří upgradují na net9.0 TFM, ale zůstanou na nižší LangVersion, obdrží chybové hlášky o nejednoznačnosti v existujícím kódu. Viz také otevřenou otázku níže.

Odvození typu

Aktualizujeme sekci odvozování typů ve specifikaci takto (změny jsou tučně).

12.6.3.9 Přesné odvozy

Následujícím způsobem je provedeno přesné odvozeníz typu Una typ V:

  • Pokud je V jedním z neopravenýchXᵢ, U se přidá do sady přesných hranic pro Xᵢ.
  • V opačném případě se sady V₁...Vₑ a U₁...Uₑ určují ověřením, zda se vztahuje některý z následujících případů:
    • V je typ pole V₁[...] a U je typ pole U₁[...] stejného pořadí.
    • V je Span<V₁> a U je pole typu U₁[] nebo Span<U₁>
    • V je ReadOnlySpan<V₁> a U je typu pole U₁[] nebo Span<U₁> nebo ReadOnlySpan<U₁>
    • V je typ V₁? a U je typ U₁
    • V je konstruovaný typ C<V₁...Vₑ> a U je vytvořený typ C<U₁...Uₑ>
      Pokud se některý z těchto případů použije, provede se přesné odvození z každého Uᵢ pro odpovídající Vᵢ.
  • Jinak se nedělají žádné závěry.

12.6.3.10 Odvození dolní meze

Inferenční dolní hranice z typu Una typ V se provádí takto:

  • Pokud je V jedním z neopravenýchXᵢ, U se přidá do sady dolních mezí pro Xᵢ.
  • V opačném případě pokud je V typ V₁? a U je typ U₁? pak se odvozuje dolní mez z U₁ do V₁.
  • V opačném případě se sady U₁...Uₑ a V₁...Vₑ určují ověřením, zda se vztahuje některý z následujících případů:
    • V je typu pole V₁[...] a U je typu pole U₁[...] stejného pořadí
    • V je Span<V₁> a U je pole typu U₁[] nebo Span<U₁>
    • V je ReadOnlySpan<V₁> a U je typu pole U₁[] nebo Span<U₁> nebo ReadOnlySpan<U₁>
    • V je jedním z IEnumerable<V₁>, ICollection<V₁>, IReadOnlyList<V₁>>, IReadOnlyCollection<V₁> nebo IList<V₁> a U je jednorozměrný typ pole U₁[]
    • V je vytvořený class, struct, interface nebo delegate typu C<V₁...Vₑ> a existuje jedinečný typ C<U₁...Uₑ>, takový, že U (nebo, pokud je U typu parameter, jeho efektivní základní třída nebo jakýkoli člen jeho efektivní sady rozhraní) je identický s, odvozený z (přímo nebo nepřímo), nebo implementuje (přímo nebo nepřímo) inherits.
    • (Omezení "jedinečnosti" znamená, že v rozhraní C<T>{} class U: C<X>, C<Y>{}, při odvozování z U do C<T>, neprobíhá žádné odvozování, protože U₁ může být X nebo Y.)
      Pokud se některý z těchto případů použije, provede se odvozování z každého Uᵢ na odpovídající Vᵢ následujícím způsobem:
    • Pokud Uᵢ není známo, že se jedná o referenční typ, provede se přesný závěr.
    • Jinak, pokud je U typu pole, pak se provede inferování dolní hraniceinferování závisí na typu V:
      • Pokud V je Span<Vᵢ>, pak bude provedeno přesné odvození.
      • Je-li V typ pole nebo ReadOnlySpan<Vᵢ>, pak se provádí inference dolní meze
    • Jinak, pokud je USpan<Uᵢ>, pak inferenční závisí na typu V:
      • Pokud V je Span<Vᵢ>, pak bude provedeno přesné odvození.
      • Pokud je VReadOnlySpan<Vᵢ>, pak se provádí odvození spodní meze
    • Jinak, pokud U je ReadOnlySpan<Uᵢ> a V je ReadOnlySpan<Vᵢ>, je učiněn závěr o dolní hranici:
    • Jinak pokud je VC<V₁...Vₑ>, odvozování závisí na parametru typu i-thC:
      • Pokud je kovariantní, provede se odvození spodní meze .
      • Pokud je kontravariantní, je provedeno odvození horní hranice .
      • Pokud je invariantní, provede se přesný závěr.
  • Jinak se nedělají žádné závěry.

Neexistují žádná pravidla pro odhad horní hranice, protože by je nebylo možné splnit. Typová inferencce nikdy nezačíná jako horní hranice, musela by projít dolní inferencí a kontravariantním typovým parametrem. Kvůli pravidlu "pokud není Uᵢ znám jako referenční typ, je provedena přesná inference," nemohla být argumentem zdrojového typu Span/ReadOnlySpan (tyto nemohou být referenčními typy). Inferování horní hranice by se však uplatnilo pouze tehdy, kdyby zdrojový typ byl Span/ReadOnlySpan, protože by měl pravidla jako:

  • U je Span<U₁> a V je pole typu V₁[] nebo Span<V₁>
  • U je ReadOnlySpan<U₁> a V je typu pole V₁[] nebo Span<V₁> nebo ReadOnlySpan<V₁>

Zásadní změny

Jako jakýkoli návrh, který mění převody existujících scénářů, tento návrh skutečně zavádí některé nové kritické změny. Zde je několik příkladů:

Volání Reverse na poli

Volání x.Reverse(), kde x je instance typu T[], by se dříve vázalo na IEnumerable<T> Enumerable.Reverse<T>(this IEnumerable<T>), zatímco nyní se váže na void MemoryExtensions.Reverse<T>(this Span<T>). Bohužel, tyto API nejsou kompatibilní (druhá z nich provádí obrácení na místě a vrací void).

.NET 10 toto zmírňuje přidáním přetížení specifického pro pole IEnumerable<T> Reverse<T>(this T[]), viz 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
}

Viz také:

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

Nejasnosti

Následující příklady dříve selhaly při odvození typu pro přetížení Span, ale nyní se odvození typů z pole na Span daří, a proto jsou nejednoznačné. Aby se tomu zabránilo, uživatelé mohou použít .AsSpan() nebo autoři API mohou použít OverloadResolutionPriorityAttribute.

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 přidává více přetížení, aby to zmírnil: https://github.com/xunit/xunit/discussions/3021.

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

Kovariantní pole

Přetížení, která používají IEnumerable<T>, fungovala na kovariantních polích, ale přetížení, která používají Span<T> (které nyní upřednostňujeme), nefungují, protože převod rozsahu vyvolá ArrayTypeMismatchException pro kovariantní pole. Dá se tvrdit, že přetížení Span<T> by nemělo existovat, místo toho by mělo přijímat ReadOnlySpan<T>. Uživatelé mohou použít .AsEnumerable(), nebo autoři API mohou použít OverloadResolutionPriorityAttribute nebo přidat přetížení ReadOnlySpan<T>, které je upřednostňováno kvůli pravidlu lepší efektivity.

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

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

Upřednostňování ReadOnlySpan před Span

Pravidlo "betterness" způsobuje upřednostnění přetížení ReadOnlySpan před přetížením Span, aby se předešlo v případech s kovariantními poli. To může vést k chybám při kompilaci v některých situacích, například když se přetížení liší podle návratového typu:

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

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

výrazové stromy

Přetížení přijímající rozsahy jako MemoryExtensions.Contains jsou upřednostňována před klasickými přetíženími jako Enumerable.Contains, a to i uvnitř výrazových stromů - ale ref struktury nejsou podporovány interpretačním enginem.

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

Podobně musí překladatelské mechanismy, jako je LINQ-to-SQL, na to reagovat, pokud jejich návštěvníci stromu očekávají Enumerable.Contains, protože místo toho narazí na MemoryExtensions.Contains.

Viz také:

Návrhářské schůzky:

Uživatelsky definované převody prostřednictvím dědičnosti.

Přidáním implicitních převodů rozsahů do seznamu standardních implicitních převodů můžeme potenciálně změnit chování, když jsou do hierarchie typů zapojeny uživatelsky definované převody. Tento příklad ukazuje změnu ve srovnání se scénářem s celými čísly, který se již chová tak, jak se očekává u nového chování v C# 14.

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

Viz také: https://github.com/dotnet/roslyn/issues/78314

Vyhledávání rozšířujících metod

Povolením implicitních konverzí rozsahu ve vyhledávání metod rozšíření můžeme potenciálně změnit, která metoda rozšíření je vyřešena rozlišením přetížení.

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

Otevřené otázky

Pravidlo neomezené nadřazenosti

Měli bychom učinit pravidlo lepšího nepodmíněné na LangVersion? To by umožnilo autorům API přidat nové API pro Span tam, kde existují ekvivalenty IEnumerable, aniž by tím narušili uživatele na starších verzích jazyka nebo v jiných překladačích či jazycích (např. VB). To by však znamenalo, že uživatelé by mohli zaznamenat odlišné chování po aktualizaci sady nástrojů (bez změny LangVersion nebo TargetFramework).

  • Kompilátor by mohl vybrat různé přetížení (technicky je to zásadní změna, ale doufejme, že tato přetížení by měla ekvivalentní chování).
  • Mohou nastat i další přestávky, které v tuto chvíli nejsou známy.

Vezměte na vědomí, že OverloadResolutionPriorityAttribute nemůže tento problém plně vyřešit, protože je také ignorován ve starších verzích jazyka. Mělo by však být možné jej použít k zabránění nejasnostem s VB, kde by měl být atribut rozpoznán.

Ignorování dalších uživatelsky definovaných konverzí

Definovali jsme sadu dvojic typů, pro které existují jazykově definované implicitní a explicitní převody rozsahu. Kdykoliv existuje převod úseku definovaný jazykem z T1 na T2, jakýkoliv uživatelsky definovaný převod z T1 na T2 je ignorován (bez ohledu na to, zda je úsek a uživatelsky definovaný převod implicitní nebo explicitní).

Upozorňujeme, že to zahrnuje všechny podmínky, takže například neexistuje žádná konverze rozsahu z Span<object> na ReadOnlySpan<string> (existuje konverze rozsahu z Span<T> na ReadOnlySpan<U>, ale musí platit, že T : U), proto by se mezi těmito typy uvažovalo o uživatelsky definované konverzi, pokud by existovala (to by musela být specializovaná konverze, jako je Span<T> na ReadOnlySpan<string>, protože operátory konverze nemohou mít generické parametry).

Měli bychom také ignorovat uživatelsky definované převody mezi dalšími kombinacemi typů pole/Span/ReadOnlySpan/string, kde neexistuje odpovídající převod span definovaný jazykem? Například, pokud existuje uživatelsky definovaná konverze z ReadOnlySpan<T> na Span<T>, měli bychom ji ignorovat?

Specifikujte možnosti k zvážení:

  1. Pokud existuje převod rozsahu z T1 na T2, ignorujte jakýkoli uživatelsky definovaný převod z T1 na T2nebo z T2 na T1.

  2. Převody definované uživatelem nejsou brány v úvahu při převodu mezi

    • jakýkoli jednorozměrný array_type a System.Span<T>/System.ReadOnlySpan<T>,
    • jakákoli kombinace System.Span<T>/System.ReadOnlySpan<T>,
    • string a System.ReadOnlySpan<char>.
  3. Stejně jako výše, ale nahrazením posledního bodu odrážky:
    • string a System.Span<char>/System.ReadOnlySpan<char>.
  4. Stejně jako výše, ale nahrazením posledního bodu odrážky:
    • string a System.Span<T>/System.ReadOnlySpan<T>.

Technicky vzato, specifikace zakazuje, aby některé z těchto uživatelsky definovaných konverzí byly dokonce definovány: není možné definovat uživatelsky definovaný operátor mezi typy, pro které existuje ne-uživatelsky definovaná konverze (§10.5.2). Ale Roslyn úmyslně porušuje tuto část specifikace. A některé konverze, jako mezi Span a string, jsou stejně povoleny (neexistuje žádná překlad definovaná jazykem mezi těmito typy).

Nicméně, než abychom ignorovali tyto konverze, mohli bychom je zcela zakázat a možná se vyhnout porušení specifikace alespoň u těchto nových konverzí rozsahu, tj. změnit Roslyn, aby skutečně nahlásil chybu při kompilaci, pokud jsou tyto konverze definovány (pravděpodobně s výjimkou těch, které již byly definovány BCL).

Alternatives

Nechte věci tak, jak jsou.