Udostępnij za pomocą


Wyrażenia kolekcji

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:

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[] lub new[] przed wartościami { ... }.
    • Zakresy, które mogą używać stackalloc i innych kłopotliwych konstrukcji.
    • Inicjatory kolekcji, które wymagają składni, takiej jak new List<T> (z pominięciem prawdopodobnie bardziej rozbudowanej T) przed przypisaniem wartości, i które mogą powodować wielokrotne przesunięcia pamięci, ponieważ używają N .Add wywoł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 jak ImmutableArray.CreateBuilder) są niewygodne i nadal produkują nieuniknione śmieci.
  • 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_expression będzie określany jako "literał" w poniższych sekcjach.

  • wystąpienia expression_element są często określane jako e1, e_nitp.

  • wystąpienia spread_element są często określane jako ..s1, ..s_nitp.

  • typ zakresu oznacza Span<T> lub ReadOnlySpan<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_element w nich.
    • Literały bez spread_element w nich.
    • Literały z dowolną kolejnością dowolnego typu elementu.
  • Typ iteracji związany z ..s_n to typ zmiennej iteracyjnej , określony tak, jakby s_n było używane jako wyrażenie w procesie iteracyjnym w foreach_statement.

  • Zmienne rozpoczynające się od __name są używane do reprezentowania wyników oceny name, przechowywanych w lokalizacji, tak aby była obliczana tylko raz. Na przykład __e1 to ocena e1.

  • List<T>, IEnumerable<T>itp. odwołują się do odpowiednich typów w przestrzeni nazw System.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żenie new int[] { 1, 2, 3 }, które samo w sobie osadza nieprzetworzone dane w kodzie, pomijając potrzebę użycia __index lub 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.
  • 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ść Count w 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.Generic są 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 iteracja x i 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.

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 jest T
  • Typ przedziału :
    • System.Span<T>
    • System.ReadOnlySpan<T>
      W tym przypadku typ elementu jest T
  • Typ z odpowiednią metodą tworzenia , w tym przypadku typ elementu jest typem iteracji określanym na podstawie metody wystąpienia GetEnumerator lub interfejsu wyliczalnego, a nie z metody rozszerzenia.
  • Typ klasy lub, który implementuje System.Collections.IEnumerable gdzie:
    • 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.

      W tym przypadku typ elementu to typ iteracji typu .

  • 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 jest T

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 z Eᵢ na T.
  • Jeśli Eᵢ jest elementem rozprzestrzeniającym ..Sᵢ, istnieje niejawna konwersja z typu iteracji z Sᵢ na T.

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 nullT?, w którym istnieje konwersji wyrażenia kolekcji z wyrażenia kolekcji na typ wartości T. Konwersja to konwersji wyrażeń kolekcji na T, po której następuje niejawna konwersja niejawna konwersja dopuszczana do wartości null z T do T?.

  • Do typu odwołania T, w którym istnieje metoda tworzenia skojarzona z T, która zwraca U typu i niejawną konwersję odwołania z U do T. Konwersja to konwersji wyrażeń kolekcji do U następnie niejawnej konwersji odwołań z U do T.

  • Do typu interfejsu I, w którym istnieje metoda tworzenia skojarzona z I, która zwraca typ V i niejawną konwersję boksu z V do I. Konwersja jest konwersją wyrażenia kolekcji na V, a następnie niejawną konwersją boksu z V na I.

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 Add lub 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łania Add nie muszą być koniecznie przeplatane.
    • Jeśli element jest elementem spread, jest używany jeden z następujących elementów:
      • Odpowiednie wystąpienie GetEnumerator lub metoda rozszerzenia jest wywoływane na wyrażenia elementu spread i dla każdego elementu z modułu wyliczającego odpowiednie wystąpienie Add lub metodę rozszerzenia jest wywoływana w wystąpieniu kolekcji z elementem jako argumentem. Jeśli moduł wyliczający implementuje IDisposable, to Dispose zostanie wywołany po wyliczeniu, niezależnie od wyjątków.
      • Wywoływane jest odpowiednie wystąpienie AddRange lub metoda rozszerzenia na instancji kolekcji , z wyrażeniem elementu spread jako argumentem.
      • Na wyrażeniu elementu rozszerzonego CopyTo wywoływane jest odpowiednie wystąpienie lub metoda rozszerzenia przy użyciu instancji kolekcji i indeksu int jako argumentów.
  • Podczas powyższych kroków budowy odpowiednia instancja EnsureCapacity lub metoda rozszerzenia może być wywoływana jeden lub więcej razy na wystąpieniu kolekcji z argumentem pojemności int.


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 GetEnumerator jest 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 implementuje IDisposable, to Dispose zostanie wywołany po wyliczeniu, niezależnie od wyjątków.
      • Odpowiednie wystąpienie CopyTo lub metoda rozszerzenia jest wywoływana w wyrażeniu elementu spread z wystąpieniem inicjowania i indeksem int jako 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 b i czliczalne, kompilator może opóźnić dodawanie elementów z a i b do czasu zakończenia oceny c, co pozwoli na przydzielenie wynikowej tablicy o oczekiwanej długości. Następnie kompilator może z niecierpliwością dodać elementy z cprzed 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 = []; // illegal
    
  • Rozszerzanie 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 b jest 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>, a T jest jednym z typów pierwotnych bool, sbyte, byte, short, ushort, char, int, uint, long, ulong, floatlub double, 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> lub System.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 zEᵢdo odpowiedniego typu parametru Tᵢ.

Wnioskowanie typu danych wejściowych jest wykonywane z wyrażenia Ena typu T w następujący sposób:

  • Jeśli E jest wyrażeniem kolekcji z elementami Eᵢ, a T jest typem typu elementu Tₑ lub T jest typu wartości dopuszczanej do wartości nullT0?, a T0 ma typ elementu Tₑ, a następnie dla każdego Eᵢ:
    • 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 typu T w następujący sposób:

  • Jeśli E jest wyrażeniem kolekcji z elementami Eᵢ, a T jest typem typu elementu Tₑ lub T jest typu wartości dopuszczanej do wartości nullT0?, a T0 ma typ elementu Tₑ, a następnie dla każdego Eᵢ:
    • 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 z Eᵢ.
  • [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żenia E na typ T₁, oraz niejawna konwersja C₂, która konwertuje z wyrażenia E na typ T₂, C₁ jest lepszą konwersją niż C₂, jeśli zachodzi jedna z następujących:

  • E to wyrażenia kolekcji i jedna z następujących blokad:
    • T₁ jest System.ReadOnlySpan<E₁>, a T₂ jest System.Span<E₂>, a niejawna konwersja istnieje z E₁ do E₂
    • T₁ jest System.ReadOnlySpan<E₁> lub System.Span<E₁>, a T₂ jest array_or_array_interface z typem elementu E₂, a niejawna konwersja istnieje z E₁ do E₂
    • T₁ nie jest span_type, a T₂ nie jest span_type, a niejawna konwersja istnieje z T₁ do T₂
  • E nie jest wyrażeniem kolekcji i zachodzi jedno z następujących:
    • E dokładnie pasuje do T₁ i E nie pasuje dokładnie do T₂
    • E dokładnie pasuje albo do obu, albo do żadnego z T₁ i T₂, a T₁ jest lepszym celem konwersji niż T₂
  • E jest 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:

  1. Użyj istniejącego typu, który implementuje wymagane interfejsy.
  2. 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].

  1. Wartość musi zwracać true podczas wykonywania zapytań dotyczących ICollection<T>.IsReadOnly (jeśli zaimplementowano) i niegenerycznych IList.IsReadOnly i IList.IsFixedSize. Dzięki temu konsumenci mogą odpowiednio stwierdzić, że kolekcja nie jest modyfikowalna, pomimo implementacji widoków modyfikowalnych.
  2. 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>:

  1. 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 T dla tego literału:

    • Jeśli T to 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 elements
      

      Implementacja 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 T to jakiś Span<T1>, literał tłumaczy się tak samo jak powyżej, z wyjątkiem tego, że inicjalizacja __result jest tłumaczona jako:

      Span<T1> __result = new T1[__len];
      
      // same assignments as the array translation
      

      Tłumaczenie może używać stackalloc T1[] lub tablicy wbudowanej zamiast new T1[], jeśli zachowane jest bezpieczeństwo rozpiętości .

    • Jeśli T jest jakiś ReadOnlySpan<T1>, literał jest tłumaczony tak samo jak w przypadku Span<T1>, z tą różnicą, że końcowy wynik będzie taki, że Span<T1>niejawnie przekonwertowany na ReadOnlySpan<T1>.

      ReadOnlySpan<T1>, w którym T1 jest 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 T to 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 T obsługuje inicjatory kolekcji , to:

        • jeśli typ T zawiera dostępny konstruktor z jednym parametrem int 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 elements
          

          Uwaga: 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 elements
          

          Umoż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 T dla literału o nieznanej długości:

    • Jeśli T obsł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 elements
      

      Umożliwia to rozłożenie dowolnego typu iterowalnego, choć z najmniejszą możliwą ilością optymalizacji.

    • Jeśli T to 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 CreateArray są używane w celu zapewnienia wskazówek dotyczących rozmiaru początkowego, aby zapobiec marnotrawnym zmianom rozmiaru.

    • Jeśli T to 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 od Children = [w1, w2, w3]. Pierwsza wielokrotnie wywołuje .Add na .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_element jest niejednoznaczna z range_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ątkowego 0..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ęć.
  • 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_expression i attributes w 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_expression w conditional_expression a null_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ć stackalloc do alokacji pamięci na stosie, gdy tablice wbudowane są niedostępne, a typ iteracji to typ prymitywny?

    Rezolucja: Nie. Zarządzanie buforem stackalloc wymaga 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_expression może być używany jako typ docelowy do interfejsu IEnumerable<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óry List<T> implementuje. Na przykład IEnumerable<long>. Jest to tak samo, jak przypisanie typu docelowego do List<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 przypadku params 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 w T[], IEnumerable<T>, IReadOnlyCollection<T> lub IReadOnlyList<T>. Nie należy używać Array.Empty<T>, gdy element docelowy jest modyfikowalny (ICollection<T> lub IList<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_element literał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 Ei istnieje niejawna konwersja T.

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.IEnumerable i 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 T jest applicable_interpolated_string_handler_type, jeśli jest oznaczony System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute. Istnieje niejawna interpolated_string_handler_conversion do T z 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 Add lub 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