Поделиться через


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

Замечание

Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Этот документ включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию 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/массив требовались перегрузки для Span и массивов, так как определяемые пользователем преобразования (существующие между Span/массив/ReadOnlySpan) не учитываются для получателей расширений.

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

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

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

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


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

  • От любого одномерного array_type с типом элемента Ei до System.Span<Ei>
  • От любого одномерного array_type с типом элемента Ei к System.ReadOnlySpan<Ui>, при условии, что Ei может быть ковариантно преобразован (§18.2.3.3) в Ui
  • От System.Span<Ti> до System.ReadOnlySpan<Ui>, при условии, что Ti ковариантно преобразуемо (§18.2.3.3) в Ui
  • От System.ReadOnlySpan<Ti> до System.ReadOnlySpan<Ui>, при условии, что Ti ковариантно преобразуемо (§18.2.3.3) в 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> при наличии явного преобразования ссылки от Ti до Ui.

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

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

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

Неявные преобразования диапазона исключены из правила, что невозможно определить определяемый пользователем оператор между типами, для которых существует неявное преобразование (§10.5.2 Разрешенные пользовательские преобразования). Это необходимо, чтобы BCL мог продолжать определять существующие операторы преобразования диапазона даже при переходе на C# 14 (они по-прежнему необходимы для более низких версий языка, а также потому, что эти операторы используются в кодогенерации новых стандартных преобразований диапазона). Но его можно рассматривать как подробные сведения о реализации (кодеген и более низкие 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>. Изменения времени выполнения потребуются для полной реализации вариативности через дженерики (см. https://github.com/dotnet/csharplang/blob/main/proposals/csharp-13.0/ref-struct-interfaces.md для использования ref struct типов в дженериках), но мы можем разрешить ограниченную степень ковариации с использованием предлагаемого API .NET 9: https://github.com/dotnet/runtime/issues/96952. Это позволит языку рассматривать System.ReadOnlySpan<T>, как если бы T был объявлен как out T в некоторых сценариях. Однако мы не рассматриваем этот вариант преобразования во всех сценариях дисперсии и не добавляем его в определение преобразуемой дисперсии в §18.2.3.3. Если в будущем мы изменим среду выполнения, чтобы более глубоко понять изменение, мы можем внести незначительное разрушительное изменение, чтобы полностью распознать это в рамках языка.

Шаблоны

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

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

Превращение Помощники
преобразование массива в Span 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₁ System.ReadOnlySpan<E₁>, T₂System.Span<E₂>, а преобразование идентичности из E₁ в E₂ существует
  • T₁ System.ReadOnlySpan<E₁>, T₂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 является одним из нефиксированныхXᵢ, U добавляется в набор точных границ для Xᵢ.
  • В противном случае наборы 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 — это тип массива V₁[...], а U — это тип массива U₁[...]одного ранга.
    • 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ᵢ>, то делается нижняя граница вывода
    • В противном случае, если U является ReadOnlySpan<Uᵢ> и V является ReadOnlySpan<Vᵢ>, выводится нижняя граница :
    • В противном случае, если V является C<V₁...Vₑ>, вывод зависит от параметра типа i-th для C:
      • Если он ковариантный, то производится вывод с нижней границой.
      • Если это контравариант, то производится вывод с верхней границой.
      • Если инвариантно, то производится точный вывод.
  • В противном случае никаких выводов не производится.

Нет правил для вывода верхнего предела, потому что их невозможно соблюсти. Вывод типа никогда не начинается как с верхней границы, ему необходимо пройти через вывод нижней границы и контравариантный параметр типа. Из-за правила "если 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>. Для этого пользователи могут использовать .AsEnumerable(), а авторы API могут использовать 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 вместо Span

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

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

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

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

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, где атрибут должен быть распознан.

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

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

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

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

Варианты спецификаций на рассмотрение:

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

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

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

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