范围Ranges
摘要Summary
此功能与提供两个新的运算符,它们允许构造 System.Index
和 System.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.Index
和 System.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.Int32
。Its 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..y
。The language will introduce a new range operator x..y
. 它是一个接受两个表达式的二元中缀运算符。It is a binary infix operator that accepts two expressions. ) 下面的示例中可以省略任一操作数 (示例,它们必须转换为 System.Index
。Either 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
该语言将为满足以下条件的类型提供具有单个类型参数的实例索引器成员 Index
:The 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 singleint
as the argument. - 该类型没有可访问的实例索引器,该索引器采用
Index
作为第一个参数。The type does not have an accessible instance indexer which takes anIndex
as the first parameter.Index
必须是唯一的参数,否则剩余的参数必须是可选的。TheIndex
must be the only parameter or the remaining parameters must be optional.
如果某个类型具有一个名为的属性 Length
或一个 Count
具有可访问 getter 的属性和一个返回类型,则该类型为可数 int
。A type is Countable if it has a property named Length
or Count
with an accessible getter and a return type of int
. 语言可以利用此属性将类型的表达式转换为 Index
int
表达式点的,而无需全部使用该类型 Index
。The 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
存在和 Count
, Length
则优先。In case both Length
and Count
are present, Length
will be preferred. 为方便起见,该建议将使用名称 Length
来表示 Count
或 Length
。For simplicity going forward, the proposal will use the name Length
to represent Count
or Length
.
对于此类类型,语言将充当格式的索引器成员, T this[Index index]
其中, T
是 int
基于索引器的返回类型(包括任何 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
与索引器匹配的可访问性的和成员 int
。The new member will have the same get
and set
members with matching accessibility as the int
indexer.
新的索引器将通过将类型的参数转换 Index
为 int
并发出对基于索引器的调用来实现 int
。The 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
int
:The conversion of expr
to int
will occur as follows:
- 如果参数的格式为
^expr2
,并且的类型expr2
为,则int
它将转换为receiver.Length - expr2
。When the argument is of the form^expr2
and the type ofexpr2
isint
, it will be translated toreceiver.Length - expr2
. - 否则,它将转换为
expr.GetOffset(receiver.Length)
。Otherwise, it will be translated asexpr.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];
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 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
该语言将为满足以下条件的类型提供具有单个类型参数的实例索引器成员 Range
:The 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
,它具有两个类型为的参数int
。The type has an accessible member namedSlice
which has two parameters of typeint
. - 该类型没有将单个
Range
作为第一个参数的实例索引器。The type does not have an instance indexer which takes a singleRange
as the first parameter.Range
必须是唯一的参数,否则剩余的参数必须是可选的。TheRange
must be the only parameter or the remaining parameters must be optional.
对于这种类型的情况,语言将绑定为,因为该窗体的索引器成员 T this[Range range]
T
是 Slice
包含任何样式批注的方法的返回类型 ref
。For 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. 新成员还将具有匹配的可访问性 Slice
。The new member will also have matching accessibility with Slice
.
在 Range
名为的表达式上绑定基于的索引器时 receiver
,将表达式转换为 Range
两个值,然后将该表达式转换为方法,这会降低 Slice
。When 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
,它将作为发出expr1
。Whenexpr
is of the formexpr1..expr2
(whereexpr2
can be omitted) andexpr1
has typeint
, then it will be emitted asexpr1
. - 当
expr
的形式为^expr1..expr2
(可以在何处expr2
省略) ,则它将作为发出receiver.Length - expr1
。Whenexpr
is of the form^expr1..expr2
(whereexpr2
can be omitted), then it will be emitted asreceiver.Length - expr1
. - 当
expr
的形式为..expr2
(可以在何处expr2
省略) ,则它将作为发出0
。Whenexpr
is of the form..expr2
(whereexpr2
can be omitted), then it will be emitted as0
. - 否则,它将作为发出
expr.Start.GetOffset(receiver.Length)
。Otherwise, it will be emitted asexpr.Start.GetOffset(receiver.Length)
.
此值将在第二个参数的计算中重复使用 Slice
。This value will be re-used in the calculation of the second Slice
argument. 执行此操作时,它将被称为 start
。When 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 - start
。Whenexpr
is of the formexpr1..expr2
(whereexpr1
can be omitted) andexpr2
has typeint
, then it will be emitted asexpr2 - start
. - 当
expr
的形式为expr1..^expr2
(可以在何处expr1
省略) ,则它将作为发出(receiver.Length - expr2) - start
。Whenexpr
is of the formexpr1..^expr2
(whereexpr1
can be omitted), then it will be emitted as(receiver.Length - expr2) - start
. - 当
expr
的形式为expr1..
(可以在何处expr1
省略) ,则它将作为发出receiver.Length - start
。Whenexpr
is of the formexpr1..
(whereexpr1
can be omitted), then it will be emitted asreceiver.Length - start
. - 否则,它将作为发出
expr.End.GetOffset(receiver.Length) - start
。Otherwise, it will be emitted asexpr.End.GetOffset(receiver.Length) - start
.
将根据需要将 receiver
、 Length
和 expr
表达式溢出,以确保只执行一次副作用。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:
string
:Substring
将使用(而不是)方法Slice
。string
: the methodSubstring
will be used instead ofSlice
.array
:System.Runtime.CompilerServices.RuntimeHelpers.GetSubArray
将使用(而不是)方法Slice
。array
: the methodSystem.Runtime.CompilerServices.RuntimeHelpers.GetSubArray
will be used instead ofSlice
.
备选方法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 to0..^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 likeSpan<T>
are ideal for index / range support.string
:不实现ICollection
并添加interface
成本较大的 。string
: does not implementICollection
and adding thatinterface
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 和子命名空间中每个集合。UseLength
: excludes pretty much every collection in System.Collections and sub-namespaces. 这些类型往往派生自ICollection
,因此Count
比长度更可取。Those tend to derive fromICollection
and hence preferCount
over length. - 使用
Count
:排除string
、数组Span<T>
和大多数基于ref struct
的类型UseCount
: excludesstring
, arrays,Span<T>
and mostref 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]
进行绑定 int
。Instead 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
表达式将目标类型转换为 int
。Whenever 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.
对于类型为 的任何表达式,将按如下所示实现目标类型转换 Index
。The 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
且 类型expr2
为int
时,它将转换为receiver.Length - expr2
。Whenexpr
is of the form^expr2
and the type ofexpr2
isint
, it will be translated toreceiver.Length - expr2
. - 否则,它将转换为
expr.GetOffset(receiver.Length)
。Otherwise, it will be translated asexpr.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>.InsertAt
。For 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 typeIndex
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