Nuta
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować się zalogować lub zmienić katalog.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Uwaga / Notatka
Ten artykuł jest specyfikacją funkcji. Specyfikacja służy jako dokument projektowy dla funkcji. Zawiera proponowane zmiany specyfikacji wraz z informacjami wymaganymi podczas projektowania i opracowywania funkcji. Te artykuły są publikowane do momentu sfinalizowania proponowanych zmian specyfikacji i włączenia ich do obecnej specyfikacji ECMA.
Mogą wystąpić pewne rozbieżności między specyfikacją funkcji a ukończoną implementacją. Te różnice są zawarte w odpowiednich notatkach ze spotkania dotyczącego projektowania języka (LDM).
Więcej informacji na temat procesu wdrażania specyfikacji funkcji można znaleźć w standardzie języka C# w artykule dotyczącym specyfikacji .
Kwestia dotycząca mistrza: https://github.com/dotnet/csharplang/issues/8714
Podsumowanie
Wprowadzamy najwyższej klasy obsługę Span<T> i ReadOnlySpan<T> w języku, w tym nowe niejawne typy konwersji i uwzględniamy je w większej liczbie miejsc, umożliwiając bardziej naturalne programowanie przy użyciu tych typów całkowitych.
Motywacja
Od czasu wprowadzenia do języka C# 7.2 Span<T> i ReadOnlySpan<T> pracowali nad językiem i biblioteką klas bazowych (BCL) na wiele kluczowych sposobów. Jest to doskonałe rozwiązanie dla deweloperów, ponieważ ich wprowadzenie poprawia wydajność bez kosztowania bezpieczeństwa deweloperów. Jednak język w kilku kluczowych aspektach zachowywał dystans wobec tych typów, co utrudnia wyrażanie zamierzeń interfejsów API i prowadzi do znacznego powielania elementów w przypadku nowych interfejsów API. Na przykład lista BCL dodała szereg nowych interfejsów API pierwotnych tensor na platformie .NET 9, ale wszystkie te interfejsy API są oferowane w ReadOnlySpan<T>. Język C# nie rozpoznaje relacji między ReadOnlySpan<T>, Span<T>i T[], więc mimo że istnieją konwersje zdefiniowane przez użytkownika między tymi typami, nie mogą być używane dla odbiorników metod rozszerzenia, nie mogą tworzyć z innymi konwersjami zdefiniowanymi przez użytkownika i nie pomagają we wszystkich scenariuszach wnioskowania typów ogólnych.
Użytkownicy będą musieli używać jawnych konwersji lub argumentów typu, co oznacza, że narzędzia IDE nie dostarczają użytkownikom wskazówek dotyczących używania tych interfejsów API, ponieważ nic nie sugeruje, że przekazywanie tych typów po konwersji jest prawidłowe. Aby zapewnić maksymalną użyteczność dla tego stylu interfejsu API, biblioteka klas bazowych (BCL) będzie musiała określić cały zestaw przeciążeń Span<T> i T[], co jest dużo zduplikowanej przestrzeni, które należy utrzymywać bez rzeczywistego zysku. Ta propozycja ma na celu rozwiązanie problemu, ponieważ język bardziej bezpośrednio rozpoznaje te typy i konwersje.
Na przykład, BCL może dodać tylko jedno przeciążenie dowolnego pomocnika MemoryExtensions, na przykład.
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);
}
Wcześniej przeciążenia Span i tablicy były potrzebne, aby metoda rozszerzenia była użyteczna dla zmiennych typu Span/tablica, ponieważ konwersje zdefiniowane przez użytkownika (które istnieją między Span/tablica/ReadOnlySpan) nie są brane pod uwagę dla odbiorników metod rozszerzeń.
Szczegółowy projekt
Zmiany w tej propozycji zostaną powiązane z LangVersion >= 14.
Konwersje zakresu
Dodajemy nowy typ niejawnej konwersji do listy w §10.2.1, niejawna konwersja zakresu . Ta konwersja jest konwersją z typu i jest zdefiniowana w następujący sposób:
Niejawna konwersja zakresu pozwala na wzajemne przekształcanie array_types, System.Span<T>, System.ReadOnlySpan<T>i string w następujący sposób:
- Z dowolnego jednowymiarowego
array_typeo typie elementuEidoSystem.Span<Ei> - Z dowolnego jednowymiarowego
array_typez typem elementuEidoSystem.ReadOnlySpan<Ui>, pod warunkiem, żeEijest kowariancyjnie konwertowalny (§18.2.3.3) doUi - Od
System.Span<Ti>doSystem.ReadOnlySpan<Ui>, pod warunkiem, żeTijest przekształcalny kowariancyjnie (§18.2.3.3) doUi - Od
System.ReadOnlySpan<Ti>doSystem.ReadOnlySpan<Ui>, pod warunkiem, żeTijest przekształcalny kowariancyjnie (§18.2.3.3) doUi - Z
stringdoSystem.ReadOnlySpan<char>
Wszelkie typy Span/ReadOnlySpan są uważane za odpowiednie do konwersji, jeśli są one ref structi pasują do ich w pełni kwalifikowanej nazwy (LDM 2024-06-24).
Dodamy również niejawną konwersję zakresu do listy standardowych konwersji niejawnych (§10.4.2). Dzięki temu rozpoznawanie przeciążenia może je wziąć pod uwagę podczas wykonywania rozpoznawania argumentów, jak we wcześniej połączonej propozycji interfejsu API.
Jawne konwersje zakresu są następujące:
- Wszystkie niejawne konwersje zakresu .
- Z array_type z typem elementu
TidoSystem.Span<Ui>lubSystem.ReadOnlySpan<Ui>pod warunkiem, że istnieje jawna konwersja odwołania zTidoUi.
Nie ma standardowej jawnej konwersji typu span, w przeciwieństwie do innych standardowych jawnych konwersji (§10.4.3), które zawsze istnieją przy odwrotnej standardowej konwersji niejawnej.
Konwersje zdefiniowane przez użytkownika
Konwersje zdefiniowane przez użytkownika nie są brane pod uwagę podczas konwertowania między typami, dla których istnieje niejawna lub jawna konwersja zakresu.
Niejawne konwersje zakresu są wyłączone z reguły, że nie można zdefiniować operatora zdefiniowanego przez użytkownika między typami, dla których istnieje konwersja niezdefiniowana przez użytkownika (§10.5.2 Dozwolone konwersje zdefiniowane przez użytkownika). Jest to konieczne, aby BCL mogła nadal definiować istniejące operatory konwersji span nawet wtedy, gdy przełączają się na C# 14 (są one nadal potrzebne dla niższych LangVersions, a także dlatego, że te operatory są używane w generacji kodu nowych standardowych konwersji span). Ale można go postrzegać jako szczegół implementacji (codegen i niższe LangVersions nie są częścią specyfikacji), a Roslyn i tak narusza tę część specyfikacji (ta konkretna reguła dotycząca konwersji zdefiniowanych przez użytkownika nie jest wymuszana).
Odbiornik rozszerzenia
Dodajemy również niejawną konwersję przedziału do listy dopuszczalnych niejawnych konwersji w pierwszym parametrze metody rozszerzenia przy określaniu zastosowania (12.8.9.3) (zmiana pogrubiona):
Metoda rozszerzenia
Cᵢ.Mₑjest uprawniona, jeśli:
Cᵢto niegeneryczna, nienagnieżdżona klasa- Nazwa
Mₑto identyfikatorMₑjest dostępny i ma zastosowanie w przypadku zastosowania do argumentów jako metody statycznej, jak pokazano powyżej- Istnieje niejawna tożsamość, odwołanie
lub boksowanie, boksowanie albo przekształcenie zakresu z wyrażenia do typu pierwszego parametruMₑ. Konwersja span nie jest uwzględniana podczas rozwiązywania przeciążeń dokonywanego na potrzeby konwersji grupy metod.
Należy pamiętać, że niejawna konwersja zakresu nie jest brana pod uwagę w przypadku odbiornika rozszerzenia w konwersji grup metod (LDM 2024-07-15), co sprawia, że następujący kod nadal działa w przeciwieństwie do błędu czasu kompilacji 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żliwą przyszłą pracę, moglibyśmy rozważyć usunięcie warunku, gdzie konwersja zakresu nie jest brana pod uwagę dla odbiornika rozszerzenia w konwersjach grupy metod i zamiast tego wdrożyć zmiany, aby taki scenariusz, jak ten powyżej, pomyślnie wywołał przeciążenie Span.
- Kompilator może generować thunk, który przejmie tablicę jako argument i wykona operację konwersji zakresu wewnątrz (podobnie do tego, jak użytkownik ręcznie tworzy delegata, taki jak
x => new int[0].M(x)). - Delegaty wartości, jeśli zostaną zaimplementowane, mogłyby bezpośrednio przyjmować
Spanjako odbiornik.
Wariancja
Celem sekcji wariancji w niejawnej konwersji zakresu jest replikowanie pewnej ilości kowariancji dla System.ReadOnlySpan<T>. Zmiany w czasie wykonywania będą wymagane do pełnego zaimplementowania wariancji za pośrednictwem typów ogólnych w tym miejscu (zobacz .. /csharp-13.0/ref-struct-interfaces.md do używania ref struct typów w typach ogólnych), ale możemy zezwolić na ograniczoną ilość kowariancji przy użyciu proponowanego interfejsu API platformy .NET 9: https://github.com/dotnet/runtime/issues/96952. Pozwoli to językowi traktować System.ReadOnlySpan<T> tak, jakby T został zadeklarowany jako out T w niektórych scenariuszach. Nie analizujemy jednak tej wariantowej konwersji poprzez we wszystkich scenariuszach wariancji i nie dodajemy jej do definicji konwertowalności wariancji w §18.2.3.3. Jeśli w przyszłości zmienimy środowisko uruchomieniowe, aby bardziej zrozumieć wariancję w tym miejscu, możemy podjąć drobne zmiany powodujące niezgodność, aby w pełni rozpoznać je w języku.
Wzorce
Należy pamiętać, że jeśli ref structs są używane jako typ w dowolnym wzorcu, dozwolone są tylko konwersje tożsamości:
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 specyfikacji operator typu is (§12.12.12.1):
Wynikiem operacji
E is T[...] jest wartość logiczna wskazująca, czyEnie jest nullem i można pomyślnie przekonwertować ją na typTpoprzez konwersję referencyjną, konwersję boksowania, konwersję rozpakowywania, konwersję zawijania lub konwersję rozpakowywania.[...]
Jeśli
Tjest typem wartości innej niż null, wynik jesttrue, jeśliDiTsą tego samego typu.
To zachowanie nie zmienia się w przypadku tej funkcji, dlatego nie będzie można zapisywać wzorców dla Span/ReadOnlySpan, chociaż podobne wzorce są możliwe dla tablic (w tym wariancji):
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
}
Generowanie kodu
Konwersje będą zawsze istnieć, niezależnie od tego, czy jakiekolwiek pomocniki środowiska uruchomieniowego używane do ich implementowania są obecne (LDM 2024-05-13). Jeśli funkcje pomocnicze nie są obecne, próba użycia konwersji spowoduje błąd kompilacji z powodu braku elementu członkowskiego wymaganego przez kompilator.
Kompilator oczekuje użycia następujących pomocników lub odpowiedników w celu zaimplementowania konwersji:
| Konwersja | Pomocnicy |
|---|---|
| tablica na Span |
static implicit operator Span<T>(T[]) (zdefiniowane w Span<T>) |
| konwersja z array na ReadOnlySpan |
static implicit operator ReadOnlySpan<T>(T[]) (zdefiniowane w ReadOnlySpan<T>) |
| Konwersja z typu Span na ReadOnlySpan |
static implicit operator ReadOnlySpan<T>(Span<T>) (zdefiniowane w Span<T>) i static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>) |
| ReadOnlySpan to ReadOnlySpan | static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>) |
| ciąg do parametru ReadOnlySpan | static ReadOnlySpan<char> MemoryExtensions.AsSpan(string) |
Należy pamiętać, że MemoryExtensions.AsSpan jest używany zamiast równoważnego niejawnego operatora zdefiniowanego na string.
Oznacza to, że generacja kodu różni się między wersjami językowymi (operator niejawny jest używany w C# 13; metoda statyczna AsSpan jest używana w C# 14).
Z drugiej strony konwersja może być emitowana w programie .NET Framework (metoda AsSpan istnieje, natomiast operator string nie).
Konwersja tablicy jawnej do (ReadOnly)Span najpierw jawnie konwertuje z tablicy źródłowej na tablicę z typem elementu docelowego, a następnie na (ReadOnly)Span za pośrednictwem tego samego mechanizmu, którego używa niejawna konwersja, tj. odpowiedniego op_Implicit(T[]).
Ulepszona konwersja wyrażeń
Lepsza konwersja z wyrażenia (§12.6.4.5) jest aktualizowana w celu preferowania niejawnych konwersji typu span. Opiera się to na zmianach rozwiązywania przeciążenia wyrażeń kolekcji .
Biorąc pod uwagę niejawną konwersję
C₁, która konwertuje z wyrażeniaEna typT₁, oraz niejawną konwersjęC₂, która konwertuje z wyrażeniaEna typT₂,C₁jest lepszą konwersją niżC₂, jeśli spełniony jest jeden z następujących warunków:
Ejest wyrażeniem zbioru , aC₁jest lepszą konwersją zbioru od wyrażenia niżC₂.Enie jest wyrażeniem kolekcji i prawdziwe jest jedno z następujących stwierdzeń:
Edokładnie pasuje doT₁iEnie pasuje dokładnie doT₂Edokładnie nie pasuje do żadnego zT₁iT₂, iC₁jest niejawną zamianą zakresu, iC₂nie jest niejawną zamianą zakresuEdokładnie pasuje zarówno doT₁, jak iT₂, zarównoC₁, jak iC₂są niejawną konwersją zakresu, aT₁jest lepszym celem konwersji niżT₂Ejest grupą metod,T₁jest zgodna z pojedynczą najlepszą metodą z grupy metod konwersjiC₁, aT₂nie jest zgodna z jedną najlepszą metodą z grupy metod konwersjiC₂
Lepszy cel konwersji
lepszy cel konwersji (§12.6.4.7) został zaktualizowany, aby preferować ReadOnlySpan<T> nad Span<T>.
Biorąc pod uwagę dwa typy
T₁iT₂,T₁jest lepszym celem konwersji niżT₂, jeśli zachodzi jeden z następujących warunków:
T₁jestSystem.ReadOnlySpan<E₁>,T₂jestSystem.Span<E₂>, a konwersja tożsamości zE₁naE₂istniejeT₁jestSystem.ReadOnlySpan<E₁>,T₂jestSystem.ReadOnlySpan<E₂>, a niejawna konwersja zT₁naT₂istnieje i nie istnieje niejawna konwersja zT₂doT₁- co najmniej jedna z
T₁alboT₂nie jestSystem.ReadOnlySpan<Eᵢ>i nie jestSystem.Span<Eᵢ>, a niejawna konwersja zT₁naT₂istnieje i nie istnieje niejawna konwersja zT₂naT₁- ...\
Spotkania projektowe:
- 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
Uwagi dotyczące ulepszeń
lepsza reguła konwersji z wyrażenia powinna zapewnić, że za każdym razem, gdy przeciążenie stanie się możliwe do zastosowania z powodu nowych konwersji zakresu, unika się wszelkich potencjalnych niejednoznaczności z innym przeciążeniem, ponieważ nowo możliwe do zastosowania przeciążenie jest preferowane.
Bez tej nowej reguły, następujący kod, który został pomyślnie skompilowany w języku C# 13, spowodowałby błąd niejednoznaczności w języku C# 14, z powodu nowej standardowej niejawnej konwersji z tablicy na ReadOnlySpan, mającej zastosowanie do odbiornika metody rozszerzenia.
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) { }
}
Reguła umożliwia również wprowadzenie nowych interfejsów API, które wcześniej powodowały niejednoznaczności, na przykład:
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
}
Ostrzeżenie
Ponieważ reguła ulepszenia jest zdefiniowana dla konwersji rozpiętości, które istnieją tylko w LangVersion >= 14, twórcy API nie mogą dodawać takich nowych przeciążeń, jeśli chcą zachować obsługę użytkowników w LangVersion <= 13.
Na przykład, jeśli platforma .NET 9 BCL wprowadzi takie przeciążenia, użytkownicy, którzy uaktualnią do net9.0 TFM, ale pozostaną na niższym poziomie LangVersion, otrzymają błędy wynikające z niejednoznaczności dla istniejącego kodu.
Zobacz również otwarte pytanie poniżej.
Wnioskowanie typów
Aktualizujemy sekcję inferencji typów specyfikacji w następujący sposób (zmiany wyróżnione w pogrubieniu).
12.6.3.9 Dokładne wnioskowania
Dokładne wnioskowanie z typu
Udo typuVjest wykonywane w następujący sposób:
- Jeśli
Vjest jednym z niefiksowanychXᵢ, toUzostanie dodany do zestawu dokładnych granic dlaXᵢ.- W przeciwnym razie zestawy
V₁...VₑiU₁...Uₑsą określane przez sprawdzenie, czy ma zastosowanie dowolny z następujących przypadków:
Vjest typem tablicyV₁[...], aUjest typem tablicyU₁[...]tej samej rangiVjestSpan<V₁>, aUjest typem tablicyU₁[]lubSpan<U₁>VjestReadOnlySpan<V₁>, aUjest typem tablicyU₁[],Span<U₁>lubReadOnlySpan<U₁>Vjest typemV₁?, aUjest typemU₁Vjest skonstruowanym typemC<V₁...Vₑ>, aUjest skonstruowanym typemC<U₁...Uₑ>
Jeśli którykolwiek z tych przypadków ma zastosowanie, dokładne wnioskowanie jest wykonywane z każdegoUᵢdo odpowiedniegoVᵢ.- W przeciwnym razie nie są wykonywane żadne wnioskowania.
12.6.3.10 Wnioskowanie dotyczące dolnej granicy
Wnioskowanie dolnego oszacowania od typu
Udo typuVjest wykonywane w następujący sposób:
- Jeśli
Vjest jednym z niefiksowanychXᵢ,Uzostanie dodany do zestawu dolnych granic dlaXᵢ.- W przeciwnym razie, jeśli
Vjest typemV₁?, aUjest typemU₁?, wtedy następuje wnioskowanie dolnej granicy zU₁doV₁.- W przeciwnym razie zestawy
U₁...UₑiV₁...Vₑsą określane przez sprawdzenie, czy ma zastosowanie dowolny z następujących przypadków:
Vjest typem tablicyV₁[...], aUjest typem tablicyU₁[...]tej samej rangiVjestSpan<V₁>, aUjest typem tablicyU₁[]lubSpan<U₁>VjestReadOnlySpan<V₁>, aUjest typem tablicyU₁[],Span<U₁>lubReadOnlySpan<U₁>Vjest jednym zIEnumerable<V₁>,ICollection<V₁>,IReadOnlyList<V₁>>,IReadOnlyCollection<V₁>lubIList<V₁>, aUjest jednowymiarowym typem tablicyU₁[]Vjest elementem skonstruowanym jakoclass,struct,interfacelubdelegatetypuC<V₁...Vₑ>i istnieje unikatowy typC<U₁...Uₑ>, taki żeU(lub, jeśliUjest typemparameter, jego efektywną klasą bazową lub dowolnym elementem wchodzącym w skład jego skutecznego zestawu interfejsów) jest identyczny zinheritslub implementujeC<U₁...Uₑ>, zarówno bezpośrednio, jak i pośrednio.- (Ograniczenie "unikatowości" oznacza, że w przypadku interfejsu
C<T>{} class U: C<X>, C<Y>{}, podczas wnioskowania zUdoC<T>nie jest wykonywane żadne wnioskowanie, ponieważU₁może byćXlubY.)
Jeśli którykolwiek z tych przypadków ma zastosowanie, wnioskowanie jest wykonywane z każdegoUᵢdo odpowiedniegoVᵢw następujący sposób:- Jeśli
Uᵢnie jest znany jako typ odniesienia, wykonuje się dokładne wnioskowanie- W przeciwnym razie, jeśli
Ujest typem tablicy, wykonywane jest wnioskowanie dolnej granicy, a wnioskowanie zależy od typuV:
- Jeśli
VjestSpan<Vᵢ>, to dokonuje się dokładne wnioskowanie- Jeśli
Vjest typem tablicy lubReadOnlySpan<Vᵢ>, wtedy wykonywane jest wnioskowanie o niższej granicy- w przeciwnym razie, jeśli
UjestSpan<Uᵢ>, wnioskowanie zależy od typuV:
- Jeśli
VjestSpan<Vᵢ>, to dokonuje się dokładne wnioskowanie- Jeśli
VjestReadOnlySpan<Vᵢ>, to wykonywane jest wnioskowanie niższego- w przeciwnym razie, jeśli
UjestReadOnlySpan<Uᵢ>iVjestReadOnlySpan<Vᵢ>, to inferencja dolnej granicy zostaje przeprowadzona:- W przeciwnym razie, jeśli
VjestC<V₁...Vₑ>, to wnioskowanie zależy od parametru typui-thwC:
- Jeśli jest kowariantny, zostanie wykonane wnioskowanie o dolnej granicy.
- Jeśli jest kontrawariantny, przeprowadza się wnioskowanie górnej granicy .
- Jeśli jest niezmienny, wykonywane jest dokładne wnioskowanie .
- W przeciwnym razie nie są wykonywane żadne wnioskowania.
Nie ma żadnych reguł dotyczących wnioskowania górnego ograniczenia , ponieważ nie byłoby możliwe ich zrealizowanie.
Wnioskowanie typu nigdy nie rozpoczyna się od górnej granicy, najpierw musi przejść przez wnioskowanie dolnej granicy i kontrawariantny parametr typu.
Ze względu na regułę "jeśli Uᵢ nie jest znany jako typ odwołania, wtedy dokonuje się dokładnego wnioskowania", argument typu źródłowego nie mógł być Span/ReadOnlySpan (ponieważ te nie mogą być typami odwołań).
Jednak wnioskowanie górnego zakresu miałoby zastosowanie tylko wtedy, gdyby typ źródła był Span/ReadOnlySpan, ponieważ miałby wtedy takie reguły jak:
UjestSpan<U₁>, aVjest typem tablicyV₁[]lubSpan<V₁>UjestReadOnlySpan<U₁>, aVjest typem tablicyV₁[],Span<V₁>lubReadOnlySpan<V₁>
Zmiany przełomowe
W przypadku każdej propozycji, która zmienia konwersje istniejących scenariuszy, wniosek wprowadza pewne nowe zmiany powodujące niezgodność. Oto kilka przykładów:
Wywoływanie Reverse w tablicy
Wywołanie x.Reverse(), w którym x jest wystąpieniem typu T[], wcześniej wiązało się z IEnumerable<T> Enumerable.Reverse<T>(this IEnumerable<T>), natomiast teraz wiąże się z void MemoryExtensions.Reverse<T>(this Span<T>).
Niestety te interfejsy API są niezgodne (ten drugi wykonuje odwrócenie w miejscu i zwraca void).
Program .NET 10 ogranicza ten problem, dodając przeciążenie specyficzne dla tablicy IEnumerable<T> Reverse<T>(this T[]), zobacz 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
}
Zobacz również:
- 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
Spotkanie projektowe: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#reverse
Niejasności
Poniższe przykłady wcześniej nie udały się próby wnioskowania typu dla przeładowania funkcji Span, ale teraz wnioskowanie typu z tablicy na Span się powiodło, w związku z tym są one teraz niejednoznaczne.
Aby obejść ten proces, użytkownicy mogą używać .AsSpan() lub autorzy interfejsów API mogą używać 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 dodaje kolejne przeciążenia, aby rozwiązać ten problem: https://github.com/xunit/xunit/discussions/3021.
Spotkanie projektowe: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#new-ambiguities
Tablice kowariantne
Przeciążenia biorące IEnumerable<T> działały na tablicach kowariantnych, ale przeciążenia biorące Span<T> (które teraz wolimy) już nie działają, ponieważ konwersja zakresu wywołuje ArrayTypeMismatchException w przypadku tablic kowariantnych.
Prawdopodobnie przeciążenie Span<T> nie powinno istnieć, powinno przyjąć ReadOnlySpan<T> zamiast tego.
Aby obejść ten problem, użytkownicy mogą używać .AsEnumerable(), a autorzy interfejsów API mogą użyć OverloadResolutionPriorityAttribute lub dodać przeciążenie ReadOnlySpan<T>, co jest preferowane ze względu na regułę lepszości.
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);
}
Spotkanie projektowe: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#covariant-arrays
Preferując ReadOnlySpan zamiast Span
Reguła lepszego działania powoduje preferencję przeciążeń ReadOnlySpan nad przeciążeniami Span, aby uniknąć ArrayTypeMismatchExceptions w scenariuszach kowariantnej tablicy .
Może to prowadzić do przerw kompilacji w niektórych scenariuszach, na przykład gdy przeciążenia różnią się od ich typu zwracanego:
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;
}
Zobacz: https://github.com/dotnet/roslyn/issues/76443.
Drzewa wyrażeń
Przeciążenia biorące zakresy, takie jak MemoryExtensions.Contains, są preferowane w przypadku przeciążeń klasycznych, takich jak Enumerable.Contains, nawet wewnątrz drzew wyrażeń — ale struktury ref nie są obsługiwane przez aparat interpretera:
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
Podobnie aparaty tłumaczenia, takie jak LINQ-to-SQL, muszą reagować na to, jeśli odwiedzający drzewo oczekują Enumerable.Contains, ponieważ napotkają MemoryExtensions.Contains zamiast tego.
Zobacz również:
- 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
Spotkania projektowe:
- 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
Konwersje zdefiniowane przez użytkownika poprzez dziedziczenie
Dodając niejawne konwersje zakresu do listy standardowych konwersji niejawnych, możemy potencjalnie zmienić zachowanie, gdy w hierarchii typów zaangażowane są konwersje zdefiniowane przez użytkownika. W tym przykładzie pokazano, że ta zmiana, w porównaniu ze scenariuszem dotyczącym liczb całkowitych, który już działa zgodnie z nowym zachowaniem w języku 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");
}
}
Zobacz też: https://github.com/dotnet/roslyn/issues/78314
Znajdowanie metody rozszerzenia
Zezwalając na niejawne konwersje zakresu podczas wyszukiwania metod rozszerzeń, możemy potencjalnie zmienić metodę rozszerzenia wybraną przez mechanizm rozwiązywania przeciążeń.
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");
}
}
}
Otwórz pytania
Nieograniczona reguła poprawy
Czy powinniśmy uczynić zasadę poprawy bezwarunkową w odniesieniu do LangVersion? Umożliwiłoby to autorom interfejsów API dodawanie nowych interfejsów API span, w których istnieją odpowiedniki IEnumerable bez przerywania pracy użytkowników w starszych wersjach LangVersions lub innych kompilatorach lub językach (np. VB). Oznaczałoby to jednak, że użytkownicy mogą uzyskać inne zachowanie po zaktualizowaniu zestawu narzędzi (bez zmiany elementu LangVersion lub TargetFramework):
- Kompilator może wybrać różne przeciążenia (technicznie rzecz biorąc, jest to zmiana łamiąca zgodność, ale, miejmy nadzieję, że te przeciążenia będą miały równoważne zachowanie).
- W tej chwili mogą wystąpić inne przerwy, nieznane.
Należy pamiętać, że OverloadResolutionPriorityAttribute nie może w pełni rozwiązać tego problemu, ponieważ jest on również ignorowany w starszych wersji LangVersions.
Jednak należy użyć go, aby uniknąć niejednoznaczności z języka VB, gdzie atrybut powinien być rozpoznawany.
Ignorowanie większej liczby konwersji zdefiniowanych przez użytkownika
Zdefiniowaliśmy zestaw par typów, dla których istnieją niejawne i jawne konwersje zakresu zdefiniowane w języku.
Zawsze, gdy istnieje konwersja zakresu zdefiniowanego przez język z T1 na T2, każda konwersja zdefiniowana przez użytkownika z T1 na T2 jest ignorowana (niezależnie od zakresu i konwersji zdefiniowanej przez użytkownika jest niejawna lub jawna).
Należy pamiętać, że obejmuje to wszystkie warunki, więc na przykład nie ma konwersji zakresu z Span<object> na ReadOnlySpan<string> (istnieje konwersja zakresu z Span<T> na ReadOnlySpan<U>, ale musi zawierać, że T : U), dlatego konwersja zdefiniowana przez użytkownika byłaby brana pod uwagę między tymi typami, jeśli istnieje (która musiałaby być wyspecjalizowaną konwersją, taką jak Span<T> do ReadOnlySpan<string>, ponieważ operatory konwersji nie mogą mieć parametrów ogólnych).
Czy powinniśmy ignorować konwersje zdefiniowane przez użytkownika dla innych kombinacji typów tablicy/span/ReadOnlySpan/ciągów, gdzie nie istnieje odpowiednia, zdefiniowana przez język konwersja span?
Jeśli na przykład istnieje konwersja zdefiniowana przez użytkownika z ReadOnlySpan<T> na Span<T>, czy należy ją zignorować?
Możliwości specyfikacji, które należy wziąć pod uwagę:
-
Zawsze, gdy istnieje konwersja zakresu z
T1naT2, zignoruj dowolną konwersję zdefiniowaną przez użytkownika zT1naT2lub zT2doT1. -
Konwersje zdefiniowane przez użytkownika nie są brane pod uwagę podczas konwertowania między
- każde jednowymiarowe
array_typeiSystem.Span<T>/System.ReadOnlySpan<T>, - dowolna kombinacja
System.Span<T>/System.ReadOnlySpan<T>, -
stringiSystem.ReadOnlySpan<char>.
- każde jednowymiarowe
- Podobnie jak powyżej, ale zastępując ostatni punktor:
-
stringiSystem.Span<char>/System.ReadOnlySpan<char>.
-
- Podobnie jak powyżej, ale zastępując ostatni punktor:
-
stringiSystem.Span<T>/System.ReadOnlySpan<T>.
-
Technicznie specyfikacja nie zezwala na zdefiniowanie niektórych z tych konwersji zdefiniowanych przez użytkownika: nie można zdefiniować operatora zdefiniowanego przez użytkownika między typami, dla których istnieje konwersja niezdefiniowana przez użytkownika (§10.5.2).
Ale Roslyn celowo narusza tę część specyfikacji. A niektóre konwersje, takie jak między Span i string są dozwolone mimo to (nie istnieje konwersja zdefiniowana przez język między tymi typami).
Niemniej jednak, alternatywnie tylko ignorowanie konwersji, możemy nie zezwalać je definiować w ogóle i być może zerwać z naruszeniem specyfikacji przynajmniej dla tych nowych konwersji zakresu, tj. zmienić Roslyn, aby rzeczywiście zgłosić błąd czasu kompilacji, jeśli te konwersje są zdefiniowane (prawdopodobnie z wyjątkiem tych, które zostały już zdefiniowane przez BCL).
Alternatywy
Zachowaj rzeczy tak, jak są.
C# feature specifications