Диапазоны
Заметка
Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Она включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.
Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия фиксируются в соответствующих собраниях по проектированию языка (LDM).
Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .
Проблема чемпиона: https://github.com/dotnet/csharplang/issues/185
Сводка
Эта функция заключается в доставке двух новых операторов, позволяющих создавать System.Index
и System.Range
объекты и использовать их для индексирования или среза коллекций во время выполнения.
Обзор
Известные типы и члены
Чтобы использовать новые синтаксические формы для System.Index
и System.Range
, могут потребоваться новые известные типы и члены, в зависимости от того, какие синтаксические формы используются.
Для использования оператора "hat" (^
) требуется следующее.
namespace System
{
public readonly struct Index
{
public Index(int value, bool fromEnd);
}
}
Чтобы использовать тип System.Index
в качестве аргумента в доступе к элементу массива, требуется следующий элемент:
int System.Index.GetOffset(int length);
Синтаксис ..
для System.Range
потребует типа System.Range
, а также одного или нескольких следующих элементов:
namespace System
{
public readonly struct Range
{
public Range(System.Index start, System.Index end);
public static Range StartAt(System.Index start);
public static Range EndAt(System.Index end);
public static Range All { get; }
}
}
Синтаксис ..
позволяет отсутствовать либо одному, либо обоим, либо ни одному из его аргументов. Независимо от количества аргументов, конструктор Range
всегда достаточно для использования синтаксиса Range
. Однако если отсутствуют какие-либо другие члены и один или несколько аргументов ..
отсутствуют, соответствующий член может быть заменен.
Наконец, для значения типа System.Range
, используемого в выражении доступа к элементу массива, должен присутствовать следующий элемент:
namespace System.Runtime.CompilerServices
{
public static class RuntimeHelpers
{
public static T[] GetSubArray<T>(T[] array, System.Range range);
}
}
System.Index
C# не предоставляет способ индексирования коллекции с конца; вместо этого в большинстве случаев индексирование производится с начала, или используется выражение "длина - i". Мы введем новое выражение индекса, которое означает "с конца". Эта функция представляет новый унарный префиксный оператор "hat" («шляпа»). Его одинарный операнд должен быть преобразован в System.Int32
. Он будет понижен в соответствующий вызов метода фабрики System.Index
.
Мы расширим грамматику для unary_expression с помощью следующей дополнительной формы синтаксиса:
unary_expression
: '^' unary_expression
;
Мы называем это индексом из конечного оператора. Предопределенный индекс из конечных операторов выглядит следующим образом:
System.Index operator ^(int fromEnd);
Поведение этого оператора определяется только для входных значений, превышающих или равных нулю.
Примеры:
var array = new int[] { 1, 2, 3, 4, 5 };
var thirdItem = array[2]; // array[2]
var lastItem = array[^1]; // array[new Index(1, fromEnd: true)]
System.Range
C# не имеет синтаксического способа доступа к "диапазонам" или "срезам" коллекций. Обычно пользователи вынуждены реализовывать сложные структуры для фильтрации и работы с срезами памяти или использовать методы LINQ, такие как list.Skip(5).Take(2)
. При добавлении System.Span<T>
и других аналогичных типов важно обеспечить поддержку такой операции на более глубоком уровне языка или среды выполнения и унифицировать интерфейс.
Язык вводит новый оператор диапазона x..y
. Это двоичный оператор infix, принимаюющий два выражения. Любой операнд может быть опущен (примеры ниже), и их необходимо преобразовать в System.Index
. Он будет снижен до соответствующего вызова метода фабрики System.Range
.
Мы заменим правила грамматики C# для multiplicative_expression следующими (чтобы ввести новый уровень приоритета):
range_expression
: unary_expression
| range_expression? '..' range_expression?
;
multiplicative_expression
: range_expression
| multiplicative_expression '*' range_expression
| multiplicative_expression '/' range_expression
| multiplicative_expression '%' range_expression
;
Все формы оператора диапазона имеют одинаковый приоритет. Эта новая группа приоритета ниже, чем унарные операторы и выше, чем умножающие арифметические операторы.
Мы называем оператор ..
оператором диапазона . Оператор встроенного диапазона можно примерно понять как соответствующий вызову встроенного оператора в такой форме:
System.Range operator ..(Index start = 0, Index end = ^0);
Примеры:
var array = new int[] { 1, 2, 3, 4, 5 };
var slice1 = array[2..^3]; // array[new Range(2, new Index(3, fromEnd: true))]
var slice2 = array[..^3]; // array[Range.EndAt(new Index(3, fromEnd: true))]
var slice3 = array[2..]; // array[Range.StartAt(2)]
var slice4 = array[..]; // array[Range.All]
Кроме того, System.Index
должно иметь неявное преобразование из System.Int32
, чтобы избежать необходимости перегружать операции смешения целых чисел и индексов в многомерных сигнатурах.
Добавление поддержки индекса и диапазона в существующие типы библиотек
Поддержка неявного индекса
Язык предоставит элемент индексатора экземпляра с одним параметром типа Index
для типов, которые соответствуют следующим критериям:
- Тип — Countable.
- Тип имеет доступный индексатор экземпляра, который в качестве аргумента принимает один элемент
int
. - Тип не имеет индексатора доступных экземпляров, который принимает
Index
в качестве первого параметра.Index
должен быть единственным параметром, или остальные параметры должны быть необязательными.
Тип countable, если он имеет свойство с именем Length
или Count
с доступным методом получения и типом возвращаемого значения int
. Язык может использовать это свойство для преобразования выражения типа Index
в int
в точке выражения без необходимости использовать тип Index
вообще. Если присутствуют оба Length
и Count
, Length
предпочтительнее. Для простоты предложение будет использовать название Length
для представления Count
или Length
.
Для таких типов язык будет вести себя так, будто существует элемент индексатора в форме T this[Index index]
, где T
является возвращаемым типом индексатора на основе int
, включая любые аннотации стиля ref
. Новый элемент будет иметь такие же элементы get
и set
с соответствующими правами доступа, как и индексатор int
.
Новый индексатор будет реализован путем преобразования аргумента типа Index
в int
и создания вызова индексатора на основе int
. В целях обсуждения рассмотрим пример receiver[expr]
. Преобразование expr
в int
будет выполняться следующим образом:
- Если аргумент имеет форму
^expr2
и типexpr2
int
, он будет переведен вreceiver.Length - expr2
. - В противном случае оно будет переведено как
expr.GetOffset(receiver.Length)
.
Независимо от конкретной стратегии преобразования порядок оценки должен быть эквивалентен следующему:
-
receiver
вычисляется; -
expr
вычисляется; -
length
вычисляется при необходимости; - Индексатор, основанный на
int
, активируется.
Это позволяет разработчикам использовать функцию Index
для существующих типов без необходимости изменения. Например:
List<char> list = ...;
var value = list[^1];
// Gets translated to
var value = list[list.Count - 1];
Выражения receiver
и Length
будут обработаны соответствующим образом, чтобы гарантировать, что все побочные эффекты выполняются только один раз. Например:
class Collection {
private int[] _array = new[] { 1, 2, 3 };
public int Length {
get {
Console.Write("Length ");
return _array.Length;
}
}
public int this[int index] => _array[index];
}
class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}
void Use() {
int i = Get()[^1];
Console.WriteLine(i);
}
}
Этот код выведет "Get Length 3".
Поддержка неявного диапазона
Язык предоставит элемент индексатора экземпляра с одним параметром типа Range
для типов, которые соответствуют следующим критериям:
- Тип — Countable.
- Тип имеет доступный элемент с именем
Slice
с двумя параметрами типаint
. - Тип не имеет индексатора экземпляров, который принимает один
Range
в качестве первого параметра.Range
должен быть единственным параметром, или остальные параметры должны быть необязательными.
Для таких типов язык будет связываться, как если бы существовал член индексатора формы T this[Range range]
, где T
является возвращаемым типом метода Slice
, включая любые аннотации стиля ref
. Новый член также будет иметь сопоставимую доступность с Slice
.
Если индексатор на основе Range
привязан к выражению с именем receiver
, он будет снижен путем преобразования выражения Range
в два значения, которые затем передаются методу Slice
. В целях обсуждения рассмотрим пример receiver[expr]
.
Первый аргумент Slice
будет получен путем преобразования типизированного выражения диапазона следующим образом:
- Если
expr
имеет формуexpr1..expr2
(гдеexpr2
может быть опущен) иexpr1
имеет типint
, он будет выдаваться какexpr1
. - Если
expr
имеет форму^expr1..expr2
(гдеexpr2
может быть опущен), она будет выдаваться какreceiver.Length - expr1
. - Если
expr
имеет форму..expr2
(гдеexpr2
может быть опущен), она будет выдаваться как0
. - В противном случае он будет выдаваться как
expr.Start.GetOffset(receiver.Length)
.
Это значение будет повторно использоваться в вычислении второго Slice
аргумента. При этом он будет называться start
. Второй аргумент Slice
будет получен путем преобразования типизированного выражения диапазона следующим образом:
- Если
expr
имеет формуexpr1..expr2
(гдеexpr1
может быть опущен) иexpr2
имеет типint
, он будет выдаваться какexpr2 - start
. - Если
expr
имеет формуexpr1..^expr2
(гдеexpr1
может быть опущен), она будет выдаваться как(receiver.Length - expr2) - start
. - Если
expr
имеет формуexpr1..
(гдеexpr1
может быть опущен), она будет выдаваться какreceiver.Length - start
. - В противном случае он будет выдаваться как
expr.End.GetOffset(receiver.Length) - start
.
Независимо от конкретной стратегии преобразования порядок оценки должен быть эквивалентен следующему:
-
receiver
вычисляется; -
expr
вычисляется; -
length
вычисляется при необходимости; - вызывается метод
Slice
.
Выражения receiver
, expr
и length
будут распределяться соответствующим образом, чтобы гарантировать, что любые побочные эффекты выполняются только один раз. Например:
class Collection {
private int[] _array = new[] { 1, 2, 3 };
public int Length {
get {
Console.Write("Length ");
return _array.Length;
}
}
public int[] Slice(int start, int length) {
var slice = new int[length];
Array.Copy(_array, start, slice, 0, length);
return slice;
}
}
class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}
void Use() {
var array = Get()[0..2];
Console.WriteLine(array.Length);
}
}
Этот код выведет "Длина 2".
Язык будет рассматривать особым образом следующие известные типы.
-
string
: методSubstring
будет использоваться вместоSlice
. -
array
: методSystem.Runtime.CompilerServices.RuntimeHelpers.GetSubArray
будет использоваться вместоSlice
.
Альтернативы
Новые операторы (^
и ..
) являются синтаксическим сахаром. Функциональность может быть реализована явными вызовами фабричных методов System.Index
и System.Range
, но это приведет к значительно большему количеству шаблонного кода, и опыт использования будет неинтуитивным.
Представление IL
Эти два оператора будут снижены до обычных вызовов индексатора или метода без изменений в последующих слоях компилятора.
Поведение среды выполнения
- Компилятор может оптимизировать индексаторы для встроенных типов, таких как массивы и строки, и снизить индексирование до соответствующих существующих методов.
-
System.Index
вызовет ошибку при создании с отрицательным значением. -
^0
не вызывает исключение, а преобразуется в длину коллекции или перечисления, к которому она применяется. -
Range.All
семантически эквивалентен0..^0
и может быть деконструенен для этих индексов.
Соображения
Определение индексируемых на основе ICollection
Вдохновение для этого поведения исходило от инициализаторов коллекций. Использование структуры типа для обозначения того, что он подключился к функции. В случае инициализаторов коллекций типы могут использовать эту возможность, реализуя интерфейс IEnumerable
(необобщённый).
Первоначально это предложение требовало, чтобы типы реализовали ICollection
, чтобы считаться индексируемыми. Это потребовало ряда особых сценариев, однако:
-
ref struct
: они пока не могут реализовать интерфейсы, но такие типы, какSpan<T>
, идеально подходят для поддержки индекса или диапазона. -
string
: не реализуетICollection
и добавляет этот интерфейс с большими затратами.
Это означает, что для поддержки специальных типов ключей уже требуется специальное регистрирование. Специальная обработка string
менее примечательна, так как язык делает это в других областях (понижениеforeach
, константы и т. д.). Специальная обработка ref struct
вызывает больше беспокойства, так как это специальная обработка всего класса типов. Они получают метку индексируемой, если у них просто есть свойство с именем Count
с возвращаемым типом int
.
После рассмотрения конструктор был нормализован, чтобы сказать, что любой тип, имеющий свойство Count
/ Length
с типом возвращаемого значения int
, является Индексируемым. Это удаляет все специальные регистры, даже для string
и массивов.
Определение простого числа
Обнаружение имен свойств Count
или Length
немного усложняет проектирование. Выбор только одного варианта для стандартизации недостаточно, поскольку это в конечном итоге исключает большое количество типов.
- Используйте
Length
: исключает практически каждую коллекцию в System.Collections и подпространствах имен. Они, как правило, являются производными отICollection
и, следовательно, предпочитаютCount
по длине. - Использование
Count
: исключаетstring
, массивы,Span<T>
и большинство типов на основеref struct
Дополнительное осложнение при первоначальном обнаружении индексируемых типов перевешивает его упрощение в других аспектах.
Выбор «Slice» в качестве названия
Имя Slice
было выбрано как имя, ставшее стандартом де-факто для операций с использованием стиля среза в .NET. Начиная с версии netcoreapp2.1 все стилевые типы диапазона используют имя Slice
для операций срезов. До netcoreapp2.1 действительно не было каких-либо примеров использования срезов, к которым можно было бы обратиться. Типы, такие как List<T>
, ArraySegment<T>
, SortedList<T>
, были бы идеальными для срезов, но концепция не существовала, когда типы добавлялись.
Таким образом, будучи единственным примером, Slice
было выбрано в качестве имени.
Преобразование целевого типа индекса
Другой способ взглянуть на преобразование Index
в выражении индексатора — это как на преобразование типу назначения. Вместо привязки, как если бы существовал элемент формы return_type this[Index]
, язык назначает целевое типизированное преобразование в int
.
Эта концепция может быть обобщена для доступа ко всем элементам типов Countable. Всякий раз, когда выражение с типом Index
используется как аргумент для вызова члена экземпляра, и получатель является счетным, выражение будет иметь преобразование в целевой тип int
. Вызовы членов, применимые для этого преобразования, включают методы, индексаторы, свойства, методы расширения и т. д. Только конструкторы исключаются, поскольку у них нет объекта-приемника.
Преобразование целевого типа будет реализовано следующим образом для любого выражения, имеющего тип Index
. В целях обсуждения можно использовать пример receiver[expr]
:
- Если
expr
имеет форму^expr2
и типexpr2
int
, он будет преобразован вreceiver.Length - expr2
. - В противном случае оно будет переведено как
expr.GetOffset(receiver.Length)
.
Выражения receiver
и Length
будут обработаны соответствующим образом, чтобы гарантировать, что все побочные эффекты выполняются только один раз. Например:
class Collection {
private int[] _array = new[] { 1, 2, 3 };
public int Length {
get {
Console.Write("Length ");
return _array.Length;
}
}
public int GetAt(int index) => _array[index];
}
class SideEffect {
Collection Get() {
Console.Write("Get ");
return new Collection();
}
void Use() {
int i = Get().GetAt(^1);
Console.WriteLine(i);
}
}
Этот код выведет "Get Length 3".
Эта функция будет полезна любому члену, у которого есть параметр, представляющий индекс. Например, List<T>.InsertAt
. Это также может привести к путанице, так как язык не может дать никаких рекомендаций о том, предназначено ли выражение для индексирования. Все, что можно сделать, — преобразовать любое выражение Index
в int
при вызове элемента в типе Countable.
Ограничения:
- Это преобразование применимо только в том случае, если выражение с типом
Index
является напрямую аргументом элемента. Он не будет применяться к вложенным выражениям.
Решения, принятые во время реализации
- Все элементы в шаблоне должны быть элементами экземпляра
- Если метод Length найден, но имеет неправильный тип возвращаемого значения, продолжить поиски метода Count.
- Индексатор, используемый для шаблона индекса, должен иметь ровно один параметр int
- Метод
Slice
, используемый для шаблона Range, должен иметь ровно два параметра int. - При поиске элементов шаблона мы ищем исходные определения, а не созданные элементы
Совещания по дизайну
C# feature specifications