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


Диапазоны

Заметка

Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Она включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию 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 и тип expr2int, он будет переведен в receiver.Length - expr2.
  • В противном случае оно будет переведено как expr.GetOffset(receiver.Length).

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

  1. receiver вычисляется;
  2. expr вычисляется;
  3. length вычисляется при необходимости;
  4. Индексатор, основанный на 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.

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

  1. receiver вычисляется;
  2. expr вычисляется;
  3. length вычисляется при необходимости;
  4. вызывается метод 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 и тип expr2int, он будет преобразован в 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.
  • При поиске элементов шаблона мы ищем исходные определения, а не созданные элементы

Совещания по дизайну