Nuta
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
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ą przechwytywane w odpowiednich spotkania projektowego 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 .
Problem z mistrzem: https://github.com/dotnet/csharplang/issues/8652
Streszczenie
Wyrażenia kolekcji wprowadzają nową zwięzłą składnię, [e1, e2, e3, etc], do tworzenia typowych wartości kolekcji. Inline'owanie innych kolekcji do tych wartości jest możliwe przy użyciu elementu typu spread ..e w następujący sposób: [e1, ..c2, e2, ..c2].
Można utworzyć kilka typów kolekcji bez konieczności obsługi zewnętrznego wsparcia BCL. Są to następujące typy:
-
typy tablic, takie jak
int[]. -
Span<T>iReadOnlySpan<T>. - Typy obsługujące inicjatory kolekcji , takie jak
List<T>.
Dalsze wsparcie jest obecne w przypadku typów podobnych do kolekcji, które nie zostały omówione powyżej za pomocą nowego atrybutu i wzorca interfejsu API, który można zastosować bezpośrednio w samym typie.
Motywacja
Wartości podobne do kolekcji są bardzo obecne w programowaniu, algorytmach, a zwłaszcza w ekosystemie języka C#/.NET. Prawie wszystkie programy będą używać tych wartości do przechowywania danych i wysyłania lub odbierania danych z innych składników. Obecnie prawie wszystkie programy w języku C# muszą używać wielu różnych i niestety rozwlekłych podejść do tworzenia instancji takich wartości. Niektóre podejścia mają również wady wydajności. Oto kilka typowych przykładów:
- Tablice, które wymagają obecności
new Type[]lubnew[]przed wartościami{ ... }. - Zakresy, które mogą używać
stackalloci innych kłopotliwych konstrukcji. - Inicjatory kolekcji, które wymagają składni, takiej jak
new List<T>(z pominięciem prawdopodobnie bardziej rozbudowanejT) przed przypisaniem wartości, i które mogą powodować wielokrotne przesunięcia pamięci, ponieważ używają N.Addwywołań bez określenia początkowej pojemności. - Niezmienne kolekcje, które wymagają składni takiej jak
ImmutableArray.Create(...)do inicjowania wartości i które mogą powodować pośrednie alokacje i kopiowanie danych. Bardziej wydajne formy budowlane (takie jakImmutableArray.CreateBuilder) są niewygodne i nadal produkują nieuniknione śmieci.
- Tablice, które wymagają obecności
Patrząc na otaczający ekosystem, znajdujemy również przykłady wszędzie, gdzie tworzenie listy jest wygodniejsze i przyjemne do użycia. TypeScript, Dart, Swift, Elm, Python i więcej decydują się na zwięzłą składnię w tym celu, co przekłada się na powszechne użycie i z dużym sukcesem. Pobieżne badania nie ujawniły żadnych merytorycznych problemów wynikających z obecności wbudowanych literałów w tych ekosystemach.
Język C# dodał również wzorce listy w języku C# 11. Ten wzorzec umożliwia dopasowywanie i dekonstrukcję wartości podobnych do listy przy użyciu czystej i intuicyjnej składni. Jednak w przeciwieństwie do prawie wszystkich innych konstrukcji wzorca ta składnia dopasowania/dekonstrukcji nie zawiera odpowiedniej składni konstrukcji.
Uzyskanie najlepszej wydajności do konstruowania każdego typu kolekcji może być trudne. Proste rozwiązania często marnuje zarówno procesor, jak i pamięć. Posiadanie formy literałowej pozwala na maksymalną elastyczność implementacji kompilatora, aby zoptymalizować literał i osiągnąć wynik co najmniej tak dobry, jak użytkownik mógłby uzyskać, ale przy użyciu prostszego kodu. Bardzo często kompilator będzie mógł zrobić lepiej, a specyfikacja ma na celu umożliwienie implementacji dużych ilości swobody w zakresie strategii implementacji w celu zapewnienia tego.
W języku C#jest potrzebne rozwiązanie inkluzywne. Powinno to spełniać zdecydowaną większość przypadków dla klientów pod względem typów i wartości podobnych do kolekcji, które już mają. Powinien również brzmieć naturalnie w języku i odzwierciedlać pracę wykonaną przy dopasowywaniu wzorców.
Prowadzi to do naturalnego wniosku, że składnia powinna być podobna do [e1, e2, e3, e-etc] lub [e1, ..c2, e2], która odpowiada odpowiednikom wzorca [p1, p2, p3, p-etc] i [p1, ..p2, p3].
Szczegółowy projekt
Dodawane są następujące reguły gramatyczne produkcji.
primary_no_array_creation_expression
...
+ | collection_expression
;
+ collection_expression
: '[' ']'
| '[' collection_element ( ',' collection_element )* ']'
;
+ collection_element
: expression_element
| spread_element
;
+ expression_element
: expression
;
+ spread_element
: '..' expression
;
Literały kolekcji są typu docelowego.
Objaśnienia dotyczące specyfikacji
Dla zwięzłości
collection_expressionbędzie określany jako "literał" w poniższych sekcjach.wystąpienia
expression_elementsą często określane jakoe1,e_nitp.wystąpienia
spread_elementsą często określane jako..s1,..s_nitp.typ zakresu oznacza
Span<T>lubReadOnlySpan<T>.Literały są często wyświetlane jako
[e1, ..s1, e2, ..s2, etc], aby przekazać dowolną liczbę elementów składowych w dowolnej kolejności. Co ważne, ten formularz będzie używany do reprezentowania wszystkich przypadków, takich jak:- Puste literały
[] - Literały bez
expression_elementw nich. - Literały bez
spread_elementw nich. - Literały z dowolną kolejnością dowolnego typu elementu.
- Puste literały
Typ iteracji związany z
..s_nto typ zmiennej iteracyjnej , określony tak, jakbys_nbyło używane jako wyrażenie w procesie iteracyjnym wforeach_statement.Zmienne rozpoczynające się od
__namesą używane do reprezentowania wyników ocenyname, przechowywanych w lokalizacji, tak aby była obliczana tylko raz. Na przykład__e1to ocenae1.List<T>,IEnumerable<T>itp. odwołują się do odpowiednich typów w przestrzeni nazwSystem.Collections.Generic.Specyfikacja definiuje tłumaczenia literału do istniejących konstrukcji języka C#. Podobnie jak tłumaczenie wyrażenia zapytania, dosłowny jest on sam legalny tylko wtedy, gdy tłumaczenie spowodowałoby poprawny kod. Celem tej zasady jest uniknięcie konieczności powtarzania innych zasad języka, które są domyślne (na przykład dotyczących konwersji wyrażeń podczas przypisywania do miejsc przechowywania).
Implementacja nie jest wymagana do tłumaczenia literałów dokładnie tak, jak określono poniżej. Każde tłumaczenie jest legalne, jeśli ten sam wynik jest generowany i nie ma zauważalnych różnic w produkcji wyniku.
- Na przykład implementacja może tłumaczyć literały, takie jak
[1, 2, 3], bezpośrednio na wyrażenienew int[] { 1, 2, 3 }, które samo w sobie osadza nieprzetworzone dane w kodzie, pomijając potrzebę użycia__indexlub sekwencji instrukcji do przypisania każdej wartości. Jeśli jakiś krok tłumaczenia może spowodować wyjątek podczas działania, oznacza to, że stan programu pozostaje w stanie wskazanym przez tłumaczenie.
- Na przykład implementacja może tłumaczyć literały, takie jak
Odwołania do "alokowania na stosie" odnoszą się do każdej strategii przydzielania na stosie, a nie na stercie. Co ważne, nie oznacza ani nie wymaga, aby ta strategia była realizowana poprzez rzeczywisty mechanizm
stackalloc. Na przykład użycie tablic wbudowanych jest również dozwolonym i pożądanym podejściem do osiągnięcia alokacji stosu tam, gdzie to możliwe. Należy pamiętać, że w języku C# 12 tablice wbudowane nie mogą być inicjowane za pomocą wyrażenia kolekcji. Pozostaje to otwarta propozycja.Zakłada się, że kolekcje są dobrze zachowywane. Na przykład:
- Przyjmuje się, że wartość
Countw kolekcji spowoduje wygenerowanie tej samej wartości co liczba elementów podczas wyliczania. - Typy używane w tej specyfikacji zdefiniowane w przestrzeni nazw
System.Collections.Genericsą uważane za wolne od skutków ubocznych. W związku z tym kompilator może zoptymalizować scenariusze, w których takie typy mogą być używane jako wartości pośrednie, ale w przeciwnym razie nie mogą być uwidocznione. - Zakłada się, że wywołanie jakiegoś odpowiedniego członka
.AddRange(x)w kolekcji spowoduje taką samą wartość końcową jak iteracjaxi dodanie wszystkich jego wyliczonych wartości indywidualnie do kolekcji przy użyciu.Add. - Zachowanie literałów kolekcji w przypadku kolekcji, które nie są prawidłowe, jest niezdefiniowane.
- Przyjmuje się, że wartość
Konwersje
Konwersja wyrażeń kolekcji umożliwia przekonwertowanie wyrażenia kolekcji na typ.
Istnieje niejawna konwersja wyrażenia kolekcji z wyrażenia kolekcji na następujące typy:
- Jednowymiarowy typ tablicy
T[], gdzie typ elementu jestT - Typ przedziału :
System.Span<T>System.ReadOnlySpan<T>
W tym przypadku typ elementu jestT
- Typ z odpowiednią metodą tworzenia , w tym przypadku typ elementu jest typem iteracji określanym na podstawie metody wystąpienia
GetEnumeratorlub interfejsu wyliczalnego, a nie z metody rozszerzenia. - Typ klasy lub, który implementuje
System.Collections.IEnumerablegdzie:Typ ma odpowiedni konstruktor, który można wywołać bez argumentów, a konstruktor jest dostępny w lokalizacji wyrażenia kolekcji.
Jeśli wyrażenie kolekcji zawiera jakiekolwiek elementy, to typ ma instancję lub metodę rozszerzenia
Add, gdzie:- Metodę można wywołać za pomocą jednego argumentu wartości.
- Jeśli metoda jest ogólna, argumenty typu można wywnioskować z kolekcji i argumentu.
- Metoda jest dostępna w lokalizacji wyrażenia kolekcji.
- Typ interfejsu :
System.Collections.Generic.IEnumerable<T>System.Collections.Generic.IReadOnlyCollection<T>System.Collections.Generic.IReadOnlyList<T>System.Collections.Generic.ICollection<T>System.Collections.Generic.IList<T>
W tym przypadku typ elementu jestT
Konwersja niejawna istnieje, jeśli typ ma typ elementu T gdzie dla każdego elementu Eᵢ w wyrażeniu kolekcji:
- Jeśli
Eᵢjest elementem wyrażenia , istnieje niejawna konwersja zEᵢnaT. - Jeśli
Eᵢjest elementem rozprzestrzeniającym..Sᵢ, istnieje niejawna konwersja z typu iteracji zSᵢnaT.
Nie ma konwersji wyrażenia kolekcji z wyrażenia kolekcji na typ tablicy wielowymiarowej .
Typy, dla których istnieje niejawna konwersja wyrażenia kolekcji z wyrażenia kolekcji, są prawidłowymi typami docelowymi dla tego wyrażenia kolekcji.
Następujące dodatkowe niejawne konwersje istnieją w wyrażeniu kolekcji :
Do typu wartości dopuszczanej do wartości null
T?, w którym istnieje konwersji wyrażenia kolekcji z wyrażenia kolekcji na typ wartościT. Konwersja to konwersji wyrażeń kolekcji naT, po której następuje niejawna konwersja niejawna konwersja dopuszczana do wartości null zTdoT?.Do typu odwołania
T, w którym istnieje metoda tworzenia skojarzona zT, która zwracaUtypu i niejawną konwersję odwołania zUdoT. Konwersja to konwersji wyrażeń kolekcji doUnastępnie niejawnej konwersji odwołań zUdoT.Do typu interfejsu
I, w którym istnieje metoda tworzenia skojarzona zI, która zwraca typVi niejawną konwersję boksu zVdoI. Konwersja jest konwersją wyrażenia kolekcji naV, a następnie niejawną konwersją boksu zVnaI.
Tworzenie metod
metody tworzenia jest wskazywana za pomocą atrybutu [CollectionBuilder(...)] w typie kolekcji .
Atrybut określa typ konstruktora - oraz nazwę metody -, która ma zostać wywołana w celu skonstruowania wystąpienia typu kolekcji.
namespace System.Runtime.CompilerServices
{
[AttributeUsage(
AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface,
Inherited = false,
AllowMultiple = false)]
public sealed class CollectionBuilderAttribute : System.Attribute
{
public CollectionBuilderAttribute(Type builderType, string methodName);
public Type BuilderType { get; }
public string MethodName { get; }
}
}
Atrybut można zastosować do class, struct, ref structlub interface.
Atrybut nie jest dziedziczony, chociaż można go zastosować do obiektu typu class lub abstract class.
Konstruktor typu musi być class niegeneryczny lub struct.
Najpierw określa się zestaw odpowiednich metod tworzeniaCM.
Składa się z metod spełniających następujące wymagania:
- Metoda musi mieć nazwę określoną w atrybucie
[CollectionBuilder(...)]. - Metoda musi być zdefiniowana bezpośrednio na typie konstrukcji .
- Metoda musi być
static. - Metoda musi być dostępna, gdy jest używane wyrażenie kolekcji.
- metody musi odpowiadać typu kolekcji.
- Metoda musi mieć jeden parametr typu
System.ReadOnlySpan<E>, przekazywany jako wartość. - Istniejekonwersji tożsamości, niejawnej konwersji odwołańlub konwersji boksu z typu zwracania metody do typu kolekcji .
Metody deklarowane na podstawie typów bazowych lub interfejsów są ignorowane i nie są częścią zestawu CM.
Jeśli zestaw CM jest pusty, typ kolekcji nie zawiera typu elementu i nie posiada metody tworzenia . Żadne z poniższych kroków nie ma zastosowania.
Jeśli tylko jedna z metod w zestawie CM ma konwersję tożsamości z E na typ elementu typu kolekcji , to jest ona metodą tworzenia dla typu kolekcji . W przeciwnym razie typ kolekcji nie ma metody tworzenia .
Zgłaszany jest błąd, jeśli atrybut [CollectionBuilder] nie odwołuje się do metody możliwej do wywołania z oczekiwanym podpisem.
W przypadku wyrażenia kolekcji z typem docelowym C<S0, S1, …>, w którym deklaracja typu C<T0, T1, …> ma skojarzoną metodę budowniczego B.M<U0, U1, …>(), argumenty typu ogólnego z typu docelowego są stosowane w kolejności — od najbardziej zewnętrznego typu zawierającego typ do najbardziej wewnętrznego — do metody budowniczego .
Parametr span dla metody tworzenia można jawnie oznaczyć scoped lub [UnscopedRef]. Jeśli parametr jest niejawnie lub jawnie scoped, kompilator może przydzielić pamięć dla zakresu na stosie zamiast na stercie.
Na przykład możliwe utworzenia metody dla ImmutableArray<T>:
[CollectionBuilder(typeof(ImmutableArray), "Create")]
public struct ImmutableArray<T> { ... }
public static class ImmutableArray
{
public static ImmutableArray<T> Create<T>(ReadOnlySpan<T> items) { ... }
}
Za pomocą powyższej metody , ImmutableArray<int> ia = [1, 2, 3]; można emitować jako:
[InlineArray(3)] struct __InlineArray3<T> { private T _element0; }
Span<int> __tmp = new __InlineArray3<int>();
__tmp[0] = 1;
__tmp[1] = 2;
__tmp[2] = 3;
ImmutableArray<int> ia =
ImmutableArray.Create((ReadOnlySpan<int>)__tmp);
Budowa
Elementy wyrażenia kolekcji są oceniane w kolejności od lewej do prawej. Każdy element jest obliczany dokładnie raz, a wszelkie dalsze odwołania do elementów odnoszą się do wyników tej wstępnej oceny.
Element spread może być iterowany przed lub po kolejnych elementach w wyrażeniu kolekcji są oceniane.
Nieobsługiwany wyjątek zgłoszony z którejkolwiek z metod stosowanych podczas budowy zostanie nieuchwycony i uniemożliwi dalsze kroki w budowie.
Length, Counti GetEnumerator zakłada się, że nie ma skutków ubocznych.
Jeśli typ docelowy jest strukturą lub typem klasy, który implementuje System.Collections.IEnumerable, a typ docelowy nie ma metody create , konstrukcja wystąpienia kolekcji jest następująca:
Elementy są oceniane w kolejności. Niektóre lub wszystkie elementy mogą być oceniane podczas poniższych kroków, a nie wcześniej.
Kompilator może określić znaną długość wyrażenia kolekcji, wywołując policzalne właściwości — lub równoważne właściwości z dobrze znanych interfejsów lub typów — na każdym wyrażeniu elementu rozwiniętego.
Konstruktor, który ma zastosowanie bez argumentów, jest wywoływany.
Dla każdego elementu w kolejności:
- Jeśli element jest wyrażeniem , odpowiednia instancja
Addlub metoda rozszerzenia jest wywoływana z wyrażeniem dla elementu jako argumentem. W odróżnieniu od klasycznego zachowania inicjatora kolekcji , ocena elementów i wywołaniaAddnie muszą być koniecznie przeplatane. - Jeśli element jest elementem spread, jest używany jeden z następujących elementów:
- Odpowiednie wystąpienie
GetEnumeratorlub metoda rozszerzenia jest wywoływane na wyrażenia elementu spread i dla każdego elementu z modułu wyliczającego odpowiednie wystąpienieAddlub metodę rozszerzenia jest wywoływana w wystąpieniu kolekcji z elementem jako argumentem. Jeśli moduł wyliczający implementujeIDisposable, toDisposezostanie wywołany po wyliczeniu, niezależnie od wyjątków. - Wywoływane jest odpowiednie wystąpienie
AddRangelub metoda rozszerzenia na instancji kolekcji , z wyrażeniem elementu spread jako argumentem. - Na wyrażeniu elementu rozszerzonego
CopyTowywoływane jest odpowiednie wystąpienie lub metoda rozszerzenia przy użyciu instancji kolekcji i indeksuintjako argumentów.
- Odpowiednie wystąpienie
- Jeśli element jest wyrażeniem , odpowiednia instancja
Podczas powyższych kroków budowy odpowiednia instancja
EnsureCapacitylub metoda rozszerzenia może być wywoływana jeden lub więcej razy na wystąpieniu kolekcji z argumentem pojemnościint.
Jeśli typem docelowym jest tablica , zakres , typ z metodą tworzenia , lub interfejs , konstrukcja wystąpienia kolekcji jest następująca:
Elementy są oceniane w kolejności. Niektóre lub wszystkie elementy mogą być oceniane podczas poniższych kroków, a nie wcześniej.
Kompilator może określić znaną długość wyrażenia kolekcji, wywołując policzalne właściwości — lub równoważne właściwości z dobrze znanych interfejsów lub typów — na każdym wyrażeniu elementu rozwiniętego.
Wystąpienie inicjowania jest tworzone w następujący sposób:
- Jeśli typ docelowy to tablica , a wyrażenie kolekcji ma znaną długość, jest przydzielana tablica o oczekiwanej długości.
- Jeśli typ docelowy to przedział lub typ z metodą tworzenia , a kolekcja ma znaną długość , zostanie utworzony przedział o oczekiwanej długości, odwołujący się do przylegającej pamięci.
- W przeciwnym razie jest przydzielany magazyn pośredni.
Dla każdego elementu w kolejności:
- Jeśli element jest elementem wyrażenia , indeksator z instancji inicjalizacji jest wywoływany, aby dodać obliczone wyrażenie do bieżącego indeksu.
- Jeśli element jest elementem spread, jest używany jeden z następujących elementów:
- Członek dobrze znanego interfejsu lub typu jest wywoływany, aby skopiować elementy z wyrażenia rozproszenia elementów do instancji inicjalizacyjnej.
- Odpowiednie wystąpienie lub metoda rozszerzenia
GetEnumeratorjest wywoływane na wyrażeniu elementu rozproszenia , a dla każdego elementu z enkapsulatora, wywoływany jest indeksator instancji inicjalizacyjnej w celu dodania elementu na bieżącym indeksie. Jeśli moduł wyliczający implementujeIDisposable, toDisposezostanie wywołany po wyliczeniu, niezależnie od wyjątków. - Odpowiednie wystąpienie
CopyTolub metoda rozszerzenia jest wywoływana w wyrażeniu elementu spread z wystąpieniem inicjowania i indeksemintjako argumentami.
Jeśli magazyn pośredni został przydzielony dla kolekcji, wystąpienie kolekcji jest tworzone z rzeczywistą długością kolekcji, a wartości z wystąpienia inicjalizacji są kopiowane do tego wystąpienia kolekcji, albo jeśli zakres jest wymagany przez kompilator, może użyć zakresu rzeczywistej długości kolekcji z magazynu pośredniego. W przeciwnym razie instancja inicjalizacji to instancja kolekcji.
Jeśli typ docelowy posiada metodę tworzenia , metoda ta jest wywoływana z użyciem instancji span.
Uwaga: Kompilator może opóźnić dodawania elementów do kolekcji — lub opóźnienia iteracji przez elementy rozłożone — do momentu oceny kolejnych elementów. (Gdy kolejne elementy rozproszone mają zliczalne właściwości, które umożliwiają obliczanie przewidywanej długości kolekcji przed przydzieleniem kolekcji). Z drugiej strony kompilator może szybko dodawać elementy do kolekcji — i szybko iterować przez elementy rozproszone — jeśli nie ma korzyści z opóźnienia.
Rozważ następujące wyrażenie kolekcji:
int[] x = [a, ..b, ..c, d];Jeśli elementy rozłożone
bicsą zliczalne, kompilator może opóźnić dodawanie elementów zaibdo czasu zakończenia ocenyc, co pozwoli na przydzielenie wynikowej tablicy o oczekiwanej długości. Następnie kompilator może z niecierpliwością dodać elementy zcprzed ocenąd.var __tmp1 = a; var __tmp2 = b; var __tmp3 = c; var __result = new int[2 + __tmp2.Length + __tmp3.Length]; int __index = 0; __result[__index++] = __tmp1; foreach (var __i in __tmp2) __result[__index++] = __i; foreach (var __i in __tmp3) __result[__index++] = __i; __result[__index++] = d; x = __result;
Pusty literał kolekcji
Pusty literał
[]nie ma typu. Jednak podobnie jak w przypadkuliterałuo wartości null, ten literał może być niejawnie konwertowany na dowolny typ kolekcji .Na przykład następujący przykład nie jest poprawny, ponieważ nie ma typu docelowego i nie ma żadnych innych konwersji.
var v = []; // illegalRozszerzanie pustego literału może być pominięte. Na przykład:
bool b = ... List<int> l = [x, y, .. b ? [1, 2, 3] : []];W tym przypadku, jeśli
bjest fałszywy, nie jest wymagane, aby żadna wartość została faktycznie skonstruowana dla pustego wyrażenia kolekcji, ponieważ natychmiast zostanie rozdzielona na ilość zerową w końcowym literał.Puste wyrażenie kolekcji może być singletonem, jeśli jest używane do konstruowania ostatecznej wartości kolekcji, która jest znana z tego, że nie jest modyfikowalna. Na przykład:
// Can be a singleton, like Array.Empty<int>() int[] x = []; // Can be a singleton. Allowed to use Array.Empty<int>(), Enumerable.Empty<int>(), // or any other implementation that can not be mutated. IEnumerable<int> y = []; // Must not be a singleton. Value must be allowed to mutate, and should not mutate // other references elsewhere. List<int> z = [];
Bezpieczeństwo sędziego
Zobacz bezpieczne ograniczenie kontekstu, aby zapoznać się z definicjami wartości bezpiecznego kontekstu : bloku deklaracji, elementu członkowskiego funkcji i kontekstu wywołującego.
bezpieczny kontekst wyrażenia kolekcji to:
Bezpieczny kontekst pustego wyrażenia kolekcji
[]to kontekst wywołujący.Jeśli typ docelowy jest typem rozszerzenia
System.ReadOnlySpan<T>, aTjest jednym z typów pierwotnychbool,sbyte,byte,short,ushort,char,int,uint,long,ulong,floatlubdouble, a wyrażenie kolekcji zawiera tylko wartości stałe , bezpieczny kontekst wyrażenia kolekcji jest kontekstem wywołującego .Jeśli docelowy typ to typ zakresu
System.Span<T>lubSystem.ReadOnlySpan<T>, bezpiecznym kontekstem wyrażenia kolekcji jest blok deklaracji .Jeśli typ docelowy jest typem struktury z metodą tworzenia , bezpieczny kontekst wyrażenia kolekcji jest bezpiecznym kontekstem wywołania metody tworzenia, gdzie wyrażenie kolekcji jest argumentem w zakresie metody.
W przeciwnym razie bezpiecznym kontekstem wyrażenia kolekcji jest kontekst wywołujący.
Wyrażenie kolekcji z bezpiecznym kontekstem bloku deklaracji nie może uniknąć otaczającego zakresu, a kompilator może przechowywać kolekcję na stosie, a nie stertę.
Aby umożliwić wyrażeniu kolekcji dla typu struktury ref opuszczenie bloku deklaracji , może być konieczne rzutowanie wyrażenia na inny typ.
static ReadOnlySpan<int> AsSpanConstants()
{
return [1, 2, 3]; // ok: span refers to assembly data section
}
static ReadOnlySpan<T> AsSpan2<T>(T x, T y)
{
return [x, y]; // error: span may refer to stack data
}
static ReadOnlySpan<T> AsSpan3<T>(T x, T y, T z)
{
return (T[])[x, y, z]; // ok: span refers to T[] on heap
}
Wnioskowanie typów
var a = AsArray([1, 2, 3]); // AsArray<int>(int[])
var b = AsListOfArray([[4, 5], []]); // AsListOfArray<int>(List<int[]>)
static T[] AsArray<T>(T[] arg) => arg;
static List<T[]> AsListOfArray<T>(List<T[]> arg) => arg;
Reguły wnioskowaniatypusą aktualizowane w następujący sposób.
Istniejące reguły dla pierwszej fazy są wyodrębniane do nowej sekcji wnioskowania typu wejściowego , a reguła jest dodawana do wnioskowania typu wejściowego i wnioskowania typu danych wyjściowych dla wyrażeń kolekcji.
11.6.3.2 Pierwsza faza
Dla każdego argumentu metody
Eᵢ:
- Wnioskowanie typu danych wejściowych jest wykonywane z
Eᵢdo odpowiedniego typu parametruTᵢ.Wnioskowanie typu danych wejściowych jest wykonywane z wyrażenia
Ena typuTw następujący sposób:
- Jeśli
Ejest wyrażeniem kolekcji z elementamiEᵢ, aTjest typem typu elementuTₑlubTjest typu wartości dopuszczanej do wartości nullT0?, aT0ma typ elementuTₑ, a następnie dla każdegoEᵢ:
- Jeśli
Eᵢjest elementem wyrażenia , wnioskowanie typu wejściowego jest wykonywane zEᵢdoTₑ.- Jeśli
Eᵢjest elementem rozłożonym z typu iteracjiSᵢ, to wnioskowanie dolnych granic jest wykonywane zSᵢdoTₑ.- [istniejące reguły z pierwszej fazy] ...
11.6.3.7 Wnioskowanie typu danych wyjściowych
Wnioskowanie typu danych wyjściowych jest wykonywane z wyrażenia
Edo typuTw następujący sposób:
- Jeśli
Ejest wyrażeniem kolekcji z elementamiEᵢ, aTjest typem typu elementuTₑlubTjest typu wartości dopuszczanej do wartości nullT0?, aT0ma typ elementuTₑ, a następnie dla każdegoEᵢ:
- Jeśli
Eᵢjest elementem wyrażenia , wnioskowanie typu danych wyjściowych jest wykonywane zEᵢdoTₑ.- Jeśli
Eᵢjest elementem rozłożonym , nie jest wykonywane wnioskowanie zEᵢ.- [istniejące reguły z wnioskowań typów danych wyjściowych] ...
Metody rozszerzeń
Brak zmian w regułach wywołania metody rozszerzenia .
Wywołania metody rozszerzenia 12.8.10.3
Metoda rozszerzenia
Cᵢ.Mₑjest kwalifikuje się, jeśli:
- ...
- Istnieje niejawna konwersja tożsamości, referencji lub opakowania z do typu pierwszego parametru
Mₑ.
Wyrażenie kolekcji nie ma naturalnego typu, więc istniejące konwersje z typu do nie mają zastosowania. W związku z tym wyrażenie kolekcji nie może być używane bezpośrednio jako pierwszy parametr wywołania metody rozszerzenia.
static class Extensions
{
public static ImmutableArray<T> AsImmutableArray<T>(this ImmutableArray<T> arg) => arg;
}
var x = [1].AsImmutableArray(); // error: collection expression has no target type
var y = [2].AsImmutableArray<int>(); // error: ...
var z = Extensions.AsImmutableArray([3]); // ok
Rozpoznawanie przeciążenia
Lepsza konwersja wyrażeń z została zaktualizowana, aby preferować określone typy docelowe w konwersjach wyrażeń kolekcji.
W zaktualizowanych regułach:
-
span_type jest jednym z:
System.Span<T>-
System.ReadOnlySpan<T>.
-
array_or_array_interface jest jednym z:
- typu tablicy
- jeden z następujących interfejsów zaimplementowany przez typ tablicy :
System.Collections.Generic.IEnumerable<T>System.Collections.Generic.IReadOnlyCollection<T>System.Collections.Generic.IReadOnlyList<T>System.Collections.Generic.ICollection<T>System.Collections.Generic.IList<T>
Biorąc pod uwagę niejawną konwersję
C₁, która konwertuje z wyrażeniaEna typT₁, oraz niejawna konwersjaC₂, która konwertuje z wyrażeniaEna typT₂,C₁jest lepszą konwersją niżC₂, jeśli zachodzi jedna z następujących:
Eto wyrażenia kolekcji i jedna z następujących blokad:
T₁jestSystem.ReadOnlySpan<E₁>, aT₂jestSystem.Span<E₂>, a niejawna konwersja istnieje zE₁doE₂T₁jestSystem.ReadOnlySpan<E₁>lubSystem.Span<E₁>, aT₂jest array_or_array_interface z typem elementuE₂, a niejawna konwersja istnieje zE₁doE₂T₁nie jest span_type, aT₂nie jest span_type, a niejawna konwersja istnieje zT₁doT₂Enie jest wyrażeniem kolekcji i zachodzi jedno z następujących:
Edokładnie pasuje doT₁iEnie pasuje dokładnie doT₂Edokładnie pasuje albo do obu, albo do żadnego zT₁iT₂, aT₁jest lepszym celem konwersji niżT₂Ejest grupą metod, ...
Przykłady różnic w rozwiązywaniu przeciążeń między inicjalizatorami tablicy i wyrażeniami kolekcji.
static void Generic<T>(Span<T> value) { }
static void Generic<T>(T[] value) { }
static void SpanDerived(Span<string> value) { }
static void SpanDerived(object[] value) { }
static void ArrayDerived(Span<object> value) { }
static void ArrayDerived(string[] value) { }
// Array initializers
Generic(new[] { "" }); // string[]
SpanDerived(new[] { "" }); // ambiguous
ArrayDerived(new[] { "" }); // string[]
// Collection expressions
Generic([""]); // Span<string>
SpanDerived([""]); // Span<string>
ArrayDerived([""]); // ambiguous
Typy rozpiętości
Typy rozpiętości ReadOnlySpan<T> i Span<T> to konstrukcyjne typy kolekcji . Obsługa ich odbywa się zgodnie z projektem params Span<T>. W szczególności konstruowanie jednego z tych zakresów spowoduje utworzenie tablicy T[] na stosie , jeśli tablica params mieści się w granicach (jeśli istnieje) ustawionych przez kompilator. W przeciwnym razie tablica zostanie przydzielona na stercie.
Jeśli kompilator zdecyduje się przydzielić pamięć na stosie, nie jest wymagane przetłumaczenie literału bezpośrednio na stackalloc w tym konkretnym miejscu. Na przykład, biorąc pod uwagę:
foreach (var x in y)
{
Span<int> span = [a, b, c];
// do things with span
}
Kompilator może przetłumaczyć to przy użyciu stackalloc tak długo, jak Span znaczenie pozostaje niezmienione, a bezpieczny zakres jest utrzymywany. Na przykład można powiedzieć, że powyższe można przetłumaczyć na:
Span<int> __buffer = stackalloc int[3];
foreach (var x in y)
{
__buffer[0] = a
__buffer[1] = b
__buffer[2] = c;
Span<int> span = __buffer;
// do things with span
}
Kompilator może również użyć tablic wbudowanych, jeśli są dostępne, podczas przydzielania na stosie. Należy pamiętać, że w języku C# 12 tablice wbudowane nie mogą być inicjowane za pomocą wyrażenia kolekcji. Ta funkcja jest otwartą propozycją.
Jeśli kompilator zdecyduje się na alokację na stercie, tłumaczenie dla Span<T> wygląda po prostu następująco:
T[] __array = [...]; // using existing rules
Span<T> __result = __array;
Tłumaczenie literału kolekcji
Wyrażenie kolekcji ma znaną długość, jeśli typ czasu kompilacji każdego elementu spreadu w wyrażeniu kolekcji jest zliczalny.
Tłumaczenie interfejsu
Tłumaczenie interfejsu niemutowalnego
Biorąc pod uwagę typ docelowy, który nie zawiera mutujących elementów członkowskich, a mianowicie IEnumerable<T>, IReadOnlyCollection<T>i IReadOnlyList<T>, wymagana jest zgodna implementacja w celu wygenerowania wartości, która implementuje ten interfejs. Jeśli typ jest syntetyzowany, zaleca się, aby syntetyzowany typ implementował wszystkie te interfejsy, a także ICollection<T> i IList<T>, niezależnie od typu interfejsu docelowego. Zapewnia to maksymalną zgodność z istniejącymi bibliotekami, w tym z tymi, które analizują interfejsy zaimplementowane przez wartość, aby zastosować optymalizacje wydajności.
Ponadto wartość musi implementować niegeneryczne interfejsy ICollection i IList. Dzięki temu wyrażenia kolekcji mogą obsługiwać dynamiczną introspekcję w scenariuszach, takich jak powiązanie danych.
Zgodna implementacja jest bezpłatna:
- Użyj istniejącego typu, który implementuje wymagane interfejsy.
- Syntetyzowanie typu, który implementuje wymagane interfejsy.
W obu przypadkach używany typ może implementować większy zestaw interfejsów niż te, które są ściśle wymagane.
Syntetyzowane typy mogą swobodnie stosować dowolną strategię, aby prawidłowo zaimplementować wymagane interfejsy. Na przykład syntetyzowany typ może umieścić elementy bezpośrednio w sobie, unikając konieczności dodatkowych wewnętrznych alokacji pamięci kolekcji. Syntetyzowany typ mógłby również w ogóle nie korzystać z pamięci, wybierając bezpośrednie obliczanie wartości. Na przykład zwracanie index + 1 dla [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].
- Wartość musi zwracać
truepodczas wykonywania zapytań dotyczącychICollection<T>.IsReadOnly(jeśli zaimplementowano) i niegenerycznychIList.IsReadOnlyiIList.IsFixedSize. Dzięki temu konsumenci mogą odpowiednio stwierdzić, że kolekcja nie jest modyfikowalna, pomimo implementacji widoków modyfikowalnych. - Wartość musi zostać wyrzucona na każde wywołanie metody mutacji (na przykład
IList<T>.Add). Zapewnia to bezpieczeństwo, uniemożliwiając przypadkową zmianę niemodyfikowalnej kolekcji.
Tłumaczenie interfejsu zmiennego
Podany typ docelowy zawierający mutujących członków, a mianowicie ICollection<T> lub IList<T>:
- Wartość musi być wystąpieniem
List<T>.
Tłumaczenie o znanej długości
Posiadanie znanej długości pozwala na wydajną konstrukcję wyniku z możliwością bez kopiowania danych i bez niepotrzebnego odstępu w wyniku.
Brak znanej długości nie uniemożliwia utworzenia żadnego wyniku. Jednak może to spowodować dodatkowe koszty związane z użyciem procesora i pamięci podczas generowania danych oraz ich przesyłania do ostatecznego miejsca docelowego.
W przypadku znanej długości literału
[e1, ..s1, etc]najpierw tłumaczenie zaczyna się od następującego:int __len = count_of_expression_elements + __s1.Count; ... __s_n.Count;Biorąc pod uwagę typ docelowy
Tdla tego literału:Jeśli
Tto jakiśT1[], literał jest tłumaczony jako:T1[] __result = new T1[__len]; int __index = 0; __result[__index++] = __e1; foreach (T1 __t in __s1) __result[__index++] = __t; // further assignments of the remaining elementsImplementacja ma pozwolenie na korzystanie z innych metod do wypełnienia tablicy. Na przykład użycie wydajnych metod kopiowania zbiorczego, takich jak
.CopyTo().Jeśli
Tto jakiśSpan<T1>, literał tłumaczy się tak samo jak powyżej, z wyjątkiem tego, że inicjalizacja__resultjest tłumaczona jako:Span<T1> __result = new T1[__len]; // same assignments as the array translationTłumaczenie może używać
stackalloc T1[]lub tablicy wbudowanej zamiastnew T1[], jeśli zachowane jest bezpieczeństwo rozpiętości .Jeśli
Tjest jakiśReadOnlySpan<T1>, literał jest tłumaczony tak samo jak w przypadkuSpan<T1>, z tą różnicą, że końcowy wynik będzie taki, żeSpan<T1>niejawnie przekonwertowany naReadOnlySpan<T1>.ReadOnlySpan<T1>, w którymT1jest typem pierwotnym, a wszystkie elementy kolekcji są stałe, nie wymaga, aby jego dane znajdowały się ani na stercie, ani na stosie. Na przykład implementacja może utworzyć ten zakres bezpośrednio jako odwołanie do części segmentu danych programu.Powyższe formularze (dla tablic i zakresów) są podstawowymi reprezentacjami wyrażenia kolekcji i są używane dla następujących reguł tłumaczenia:
Jeśli
Tto jakiśC<S0, S1, …>, który ma odpowiedni metodyB.M<U0, U1, …>()create, literał jest tłumaczony jako:// Collection literal is passed as is as the single B.M<...>(...) argument C<S0, S1, …> __result = B.M<S0, S1, …>([...])Ponieważ metoda create musi mieć typ argumentu określonego wystąpienia
ReadOnlySpan<T>, reguła tłumaczenia dla zakresów ma zastosowanie, gdy wyrażenie kolekcji jest przekazywane do metody create.Jeśli
Tobsługuje inicjatory kolekcji , to:jeśli typ
Tzawiera dostępny konstruktor z jednym parametremint capacity, to dosłownie jest tłumaczony jako:T __result = new T(capacity: __len); __result.Add(__e1); foreach (var __t in __s1) __result.Add(__t); // further additions of the remaining elementsUwaga: nazwa parametru musi być
capacity.Ten formularz pozwala określić nowy typ za pomocą literału, który informuje o liczbie elementów, aby umożliwić wydajną alokację pamięci wewnętrznej. Pozwala to uniknąć marnotrawnych realokacji podczas dodawania elementów.
w przeciwnym razie literał jest tłumaczony jako:
T __result = new T(); __result.Add(__e1); foreach (var __t in __s1) __result.Add(__t); // further additions of the remaining elementsUmożliwia to utworzenie typu docelowego, choć bez optymalizacji pojemności, aby zapobiec wewnętrznej reallokacji magazynu.
Tłumaczenie nieznanej długości
Biorąc pod uwagę typ docelowy
Tdla literału o nieznanej długości:Jeśli
Tobsługuje inicjatory kolekcji , literał jest tłumaczony jako:T __result = new T(); __result.Add(__e1); foreach (var __t in __s1) __result.Add(__t); // further additions of the remaining elementsUmożliwia to rozłożenie dowolnego typu iterowalnego, choć z najmniejszą możliwą ilością optymalizacji.
Jeśli
Tto jakiśT1[], literał ma taką samą semantykę jak:List<T1> __list = [...]; /* initialized using predefined rules */ T1[] __result = __list.ToArray();Powyższe jest jednak nieefektywne; tworzy listę pośrednią, a następnie tworzy kopię końcowej tablicy z niej. Implementacje mogą eliminować to poprzez optymalizację, na przykład produkując kod w następujący sposób:
T1[] __result = <private_details>.CreateArray<T1>( count_of_expression_elements); int __index = 0; <private_details>.Add(ref __result, __index++, __e1); foreach (var __t in __s1) <private_details>.Add(ref __result, __index++, __t); // further additions of the remaining elements <private_details>.Resize(ref __result, __index);Dzięki temu możliwe są minimalne odpady i kopiowanie, bez dodatkowego obciążenia kosztami, które mogłyby ponosić zbiory biblioteczne.
Liczby przekazywane do
CreateArraysą używane w celu zapewnienia wskazówek dotyczących rozmiaru początkowego, aby zapobiec marnotrawnym zmianom rozmiaru.Jeśli
Tto jakiś typ zakresu , implementacja może zastosować powyższą strategięT[]lub inną strategię z tą samą semantyką, ale lepszą wydajnością. Na przykład zamiast alokować tablicę jako kopię elementów listy, można użyćCollectionsMarshal.AsSpan(__list), aby bezpośrednio uzyskać wartość zakresu.
Nieobsługiwane scenariusze
Podczas gdy literały kolekcji mogą być używane w wielu scenariuszach, istnieje kilka scenariuszy, których nie mogą zastąpić. Należą do nich:
- Tablice wielowymiarowe (np.
new int[5, 10] { ... }). Nie istnieje funkcja uwzględniająca wymiary, a wszystkie literały kolekcji to wyłącznie struktury liniowe lub mapowe. - Kolekcje, które przekazują specjalne wartości do ich konstruktorów. Nie ma możliwości uzyskania dostępu do używanego konstruktora.
- Zagnieżdżone inicjatory kolekcji, np.
new Widget { Children = { w1, w2, w3 } }. Ten formularz musi pozostać, ponieważ ma bardzo różne semantyki odChildren = [w1, w2, w3]. Pierwsza wielokrotnie wywołuje.Addna.Children, podczas gdy druga z nich przypisze nową kolekcję do.Children. Możemy rozważyć, żeby ten ostatni formularz, jeśli nie można przypisać.Children, wrócił do dodawania do istniejącej kolekcji, ale to może być bardzo mylące.
Niejednoznaczności składni
Istnieją dwie "prawdziwe" niejednoznaczności składniowe, w których istnieje wiele prawnych interpretacji składni kodu, które używają
collection_literal_expression.spread_elementjest niejednoznaczna zrange_expression. Technicznie rzecz biorąc, można mieć:Range[] ranges = [range1, ..e, range2];Aby rozwiązać ten problem, możemy:
- Wymagaj od użytkowników wstawienia nawiasów wokół
(..e)lub dodania indeksu początkowego0..e, jeśli chcą zakresu. - Wybierz inną składnię (na przykład
...) do rozłożenia. Byłoby to niefortunne z powodu braku spójności ze schematami cięć.
- Wymagaj od użytkowników wstawienia nawiasów wokół
Istnieją dwa przypadki, w których nie ma prawdziwej niejednoznaczności, ale gdy składnia znacznie zwiększa złożoność analizy. Chociaż nie stanowi to problemu, gdy zostanie poświęcony czas na inżynierię, nadal zwiększa obciążenie poznawcze użytkowników przy przeglądaniu kodu.
Niejednoznaczność między
collection_literal_expressioniattributesw wyrażeniach lub funkcjach lokalnych. Rozważ:[X(), Y, Z()]Może to być jeden z:
// A list literal inside some expression statement [X(), Y, Z()].ForEach(() => ...); // The attributes for a statement or local function [X(), Y, Z()] void LocalFunc() { }Bez złożonego przeglądu do przodu, byłoby niemożliwe stwierdzić to bez zużycia całego literału.
Opcje rozwiązania tego problemu obejmują:
- Zezwól na to, dokonując analizy w celu określenia, który z tych przypadków ma miejsce.
- Nie zezwalaj na to i wymagaj, aby użytkownik opakowuje literał w nawiasach, takich jak
([X(), Y, Z()]).ForEach(...). - Niejednoznaczność pomiędzy
collection_literal_expressionwconditional_expressionanull_conditional_operations. Rozważ:
M(x ? [a, b, c]Może to być jeden z:
// A ternary conditional picking between two collections M(x ? [a, b, c] : [d, e, f]); // A null conditional safely indexing into 'x': M(x ? [a, b, c]);Bez złożonego przeglądu do przodu, byłoby niemożliwe stwierdzić to bez zużycia całego literału.
Uwaga: jest to problem nawet bez typu naturalnego, ponieważ typowanie docelowe ma zastosowanie za pośrednictwem
conditional_expressions.Podobnie jak w przypadku innych, możemy wymagać nawiasów dla doprecyzowania. Innymi słowy, domniemywaj interpretację
null_conditional_operation, chyba że jest napisane w ten sposób:x ? ([1, 2, 3]) :. Wydaje się to jednak dość niefortunne. Tego rodzaju kod nie wydaje się trudny do napisania, ale prawdopodobnie sprawi ludziom kłopot.
Wady
- To wprowadza kolejną formę dla wyrażeń kolekcji, do licznych już posiadanych przez nas sposobów. Jest to dodatkowa złożoność języka. Oznacza to również, że umożliwia to ujednolicenie jednej
pierścieńskładni, aby rządzić nimi wszystkimi, co oznacza, że istniejące bazy kodu można uprościć i przenieść do jednolitego wyglądu wszędzie. - Użycie
[...]zamiast{...}odchodzi od składni, którą zazwyczaj stosujemy do tablic i inicjatorów kolekcji. W szczególności, że używa[...]zamiast{...}. Jednak zostało to już rozstrzygnięte przez zespół językowy, gdy wprowadziliśmy wzorce listy. Podjęliśmy próbę dostosowania{...}do pracy z wzorcem listy i napotkaliśmy problemy nie do pokonania. Z tego powodu przenieśliśmy się do[...], co, choć nowe dla języka C#, wydaje się naturalne w wielu językach programowania i pozwoliło nam rozpocząć od nowa bez niejednoznaczności. Korzystanie z[...]jako odpowiednia forma literału jest zgodne z naszymi najnowszymi decyzjami i zapewnia nam czyste miejsce do pracy bez problemu.
Wprowadza to niedoskonałości do języka. Na przykład następujące elementy są zarówno prawne, jak i (na szczęście) oznaczają dokładnie to samo:
int[] x = { 1, 2, 3 };
int[] x = [ 1, 2, 3 ];
Jednak, biorąc pod uwagę zakres i spójność, jaką wnosi nowa składnia literałów, powinniśmy rozważyć zalecanie, aby ludzie zaczęli stosować nową formę. Sugestie i poprawki środowiska IDE mogą pomóc w tym względzie.
Alternatywy
- Jakie inne projekty zostały uznane? Jaki jest wpływ niewykonania tego?
Rozwiązane pytania
Czy kompilator powinien używać
stackallocdo alokacji pamięci na stosie, gdy tablice wbudowane są niedostępne, a typ iteracji to typ prymitywny?Rezolucja: Nie. Zarządzanie buforem
stackallocwymaga dodatkowego nakładu pracy na wbudowanej tablicy, aby upewnić się, że bufor nie jest przydzielany wielokrotnie, gdy wyrażenie kolekcji znajduje się w pętli. Dodatkowa złożoność w kompilatorze i w wygenerowanym kodzie przewyższa korzyści wynikające z alokacji na stosie na starszych platformach.W jakiej kolejności należy porównywać elementy literału w porównaniu z oceną właściwości długości/liczby? Czy najpierw należy ocenić wszystkie elementy, a następnie wszystkie długości? Czy też powinniśmy ocenić element, a następnie jego długość, następny element itd.?
Rozwiązanie: Najpierw oceniamy wszystkie elementy, a potem wszystko inne się z tym wiąże.
Czy literał o nieznanej długości może utworzyć typ kolekcji, który wymaga znanej długości, na przykład tablicy, zakresu lub kolekcji Construct(array/span)? Byłoby to trudniejsze do wydajnego wykonania, ale może być możliwe dzięki sprytnemu użyciu tablic w puli i/lub konstruktorów.
Rozwiązanie: Tak, umożliwiamy tworzenie kolekcji o stałej długości z literału o nieznanej długości. Kompilator może zaimplementować to w możliwie najbardziej wydajny sposób.
Poniższy tekst istnieje, aby zarejestrować oryginalną dyskusję na ten temat.
Użytkownicy zawsze mogą przekształcić literał o nieznanej długości w taki o znanej długości za pomocą kodu, jak pokazano poniżej:
ImmutableArray<int> x = [a, ..unknownLength.ToArray(), b];Jest to jednak niefortunne ze względu na potrzebę wymuszenia alokacji magazynu tymczasowego. Potencjalnie moglibyśmy być bardziej wydajni, jeśli kontrolowaliśmy sposób emisji.
Czy
collection_expressionmoże być używany jako typ docelowy do interfejsuIEnumerable<T>lub innych interfejsów kolekcji?Na przykład:
void DoWork(IEnumerable<long> values) { ... } // Needs to produce `longs` not `ints` for this to work. DoWork([1, 2, 3]);Rozwiązanie: Tak, literał może być przypisany do dowolnego typu interfejsu
I<T>, któryList<T>implementuje. Na przykładIEnumerable<long>. Jest to tak samo, jak przypisanie typu docelowego doList<long>, a następnie przypisanie tych wyników do określonego typu interfejsu. Poniższy tekst istnieje, aby zarejestrować oryginalną dyskusję na ten temat.Otwarte pytanie polega na określeniu, jaki typ bazowy ma zostać utworzony. Jedną z opcji jest przyjrzenie się propozycji dotyczącej
params IEnumerable<T>. W tym miejscu wygenerujemy tablicę w celu przekazania wartości, podobnie jak w przypadkuparams T[].Czy kompilator może emitować
Array.Empty<T>()dla[]? Czy powinniśmy nakazać, aby to robiło to, aby uniknąć alokacji, gdy jest to możliwe?Tak. Kompilator powinien emitować
Array.Empty<T>()w każdym przypadku, gdy jest to legalne, a końcowy wynik nie jest modyfikowalny. Na przykład celowanie wT[],IEnumerable<T>,IReadOnlyCollection<T>lubIReadOnlyList<T>. Nie należy używaćArray.Empty<T>, gdy element docelowy jest modyfikowalny (ICollection<T>lubIList<T>).Czy powinniśmy rozszerzyć inicjatory kolekcji, aby wyszukać bardzo typową metodę
AddRange? Może on być używany przez podstawowy skonstruowany typ do wykonywania dodawania elementów rozłożonych potencjalnie wydajniej. Możemy również szukać takich rzeczy jak.CopyTo. W tym miejscu mogą wystąpić wady, ponieważ te metody mogą powodować nadmiar alokacji/wysyłania w porównaniu z bezpośrednim wyliczaniem w przetłumaczonym kodzie.Tak. Implementacja może wykorzystywać inne metody do inicjowania wartości kolekcji, zgodnie z domniemaniem, że te metody mają dobrze zdefiniowaną semantykę i że typy kolekcji powinny dobrze się zachowywać. W praktyce jednak implementacja powinna być ostrożna, ponieważ korzyści w jednej dziedzinie (zbiorcze kopiowanie) mogą również prowadzić do negatywnych konsekwencji (na przykład opakowanie kolekcji struktur).
Implementacja powinna korzystać z zalet w przypadkach, w których nie ma żadnych wad. Na przykład za pomocą metody
.AddRange(ReadOnlySpan<T>).
Nierozwiązane pytania
- Czy powinniśmy zezwolić na wnioskowanie typu elementu , gdy typ iteracji jest "niejednoznaczny" według pewnej definicji? Na przykład:
Collection x = [1L, 2L];
// error CS1640: foreach statement cannot operate on variables of type 'Collection' because it implements multiple instantiations of 'IEnumerable<T>'; try casting to a specific interface instantiation
foreach (var x in new Collection) { }
static class Builder
{
public Collection Create(ReadOnlySpan<long> items) => throw null;
}
[CollectionBuilder(...)]
class Collection : IEnumerable<int>, IEnumerable<string>
{
IEnumerator<int> IEnumerable<int>.GetEnumerator() => throw null;
IEnumerator<string> IEnumerable<string>.GetEnumerator() => throw null;
IEnumerator IEnumerable.GetEnumerator() => throw null;
}
Czy powinno być legalne utworzenie i natychmiastowe indeksowanie literału kolekcji? Uwaga: wymaga to odpowiedzi na nierozwiązane pytanie poniżej, czy literały kolekcji mają typ naturalny.
Przydzielanie pamięci na stosie dla dużych kolekcji może przeciążać stos. Czy kompilator powinien mieć heurystyczny sposób umieszczania tych danych na stercie? Czy język powinien być nieokreślony, aby umożliwić tę elastyczność? Należy przestrzegać specyfikacji dla
params Span<T>.Czy musimy określić typ docelowy dla
spread_element? Rozważmy na przykład:Span<int> span = [a, ..b ? [c] : [d, e], f];Uwaga: często może się to pojawić w następującej postaci, aby umożliwić warunkowe włączenie niektórych zestawów elementów lub nic, jeśli warunek jest fałszywy:
Span<int> span = [a, ..b ? [c, d, e] : [], f];Aby ocenić ten pełny literał, musimy ocenić wyrażenia elementów wewnątrz niego. Oznacza to możliwość oceny
b ? [c] : [d, e]. Jednak przy braku typu docelowego do oceny tego wyrażenia w kontekście oraz braku jakiegokolwiek rodzaju typu naturalnego, nie moglibyśmy określić, co zrobić z[c]lub[d, e]tutaj.Aby rozwiązać ten problem, możemy powiedzieć, że podczas obliczania wyrażenia
spread_elementliterału istnieje niejawny typ docelowy odpowiadający typowi docelowemu samego literału. Zatem powyższe można przeformułować jako:int __e1 = a; Span<int> __s1 = b ? [c] : [d, e]; int __e2 = f; Span<int> __result = stackalloc int[2 + __s1.Length]; int __index = 0; __result[__index++] = a; foreach (int __t in __s1) __result[index++] = __t; __result[__index++] = f; Span<int> span = __result;
Specyfikacja typu kolekcjikonstruowalnego przy użyciu metody tworzenia jest wrażliwa na kontekst, w którym jest klasyfikowana konwersja
Istnienie konwersji w tym przypadku zależy od pojęcia typu iteracji typu kolekcji . Jeśli istnieje metoda tworzenia, która przyjmuje ReadOnlySpan<T>, gdzie T jest typem iteracji , istnieje konwersja. W przeciwnym razie nie działa.
Jednak typ iteracji jest wrażliwy na kontekst, w którym foreach jest wykonywana. W przypadku tego samego typu kolekcji może on różnić się w zależności od zakresu metod rozszerzeń i może być również niezdefiniowany.
To wydaje się odpowiednie dla celu foreach, gdy typ nie jest zaprojektowany do iterowania sam siebie. Jeśli tak jest, metody rozszerzeń nie mogą zmienić sposobu, w jaki typ jest foreach-ed over, niezależnie od kontekstu.
Jednak to wydaje się nieco dziwne, że konwersja jest wrażliwa na kontekst w ten sposób. W rzeczywistości konwersja jest "niestabilna". Typ kolekcji jawnie zaprojektowany tak, aby był konstruowalny, może pominąć definicję bardzo ważnego szczegółu — jego typ iteracji . Pozostawienie typu "niekonwertowalny" samemu sobie.
Oto przykład:
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
[CollectionBuilder(typeof(MyCollectionBuilder), nameof(MyCollectionBuilder.Create))]
class MyCollection
{
}
class MyCollectionBuilder
{
public static MyCollection Create(ReadOnlySpan<long> items) => throw null;
public static MyCollection Create(ReadOnlySpan<string> items) => throw null;
}
namespace Ns1
{
static class Ext
{
public static IEnumerator<long> GetEnumerator(this MyCollection x) => throw null;
}
class Program
{
static void Main()
{
foreach (var l in new MyCollection())
{
long s = l;
}
MyCollection x1 = ["a", // error CS0029: Cannot implicitly convert type 'string' to 'long'
2];
}
}
}
namespace Ns2
{
static class Ext
{
public static IEnumerator<string> GetEnumerator(this MyCollection x) => throw null;
}
class Program
{
static void Main()
{
foreach (var l in new MyCollection())
{
string s = l;
}
MyCollection x1 = ["a",
2]; // error CS0029: Cannot implicitly convert type 'int' to 'string'
}
}
}
namespace Ns3
{
class Program
{
static void Main()
{
// error CS1579: foreach statement cannot operate on variables of type 'MyCollection' because 'MyCollection' does not contain a public instance or extension definition for 'GetEnumerator'
foreach (var l in new MyCollection())
{
}
MyCollection x1 = ["a", 2]; // error CS9188: 'MyCollection' has a CollectionBuilderAttribute but no element type.
}
}
}
Biorąc pod uwagę bieżący projekt, jeśli typ sam nie definiuje typu iteracji , kompilator nie może niezawodnie zweryfikować zastosowania atrybutu CollectionBuilder. Jeśli nie znamy typu iteracji , nie wiemy, jaki powinien być podpis metody tworzenia . Jeśli typ iteracji pochodzi z kontekstu, nie ma gwarancji, że typ będzie zawsze używany w podobnym kontekście.
Kolekcja parametrów jest również na to obciążona. Odczuwalne jest dziwne uczucie niemożności niezawodnego przewidzenia typu elementu parametru params w punkcie deklaracji. Obecna propozycja wymaga również zapewnienia, że metoda tworzenia jest co najmniej tak dostępna, jak typ kolekcji params. Nie można wykonać tej kontroli w niezawodny sposób, chyba że typ kolekcji definiuje jego typ iteracji .
Należy pamiętać, że mamy również https://github.com/dotnet/roslyn/issues/69676 otwarty dla kompilatora, który w zasadzie obserwuje ten sam problem, ale mówi o nim z perspektywy optymalizacji.
Propozycja
Wymagaj typu korzystającego z atrybutu CollectionBuilder, aby zdefiniować swój typ iteracji na sobie.
Innymi słowy oznacza to, że typ powinien implementować IEnumarable/IEnumerable<T>lub powinien mieć publiczną metodę GetEnumerator z prawidłowym podpisem (wyklucza to wszelkie metody rozszerzenia).
Ponadto, teraz metoda create jest wymagana, aby "być dostępną tam, gdzie jest używane wyrażenie kolekcji". Jest to kolejny punkt zależności kontekstu w oparciu o dostępność. Cel tej metody jest bardzo podobny do celu metody konwersji zdefiniowanej przez użytkownika i że musi być publiczny. Dlatego należy rozważyć wymaganie, aby metoda tworzenia również była publiczna.
Konkluzja
Zatwierdzone z modyfikacjami LDM-2024-01-08
Pojęcie typu iteracji nie jest stosowane spójnie w ramach konwersji
- Do struktury lub typu klasy , który implementuje
System.Collections.Generic.IEnumerable<T>gdzie:
- Dla każdego elementu
Eiistnieje niejawna konwersjaT.
Wygląda na to, że przyjmuje się założenie, że T jest konieczne typu iteracji struktury lub typu klasy w tym przypadku.
Jednak to założenie jest nieprawidłowe. Co może prowadzić do bardzo dziwnego zachowania. Na przykład:
using System.Collections;
using System.Collections.Generic;
class MyCollection : IEnumerable<long>
{
IEnumerator<long> IEnumerable<long>.GetEnumerator() => throw null;
IEnumerator IEnumerable.GetEnumerator() => throw null;
public void Add(string l) => throw null;
public IEnumerator<string> GetEnumerator() => throw null;
}
class Program
{
static void Main()
{
foreach (var l in new MyCollection())
{
string s = l; // Iteration type is string
}
MyCollection x1 = ["a", // error CS0029: Cannot implicitly convert type 'string' to 'long'
2];
MyCollection x2 = new MyCollection() { "b" };
}
}
- W przypadku struktury lub typ klasy implementujący
System.Collections.IEnumerablei nie implementujeSystem.Collections.Generic.IEnumerable<T>.
Wygląda na to, że implementacja zakłada, że typ iteracji jest object, ale specyfikacja nie określa tego faktu i w ogóle nie wymaga, aby każdy element był przekonwertowany na cokolwiek. Ogólnie jednak typ iteracji nie jest konieczny dla typu object. Które można zaobserwować w poniższym przykładzie:
using System.Collections;
using System.Collections.Generic;
class MyCollection : IEnumerable
{
public IEnumerator<string> GetEnumerator() => throw null;
IEnumerator IEnumerable.GetEnumerator() => throw null;
}
class Program
{
static void Main()
{
foreach (var l in new MyCollection())
{
string s = l; // Iteration type is string
}
}
}
Pojęcie typu iteracji ma podstawowe znaczenie dla kolekcji params funkcji. I ten problem prowadzi do dziwnej rozbieżności między dwiema funkcjami. Na przykład:
using System.Collections;
using System.Collections.Generic;
class MyCollection : IEnumerable<long>
{
IEnumerator<long> IEnumerable<long>.GetEnumerator() => throw null;
IEnumerator IEnumerable.GetEnumerator() => throw null;
public IEnumerator<string> GetEnumerator() => throw null;
public void Add(long l) => throw null;
public void Add(string l) => throw null;
}
class Program
{
static void Main()
{
Test("2"); // error CS0029: Cannot implicitly convert type 'string' to 'long'
Test(["2"]); // error CS1503: Argument 1: cannot convert from 'collection expressions' to 'string'
Test(3); // error CS1503: Argument 1: cannot convert from 'int' to 'string'
Test([3]); // Ok
MyCollection x1 = ["2"]; // error CS0029: Cannot implicitly convert type 'string' to 'long'
MyCollection x2 = [3];
}
static void Test(params MyCollection a)
{
}
}
using System.Collections;
using System.Collections.Generic;
class MyCollection : IEnumerable
{
IEnumerator IEnumerable.GetEnumerator() => throw null;
public IEnumerator<string> GetEnumerator() => throw null;
public void Add(object l) => throw null;
}
class Program
{
static void Main()
{
Test("2", 3); // error CS1503: Argument 2: cannot convert from 'int' to 'string'
Test(["2", 3]); // Ok
}
static void Test(params MyCollection a)
{
}
}
Prawdopodobnie dobrze będzie zdecydować się na jedną z opcji.
Propozycja
Określ możliwość konwersji struktury lub typu klasy , która implementuje System.Collections.Generic.IEnumerable<T> lub System.Collections.IEnumerable, w kontekście typu iteracji , i wymaga niejawnej konwersji dla każdego elementu Ei do typu iteracji .
Konkluzja
Zatwierdzone LDM-2024-01-08
Czy konwersja wyrażeń kolekcji wymaga dostępności minimalnego zestawu interfejsów API do budowy?
konstruowalny typ kolekcji zgodnie z konwersjami może się okazać nieskonstruowalny, co może prowadzić do nieoczekiwanego zachowania podczas rozwiązywania przeciążeń. Na przykład:
class C1
{
public static void M1(string x)
{
}
public static void M1(char[] x)
{
}
void Test()
{
M1(['a', 'b']); // error CS0121: The call is ambiguous between the following methods or properties: 'C1.M1(string)' and 'C1.M1(char[])'
}
}
Jednak 'C1.M1(string)' nie jest kandydatem, którego można użyć, ponieważ:
error CS1729: 'string' does not contain a constructor that takes 0 arguments
error CS1061: 'string' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'string' could be found (are you missing a using directive or an assembly reference?)
Oto inny przykład z typem zdefiniowanym przez użytkownika i silniejszym błędem, który nawet nie wspomina o prawidłowym kandydatu:
using System.Collections;
using System.Collections.Generic;
class C1 : IEnumerable<char>
{
public static void M1(C1 x)
{
}
public static void M1(char[] x)
{
}
void Test()
{
M1(['a', 'b']); // error CS1061: 'C1' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'C1' could be found (are you missing a using directive or an assembly reference?)
}
public static implicit operator char[](C1 x) => throw null;
IEnumerator<char> IEnumerable<char>.GetEnumerator() => throw null;
IEnumerator IEnumerable.GetEnumerator() => throw null;
}
Wygląda na to, że sytuacja jest bardzo podobna do tego, co użyliśmy z grupą metod w celu delegowania konwersji. To znaczy, były scenariusze, w których konwersja istniała, ale była błędna. Postanowiliśmy to poprawić, upewniając się, że jeśli konwersja jest błędna, to nie istnieje.
Należy pamiętać, że w przypadku funkcji "Kolekcje parametrów" możemy napotkać podobny problem. Dobrym rozwiązaniem może być uniemożliwienie używania modyfikatora params w przypadku kolekcji nieskonstruowalnych. Jednak w bieżącej propozycji to sprawdzenie opiera się na konwersji sekcji. Oto przykład:
using System.Collections;
using System.Collections.Generic;
class C1 : IEnumerable<char>
{
public static void M1(params C1 x) // It is probably better to report an error about an invalid `params` modifier
{
}
public static void M1(params ushort[] x)
{
}
void Test()
{
M1('a', 'b'); // error CS1061: 'C1' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'C1' could be found (are you missing a using directive or an assembly reference?)
M2('a', 'b'); // Ok
}
public static void M2(params ushort[] x)
{
}
IEnumerator<char> IEnumerable<char>.GetEnumerator() => throw null;
IEnumerator IEnumerable.GetEnumerator() => throw null;
}
Wygląda na to, że problem został nieco omówiony wcześniej, zobacz https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-02.md#collection-expressions. W tym czasie podniesiono argument, że reguły, jak obecnie określono, są zgodne ze sposobem określenia procedury obsługi ciągów interpolowanych. Oto cytat:
W szczególności programy obsługi ciągów interpolowanych zostały pierwotnie określone w ten sposób, ale zmieniliśmy specyfikację po rozważeniu tego problemu.
Chociaż istnieje pewne podobieństwo, istnieje również ważne rozróżnienie, które warto wziąć pod uwagę. Oto cytat z https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/improved-interpolated-strings.md#interpolated-string-handler-conversion:
Mówi się, że typ
Tjest applicable_interpolated_string_handler_type, jeśli jest oznaczonySystem.Runtime.CompilerServices.InterpolatedStringHandlerAttribute. Istnieje niejawna interpolated_string_handler_conversion doTz interpolated_string_expressionlub additive_expression składa się wyłącznie z _interpolated_string_expression_s i używania tylko operatorów+.
Typ docelowy musi mieć specjalny atrybut, który jest silnym wskaźnikiem intencji autora, aby typ był obsługiwaczem ciągów interpolowanych. Należy założyć, że obecność atrybutu nie jest zbieg okoliczności.
Natomiast fakt, że typ jest "wyliczalny", nie musi oznaczać, że istnieje intencja autora, aby typ był konstruowalny. Obecność metody create , która jest jednak wskazywana za pomocą atrybutu [CollectionBuilder(...)] w typie kolekcji , wydaje się być silnym wskaźnikiem intencji autora, aby typ był konstruowalny.
Propozycja
W przypadku struktury lub typu klasy, który implementuje System.Collections.IEnumerable i nie ma metody create, sekcjakonwersji powinna wymagać obecności co najmniej następujących API:
- Dostępny konstruktor, który może być użyty bez argumentów.
- Dostępne wystąpienie
Addlub metoda rozszerzenia, którą można wywołać przy użyciu wartości typu iteracji jako argumentu.
Na potrzeby funkcji Params Collectons takie typy są uznawane za prawidłowe params typy, gdy te interfejsy API są deklarowane jako publiczne oraz są metodami instancji, w odróżnieniu od metod rozszerzeń.
Konkluzja
Zatwierdzone z modyfikacjami LDM-2024-01-10
C# feature specifications