Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Замечание
Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Этот документ включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию 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₁
не существует.- ...
Собрания по дизайну
- https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-12-04.md#preferring-readonlyspant-over-spant-conversions
- https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-12-09.md#first-class-span-open-questions
Замечания по улучшению
Правило лучшего преобразования выражений гарантирует, что всякий раз, когда перегрузка становится применимой из-за новых преобразований диапазона, любая потенциальная неоднозначность с другой перегрузкой исключается, поскольку предпочтение отдаётся перегрузке, которая стала применимой.
Без этого правила следующий код, успешно скомпилированный в 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://developercommunity.visualstudio.com/t/Extension-method-SystemLinqEnumerable/10790323
- https://developercommunity.visualstudio.com/t/Compilation-Error-When-Calling-Reverse/10818048
- https://developercommunity.visualstudio.com/t/Version-17131-has-an-obvious-defect-th/10858254
- https://developercommunity.visualstudio.com/t/Visual-Studio-2022-update-breaks-build-w/10856758
- https://github.com/dotnet/runtime/issues/111532
Собрание по проектированию: 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
вместо этого.
См. также:
- https://github.com/dotnet/runtime/issues/109757
- https://github.com/dotnet/docs/issues/43952
- https://github.com/dotnet/efcore/issues/35100
- https://github.com/dotnet/csharplang/discussions/8959
Собрания по дизайну
- https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-12-04.md#conversions-in-expression-trees
- https://github.com/dotnet/csharplang/blob/main/meetings/2025/LDM-2025-01-06.md#ignoring-ref-structs-in-expressions
Определяемые пользователем преобразования через наследование
Добавив неявные преобразования диапазона в список стандартных неявных преобразований, мы можем изменить поведение, если определяемые пользователем преобразования участвуют в иерархии типов. В этом примере показано, как изменение по сравнению с поведением в сценарии с целым числом уже ведет себя так, как это будет в 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>
, следует ли игнорировать его?
Варианты спецификаций на рассмотрение:
-
Всякий раз, когда преобразование диапазона существует от
T1
доT2
, игнорируйте любое пользовательское преобразование изT1
вT2
или изT2
вT1
. -
Определяемые пользователем преобразования не учитываются при преобразовании между ними
- любые одномерные
array_type
иSystem.Span<T>
/System.ReadOnlySpan<T>
, - любое сочетание
System.Span<T>
/System.ReadOnlySpan<T>
, -
string
иSystem.ReadOnlySpan<char>
.
- любые одномерные
- Как выше, но замените последний пункт маркера на:
-
string
иSystem.Span<char>
/System.ReadOnlySpan<char>
.
-
- Как выше, но замените последний пункт маркера на:
-
string
иSystem.Span<T>
/System.ReadOnlySpan<T>
.
-
Технически спецификация запрещает определять некоторые из этих пользовательских преобразований: невозможно определить пользовательский оператор между типами, для которых существует непользовательское преобразование (§10.5.2).
Но Рослин намеренно нарушает эту часть спецификации. И некоторые преобразования, такие как между Span
и string
, допускаются в любом случае (между этими типами не существует определяемого языком преобразования).
Тем не менее, вместо того чтобы просто игнорировать преобразования, мы можем запретить их определять вообще и, возможно, устранить нарушение спецификации по крайней мере для этих новых преобразований диапазона, т. е. изменить Roslyn так, чтобы фактически сообщать об ошибке во время компиляции, если эти преобразования определены (вероятно, за исключением тех, которые уже определены BCL).
Альтернативы
Держите вещи, как они есть.
C# feature specifications