Бөлісу құралы:


Выражения коллекции

Замечание

Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Этот документ включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.

Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия отражены в соответствующих заметках с заседания по дизайну языка (LDM) .

Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .

Вопрос чемпиона: https://github.com/dotnet/csharplang/issues/8652

Сводка

Выражения коллекции представляют новый синтаксис terse, [e1, e2, e3, etc]чтобы создать общие значения коллекции. Встраивание других коллекций в эти значения возможно с помощью распределенного элемента ..e , как показано ниже [e1, ..c2, e2, ..c2].

Можно создать несколько типов, таких как коллекция, без поддержки внешнего BCL. Ниже приведены следующие типы:

Дополнительная поддержка имеется для типов, таких как коллекция, которые не рассматриваются выше с помощью нового атрибута и шаблона API, которые можно применить непосредственно к самому типу.

Мотивация

  • Значения, подобные коллекции, очень присутствуют в программировании, алгоритмах и особенно в экосистеме C#/.NET. Почти все программы будут использовать эти значения для хранения данных и отправки или получения данных из других компонентов. В настоящее время почти все программы C# должны использовать множество различных и, к сожалению, подробные подходы для создания экземпляров таких значений. Некоторые подходы также имеют недостатки производительности. Ниже приведены некоторые распространенные примеры.

    • Массивы, требующие либо до значений, либо new Type[]new[] перед значениями { ... } .
    • Диапазоны, которые могут использовать stackalloc и другие громоздкие конструкции.
    • Инициализаторы коллекций, которые требуют синтаксиса, например new List<T> (без вывода возможно подробного T) до их значений, и которые могут вызвать несколько перемещений памяти, так как они используют вызовы N .Add без предоставления начальной емкости.
    • Неизменяемые коллекции, которые требуют синтаксиса, например ImmutableArray.Create(...) инициализации значений, и которые могут привести к промежуточным выделениям и копированию данных. Более эффективные строительные формы (например ImmutableArray.CreateBuilder, неуправляемые) и по-прежнему создают неизменяемый мусор.
  • Глядя на окружающую экосистему, мы также находим примеры во всем мире создания списка, будучи более удобным и приятным для использования. TypeScript, Dart, Swift, Elm, Python и многое другое выбирают краткий синтаксис для этой цели, с широким использованием и большим эффектом. Курсорные исследования не показали никаких существенных проблем, возникающих в этих экосистемах с наличием этих литерала встроенных.

  • C# также добавил шаблоны списков в C# 11. Этот шаблон позволяет сопоставить и деконструкция значений, таких как список, с помощью чистого и интуитивно понятного синтаксиса. Однако, в отличие от почти всех других конструкций шаблонов, этот синтаксис сопоставления и деконструкции не имеет соответствующего синтаксиса построения.

  • Получение оптимальной производительности для создания каждого типа коллекции может быть сложной задачей. Простые решения часто тратят как ЦП, так и память. Наличие литеральной формы позволяет обеспечить максимальную гибкость от реализации компилятора, чтобы оптимизировать литерал, чтобы получить по крайней мере хороший результат, как пользователь мог предоставить, но с простым кодом. Очень часто компилятор сможет сделать лучше, и спецификация направлена на то, чтобы обеспечить реализацию больших объемов свободы с точки зрения стратегии реализации.

Для C#требуется инклюзивное решение. Он должен соответствовать подавляющему большинству casse для клиентов с точки зрения типов коллекций и значений, которые у них уже есть. Он также должен чувствовать себя естественным на языке и зеркально работать в сопоставлении шаблонов.

Это приводит к естественному выводу о том, что синтаксис должен быть похож [e1, e2, e3, e-etc] или [e1, ..c2, e2], который соответствует эквивалентам [p1, p2, p3, p-etc] шаблона и [p1, ..p2, p3].

Подробный дизайн

Добавлены следующие грамматические производства:

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
  ;

Известно, что грамматика collection_element представляет неоднозначность синтаксиса. .. expr В частности, именно производственный орган spread_elementдля , а также доступен через expression_element -> expression -> ... -> range_expression. Существует простое правило collection_elementsдля иерархии. В частности, если элемент лексически начинается с.., то он всегда рассматривается как .spread_element Например, всегда рассматривается как (так) ..x ? y : z; даже если он может быть юридически проанализирован как выражение (например(..x) ? y : z)... (x ? y : z)spread_element

Это полезно двумя способами. Во-первых, реализация компилятора должна рассматривать только первый маркер, который он видит, чтобы определить, что нужно проанализировать далее (a spread_element или expression_element). Во-вторых, пользователь может тривиально понять, какой элемент у них есть, не пытаясь проанализировать то, что следует, чтобы увидеть, следует ли думать о нем как о распространении или выражении.

Литералы коллекции являются целевыми типами.

Уточнения спецификаций

  • Для краткости collection_expression будет называться "литеральным" в следующих разделах.

  • expression_element Экземпляры обычно называются e1и e_nт. д.

  • spread_element Экземпляры обычно называются ..s1и ..s_nт. д.

  • Тип диапазона означает либоSpan<T>.ReadOnlySpan<T>

  • Литералы обычно отображаются как [e1, ..s1, e2, ..s2, etc] для передачи любого количества элементов в любом порядке. Важно, что эта форма будет использоваться для представления всех случаев, таких как:

    • Пустые литералы []
    • Литералы без expression_element них.
    • Литералы без spread_element них.
    • Литералы с произвольным упорядочением любого типа элемента.
  • Тип ..s_n — это тип переменной итерации, определяемой как если бы s_n они использовались в качестве итерации выражения в объектеforeach_statement.

  • Переменные, начиная __name с, используются для представления результатов вычисления name, хранящегося в расположении, чтобы он был оценен только один раз. Например __e1 , оценка e1.

  • List<T>, IEnumerable<T>и т. д. относятся к соответствующим типам System.Collections.Generic в пространстве имен.

  • Спецификация определяет перевод литерала в существующие конструкции C#. Как и перевод выражений запроса, литерал сам по себе является законным, если перевод приведет к юридическому коду. Цель этого правила заключается в том, чтобы избежать необходимости повторять другие правила языка, подразумеваемого (например, о преобразовании выражений при назначении расположениям хранилища).

  • Реализация не требуется для перевода литералы точно так же, как указано ниже. Любой перевод является законным, если тот же результат производится и нет заметных различий в производстве результата.

    • Например, реализация может переводить литералы, такие как [1, 2, 3] непосредственно в new int[] { 1, 2, 3 } выражение, которое сама запекает необработанные данные в сборку, ускользая от необходимости __index или последовательности инструкций для назначения каждого значения. Важно, что это означает, что любой шаг перевода может вызвать исключение во время выполнения, что состояние программы по-прежнему остается в состоянии, указанном переводом.
  • Ссылки на "выделение стека" ссылаются на любую стратегию выделения в стеке, а не кучу. Важно, что она не подразумевает или не требует того, чтобы эта стратегия была через фактический stackalloc механизм. Например, использование встроенных массивов также является допустимым и желательным подходом для выполнения выделения стека, где он доступен. Обратите внимание, что в C# 12 встроенные массивы нельзя инициализировать с выражением коллекции. Это остается открытым предложением.

  • Предполагается, что коллекции хорошо ведут себя. Рассмотрим пример.

    • Предполагается, что значение Count коллекции будет производить то же значение, что и количество элементов при перечислении.
    • Типы, используемые в этом спецификации, определенные в System.Collections.Generic пространстве имен, считаются побочными эффектами без побочных эффектов. Таким образом, компилятор может оптимизировать сценарии, в которых такие типы могут использоваться в качестве промежуточных значений, но в противном случае не предоставляются.
    • Предполагается, что вызов определенного применимого .AddRange(x) элемента в коллекции приведет к тому же окончательному значению, что итерация x и добавление всех перечисленных значений отдельно в коллекцию..Add
    • Поведение литерала коллекций с коллекциями, которые не хорошо ведут себя, не определены.

Conversions

Преобразование выражения коллекции позволяет преобразовать выражение коллекции в тип.

Неявное преобразование выражения коллекции существует из выражения коллекции в следующие типы:

  • Одномерный типT[] массива, в котором тип элемента является T
  • Тип диапазона:
    • System.Span<T>
    • System.ReadOnlySpan<T>
      В этом случае тип элемента имеет значение T
  • Тип с соответствующим методом создания, в этом случае тип элемента является типомGetEnumerator итерации, определенным из метода экземпляра или перечисленного интерфейса, а не из метода расширения.
  • Тип структуры или класса , реализующий System.Collections.IEnumerable :
    • Тип имеетприменимый конструктор, который можно вызвать без аргументов, и конструктор доступен в расположении выражения коллекции.

    • Если выражение коллекции имеет какие-либо элементы, тип имеет экземпляр или метод Add расширения, где:

      • Метод можно вызвать с одним аргументом значения.
      • Если метод является универсальным, аргументы типа можно вывести из коллекции и аргумента.
      • Метод доступен в расположении выражения коллекции.

      В этом случае тип элемента является типом итерациитипа.

  • Тип интерфейса:
    • 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>
      В этом случае тип элемента имеет значение T

Неявное преобразование существует, если тип имеет типT элемента, где для каждого элементаEᵢ в выражении коллекции:

  • Если Eᵢ это элемент выражения, происходит неявное преобразование из EᵢT.
  • Если Eᵢ это элемент..Sᵢ spread, происходит неявное преобразование из типаSᵢTитерации в .

Преобразование выражений коллекции из выражения коллекции в многомерный тип массива отсутствует.

Типы, для которых существует неявное преобразование выражений коллекции из выражения коллекции, являются допустимыми целевыми типами для этого выражения коллекции.

Следующие дополнительные неявные преобразования существуют из выражения коллекции:

  • Для типа T?, в котором выполняется преобразование выражения коллекции из выражения коллекции в тип Tзначения. Преобразование — это преобразование выражения коллекции , за T которым следует неявное преобразование, допускающего значение NULL , в TT?.

  • Для ссылочного типа T , в котором существует метод создания , связанный с T этим, возвращает тип U и неявное преобразование ссылок из UT. Преобразование — это преобразование выражения коллекции , за U которым следует неявное преобразование ссылок в UT.

  • Тип интерфейса I , в котором существует метод создания , связанный с I этим, возвращает тип V и неявное преобразование бокса из VI. Преобразование — это преобразование выражения коллекции , за V которым следует неявное преобразование бокса в VI.

Создание методов

Метод создания указывается атрибутом [CollectionBuilder(...)] типа коллекции. Атрибут указывает тип построителя и имя метода , вызываемого для создания экземпляра типа коллекции.

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

Атрибут может применяться к объекту class, или structref structinterface. Атрибут не наследуется, хотя атрибут может применяться к базе class или к объекту abstract class.

Тип построителя должен быть не универсальным class илиstruct.

Сначала определяется набор применимых методовCM создания.
Он состоит из методов, которые соответствуют следующим требованиям:

  • Метод должен иметь имя, указанное в атрибуте [CollectionBuilder(...)] .
  • Метод должен быть определен непосредственно в типе построителя .
  • Метод должен быть static.
  • Метод должен быть доступен, где используется выражение коллекции.
  • Arity метода должен соответствовать arity типа коллекции.
  • Метод должен иметь один параметр типа System.ReadOnlySpan<E>, передаваемый по значению.
  • Существует преобразование удостоверений, неявное преобразование ссылок или преобразование бокса из возвращаемого типа метода в тип коллекции.

Методы, объявленные в базовых типах или интерфейсах, игнорируются и не являются частью CM набора.

CM Если набор пуст, тип коллекции не имеет типа элемента и не имеет метода создания. Ни один из следующих шагов не применяется.

Если только один метод из набора CM имеет преобразование удостоверений из Eтипа элемента типа коллекции, то это метод создания для типа коллекции. В противном случае тип коллекции не имеет метода создания.

Сообщается об ошибке, если [CollectionBuilder] атрибут не ссылается на вызываемый метод с ожидаемой сигнатурой.

Для выражения коллекции с целевым типом C<S0, S1, …> , в котором объявлениеC<T0, T1, …> типа имеет связанный методB.M<U0, U1, …>() построителя, аргументы универсального типа из целевого типа применяются по порядку — и от самого внешнего содержащего типа к самому внутреннему — к методу построителя.

Параметр диапазона для метода создания можно явно пометить scoped или [UnscopedRef]. Если параметр неявно или явно scoped, компилятор может выделить хранилище для диапазона в стеке, а не кучу.

Например, возможный метод создания для ImmutableArray<T>:

[CollectionBuilder(typeof(ImmutableArray), "Create")]
public struct ImmutableArray<T> { ... }

public static class ImmutableArray
{
    public static ImmutableArray<T> Create<T>(ReadOnlySpan<T> items) { ... }
}

С помощью приведенного выше ImmutableArray<int> ia = [1, 2, 3]; можно создать следующим образом:

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

Construction

Элементы выражения коллекции вычисляются в порядке слева направо. Каждый элемент вычисляется ровно один раз, и все дополнительные ссылки на элементы ссылаются на результаты этой начальной оценки.

Элемент распространения может быть итерирован до или после вычисления последующих элементов в выражении коллекции.

Необработанное исключение, вызванное любым из методов, используемых во время строительства, будет неуправляемым и будет препятствовать дальнейшим шагам в строительстве.

Length, Countи GetEnumerator предполагается, что побочные эффекты не имеют.


Если целевой тип является структурой или типом класса , реализующим System.Collections.IEnumerable, а целевой тип не имеет метода создания, создание экземпляра коллекции выглядит следующим образом:

  • Элементы вычисляются по порядку. Некоторые или все элементы могут быть оценены во время приведенных ниже шагов, а не раньше.

  • Компилятор может определить известную длину выражения коллекции путем вызова числовых свойств ( или эквивалентных свойств из известных интерфейсов или типов) для каждого выражения распределенного элемента.

  • Конструктор, применимый без аргументов.

  • Для каждого элемента в порядке:

    • Если элемент является элементом выражения, применимый Add экземпляр или метод расширения вызывается с выражением элемента в качестве аргумента. (В отличие от поведения инициализатора классической коллекции, оценка элементов и Add вызовы не обязательно пересекаются.)
    • Если элемент является элементом spread , то используется один из следующих элементов:
      • GetEnumerator Применимый экземпляр или метод расширения вызывается в выражении распределенного элемента, и для каждого элемента из перечислителя применимый Add экземпляр или метод расширения вызывается в экземпляре коллекции с элементом в качестве аргумента. Если перечислитель реализует IDisposable, вызовется Dispose после перечисления независимо от исключений.
      • AddRange Применимый экземпляр или метод расширения вызывается в экземпляре коллекции с выражением распределенного элемента в качестве аргумента.
      • Применимый CopyTo экземпляр или метод расширения вызывается в выражении распределенного элемента с экземпляром коллекции и int индексом в качестве аргументов.
  • Во время описанных выше действий по созданию применимый EnsureCapacity экземпляр или метод расширения может вызываться один или несколько раз в экземпляре коллекции с аргументом int емкости.


Если целевой тип является массивом, диапазоном, типом с методом создания или интерфейсом, построение экземпляра коллекции выглядит следующим образом:

  • Элементы вычисляются по порядку. Некоторые или все элементы могут быть оценены во время приведенных ниже шагов, а не раньше.

  • Компилятор может определить известную длину выражения коллекции путем вызова числовых свойств ( или эквивалентных свойств из известных интерфейсов или типов) для каждого выражения распределенного элемента.

  • Экземпляр инициализации создается следующим образом:

    • Если целевой тип является массивом , а выражение коллекции имеет известную длину, массив выделяется ожидаемой длиной.
    • Если целевой тип является диапазоном или типом с методом создания, а коллекция имеет известную длину, то создается диапазон с ожидаемой длиной, ссылающийся на непрерывное хранилище.
    • В противном случае выделяется промежуточное хранилище.
  • Для каждого элемента в порядке:

    • Если элемент является элементом выражения, индексатор экземпляра инициализации вызывается для добавления вычисляемого выражения в текущем индексе.
    • Если элемент является элементом spread , то используется один из следующих элементов:
      • Элемент хорошо известного интерфейса или типа вызывается для копирования элементов из выражения распределенного элемента в экземпляр инициализации.
      • GetEnumerator Применимый экземпляр или метод расширения вызывается в выражении распределенного элемента и для каждого элемента из перечислителя, индексатор экземпляра инициализации вызывается для добавления элемента в текущий индекс. Если перечислитель реализует IDisposable, вызовется Dispose после перечисления независимо от исключений.
      • Применимый CopyTo экземпляр или метод расширения вызывается в выражении распределенного элемента с экземпляром инициализации и int индексом в качестве аргументов.
  • Если промежуточное хранилище было выделено для коллекции, экземпляр коллекции выделяется с фактической длиной коллекции, а значения из экземпляра инициализации копируются в экземпляр коллекции или если диапазон требуется компилятору, может использовать диапазон фактической длины коллекции из промежуточного хранилища. В противном случае экземпляр инициализации — это экземпляр коллекции.

  • Если целевой тип имеет метод создания, метод создания вызывается с экземпляром диапазона.


Заметка: Компилятор может отложить добавление элементов в коллекцию или отложить итерацию с помощью распределенных элементов до тех пор, пока не будет оценивать последующие элементы. (Если последующие элементы распространения имеют подсчитываемые свойства, которые позволяют вычислять ожидаемую длину коллекции перед выделением коллекции.) И наоборот, компилятор может с нетерпением добавлять элементы в коллекцию — и с нетерпением итерировать с помощью распределенных элементов — когда нет преимуществ для задержки.

Рассмотрим следующее выражение коллекции:

int[] x = [a, ..b, ..c, d];

Если распределенные элементы b и c являются подсчитываемыми, компилятор может отложить добавление элементов и ab до тех пор c , пока не будет оценено, чтобы разрешить выделение результирующего массива по ожидаемой длине. После этого компилятор может с нетерпением добавлять элементы из c, прежде чем оценивать 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;

Пустой литерал коллекции

  • Пустой литерал [] не имеет типа. Однако, аналогично null-литералу, этот литерал может быть неявно преобразован в любой конструкторный тип коллекции.

    Например, следующее не является законным, так как не существует целевого типа и нет других преобразований, связанных с:

    var v = []; // illegal
    
  • Распространение пустого литерала разрешено уточить. Рассмотрим пример.

    bool b = ...
    List<int> l = [x, y, .. b ? [1, 2, 3] : []];
    

    Здесь, если b значение равно false, не требуется, чтобы любое значение фактически было создано для пустого выражения коллекции, так как оно сразу же будет распространяться на нулевые значения в окончательном литерале.

  • Пустое выражение коллекции может быть однотонным, если используется для создания окончательного значения коллекции, которое, как известно, не является изменяемым. Рассмотрим пример.

    // 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 = [];
    

Безопасность ссылок

Сведения об ограничении безопасного контекста см. в определениях значений безопасного контекста : объявление-блок, член-функция и вызывающий контекст.

Безопасный контекст выражения коллекции:

  • Безопасный контекст пустого выражения [] коллекции — это вызывающий контекст.

  • Если целевой тип является типомSystem.ReadOnlySpan<T> диапазона и T является одним из примитивных типовbool, sbyte, byteshortushortcharintuintlongulongfloatили double, а выражение коллекции содержит только константные значения, безопасный контекст выражения коллекции является вызывающим контекстом.

  • Если целевой тип является типомSystem.Span<T> диапазона или System.ReadOnlySpan<T>безопасным контекстом выражения коллекции является блок объявления.

  • Если целевой тип является типом структуры ref с методом создания, безопасный контекст выражения коллекции является безопасным контекстом вызова метода создания, где выражение коллекции является аргументом диапазона для метода.

  • В противном случае безопасный контекст выражения коллекции является вызывающим контекстом.

Выражение коллекции с безопасным контекстом блока объявления не может избежать включающей области, и компилятор может хранить коллекцию в стеке, а не кучу.

Чтобы разрешить выражению коллекции для типа структуры ссылок экранировать блок объявления, может потребоваться приведение выражения к другому типу.

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
}

Определение типа

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;

Правила вывода типов обновляются следующим образом.

Существующие правила для первого этапа извлекаются в новый раздел вывода типа ввода , а правило добавляется в вывод типов входных данных и вывод выходного типа для выражений выражений коллекции.

11.6.3.2 Первый этап

Для каждого аргумента Eᵢметода:

  • Вывод типа ввода производится изEᵢсоответствующеготипаTᵢ параметра.

Вывод типа входных данных производится из выражения E в тип T следующим образом:

  • Если E выражение коллекции с элементами Eᵢи T является типом с типомTₑ элемента или T имеет типT0? значения NULL и T0 имеет типTₑ элемента, то для каждого из них Eᵢ:
    • Если Eᵢ является элементом выражения, вывод типа входных данных производится изEᵢ.Tₑ
    • Если Eᵢ является элементом распространения с типомSᵢ итерации, то вывод с нижней границы производится изSᵢTₑ.
  • [существующие правила с первого этапа] ...

Вывод типа вывода типа вывода 11.6.3.7

Вывод типа выходных данных производится из выражения E в тип T следующим образом:

  • Если E выражение коллекции с элементами Eᵢи T является типом с типомTₑ элемента или T имеет типT0? значения NULL и T0 имеет типTₑ элемента, то для каждого из них Eᵢ:
    • Если Eᵢ это элемент выражения, вывод выходного типа выполняется изEᵢ.Tₑ
    • Если Eᵢ это элемент spread, вывод не производится.Eᵢ
  • [существующие правила вывода типа вывода] ...

Методы расширения

Никаких изменений в правилах вызова метода расширения .

Вызовы метода расширения 12.8.10.3

Метод Cᵢ.Mₑ расширения имеет право, если:

  • ...
  • Неявное удостоверение, ссылка или преобразование бокса существует от экспра к типу первого параметра Mₑ.

Выражение коллекции не имеет естественного типа, поэтому существующие преобразования из типа неприменимо. В результате выражение коллекции нельзя использовать непосредственно в качестве первого параметра для вызова метода расширения.

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

Разрешение перегрузки

Лучшее преобразование из выражения обновляется, чтобы предпочесть определенные целевые типы в преобразованиях выражений коллекции.

В обновленных правилах:

  • Одно из следующих span_type:
    • System.Span<T>
    • System.ReadOnlySpan<T>.
  • Одно из следующих array_or_array_interface:
    • Тип массива
    • один из следующих типов интерфейса , реализованных типом массива:
      • 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>

Учитывая неявное преобразование C₁, представляющее собой преобразование из выражения E в тип T₁, и неявное преобразование C₂, представляющее собой преобразование из выражения E в тип T₂, C₁ — это более подходящее преобразование, чем C₂, если выполняется одно из следующих условий:

  • E является выражением коллекции и одним из следующих удержаний:
    • T₁is , и is System.ReadOnlySpan<E₁>T₂, и System.Span<E₂> неявное преобразование существует из E₁E₂
    • T₁is System.ReadOnlySpan<E₁> or System.Span<E₁>, и T₂ является array_or_array_interface с типомE₂ элемента, и неявное преобразование существует из E₁E₂
    • T₁не является span_type и T₂ не является span_type, а неявное преобразование существует из T₁T₂
  • E не является выражением коллекции и одним из следующих удержаний:
  • E — это группа методов, ...

Примеры различий с разрешением перегрузки между инициализаторами массивов и выражениями коллекции:

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

Типы диапазонов

Типы диапазонов ReadOnlySpan<T> и Span<T> оба типа являются конструкторными типами коллекций. Поддержка для них соответствует проектированию params Span<T>. В частности, создание любого из этих диапазонов приведет к созданию массива T[], созданного в стеке , если массив params находится в пределах (если таковой) задан компилятором. В противном случае массив будет выделен в куче.

Если компилятор выбирает выделение в стеке, он не требуется переводить литерал непосредственно в определенный stackalloc момент. Например, задано:

foreach (var x in y)
{
    Span<int> span = [a, b, c];
    // do things with span
}

Компилятор может перевести это значение stackalloc до тех пор, пока Span значение остается неизменным и сохраняется безопасность диапазона . Например, он может перевести приведенный выше текст в следующее:

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
}

Компилятор также может использовать встроенные массивы, если они доступны, при выборе выделения в стеке. Обратите внимание, что в C# 12 встроенные массивы нельзя инициализировать с выражением коллекции. Эта функция является открытым предложением.

Если компилятор решит выделить кучу, перевод просто Span<T> :

T[] __array = [...]; // using existing rules
Span<T> __result = __array;

Перевод литерала коллекции

Выражение коллекции имеет известную длину , если тип времени компиляции каждого распределенного элемента в выражении коллекции подсчитывается.

Перевод интерфейса

Преобразование интерфейса, не изменяемого

Учитывая целевой тип, который не содержит мутирующие элементы, а именно IEnumerable<T>IReadOnlyCollection<T>, и IReadOnlyList<T>, соответствующая реализация требуется для создания значения, реализующего этот интерфейс. Если синтезируется тип, рекомендуется использовать синтезированный тип, реализующий все эти интерфейсы, а также ICollection<T> и IList<T>независимо от типа интерфейса. Это обеспечивает максимальную совместимость с существующими библиотеками, в том числе интерфейсы, реализованные значением, чтобы повысить эффективность оптимизации производительности.

Кроме того, значение должно реализовывать негенерические ICollection и IList интерфейсы. Это позволяет выражениям коллекции поддерживать динамическое интроспекция в таких сценариях, как привязка данных.

Совместимая реализация является бесплатной для:

  1. Используйте существующий тип, реализующий необходимые интерфейсы.
  2. Синтезирует тип, реализующий необходимые интерфейсы.

В любом случае тип, используемый тип, разрешено реализовать более большой набор интерфейсов, чем те, которые строго требуются.

Синтезированные типы могут использовать любую стратегию, которую они хотят правильно реализовать необходимые интерфейсы. Например, синтезированный тип может встраиваться непосредственно в себя, избегая необходимости дополнительного выделения внутренней коллекции. Синтезированный тип также не может использовать любое хранилище, выбирая вычисление значений напрямую. Например, возврат index + 1 для [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].

  1. Значение должно возвращаться true при запросе ICollection<T>.IsReadOnly (при реализации) и негенерическом IList.IsReadOnly и IList.IsFixedSize. Это гарантирует, что потребители могут соответствующим образом сообщить, что коллекция не изменяется, несмотря на реализацию изменяемых представлений.
  2. Значение должно вызывать любой вызов метода мутации (например IList<T>.Add). Это гарантирует безопасность, предотвращая случайное изменение неизменяемой коллекции.

Перевод изменяемого интерфейса

Заданный целевой тип, содержащий мутирующие элементы, а именно ICollection<T> или IList<T>:

  1. Значение должно быть экземпляром List<T>.

Известный перевод длины

Наличие известной длины позволяет эффективно создать результат с потенциалом для отсутствия копирования данных и не требуется свободного места в результате.

Отсутствие известной длины не препятствует созданию какого-либо результата. Однако это может привести к дополнительным затратам на ЦП и память, создающим данные, а затем перейти к окончательному назначению.

  • Для известного литерала[e1, ..s1, etc]длины перевод сначала начинается со следующего:

    int __len = count_of_expression_elements +
                __s1.Count;
                ...
                __s_n.Count;
    
  • Учитывая целевой тип T для этого литерала:

    • Если T есть некоторые T1[], то литерал преобразуется как:

      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
      

      Реализация может использовать другие средства для заполнения массива. Например, использование эффективных методов массового копирования, таких как .CopyTo().

    • Если T есть некоторые Span<T1>, то литерал преобразуется так же, как и выше, за исключением того, что __result инициализация преобразуется следующим образом:

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

      Перевод может использоваться stackalloc T1[] или встроенный массив , а не new T1[] при сохранении безопасности диапазона .

    • Если T есть некоторыеReadOnlySpan<T1>, то литерал преобразуется так же, как и в Span<T1> случае, за исключением того, что окончательный результат будет таким, что Span<T1>неявно преобразован в .ReadOnlySpan<T1>

      ReadOnlySpan<T1> Где T1 есть некоторый примитивный тип, и все элементы коллекции являются константой, не требуется, чтобы данные были в куче или на стеке. Например, реализация может создать этот диапазон непосредственно в качестве ссылки на часть сегмента данных программы.

      Приведенные выше формы (для массивов и диапазонов) представляют собой базовые представления выражения коллекции и используются для следующих правил перевода:

      • Если T имеется C<S0, S1, …> соответствующий метод create-methodB.M<U0, U1, …>(), то литерал преобразуется как:

        // Collection literal is passed as is as the single B.M<...>(...) argument
        C<S0, S1, …> __result = B.M<S0, S1, …>([...])
        

        Так как метод создания должен иметь тип аргумента определенного экземпляра ReadOnlySpan<T>, правило перевода для диапазонов применяется при передаче выражения коллекции в метод создания.

      • Если T поддерживает инициализаторы коллекций, выполните следующие действия.

        • Если тип T содержит конструктор со специальными возможностями с одним параметром int capacity, то литерал преобразуется как:

          T __result = new T(capacity: __len);
          __result.Add(__e1);
          foreach (var __t in __s1)
              __result.Add(__t);
          
          // further additions of the remaining elements
          

          Примечание. Имя параметра требуется capacity.

          Эта форма позволяет литералу информировать только что созданный тип количества элементов, чтобы обеспечить эффективное выделение внутреннего хранилища. Это позволяет избежать расточительного перемещения по мере добавления элементов.

        • в противном случае литерал преобразуется как:

          T __result = new T();
          
          __result.Add(__e1);
          foreach (var __t in __s1)
              __result.Add(__t);
          
          // further additions of the remaining elements
          

          Это позволяет создавать целевой тип, хотя и без оптимизации емкости, чтобы предотвратить внутреннее перемещение хранилища.

Неизвестный перевод длины

  • Учитывая целевой тип T для неизвестного литерала длины:

    • Если T поддерживает инициализаторы коллекций, то литерал преобразуется как:

      T __result = new T();
      
      __result.Add(__e1);
      foreach (var __t in __s1)
          __result.Add(__t);
      
      // further additions of the remaining elements
      

      Это позволяет распространять любой итерируемый тип, хотя и с минимальным количеством оптимизации.

    • Если T есть некоторые T1[], то литерал имеет ту же семантику, что и:

      List<T1> __list = [...]; /* initialized using predefined rules */
      T1[] __result = __list.ToArray();
      

      Приведенный выше неэффективный, хотя; он создает промежуточный список, а затем создает копию окончательного массива из него. Реализации могут быть свободны для оптимизации этого типа, например для создания кода, как показано ниже.

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

      Это позволяет использовать минимальные затраты и копирование без дополнительных накладных расходов, которые могут возникнуть в коллекциях библиотек.

      Количество передаваемых CreateArray данных используется для указания начального размера, чтобы предотвратить расточительное изменение размера.

    • Если T это какой-либо тип диапазона, реализация может следовать приведенной выше T[] стратегии или любой другой стратегии с той же семантикой, но более высокую производительность. Например, вместо выделения массива в качестве копии элементов CollectionsMarshal.AsSpan(__list) списка можно использовать для получения значения диапазона напрямую.

Неподдерживаемые сценарии

Хотя литералы коллекции можно использовать для многих сценариев, существует несколько вариантов, которые они не способны заменить. К ним относятся:

  • Многомерные массивы (например, new int[5, 10] { ... }). Нет возможности включать измерения, и все литералы коллекции являются либо линейными, либо структурами карты.
  • Коллекции, которые передают специальные значения конструкторам. Доступ к используемому конструктору отсутствует.
  • Инициализаторы вложенных коллекций, например new Widget { Children = { w1, w2, w3 } }. Эта форма должна оставаться, так как она имеет очень разные семантики от Children = [w1, w2, w3]. Предыдущие вызовы .Add повторяются, .Children в то время как последний назначит новую коллекцию .Children. Мы могли бы рассмотреть возможность возврата последней формы к добавлению в существующую коллекцию, если .Children ее нельзя назначить, но это может быть очень запутанно.

Неоднозначность синтаксиса

  • Существует две "истинные" синтаксические неоднозначности, в которых существует несколько юридических синтактических интерпретаций кода, использующего код collection_literal_expression.

    • Неоднозначный spread_element с .range_expression Можно технически иметь следующее:

      Range[] ranges = [range1, ..e, range2];
      

      Чтобы устранить эту проблему, мы можем:

      • Требовать от пользователей круглые (..e) скобки или включения начального индекса 0..e , если они хотят диапазон.
      • Выберите другой синтаксис (например ..., для распространения). Это было бы неудачно в связи с отсутствием согласованности с шаблонами среза.
  • Существует два случая, когда не существует истинной неоднозначности, но где синтаксис значительно увеличивает сложность синтаксического анализа. Хотя и не проблема с учетом времени разработки, это по-прежнему увеличивает когнитивные издержки для пользователей при просмотре кода.

    • Неоднозначность между collection_literal_expressionattributes операторами или локальными функциями. Рассматривать:

      [X(), Y, 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() { }
      

      Без сложного внешнего вида, было бы невозможно сказать, не потребляя всю полноту литерала.

      Возможные варианты для решения этой проблемы:

      • Разрешите это, выполняя синтаксический анализ, чтобы определить, какие из этих случаев это.
      • Запретить это и требовать, чтобы пользователь завернуть литерал в круглые скобки, например ([X(), Y, Z()]).ForEach(...).
      • Неоднозначность между collection_literal_expressionconditional_expression a и a null_conditional_operations. Рассматривать:
      M(x ? [a, b, c]
      

      Это может быть одно из следующих вариантов:

      // 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]);
      

      Без сложного внешнего вида, было бы невозможно сказать, не потребляя всю полноту литерала.

      Примечание. Это проблема даже без естественного типа , так как целевая типизация применяется через conditional_expressions.

      Как и в других, мы могли бы требовать, чтобы круглые скобки диамбигуат. Другими словами, предположимnull_conditional_operation, что интерпретация не написана так: x ? ([1, 2, 3]) : Тем не менее, это кажется довольно несчастным. Этот вид кода, кажется, не является необоснованным для написания и, скорее всего, поездок людей вверх.

Недостатки

  • В этой статье представлена еще одна форма для выражений коллекции поверх уже имеющихся многомерных способов. Это дополнительная сложность для языка. Тем не более чем это делает возможным объединение по одному синтаксису кольца для правила всех, что означает, что существующие базы кода могут быть упрощены и перемещены в единый вид везде.
  • Использование [... вместо ]...{} переходит от синтаксиса, который мы обычно использовали для инициализаторов массивов и коллекций. В частности, что он использует [...] вместо {...}. Однако это уже было решено командой языка, когда мы сделали шаблоны списка. Мы пытались сделать {...} работать с шаблонами списка и столкнулись с непреодолимыми проблемами. Из-за этого мы переехали [в ...] , что, в то время как новое для C#, чувствует себя естественным во многих языках программирования и позволил нам начать свежие без неоднозначности. Использование [...] как соответствующая литеральная форма дополняет наши последние решения, и дает нам чистое место для работы без проблем.

Это вводит бородавки на язык. Например, следующие как юридические, так и (к счастью) означают то же самое:

int[] x = { 1, 2, 3 };
int[] x = [ 1, 2, 3 ];

Тем не менее, учитывая широту и согласованность нового синтаксиса литерала, мы должны рекомендовать, чтобы люди переходили в новую форму. Предложения и исправления интегрированной среды разработки могут помочь в этом отношении.

Alternatives

  • Какие другие проекты были рассмотрены? Что такое влияние не делать этого?

Устраненные вопросы

  • Следует ли компилятору использовать stackalloc выделение стека, если встроенные массивы недоступны, а тип итерации является примитивным типом?

    Разрешение: Нет. Управление буфером требует дополнительных усилий stackalloc по встроенному массиву , чтобы убедиться, что буфер не выделяется многократно, когда выражение коллекции находится в цикле. Дополнительная сложность компилятора и в созданном коде перевешивает преимущество выделения стека на старых платформах.

  • В каком порядке следует оценивать литеральные элементы по сравнению с оценкой свойств Length/Count? Следует ли сначала оценивать все элементы, а затем все длины? Или мы должны оценить элемент, а затем его длину, а затем следующий элемент и т. д.?

    Решение: сначала мы вычисляем все элементы, а все остальные следуют этому.

  • Может ли ли литерал неизвестной длины создать тип коллекции, требующий известной длины, например коллекции массива, диапазона или конструктора (массива или диапазона) ? Это было бы трудно сделать эффективно, но это может быть возможно с помощью умного использования объединенных массивов и /или построителей.

    Разрешение. Да, мы разрешаем создавать коллекцию исправлений длины из неизвестного литерала длины. Компилятор может реализовать это как можно эффективнее.

    Следующий текст существует для записи первоначального обсуждения этого раздела.

    Пользователи всегда могут сделать неизвестный литерал длины в известную длину одной с кодом следующим образом:

    ImmutableArray<int> x = [a, ..unknownLength.ToArray(), b];
    

    Однако это связано с необходимостью принудительного выделения временного хранилища. Мы могли бы быть более эффективными, если бы мы контролировали, как это было создано.

  • Можно ли использовать целевой collection_expression тип IEnumerable<T> для интерфейсов коллекции или других интерфейсов коллекции?

    Рассмотрим пример.

    void DoWork(IEnumerable<long> values) { ... }
    // Needs to produce `longs` not `ints` for this to work.
    DoWork([1, 2, 3]);
    

    Разрешение. Да, литерал может быть предназначен для любого типа I<T> интерфейса, реализующего List<T> . Например: IEnumerable<long>. Это аналогично вводу целевого List<long> объекта, а затем назначению этого результата указанному типу интерфейса. Следующий текст существует для записи первоначального обсуждения этого раздела.

    Открытый вопрос здесь определяет, какой базовый тип фактически создается. Один из вариантов заключается в том, чтобы взглянуть на предложение params IEnumerable<T>. Там мы создадим массив для передачи значений вместе, как и с тем, с params T[]чем происходит.

  • Может или должен ли компилятор выпуститьArray.Empty<T>()?[] Следует ли нам заставить это сделать, чтобы избежать выделения по возможности?

    Да. Компилятор должен выдаваться Array.Empty<T>() в любом случае, если это является законным, и окончательный результат не изменяется. Например, нацеливание T[]или IEnumerable<T>IReadOnlyCollection<T>IReadOnlyList<T>. Он не должен использовать Array.Empty<T> , если целевой объект является изменяемым (ICollection<T> или IList<T>).

  • Следует ли расширить инициализаторы коллекций, чтобы найти очень распространенный AddRange метод? Он может использоваться базовым созданным типом для эффективного добавления элементов распространения. Мы также можем искать такие вещи, как .CopyTo и. Здесь могут возникнуть недостатки, так как эти методы могут привести к возникновению избыточных выделений или отправки, а также непосредственно перечисления в переведенном коде.

    Да. Реализация позволяет использовать другие методы для инициализации значения коллекции в соответствии с предположением, что эти методы имеют четко определенную семантику, и типы коллекций должны быть "хорошо вести себя". На практике реализация должна быть осторожной, так как преимущества в одном из способов (массовое копирование) могут привести к негативным последствиям (например, боксирование коллекции структур).

    Реализация должна воспользоваться преимуществами в тех случаях, когда нет недостатков. Например, с методом .AddRange(ReadOnlySpan<T>) .

Неразрешенные вопросы

  • Следует ли разрешать вывод типа элемента , когда тип итерации является неоднозначным (по некоторым определениям)? Рассмотрим пример.
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;
}
  • Должен ли он быть законным для создания и немедленного индексирования в литерал коллекции? Примечание. Это требует ответа на неразрешенный вопрос ниже о том, имеют ли литералы коллекции естественный тип.

  • Выделение стека для огромных коллекций может привести к стеку. Должен ли компилятор иметь эвристические данные для размещения этих данных в куче? Должен ли язык не быть указан, чтобы обеспечить эту гибкость? Мы должны следовать спецификации для params Span<T>.

  • Нужно ли нам использовать целевой тип spread_element? Рассмотрим, например:

    Span<int> span = [a, ..b ? [c] : [d, e], f];
    

    Примечание. Обычно это может возникнуть в следующей форме, чтобы разрешить условное включение некоторых элементов или ничего, если условие равно false:

    Span<int> span = [a, ..b ? [c, d, e] : [], f];
    

    Чтобы оценить этот полный литерал, необходимо оценить выражения элементов внутри. Это означает, что возможность оценить b ? [c] : [d, e]. Тем не менее, отсутствует целевой тип для оценки этого выражения в контексте и отсутствует какой-либо естественный тип, это не позволит нам определить, что делать с любым [c] или [d, e] здесь.

    Чтобы устранить эту проблему, можно сказать, что при оценке выражения литерала spread_element существовал неявный целевой тип, эквивалентный целевому типу самого литерала. Таким образом, в приведенном выше коде будет перезаписан следующим образом:

    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;
    

Спецификация типа создаваемой коллекции, использующая метод создания , учитывает контекст, по которому преобразование классифицируется.

Существование преобразования в данном случае зависит от понятия типа итерациитипа коллекции. Если существует метод создания , который принимает ReadOnlySpan<T>Tтип итерации, преобразование существует. В противном случае это не так.

Однако тип итерации учитывает контекст, с которым foreach выполняется. Для одного типа коллекции он может отличаться в зависимости от того, какие методы расширения находятся в области, и он также может быть неопределенным.

Это чувствует себя хорошо для целей, foreach когда тип не предназначен для того, чтобы быть на самом деле доступным для себя. Если это так, методы расширения не могут изменить способ перебора типа, независимо от контекста.

Тем не менее, это чувствует себя несколько странно для преобразования, чтобы быть контекст чувствительным к такому. Фактически преобразование является "неустойчивым". Тип коллекции, явно разработанный для создания, позволяет исключить определение очень важных деталей — его тип итерации. Оставляя тип "непобедимым" на самом себе.

Рассмотрим пример:

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

Учитывая текущую структуру, если тип не определяет сам тип итерации , компилятор не может надежно проверить приложение атрибута CollectionBuilder . Если мы не знаем тип итерации, мы не знаем, какая подпись метода создания должна быть. Если тип итерации поступает из контекста, нет никакой гарантии, что тип всегда будет использоваться в аналогичном контексте.

Функция коллекций params также влияет на эту функцию. Это чувствует себя странно, чтобы не удалось надежно прогнозировать тип params элемента параметра в точке объявления. Текущее предложение также требует убедиться, что метод создания по крайней мере доступен params как тип коллекции. Невозможно выполнить эту проверку надежным способом, если только тип коллекции не определяет сам тип итерации .

Обратите внимание, что мы также https://github.com/dotnet/roslyn/issues/69676 открыли для компилятора, который в основном наблюдает ту же проблему, но говорит об этом с точки зрения оптимизации.

Предложение

Требовать тип, используюющий CollectionBuilder атрибут, чтобы определить его тип итерации на самом себе. Другими словами, это означает, что тип должен либо реализовать IEnumarable/IEnumerable<T>, либо он должен иметь открытый GetEnumerator метод с правильной подписью (это исключает любые методы расширения).

Кроме того, теперь для создания метода требуется "быть доступным, где используется выражение коллекции". Это еще одна точка зависимости контекста на основе специальных возможностей. Цель этого метода очень похожа на назначение определяемого пользователем метода преобразования, и что он должен быть общедоступным. Поэтому мы должны рассмотреть вопрос о том, чтобы метод создания был открытым, а также.

Conclusion

Утверждено с изменениями LDM-2024-01-08

Понятие типа итерации не применяется последовательно во время преобразований

  • Для структуры или типа класса , реализующего System.Collections.Generic.IEnumerable<T> :
    • Для каждого элементаEi имеется неявное преобразованиеTв .

В данном случае предполагается, что T необходимо использовать тип итерацииструктуры или типа класса . Однако это предположение неверно. Что может привести к очень странному поведению. Рассмотрим пример.

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" };
    }
}
  • В структуру или тип класса, реализующий и System.Collections.IEnumerableSystem.Collections.Generic.IEnumerable<T>.

Похоже, реализация предполагает, что тип итерации имеет значение object, но спецификация оставляет этот факт не указан, и просто не требует, чтобы каждый элемент преобразовывалось в что-либо. Однако в целом тип итерации не требуется object . Что можно увидеть в следующем примере:

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

Понятие типа итерации является фундаментальным для функции Params Collections . И этот вопрос приводит к странным расхождениям между двумя признаками. Пример:

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

Вероятно, это будет хорошо выровнять один или другой.

Предложение

Укажите преобразование структуры или типа класса , реализующего System.Collections.Generic.IEnumerable<T> или System.Collections.IEnumerable с точки зрения типа итерации , и требует неявного преобразования для каждого элементаEi в тип итерации.

Conclusion

Утвержден LDM-2024-01-08

Требуется ли преобразование выражений коллекции для создания минимального набора API?

Конструкторный тип коллекции в соответствии с преобразованиями может быть неконструируемым, что, скорее всего, приведет к некоторому непредвиденному поведению разрешения перегрузки. Рассмотрим пример.

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[])'
    }
}

Тем не менее, 'C1. M1(string)' не является кандидатом, который можно использовать, так как:

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?)

Ниже приведен еще один пример с определяемым пользователем типом и более сильной ошибкой, которая даже не упоминает допустимого кандидата:

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

Похоже, что ситуация очень похожа на то, что мы использовали с группой методов для делегирования преобразований. То есть существовали сценарии, в которых существовало преобразование, но было ошибочно. Мы решили улучшить это, убедив, что, если преобразование ошибочно, то оно не существует.

Обратите внимание, что с функцией Params Collections мы будем работать с аналогичной проблемой. Может быть хорошо запретить использование params модификатора для неконструируемых коллекций. Однако в текущем предложении , которое проверяется на основе раздела преобразования. Рассмотрим пример:

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

Похоже, что вопрос был несколько рассмотрен ранее, см. в статье https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-02.md#collection-expressions. В то время был сделан аргумент, что правила, указанные прямо сейчас, соответствуют тому, как интерполированные обработчики строк указаны. Ниже приведена цитата:

В частности, интерполированные обработчики строк были первоначально указаны таким образом, но мы пересмотрели спецификацию после рассмотрения этой проблемы.

Хотя существует некоторое сходство, существует также важное различие, которое стоит рассмотреть. Вот цитата из https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/improved-interpolated-strings.md#interpolated-string-handler-conversion:

Тип T , как говорят, является applicable_interpolated_string_handler_type , если он относится к System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute. Существует неявная interpolated_string_handler_conversionT из interpolated_string_expression или additive_expression , полностью состоящая из _interpolated_string_expression_s и использующего только + операторы.

Целевой тип должен иметь специальный атрибут, который является сильным индикатором намерения автора для того, чтобы тип был интерполированным обработчиком строк. Справедливо предположить, что наличие атрибута не является совпадением. В отличие от этого, тот факт, что тип является "перечисленным", не требуется означать, что у автора было намерение автора для того, чтобы тип был конструируемым. Однако наличие метода создания, указывающего атрибутом [CollectionBuilder(...)] в типе коллекции, чувствует себя сильным индикатором намерения автора для того, чтобы тип был конструируемым.

Предложение

Для типа структуры или класса, реализующего System.Collections.IEnumerable и не имеющих раздела преобразованияметодов создания, должно потребоваться наличие по крайней мере следующих API:

  • Конструктор со специальными возможностями, применимый без аргументов.
  • Add Доступный экземпляр или метод расширения, который можно вызвать со значением типа итерации в качестве аргумента.

Для функции Params Collectons такие типы являются допустимыми params типами, когда эти API объявлены общедоступными и являются методами экземпляра (vs. extension).

Conclusion

Утверждено с изменениями LDM-2024-01-10