Типы диапазонов первого класса

Note

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

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

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

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

Сводка

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

Мотивация

С момента их внедрения в C# 7.2 Span<T> и ReadOnlySpan<T> работали в языковой и базовой библиотеке классов (BCL) различными ключевыми способами. Это отлично подходит для разработчиков, так как их введение повышает производительность без затрат на безопасность разработчиков. Однако язык держал эти типы на длине руки несколькими ключевыми способами, что делает его трудно выразить намерение API и приводит к значительному количеству дублирования поверхностной области для новых API. Например, BCL добавил ряд новых тензорных примитивных API в .NET 9, но все эти API доступны.ReadOnlySpan<T> C# не распознает связь между ReadOnlySpan<T>, Span<T>и T[], хотя между этими типами существуют определяемые пользователем преобразования, они не могут использоваться для приемников методов расширения, не могут создаваться с другими пользовательскими преобразованиями и не помогают во всех сценариях вывода универсальных типов. Пользователям потребуется использовать явные преобразования или аргументы типа, что означает, что средства интегрированной среды разработки не позволяют пользователям использовать эти API, так как ничего не будет указывать интегрированной среде разработки, которая допустима для передачи этих типов после преобразования. Чтобы обеспечить максимальную удобство использования для этого стиля API, BCL придется определить весь набор Span<T> и T[] перегрузки, что является большим количеством повторяющихся областей поверхности для поддержания без реального получения. Это предложение стремится решить проблему путем более непосредственного распознавания этих типов и преобразований.

Например, BCL может добавить только одну перегрузку любого MemoryExtensions вспомогательного средства, например:

int[] arr = [1, 2, 3];
Console.WriteLine(
    arr.StartsWith(1) // CS8773 in C# 13, permitted with this proposal
    );

public static class MemoryExtensions
{
    public static bool StartsWith<T>(this ReadOnlySpan<T> span, T value) where T : IEquatable<T> => span.Length != 0 && EqualityComparer<T>.Default.Equals(span[0], value);
}

Ранее для использования метода расширения в переменных span/array и массивов требуется перегрузка диапазонов и массивов, так как определяемые пользователем преобразования (которые существуют между Span/array/ReadOnlySpan) не учитываются для приемников расширений.

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

Изменения в этом предложении будут связаны с LangVersion >= 14.

Преобразования диапазона

Мы добавим новый тип неявного преобразования в список в §10.2.1, неявное преобразование диапазона. Это преобразование из типа и определяется следующим образом:


Неявное преобразование диапазона разрешает array_types, System.ReadOnlySpan<T>System.Span<T>и string преобразуется между собой следующим образом:

  • От любого одномерного array_type с типом Ei элемента до System.Span<Ei>
  • Из любого одномерного array_type с типом EiSystem.ReadOnlySpan<Ui>элемента в , если Ei это ковариантно-преобразуемый (§18.2.3.3) в Ui
  • От System.Span<Ti> до , если Ti это ковариантное преобразование (§18.2.3.3System.ReadOnlySpan<Ui>) вUi
  • От System.ReadOnlySpan<Ti> до , если Ti это ковариантное преобразование (§18.2.3.3System.ReadOnlySpan<Ui>) вUi
  • От string до System.ReadOnlySpan<char>

Любые типы Span/ReadOnlySpan считаются применимыми для преобразования, если они совпадают ref structс полным именем (LDM 2024-06-24).

Мы также добавим неявное преобразование диапазона в список стандартных неявных преобразований (§10.4.2). Это позволяет разрешать перегрузки при разрешении аргументов, как и в предложении API, связанном ранее.

Явные преобразования диапазона приведены ниже.

  • Все неявные преобразования диапазона.
  • Из array_type с типом Ti элемента в System.Span<Ui> или System.ReadOnlySpan<Ui> при условии явного преобразования ссылок существует от TiUi.

Нет стандартного явного преобразования диапазона в отличие от других стандартных явных преобразований (§10.4.3), которые всегда существуют с учетом противоположного стандартного неявного преобразования.

Определяемые пользователем преобразования

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

Неявные преобразования диапазона исключены из правила, которое невозможно определить определяемый пользователем оператор между типами, для которых существует неявное преобразование (§10.5.2 Разрешенные пользовательские преобразования). Это необходимо, чтобы BCL смог определить существующие операторы преобразования span даже при переходе на C# 14 (они по-прежнему необходимы для более низких LangVersions, а также потому, что эти операторы используются в кодовом гене новых стандартных преобразований диапазона). Но его можно рассматривать как подробные сведения о реализации (кодеген и более низкие LangVersions не являются частью спецификации) и Roslyn нарушает эту часть спецификации в любом случае (это конкретное правило о определяемых пользователем преобразованиях не применяется).

Приемник расширений

Мы также добавим неявное преобразование диапазона в список допустимых неявных преобразований в первом параметре метода расширения при определении применимости (12.8.9.3) (изменение полужирного шрифта):

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

  • Cᵢ — это необобщенный, невложенный класс
  • Имя Mₑ — это идентификатор
  • Mₑ доступно и применимо при применении к аргументам в качестве статического метода, как показано выше.
  • Неявное удостоверение, ссылка или бокс, бокс или преобразование диапазона существует от экспра к типу первого параметра Mₑ. Преобразование диапазона не учитывается при выполнении разрешения перегрузки для преобразования группы методов.

Обратите внимание, что неявное преобразование диапазона не учитывается для приемника расширений в преобразованиях групп методов (LDM 2024-07-15), что делает следующий код продолжать работать в отличие от ошибки CS1113: Extension method 'E.M<int>(Span<int>, int)' defined on value type 'Span<int>' cannot be used to create delegatesво время компиляции:

using System;
using System.Collections.Generic;
Action<int> a = new int[0].M; // binds to M<int>(IEnumerable<int>, int)
static class E
{
    public static void M<T>(this Span<T> s, T x) => Console.Write(1);
    public static void M<T>(this IEnumerable<T> e, T x) => Console.Write(2);
}

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

  • Компилятор может выдавать thunk, который принимает массив в качестве приемника и выполняет преобразование диапазона внутри (аналогично пользователю, создав делегат x => new int[0].M(x)вручную).
  • Делегаты значений, если реализованы, могут напрямую принимать получатель Span .

Дисперсия

Цель раздела дисперсии в неявном преобразовании диапазона заключается в репликации некоторого количества ковариации для System.ReadOnlySpan<T>. Изменения среды выполнения потребуются для полной реализации дисперсии через универсальные шаблоны здесь (см. раздел .. /csharp-13.0/ref-struct-interfaces.md для использования ref struct типов в универсальных наборах), но мы можем разрешить ограниченное количество ковариации с помощью предлагаемого .NET 9 API: https://github.com/dotnet/runtime/issues/96952 Это позволит языку рассматриваться System.ReadOnlySpan<T> как будто объявленный Tout T как в некоторых сценариях. Однако мы не добавляем его в определение преобразуемого вариативности в §18.2.3.3. Если в будущем мы изменим среду выполнения, чтобы более глубоко понять дисперсию здесь, мы можем принять незначительные критические изменения, чтобы полностью распознать его на языке.

Patterns

Обратите внимание, что при ref structиспользовании в качестве типа в любом шаблоне разрешены только преобразования удостоверений:

class C<T> where T : allows ref struct
{
    void M1(T t) { if (t is T x) { } } // ok (T is T)
    void M2(R r) { if (r is R x) { } } // ok (R is R)
    void M3(T t) { if (t is R x) { } } // error (T is R)
    void M4(R r) { if (r is T x) { } } // error (R is T)
}
ref struct R { }

Из спецификации оператора is-type (§12.12.12.12.1):

Результат операции E is T [...] — логическое значение, указывающее, является ли E преобразование не null и может быть успешно преобразовано в тип T путем преобразования ссылок, преобразования бокса, преобразования распаковки, преобразования упаковки или распаковки преобразования.

[...]

Если T является типом ненулевого значения, результат true, если D и T одинаковы.

Это поведение не изменяется с этой функцией, поэтому не удастся написать шаблоны для Span/ReadOnlySpan, хотя аналогичные шаблоны возможны для массивов (включая дисперсию):

using System;

M1<object[]>(["0"]); // prints
M1<string[]>(["1"]); // prints

void M1<T>(T t)
{
    if (t is object[] r) Console.WriteLine(r[0]); // ok
}

void M2<T>(T t) where T : allows ref struct
{
    if (t is ReadOnlySpan<object> r) Console.WriteLine(r[0]); // error
}

Создание кода

Преобразования всегда будут существовать независимо от того, существуют ли вспомогательные средства среды выполнения, используемые для их реализации (LDM 2024-05-13). Если вспомогательные элементы отсутствуют, попытка использовать преобразование приведет к ошибке во время компиляции, в результате чего отсутствует необходимый компилятором элемент.

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

Conversion Helpers
массив в диапазон static implicit operator Span<T>(T[]) (определено в Span<T>)
массив для ReadOnlySpan static implicit operator ReadOnlySpan<T>(T[]) (определено в ReadOnlySpan<T>)
Диапазон до ReadOnlySpan static implicit operator ReadOnlySpan<T>(Span<T>) (определено в Span<T>) и static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>)
ReadOnlySpan для ReadOnlySpan static ReadOnlySpan<T>.CastUp<TDerived>(ReadOnlySpan<TDerived>)
Строка для ReadOnlySpan static ReadOnlySpan<char> MemoryExtensions.AsSpan(string)

Обратите внимание, что MemoryExtensions.AsSpan вместо эквивалентного неявного оператора, определенного в string. Это означает, что кодеген отличается от LangVersions (неявный оператор используется в C# 13; статический метод AsSpan используется в C# 14). С другой стороны, преобразование может быть создано в .NET Framework (AsSpanметод существует там, в то время как string оператор не).

Явный массив в преобразование (ReadOnly)Span сначала преобразуется из исходного массива в массив с типом целевого элемента, а затем в (ReadOnly)Span через тот же вспомогательный элемент, что и неявное преобразование, будет использоваться, т. е. соответствующее op_Implicit(T[]).

Более качественное преобразование выражения

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

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

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

Лучший целевой объект преобразования

Улучшенная цель преобразования (§12.6.4.7) обновляется, чтобы предпочесть ReadOnlySpan<T> больше Span<T>.

Учитывая два типа T₁ и T₂, T₁ является лучшей целью преобразования, чем T₂, если выполняется одно из следующих условий:

  • T₁is System.ReadOnlySpan<E₁>, T₂ is System.Span<E₂>, и преобразование удостоверений из E₁ существующего E₂
  • T₁is System.ReadOnlySpan<E₁>, T₂ is System.ReadOnlySpan<E₂>, и неявное преобразование из T₂T₁ существующего и неявное преобразование из T₂ существующего T₁
  • По крайней мере один из T₁ или T₂ не является и не System.ReadOnlySpan<Eᵢ> является и неявным System.Span<Eᵢ> преобразованием из T₁T₂ существующего и неявным преобразованием из T₂ существующего T₁
  • ...

Проектирование собраний:

Замечания по улучшению

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

Без этого правила следующий код, успешно скомпилированный в C# 13, приведет к неоднозначности в C# 14 из-за нового стандартного неявного преобразования из массива в ReadOnlySpan, применимого к приемнику метода расширения:

using System;
using System.Collections.Generic;

var a = new int[] { 1, 2, 3 };
a.M();

static class E
{
    public static void M(this IEnumerable<int> x) { }
    public static void M(this ReadOnlySpan<int> x) { }
}

Правило также позволяет вводить новые API, которые ранее приводят к неоднозначности, например:

using System;
using System.Collections.Generic;

C.M(new int[] { 1, 2, 3 }); // would be ambiguous before

static class C
{
    public static void M(IEnumerable<int> x) { }
    public static void M(ReadOnlySpan<int> x) { } // can be added now
}

Предупреждение

Так как правило улучшения определяется для преобразований диапазона, которые существуют только вLangVersion >= 14, авторы API не могут добавлять такие новые перегрузки, если они хотят поддерживать пользователей.LangVersion <= 13 Например, если .NET 9 BCL вводит такие перегрузки, пользователи, которые обновляются до net9.0 TFM, но остаются на более низком LangVersion, получат неоднозначность для существующего кода. См. также открытый вопрос ниже.

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

Мы обновим раздел выводов типов спецификации следующим образом (изменения полужирного шрифта).

12.6.3.9 Точные выводы

точный выводиз типа Uв типа V выполняется следующим образом:

  • Если является одним из нефиксированных , то добавляется в набор точных границ для .
  • В противном случае наборы V₁...Vₑ и U₁...Uₑ определяются путем проверки, применяются ли какие-либо из следующих случаев:
    • V — это тип массива V₁[...], а U — это тип массива U₁[...] того же ранга.
    • V Span<V₁> U является типом U₁[] массива или типомSpan<U₁>
    • V ReadOnlySpan<V₁> U— это тип U₁[] массива или или Span<U₁>ReadOnlySpan<U₁>
    • V относится к типу V₁?, а U относится к типу U₁
    • V является созданным типом C<V₁...Vₑ> и U является созданным типом C<U₁...Uₑ>
      Если любое из этих случаев применяется, то точный вывод производится из каждого Uᵢ к соответствующей Vᵢ.
  • В противном случае никаких выводов не производится.

12.6.3.10 Вывод с нижней границой

Определение нижней границы для типа Uдо типа V выполняется следующим образом:

  • Если V является одним из нефиксированныхXᵢ, U добавляется в набор нижних границ для Xᵢ.
  • В противном случае, если V является типом V₁? и U является типом U₁? то вывод нижней границы выполняется из U₁ до V₁.
  • В противном случае наборы U₁...Uₑ и V₁...Vₑ определяются путем проверки, применяются ли какие-либо из следующих случаев:
    • Vявляется типом массива и U является типом U₁[...]V₁[...] массива того же ранга
    • V Span<V₁> U является типом U₁[] массива или типомSpan<U₁>
    • V ReadOnlySpan<V₁> U— это тип U₁[] массива или или Span<U₁>ReadOnlySpan<U₁>
    • V является одним из IEnumerable<V₁>, ICollection<V₁>, IReadOnlyList<V₁>>, IReadOnlyCollection<V₁> или IList<V₁> и U является одномерным типом массива U₁[]
    • V является сконструированным class, struct, interface или delegate типа C<V₁...Vₑ>, и существует уникальный тип C<U₁...Uₑ>, такой, что U (или, если U — это тип parameter, его эффективный базовый класс или любой член его эффективного набора интерфейсов) идентичен inherits или (прямо или косвенно) реализует C<U₁...Uₑ>.
    • (Ограничение "уникальность" означает, что в интерфейсе C<T>{} class U: C<X>, C<Y>{}вывод из U на C<T> не осуществляется, так как U₁ может быть X или Y.)
      Если любое из этих случаев применяется, вывод производится из каждой Uᵢ к соответствующему Vᵢ следующим образом:
    • Если Uᵢ не известно как ссылочный тип, тогда делается точный вывод
    • В противном случае, если U является типом массива, то вывод с нижней границой зависитот типа V:
      • Если V это Span<Vᵢ>, то производится точное вывод
      • Если V является типом массива или типом ReadOnlySpan<Vᵢ>, то производится вывод с нижней границой .
    • В противном случае, если U это Span<Uᵢ> вывод, зависит от типа V:
      • Если V это Span<Vᵢ>, то производится точное вывод
      • Если V это ReadOnlySpan<Vᵢ>, то производится вывод с нижней границой
    • В противном случае выполняется ReadOnlySpan<Uᵢ>UVвывод с нижнейReadOnlySpan<Vᵢ> границой:
    • В противном случае, если VC<V₁...Vₑ>, вывод основывается на параметре типа i-thC:
      • Если он ковариантный, то производится вывод с нижней границой.
      • Если это контравариант, то производится вывод с верхней границой.
      • Если инвариантный, то производится точный вывод.
  • В противном случае никаких выводов не производится.

Нет правил для вывода верхнего предела , потому что их нельзя было бы ударить. Вывод типов никогда не начинается как верхний, он должен пройти через вывод нижней границы и параметр контравариантного типа. Из-за правила "если Uᵢ не известно, что тип ссылки, то производится точное вывод ", аргумент исходного типа не может быть (они не могут быть Span/ReadOnlySpan ссылочными типами). Однако вывод верхнего диапазона будет применяться только в том случае, если исходный тип был типом Span/ReadOnlySpan, так как он будет иметь такие правила:

  • U Span<U₁> V является типом V₁[] массива или типомSpan<V₁>
  • U ReadOnlySpan<U₁> V— это тип V₁[] массива или или Span<V₁>ReadOnlySpan<V₁>

Кардинальные изменения

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

Вызов Reverse массива

Вызов x.Reverse() , где x является экземпляр типа T[] , ранее привязывается IEnumerable<T> Enumerable.Reverse<T>(this IEnumerable<T>)к , в то время как теперь он привязывается к void MemoryExtensions.Reverse<T>(this Span<T>). К сожалению, эти API несовместимы (последний делает разворот на месте и возвращает).void

.NET 10 устраняет это путем добавления перегрузки IEnumerable<T> Reverse<T>(this T[])для конкретного массива, см. раздел https://github.com/dotnet/runtime/issues/107723.

void M(int[] a)
{
    foreach (var x in a.Reverse()) { } // fine previously, an error now (`Reverse` returns `void`)
    foreach (var x in Enumerable.Reverse(a)) { } // workaround
}

См. также:

Собрание по проектированию: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#reverse

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

В следующих примерах ранее не удалось определить тип для перегрузки Span, но теперь вывод типа из массива в Span завершается успешно, поэтому они являются неоднозначными. Для этого пользователи могут использовать .AsSpan() или авторы API.OverloadResolutionPriorityAttribute

var x = new long[] { 1 };
Assert.Equal([2], x); // previously Assert.Equal<T>(T[], T[]), now ambiguous with Assert.Equal<T>(ReadOnlySpan<T>, Span<T>)
Assert.Equal([2], x.AsSpan()); // workaround
var x = new int[] { 1, 2 };
var s = new ArraySegment<int>(x, 1, 1);
Assert.Equal(x, s); // previously Assert.Equal<T>(T, T), now ambiguous with Assert.Equal<T>(Span<T>, Span<T>)
Assert.Equal(x.AsSpan(), s); // workaround

xUnit добавляет дополнительные перегрузки, чтобы устранить эту проблему: https://github.com/xunit/xunit/discussions/3021

Собрание по проектированию: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#new-ambiguities

Ковариантные массивы

Перегрузки, принимающие IEnumerable<T> ковариантные массивы, но перегрузки, принимающие Span<T> (которые мы сейчас предпочитаем), не делают, так как преобразование диапазона создает ArrayTypeMismatchException исключение для ковариантных массивов. Возможно, перегрузка Span<T> не должна существовать, она должна принимать ReadOnlySpan<T> вместо этого. Чтобы обойти эту проблему, пользователи могут использовать, или авторы API могут использовать .AsEnumerable()OverloadResolutionPriorityAttribute или добавить ReadOnlySpan<T> перегрузку, которая предпочтительна из-за правила улучшения.

string[] s = new[] { "a" };
object[] o = s;

C.R(o); // wrote 1 previously, now crashes in Span<T> constructor with ArrayTypeMismatchException
C.R(o.AsEnumerable()); // workaround

static class C
{
    public static void R<T>(IEnumerable<T> e) => Console.Write(1);
    public static void R<T>(Span<T> s) => Console.Write(2);
    // another workaround:
    public static void R<T>(ReadOnlySpan<T> s) => Console.Write(3);
}

Собрание по проектированию: https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-11.md#covariant-arrays

Предпочтительный вариант ReadOnlySpan по диапазону

Правило улучшения приводит к предпочтению перегрузки ReadOnlySpan по сравнению с перегрузками Span, чтобы избежать ArrayTypeMismatchExceptions в сценариях ковариантного массива. Это может привести к разрывам компиляции в некоторых сценариях, например, если перегрузки отличаются по их типу возвращаемого значения:

double[] x = new double[0];
Span<ulong> y = MemoryMarshal.Cast<double, ulong>(x); // previously worked, now a compilation error (returns ReadOnlySpan, not Span)
Span<ulong> z = MemoryMarshal.Cast<double, ulong>(x.AsSpan()); // workaround

static class MemoryMarshal
{
    public static ReadOnlySpan<TTo> Cast<TFrom, TTo>(ReadOnlySpan<TFrom> span) => default;
    public static Span<TTo> Cast<TFrom, TTo>(Span<TFrom> span) => default;
}

См. https://github.com/dotnet/roslyn/issues/76443.

Деревья выражений

Перегрузки, принимающие диапазоны, такие как MemoryExtensions.Contains предпочтительнее по сравнению с классическими перегрузками, например Enumerable.Contains, даже внутри деревьев выражений, но ссылочные структуры не поддерживаются обработчиком интерпретаторов:

Expression<Func<int[], int, bool>> exp = (array, num) => array.Contains(num);
exp.Compile(preferInterpretation: true); // fails at runtime in C# 14

Expression<Func<int[], int, bool>> exp2 = (array, num) => Enumerable.Contains(array, num); // workaround
exp2.Compile(preferInterpretation: true); // ok

Аналогичным образом, механизмы перевода, такие как LINQ-to-SQL, должны реагировать на это, если их посетители дерева ожидают Enumerable.Contains , потому что они будут сталкиваться MemoryExtensions.Contains вместо этого.

См. также:

Проектирование собраний:

Определяемые пользователем преобразования через наследование

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

Span<string> span = [];
var d = new Derived();
d.M(span); // Base today, Derived tomorrow
int i = 1;
d.M(i); // Derived today, demonstrates new behavior

class Base
{
    public void M(Span<string> s)
    {
        Console.WriteLine("Base");
    }

    public void M(int i)
    {
        Console.WriteLine("Base");
    }
}

class Derived : Base
{
    public static implicit operator Derived(ReadOnlySpan<string> r) => new Derived();
    public static implicit operator Derived(long l) => new Derived();

    public void M(Derived s)
    {
        Console.WriteLine("Derived");
    }
}

Дополнительные сведения см. по ссылке https://github.com/dotnet/roslyn/issues/78314

Поиск метода расширения

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

namespace N1
{
    using N2;

    public class C
    {
        public static void M()
        {
            Span<string> span = new string[0];
            span.Test(); // Prints N2 today, N1 tomorrow
        }
    }

    public static class N1Ext
    {
        public static void Test(this ReadOnlySpan<string> span)
        {
            Console.WriteLine("N1");
        }
    }
}

namespace N2
{
    public static class N2Ext
    {
        public static void Test(this Span<string> span)
        {
            Console.WriteLine("N2");
        }
    }
}

Открытые вопросы

Неограниченное правило улучшения

Следует ли сделать правило улучшения безусловным для LangVersion? Это позволит авторам API добавлять новые API Span, в которых существуют эквиваленты IEnumerable без нарушения пользователей на старых LangVersions или других компиляторах или языках (например, VB). Однако это означает, что пользователи могут получить другое поведение после обновления набора инструментов (без изменения LangVersion или TargetFramework):

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

Обратите внимание, что не удается полностью решить эту проблему, OverloadResolutionPriorityAttribute так как она также игнорируется в старых LangVersions. Однако его следует использовать, чтобы избежать неоднозначности из VB, где следует распознать атрибут.

Игнорируя более определяемые пользователем преобразования

Мы определили набор пар типов, для которых существуют неявные и явные преобразования диапазона, определяемые языком. При каждом преобразовании, определяемом языком, T1T2в любой определяемый пользователем преобразование из T1T2нее игнорируется (независимо от диапазона и определяемого пользователем преобразования неявным или явным).

Обратите внимание, что это включает в себя все условия, поэтому, например, нет преобразования диапазона в Span<object>ReadOnlySpan<string> (имеется преобразование диапазона из Span<T>ReadOnlySpan<U> него, но оно должно храниться T : U), поэтому определяемое пользователем преобразование будет считаться между этими типами, если оно существует (это должно быть специализированное преобразование, например Span<T>ReadOnlySpan<string> , так как операторы преобразования не могут иметь универсальные параметры).

Следует ли игнорировать определяемые пользователем преобразования также между другими сочетаниями массивов/ Span/ReadOnlySpan/string, в которых не существует соответствующего преобразования диапазона, определяемого языком? Например, если у пользователя есть определяемое пользователем преобразование ReadOnlySpan<T>Span<T>, следует ли игнорировать его?

Возможности спецификации для рассмотрения:

  1. Всякий раз, когда преобразование диапазона существует в T1T2, игнорируйте любое пользовательское преобразование из T2T1или из T2T1.

  2. Определяемые пользователем преобразования не учитываются при преобразовании между ними.

    • любой одномерный array_type и System.Span<T>/System.ReadOnlySpan<T>,
    • любое сочетание System.Span<T>/System.ReadOnlySpan<T>,
    • string и System.ReadOnlySpan<char>.
  3. Как показано выше, но замена последней точки маркера на:
    • string и System.Span<char>/System.ReadOnlySpan<char>.
  4. Как показано выше, но замена последней точки маркера на:
    • string и System.Span<T>/System.ReadOnlySpan<T>.

Технически спецификация запрещает некоторые из этих пользовательских преобразований даже определять: невозможно определить определяемый пользователем оператор между типами, для которых существует неясное преобразование (§10.5.2). Но Рослин намеренно нарушает эту часть спецификации. И некоторые преобразования, такие как между Span и string разрешены в любом случае (между этими типами не существует определяемого языком преобразования).

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

Альтернативные варианты

Держите вещи, как они есть.