范围Ranges

摘要Summary

此功能与提供两个新的运算符,它们允许构造 System.IndexSystem.Range 对象,并使用它们在运行时索引/切片集合。This feature is about delivering two new operators that allow constructing System.Index and System.Range objects, and using them to index/slice collections at runtime.

概述Overview

已知类型和成员Well-known types and members

若要将新的句法形式用于 System.IndexSystem.Range ,可能需要新的已知类型和成员,具体取决于所使用的语法格式。To use the new syntactic forms for System.Index and System.Range, new well-known types and members may be necessary, depending on which syntactic forms are used.

若要使用 () 的 "hat" 运算符 ^ ,则需要以下项To use the "hat" operator (^), the following is required

namespace System
{
    public readonly struct Index
    {
        public Index(int value, bool fromEnd);
    }
}

若要 System.Index 在数组元素访问中使用类型作为参数,需要以下成员:To use the System.Index type as an argument in an array element access, the following member is required:

int System.Index.GetOffset(int length);

.. 语法 System.Range 需要该 System.Range 类型,以及以下一个或多个成员:The .. syntax for System.Range will require the System.Range type, as well as one or more of the following members:

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

..语法允许不存在它的任何一个、两个或不存在任何参数。The .. syntax allows for either, both, or none of its arguments to be absent. 无论参数的数量如何, Range 构造函数始终都足以使用 Range 语法。Regardless of the number of arguments, the Range constructor is always sufficient for using the Range syntax. 但是,如果存在任何其他成员并且缺少一个或多个 .. 参数,则可以替换相应的成员。However, if any of the other members are present and one or more of the .. arguments are missing, the appropriate member may be substituted.

最后,对于 System.Range 要在数组元素访问表达式中使用的类型值,必须存在以下成员:Finally, for a value of type System.Range to be used in an array element access expression, the following member must be present:

namespace System.Runtime.CompilerServices
{
    public static class RuntimeHelpers
    {
        public static T[] GetSubArray<T>(T[] array, System.Range range);
    }
}

System.objectSystem.Index

C # 无法从末尾开始为集合编制索引,而是大多数索引器使用 "从开始" 概念,或执行 "长度-i" 表达式。C# has no way of indexing a collection from the end, but rather most indexers use the "from start" notion, or do a "length - i" expression. 我们引入了一个表示 "从末尾" 的新索引表达式。We introduce a new Index expression that means "from the end". 此功能将引入一个新的一元前缀 "hat" 运算符。The feature will introduce a new unary prefix "hat" operator. 其单个操作数必须可以转换为 System.Int32Its single operand must be convertible to System.Int32. 它将降低为适当的 System.Index 工厂方法调用。It will be lowered into the appropriate System.Index factory method call.

我们通过以下附加语法形式补充 unary_expression 的语法:We augment the grammar for unary_expression with the following additional syntax form:

unary_expression
    : '^' unary_expression
    ;

我们 从 end 运算符将此索引称为索引。We call this the index from end operator. 最终运算符 的预定义索引如下所示:The predefined index from end operators are as follows:

System.Index operator ^(int fromEnd);

仅为大于或等于零的输入值定义此运算符的行为。The behavior of this operator is only defined for input values greater than or equal to zero.

示例:Examples:

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.objectSystem.Range

C # 没有语法方法来访问集合的 "范围" 或 "切片"。C# has no syntactic way to access "ranges" or "slices" of collections. 通常,用户被迫实现复杂的结构来筛选/操作内存的切片,或利用 LINQ 方法(如) list.Skip(5).Take(2)Usually users are forced to implement complex structures to filter/operate on slices of memory, or resort to LINQ methods like list.Skip(5).Take(2). 如果添加了 System.Span<T> 和其他类似类型,则更重要的是在语言/运行时中的更深级别支持此类操作,并使接口具有统一的。With the addition of System.Span<T> and other similar types, it becomes more important to have this kind of operation supported on a deeper level in the language/runtime, and have the interface unified.

语言会引入新的范围运算符 x..yThe language will introduce a new range operator x..y. 它是一个接受两个表达式的二元中缀运算符。It is a binary infix operator that accepts two expressions. ) 下面的示例中可以省略任一操作数 (示例,它们必须转换为 System.IndexEither operand can be omitted (examples below), and they have to be convertible to System.Index. 它将降低到适当的 System.Range 工厂方法调用。It will be lowered to the appropriate System.Range factory method call.

我们将 multiplicative_expression 的 c # 语法规则替换为以下 (以便引入新的优先级别) :We replace the C# grammar rules for multiplicative_expression with the following (in order to introduce a new precedence level):

range_expression
    : unary_expression
    | range_expression? '..' range_expression?
    ;

multiplicative_expression
    : range_expression
    | multiplicative_expression '*' range_expression
    | multiplicative_expression '/' range_expression
    | multiplicative_expression '%' range_expression
    ;

所有形式的 range 运算符 都具有相同的优先级。All forms of the range operator have the same precedence. 此新优先级组低于 一元运算符 ,大于 乘法算术运算符This new precedence group is lower than the unary operators and higher than the multiplicative arithmetic operators.

我们 .. 将运算符称为 范围运算符We call the .. operator the range operator. 可以大致理解内置范围运算符,使其与此窗体的内置运算符的调用相对应:The built-in range operator can roughly be understood to correspond to the invocation of a built-in operator of this form:

System.Range operator ..(Index start = 0, Index end = ^0);

示例:Examples:

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 ,以避免对多维签名重载混合整数和索引的需求。Moreover, System.Index should have an implicit conversion from System.Int32, in order to avoid the need to overload mixing integers and indexes over multi-dimensional signatures.

向现有库类型添加索引和范围支持Adding Index and Range support to existing library types

隐式索引支持Implicit Index support

该语言将为满足以下条件的类型提供具有单个类型参数的实例索引器成员 IndexThe language will provide an instance indexer member with a single parameter of type Index for types which meet the following criteria:

  • 类型为可数。The type is Countable.
  • 该类型具有可访问的实例索引器,该索引器采用单个 int 作为参数。The type has an accessible instance indexer which takes a single int as the argument.
  • 该类型没有可访问的实例索引器,该索引器采用 Index 作为第一个参数。The type does not have an accessible instance indexer which takes an Index as the first parameter. Index必须是唯一的参数,否则剩余的参数必须是可选的。The Index must be the only parameter or the remaining parameters must be optional.

如果某个类型具有一个名为的属性 Length 或一个 Count 具有可访问 getter 的属性和一个返回类型,则该类型为可数 intA type is Countable if it has a property named Length or Count with an accessible getter and a return type of int. 语言可以利用此属性将类型的表达式转换为 Index int 表达式点的,而无需全部使用该类型 IndexThe language can make use of this property to convert an expression of type Index into an int at the point of the expression without the need to use the type Index at all. 如果同时 Length 存在和 CountLength 则优先。In case both Length and Count are present, Length will be preferred. 为方便起见,该建议将使用名称 Length 来表示 CountLengthFor simplicity going forward, the proposal will use the name Length to represent Count or Length.

对于此类类型,语言将充当格式的索引器成员, T this[Index index] 其中, Tint 基于索引器的返回类型(包括任何 ref 样式批注)。For such types, the language will act as if there is an indexer member of the form T this[Index index] where T is the return type of the int based indexer including any ref style annotations. 新成员将具有 get set 与索引器匹配的可访问性的和成员 intThe new member will have the same get and set members with matching accessibility as the int indexer.

新的索引器将通过将类型的参数转换 Indexint 并发出对基于索引器的调用来实现 intThe new indexer will be implemented by converting the argument of type Index into an int and emitting a call to the int based indexer. 出于讨论目的,我们使用的示例 receiver[expr]For discussion purposes, let's use the example of receiver[expr]. 将转换为,如下所示 expr intThe conversion of expr to int will occur as follows:

  • 如果参数的格式为 ^expr2 ,并且的类型 expr2 为,则 int 它将转换为 receiver.Length - expr2When the argument is of the form ^expr2 and the type of expr2 is int, it will be translated to receiver.Length - expr2.
  • 否则,它将转换为 expr.GetOffset(receiver.Length)Otherwise, it will be translated as expr.GetOffset(receiver.Length).

这允许开发人员 Index 在现有类型上使用该功能,而无需修改。This allows for developers to use the Index feature on existing types without the need for modification. 例如:For example:

List<char> list = ...;
var value = list[^1];

// Gets translated to
var value = list[list.Count - 1];

receiverLength 表达式会被适当地溅入,以确保任何副作用仅执行一次。The receiver and Length expressions will be spilled as appropriate to ensure any side effects are only executed once. 例如:For example:

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

此代码将打印 "获取长度 3"。This code will print "Get Length 3".

隐式范围支持Implicit Range support

该语言将为满足以下条件的类型提供具有单个类型参数的实例索引器成员 RangeThe language will provide an instance indexer member with a single parameter of type Range for types which meet the following criteria:

  • 类型为可数。The type is Countable.
  • 该类型具有一个名为的可访问成员 Slice ,它具有两个类型为的参数 intThe type has an accessible member named Slice which has two parameters of type int.
  • 该类型没有将单个 Range 作为第一个参数的实例索引器。The type does not have an instance indexer which takes a single Range as the first parameter. Range必须是唯一的参数,否则剩余的参数必须是可选的。The Range must be the only parameter or the remaining parameters must be optional.

对于这种类型的情况,语言将绑定为,因为该窗体的索引器成员 T this[Range range] TSlice 包含任何样式批注的方法的返回类型 refFor such types, the language will bind as if there is an indexer member of the form T this[Range range] where T is the return type of the Slice method including any ref style annotations. 新成员还将具有匹配的可访问性 SliceThe new member will also have matching accessibility with Slice.

Range 名为的表达式上绑定基于的索引器时 receiver ,将表达式转换为 Range 两个值,然后将该表达式转换为方法,这会降低 SliceWhen the Range based indexer is bound on an expression named receiver, it will be lowered by converting the Range expression into two values that are then passed to the Slice method. 出于讨论目的,我们使用的示例 receiver[expr]For discussion purposes, let's use the example of receiver[expr].

Slice将通过以下方式转换范围类型化表达式来获取的第一个参数:The first argument of Slice will be obtained by converting the range typed expression in the following way:

  • expr 的格式 expr1..expr2 (expr2 可以省略的位置) 并且 expr1 具有类型时 int ,它将作为发出 expr1When expr is of the form expr1..expr2 (where expr2 can be omitted) and expr1 has type int, then it will be emitted as expr1.
  • expr 的形式为 ^expr1..expr2 (可以在何处 expr2 省略) ,则它将作为发出 receiver.Length - expr1When expr is of the form ^expr1..expr2 (where expr2 can be omitted), then it will be emitted as receiver.Length - expr1.
  • expr 的形式为 ..expr2 (可以在何处 expr2 省略) ,则它将作为发出 0When expr is of the form ..expr2 (where expr2 can be omitted), then it will be emitted as 0.
  • 否则,它将作为发出 expr.Start.GetOffset(receiver.Length)Otherwise, it will be emitted as expr.Start.GetOffset(receiver.Length).

此值将在第二个参数的计算中重复使用 SliceThis value will be re-used in the calculation of the second Slice argument. 执行此操作时,它将被称为 startWhen doing so it will be referred to as start. Slice将通过以下方式转换范围类型化表达式来获取的第二个参数:The second argument of Slice will be obtained by converting the range typed expression in the following way:

  • expr 的格式 expr1..expr2 (expr1 可以省略的位置) 并且 expr2 具有类型时 int ,它将作为发出 expr2 - startWhen expr is of the form expr1..expr2 (where expr1 can be omitted) and expr2 has type int, then it will be emitted as expr2 - start.
  • expr 的形式为 expr1..^expr2 (可以在何处 expr1 省略) ,则它将作为发出 (receiver.Length - expr2) - startWhen expr is of the form expr1..^expr2 (where expr1 can be omitted), then it will be emitted as (receiver.Length - expr2) - start.
  • expr 的形式为 expr1.. (可以在何处 expr1 省略) ,则它将作为发出 receiver.Length - startWhen expr is of the form expr1.. (where expr1 can be omitted), then it will be emitted as receiver.Length - start.
  • 否则,它将作为发出 expr.End.GetOffset(receiver.Length) - startOtherwise, it will be emitted as expr.End.GetOffset(receiver.Length) - start.

将根据需要将 receiverLengthexpr 表达式溢出,以确保只执行一次副作用。The receiver, Length, and expr expressions will be spilled as appropriate to ensure any side effects are only executed once. 例如:For example:

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"。This code will print "Get Length 2".

此语言将用以下已知类型作为特例:The language will special case the following known types:

  • stringSubstring 将使用(而不是)方法 Slicestring: the method Substring will be used instead of Slice.
  • arraySystem.Runtime.CompilerServices.RuntimeHelpers.GetSubArray 将使用(而不是)方法 Slicearray: the method System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray will be used instead of Slice.

备选方法Alternatives

新操作员 (^..) 为句法糖。The new operators (^ and ..) are syntactic sugar. 此功能可以通过对和工厂方法的显式调用来实现 System.Index System.Range ,但它会导致更多样板代码,并将 unintuitive。The functionality can be implemented by explicit calls to System.Index and System.Range factory methods, but it will result in a lot more boilerplate code, and the experience will be unintuitive.

IL 表示形式IL Representation

这两个运算符将降低为常规索引器/方法调用,后续编译器层不会更改。These two operators will be lowered to regular indexer/method calls, with no change in subsequent compiler layers.

运行时行为Runtime behavior

  • 编译器可以优化内置类型(如数组和字符串)的索引器,并将索引的索引减小到适当的现有方法。Compiler can optimize indexers for built-in types like arrays and strings, and lower the indexing to the appropriate existing methods.
  • System.Index 如果用负值构造,将引发。System.Index will throw if constructed with a negative value.
  • ^0 不会引发,但会转换为其提供的集合/可枚举的长度。^0 does not throw, but it translates to the length of the collection/enumerable it is supplied to.
  • Range.All 在语义上等效于 0..^0 ,可以析构这些索引。Range.All is semantically equivalent to 0..^0, and can be deconstructed to these indices.

注意事项Considerations

基于 ICollection 检测可索引Detect Indexable based on ICollection

此行为的灵感是集合初始值设置项。The inspiration for this behavior was collection initializers. 使用 类型的 结构来传达它已选择加入功能。Using the structure of a type to convey that it had opted into a feature. 在集合初始值设置项的情况下,类型可以通过实现非泛型 (IEnumerable 选择加入) 。In the case of collection initializers types can opt into the feature by implementing the interface IEnumerable (non generic).

此建议最初要求类型实现 ICollection 才能限定为可索引。This proposal initially required that types implement ICollection in order to qualify as Indexable. 但是,这需要许多特殊情况:That required a number of special cases though:

  • ref struct:它们不能实现接口,但 类型 Span<T> (如 )非常适合用于索引/范围支持。ref struct: these cannot implement interfaces yet types like Span<T> are ideal for index / range support.
  • string:不实现 ICollection 并添加 interface 成本较大的 。string: does not implement ICollection and adding that interface has a large cost.

这意味着需要支持密钥类型特殊大小写。This means to support key types special casing is already needed. 的特殊大小写不太有趣,因为语言在降低、常量 string (foreach 等等的) 。的特殊大小写 ref struct 更值得关注,因为它对于整个类型类的特殊大小写。The special casing of string is less interesting as the language does this in other areas (foreach lowering, constants, etc ...). The special casing of ref struct is more concerning as it's special casing an entire class of types. 如果它们只有一个名为 的属性,其返回类型为 ,则它们 Count 被标记为"可索引 int "。They get labeled as Indexable if they simply have a property named Count with a return type of int.

考虑后,设计经过规范化,以表明具有返回类型 为 的属性的任何类型 Count / Length int 都是可索引的。After consideration the design was normalized to say that any type which has a property Count / Length with a return type of int is Indexable. 这样会删除所有特殊大小写,即使对于 string 和 数组也一样。That removes all special casing, even for string and arrays.

仅检测计数Detect just Count

检测属性名称或 Count Length 确实会使设计变得复杂一些。Detecting on the property names Count or Length does complicate the design a bit. 不过,仅选取一个进行标准化是不够的,因为它最终会排除大量类型:Picking just one to standardize though is not sufficient as it ends up excluding a large number of types:

  • 使用 Length :几乎排除 System.Collections 和子命名空间中每个集合。Use Length: excludes pretty much every collection in System.Collections and sub-namespaces. 这些类型往往派生自 ICollection ,因此 Count 比长度更可取。Those tend to derive from ICollection and hence prefer Count over length.
  • 使用 Count :排除 string 、数组 Span<T> 和大多数基于 ref struct 的类型Use Count: excludes string, arrays, Span<T> and most ref struct based types

在可索引类型的初始检测方面,额外的复杂性被其他方面的简化所超过。The extra complication on the initial detection of Indexable types is outweighed by its simplification in other aspects.

选择切片作为名称Choice of Slice as a name

之所以 Slice 选择该名称,是 .NET 中切片样式操作的实际标准名称。The name Slice was chosen as it's the de-facto standard name for slice style operations in .NET. 从 netcoreapp2.1 开始,所有范围样式类型都使用名称 Slice 进行切片操作。Starting with netcoreapp2.1 all span style types use the name Slice for slicing operations. 在 netcoreapp2.1 之前,实际上没有任何切片示例可查找示例。Prior to netcoreapp2.1 there really aren't any examples of slicing to look to for an example. 、、 等类型非常适合进行切片,但在添加类型时 List<T> ArraySegment<T> SortedList<T> 不存在概念。Types like List<T>, ArraySegment<T>, SortedList<T> would've been ideal for slicing but the concept didn't exist when types were added.

因此 Slice ,作为唯一的示例,已选择它作为名称。Thus, Slice being the sole example, it was chosen as the name.

索引目标类型转换Index target type conversion

在索引器表达式 Index 中查看转换的另一种方式是作为目标类型转换。Another way to view the Index transformation in an indexer expression is as a target type conversion. 语言将目标类型转换分配给 ,而不是像存在 窗体的成员一样 return_type this[Index] 进行绑定 intInstead of binding as if there is a member of the form return_type this[Index], the language instead assigns a target typed conversion to int.

此概念可以通用化为对可计数类型进行的所有成员访问。This concept could be generalized to all member access on Countable types. 每当类型为 的表达式用作实例成员调用的参数且接收方为 Countable 时, Index 表达式将目标类型转换为 intWhenever an expression with type Index is used as an argument to an instance member invocation and the receiver is Countable then the expression will have a target type conversion to int. 适用于此转换的成员调用包括方法、索引器、属性、扩展方法等。仅排除构造函数,因为它们没有接收器。The member invocations applicable for this conversion include methods, indexers, properties, extension methods, etc ... Only constructors are excluded as they have no receiver.

对于类型为 的任何表达式,将按如下所示实现目标类型转换 IndexThe target type conversion will be implemented as follows for any expression which has a type of Index. 出于讨论目的,让我们使用 的示例 receiver[expr]For discussion purposes lets use the example of receiver[expr]:

  • expr 为 形式 ^expr2 且 类型 expr2int 时,它将转换为 receiver.Length - expr2When expr is of the form ^expr2 and the type of expr2 is int, it will be translated to receiver.Length - expr2.
  • 否则,它将转换为 expr.GetOffset(receiver.Length)Otherwise, it will be translated as expr.GetOffset(receiver.Length).

receiver 适当地溢出 和 Length 表达式,以确保仅执行一次任何副作用。The receiver and Length expressions will be spilled as appropriate to ensure any side effects are only executed once. 例如:For example:

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

此代码将打印"获取长度 3"。This code will print "Get Length 3".

此功能对具有表示索引的参数的任何成员都有利。This feature would be beneficial to any member which had a parameter that represented an index. 例如,List<T>.InsertAtFor example List<T>.InsertAt. 这还可能会引起混淆,因为语言无法就表达式是否用于索引提供任何指导。This also has the potential for confusion as the language can't give any guidance as to whether or not an expression is meant for indexing. 它可以执行的所有操作是在对可计数类型调用成员时将任何表达式 Index int 转换为 。All it can do is convert any Index expression to int when invoking a member on a Countable type.

限制:Restrictions:

  • 只有当类型为 的表达式是成员的参数 Index 时,此转换才适用。This conversion is only applicable when the expression with type Index is directly an argument to the member. 它不适用于任何嵌套表达式。It would not apply to any nested expressions.

实现期间做出的决策Decisions made during implementation

  • 模式中的所有成员都必须是实例成员All members in the pattern must be instance members
  • 如果找到 Length 方法,但返回类型错误,请继续查找 CountIf a Length method is found but it has the wrong return type, continue looking for Count
  • 用于索引模式的索引器必须正好具有一个 int 参数The indexer used for the Index pattern must have exactly one int parameter
  • 用于范围模式的 Slice 方法必须正好具有两个 int 参数The Slice method used for the Range pattern must have exactly two int parameters
  • 查找模式成员时,我们将查找原始定义,而不是构造成员When looking for the pattern members, we look for original definitions, not constructed members

设计会议Design meetings