范围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.Reflection.CompilerServices.GetSubArray 将使用(而不是)方法 Slicearray: the method System.Reflection.CompilerServices.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 intThey 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. 这将删除所有特殊大小写,即使对于和数组也是如此 stringThat 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.web 和子命名空间中的每个集合。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. 从 netcoreapp 2.1 开始,所有范围样式类型使用 Slice 切片操作的名称。Starting with netcoreapp2.1 all span style types use the name Slice for slicing operations. 在 netcoreapp 2.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. 只要使用类型的表达式 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).

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 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:

  • 仅当类型为的表达式直接是成员的参数时,此转换才适用 IndexThis 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
  • 如果找到长度方法但其返回类型错误,则继续查找计数If 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
  • 用于范围模式的切片方法必须正好有两个 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