Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
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₁isSystem.ReadOnlySpan<E₁>,T₂isSystem.Span<E₂>, и преобразование удостоверений изE₁существующегоE₂T₁isSystem.ReadOnlySpan<E₁>,T₂isSystem.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₁...VₑиU₁...Uₑопределяются путем проверки, применяются ли какие-либо из следующих случаев:
V— это тип массиваV₁[...], аU— это тип массиваU₁[...]того же ранга.VSpan<V₁>Uявляется типомU₁[]массива или типомSpan<U₁>VReadOnlySpan<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₁[...]массива того же рангаVSpan<V₁>Uявляется типомU₁[]массива или типомSpan<U₁>VReadOnlySpan<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, так как он будет иметь такие правила:
USpan<U₁>Vявляется типомV₁[]массива или типомSpan<V₁>UReadOnlySpan<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://developercommunity.visualstudio.com/t/Backward-compatibility-issue-:-IEnumerab/10896189#T-ND10896782
Собрание по проектированию: 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 вместо этого.
См. также:
- 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");
}
}
Дополнительные сведения см. по ссылке 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>, следует ли игнорировать его?
Возможности спецификации для рассмотрения:
-
Всякий раз, когда преобразование диапазона существует в
T1T2, игнорируйте любое пользовательское преобразование изT2T1или изT2T1. -
Определяемые пользователем преобразования не учитываются при преобразовании между ними.
- любой одномерный
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