Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
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_types prvkovým typemEinaSystem.Span<Ei> - Z jakéhokoli jednorozměrného
array_types typem prvkuEidoSystem.ReadOnlySpan<Ui>, za předpokladu, žeEije kovariančně konvertovatelný (§18.2.3.3) doUi - Od
System.Span<Ti>doSystem.ReadOnlySpan<Ui>, za předpokladu, žeTije kovarianti-konvertovatelné (§18.2.3.3) naUi - Od
System.ReadOnlySpan<Ti>doSystem.ReadOnlySpan<Ui>, za předpokladu, žeTije kovarianti-konvertovatelné (§18.2.3.3) naUi - Od
stringdoSystem.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
TinaSystem.Span<Ui>neboSystem.ReadOnlySpan<Ui>za předpokladu, že existuje explicitní převod referencia zTinaUi.
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átorMₑ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 parametruMₑ. 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
Spanpří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 jeEnenulový a zda jej lze úspěšně převést na typTpomocí konverze odkazu, konverze při balení, konverze při rozbalování, konverze obalení nebo konverze rozbalení.[...]
Pokud je
Thodnotový typ, který nepřipouští hodnotu null, výsledek jetrue, pokud jsouDaTstejné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ýrazuEna typT₁, a implicitní konverzeC₂, která převádí z výrazuEna typT₂,C₁je lepší konverze nežC₂, pokud platí jedno z následujícího:
Eje výraz kolekce aC₁je lepší konverze kolekce z výrazu nežC₂Enení výraz kolekce a platí jedna z následujících možností:
Epřesně odpovídáT₁aEpřesně neodpovídáT₂Epřesně neodpovídá aniT₁aT₂, aC₁je implicitní konverze rozsahu aC₂není implicitní konverze rozsahuEpřesně odpovídá buď oběma, nebo žádnému zT₁aT₂, buď obě, nebo žádné zC₁aC₂jsou implicitní konverze rozsahu, aT₁je lepší cílová konverze nežT₂.Eje skupina metod,T₁je kompatibilní s nejlepší metodou ze skupiny metod pro převodC₁, aT₂není kompatibilní s nejlepší metodou ze skupiny metod pro převodC₂.
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₁aT₂jeT₁lepším cílem převodu nežT₂, pokud platí některá z následujících možností:
T₁jeSystem.ReadOnlySpan<E₁>,T₂jeSystem.Span<E₂>, a existuje identická konverze zE₁doE₂T₁jeSystem.ReadOnlySpan<E₁>,T₂jeSystem.ReadOnlySpan<E₂>, implicitní konverze zT₁naT₂existuje a žádná implicitní konverze zT₂naT₁neexistuje- Alespoň jeden z
T₁neboT₂neníSystem.ReadOnlySpan<Eᵢ>a neníSystem.Span<Eᵢ>, a existuje implicitní konverze zT₁naT₂a neexistuje žádná implicitní konverze zT₂naT₁- ...
Návrhářské schůzky:
- https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-12-04.md#preferring-readonlyspant-over-spant-conversions
- https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-12-09.md#first-class-span-open-questions
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 typV:
- Pokud je
Vjedním z neopravenýchXᵢ,Use přidá do sady přesných hranic proXᵢ.- V opačném případě se sady
V₁...VₑaU₁...Uₑurčují ověřením, zda se vztahuje některý z následujících případů:
Vje typ poleV₁[...]aUje typ poleU₁[...]stejného pořadí.VjeSpan<V₁>aUje pole typuU₁[]neboSpan<U₁>VjeReadOnlySpan<V₁>aUje typu poleU₁[]neboSpan<U₁>neboReadOnlySpan<U₁>Vje typV₁?aUje typU₁Vje konstruovaný typC<V₁...Vₑ>aUje vytvořený typC<U₁...Uₑ>
Pokud se některý z těchto případů použije, provede se přesné odvození z každéhoUᵢ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 typVse provádí takto:
- Pokud je
Vjedním z neopravenýchXᵢ,Use přidá do sady dolních mezí proXᵢ.- V opačném případě pokud je
VtypV₁?aUje typU₁?pak se odvozuje dolní mez zU₁doV₁.- V opačném případě se sady
U₁...UₑaV₁...Vₑurčují ověřením, zda se vztahuje některý z následujících případů:
Vje typu poleV₁[...]aUje typu poleU₁[...]stejného pořadíVjeSpan<V₁>aUje pole typuU₁[]neboSpan<U₁>VjeReadOnlySpan<V₁>aUje typu poleU₁[]neboSpan<U₁>neboReadOnlySpan<U₁>Vje jedním zIEnumerable<V₁>,ICollection<V₁>,IReadOnlyList<V₁>>,IReadOnlyCollection<V₁>neboIList<V₁>aUje jednorozměrný typ poleU₁[]Vje vytvořenýclass,struct,interfacenebodelegatetypuC<V₁...Vₑ>a existuje jedinečný typC<U₁...Uₑ>, takový, žeU(nebo, pokud jeUtypuparameter, 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í zUdoC<T>, neprobíhá žádné odvozování, protožeU₁může býtXneboY.)
Pokud se některý z těchto případů použije, provede se odvozování z každéhoUᵢ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
Utypu pole, pak se provedeinferování dolní hraniceinferování závisí na typuV:
- Pokud
VjeSpan<Vᵢ>, pak bude provedeno přesné odvození.- Je-li
Vtyp pole neboReadOnlySpan<Vᵢ>, pak se provádí inference dolní meze- Jinak, pokud je
USpan<Uᵢ>, pak inferenční závisí na typuV:
- Pokud
VjeSpan<Vᵢ>, pak bude provedeno přesné odvození.- Pokud je
VReadOnlySpan<Vᵢ>, pak se provádí odvození spodní meze- Jinak, pokud
UjeReadOnlySpan<Uᵢ>aVjeReadOnlySpan<Vᵢ>, je učiněn závěr o dolní hranici:- Jinak pokud je
VC<V₁...Vₑ>, odvozování závisí na parametru typui-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:
UjeSpan<U₁>aVje pole typuV₁[]neboSpan<V₁>UjeReadOnlySpan<U₁>aVje typu poleV₁[]neboSpan<V₁>neboReadOnlySpan<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é:
- https://developercommunity.visualstudio.com/t/Extension-method-SystemLinqEnumerable/10790323
- https://developercommunity.visualstudio.com/t/Compilation-Error-When-Calling-Reverse/10818048
- https://developercommunity.visualstudio.com/t/Version-17131-has-an-obvious-defect-th/10858254
- https://developercommunity.visualstudio.com/t/Visual-Studio-2022-update-breaks-build-w/10856758
- https://github.com/dotnet/runtime/issues/111532
- https://developercommunity.visualstudio.com/t/Backward-compatibility-issue-:-IEnumerab/10896189#T-ND10896782
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é:
- https://github.com/dotnet/runtime/issues/109757
- https://github.com/dotnet/docs/issues/43952
- https://github.com/dotnet/efcore/issues/35100
- https://github.com/dotnet/csharplang/discussions/8959
Návrhářské schůzky:
- https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-12-04.md#conversions-in-expression-trees
- https://github.com/dotnet/csharplang/blob/main/meetings/2025/LDM-2025-01-06.md#ignoring-ref-structs-in-expressions
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í:
-
Pokud existuje převod rozsahu z
T1naT2, ignorujte jakýkoli uživatelsky definovaný převod zT1naT2nebo zT2naT1. -
Převody definované uživatelem nejsou brány v úvahu při převodu mezi
- jakýkoli jednorozměrný
array_typeaSystem.Span<T>/System.ReadOnlySpan<T>, - jakákoli kombinace
System.Span<T>/System.ReadOnlySpan<T>, -
stringaSystem.ReadOnlySpan<char>.
- jakýkoli jednorozměrný
- Stejně jako výše, ale nahrazením posledního bodu odrážky:
-
stringaSystem.Span<char>/System.ReadOnlySpan<char>.
-
- Stejně jako výše, ale nahrazením posledního bodu odrážky:
-
stringaSystem.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.
C# feature specifications