开篇解读:
索引和范围的特性,可以使我们很方便的获取序列(比如数组)中的某一段数据或者某个位置的数据,跟python的索引差不多。为此,C#添加了两个新的类型和两个新的运算符以达到此目的。
一、定义
索引和范围为访问序列中的单个元素或范围提供了简洁的语法。
此语言支持依赖于两个新类型和两个新运算符:
- System.Index 表示一个序列索引。
- 来自末尾运算符
^
的索引,指定一个索引与序列末尾相关。 - System.Range 表示序列的子范围。
- 范围运算符
..
,用于指定范围的开始和末尾,就像操作数一样。
二、使用
2.1 索引
string[] words = new string[]
{
// index from start index from end
"The", // 0 ^9
"quick", // 1 ^8
"brown", // 2 ^7
"fox", // 3 ^6
"jumped", // 4 ^5
"over", // 5 ^4
"the", // 6 ^3
"lazy", // 7 ^2
"dog" // 8 ^1
}; // 9 (or words.Length) ^0
//1. 正向索引是从0~9,负向索引是从^9~^0 其中9跟^0等同于Length,无效的索引,同样会引发异常。
var dogStr = words[^1]; // "dog"
var quickStr = words[1]; // "quick" 等同于 words[^8]
Index the = ^3;
var theStr = words[the]; // "the"
//2. 对于任何数字 n, words[^n] = words[words.Length - n]
2.2 范围
//3. 范围Range表示:n..m, 是前闭后开的一个范围[1,3)
string[] quickBrownFox = words[1..4]; // 含有quick,Brown,Fox三个字符串的数组,不包含索引4的值
string[] allWords = words[..]; // contains "The" through "dog".
string[] firstPhrase = words[..4]; // contains "The" through "fox"
string[] lastPhrase = words[6..]; // contains "the, "lazy" and "dog"
//4. 直接声明变量
Range phrase = 1..4;
string[] text = words[phrase];
2.3 类比其他语言
C#表示一个索引范围: 0..^0
Python表示一个索引范围:0:-0
三、目的以及实现原理
3.1 目的:为访问序列中的单个元素或范围提供了简洁的语法。
3.2 实现原理
新的索引器将通过将类型的参数转换 System.Index
为 int
并发出对基于索引器的调用来实现 int。
其实就是编译后对索引进行了额外处理。
// 字符串:编译前
string str = "hello world";
var s2 = str[0..^1];
// 字符串:编译后(计算0..^1 对应的"真实"索引)
int num = 0;
int num2 = length - 1 - num;
string s2 = text.Substring(num, num2);
// List:编译前
List<string> m_List = new List<string>();
var l1 = m_List[^1];
// List:编译后(计算^1 对应的"真实"索引)
num2 = list.Count - 1;
string l = list[num2];
// 数组:编译前
var strs = words[^3..2];
// 数组:编译后
string[] str = RuntimeHelpers.GetSubArray<string>(words, new Range(new Index(3, true), 2));
关于System.Index和system.Range类型的定义如下:
说明:
1、从上面的定义中就可以看到,Index类型的实现实际上就是存了一个int的值,在构造函数中,根据传入的fromEnd的值,if true,则会将value取反,否则正常存为一个int。
当然还有一个重要的属性,IsFromEnd(是否是从后开始),取决于这个值的正负。如果是正,则是正向索引,如果是负(因为取反了),则是逆向索引。如果是负的,当取值的时候会再次取反。
2、Range就是持有了两个Index,一个start,一个end。
3.3 有哪些类型支持索引和范围?
1、若任何类型提供带 Index 或 Range 参数的索引器,则该类型可分别显式支持索引或范围
2、若类型包含名称为 Length 或 Count 的属性,属性有可访问的 Getter 并且其返回类型为 int,则此类型为可计数类型。 不显式支持索引或范围的可计数类型可能为它们提供隐式支持。
比如:
以下 .NET 类型同时支持索引和范围:String、Span<T> 和 ReadOnlySpan<T>。 List<T> 支持索引,但不支持范围。
Array 具有更多的微妙行为。 单个维度数组同时支持索引和范围。 多维数组不支持索引器或范围。
详情参考:索引和范围的类型支持
3.4 自己定义的类型也想支持索引和范围怎么做
1、显示支持索引或范围
public class MyClass
{
List<int> m_List = new List<int>();
public int this[Index index]
{
get
{
var idx = index.IsFromEnd ? m_List.Count - index.Value : index.Value;
if (idx < 0 || idx >= m_List.Count)
{
return -1;
}
return m_List[idx];
}
}
public List<int> this[Range range]
{
get
{
var sIdx = range.Start;
var eIdx = range.End;
var startIndex = sIdx.IsFromEnd ? m_List.Count - sIdx.Value : sIdx.Value;
var endIndex = eIdx.IsFromEnd ? m_List.Count - eIdx.Value : eIdx.Value;
var newList = new List<int>();
for (int i = startIndex; i < endIndex; i++)
{
newList.Add(m_List[i]);
}
return newList;
}
}
}
2、满足以下条件,会隐式提供支持
- 类型为可数(提供名为Length或Count的属性,并开放Getter,返回值为int)
- 该类型具有可访问的实例索引器,该索引器采用单个
int
作为参数。
其实就是通过长度和int的索引器实现,来支撑查找逆序索引,就可以实现Index的索引器
public class MyClass
{
List<int> m_List = new List<int>();
public int Length
{
get { return m_List.Count; }
}
public int this[int index]
{
get
{
var idx = index;
if (idx < 0 || idx >= m_List.Count)
{
return -1;
}
return m_List[idx];
}
}
}
隐式索引支持
隐式范围支持
四、 总结
4.1 优点
方便访问序列中的单个元素或范围
4.2 限制
有一些类型不支持,自己要实现的话有一些限制条件
4.3 注意事项
注意性能问题,比如:
在所有情况下,Array 的范围运算符都会分配一个新的数组来存储返回的元素。
public static T[] GetSubArray<[Nullable(2)] T>(T[] array, Range
{
if (array == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.array);
}
ValueTuple<int, int> offsetAndLength = range.GetOffsetAndLength(array.Length);
int item = offsetAndLength.Item1;
int item2 = offsetAndLength.Item2;
if (default(T) == null && !(typeof(T[]) == array.GetType()))
{
T[] array2 = (T[])Array.CreateInstance(array.GetType().GetElementType(), item2);
Array.Copy(array, item, array2, 0, item2);
return array2;
}
if (item2 == 0)
{
return Array.Empty<T>();
}
T[] array3 = new T[item2];
// 从array里第item个元素开始,copy到array3里,长度为item2
Buffer.Memmove<T>(Unsafe.As<byte, T>(array3.GetRawSzArrayData()), Unsafe.Add<T>(Unsafe.As<byte, T>(array.GetRawSzArrayData()), item), (ulong)item2);
return array3;
}
参考:官方文档
使用索引和范围探索数据范围 | Microsoft Docs
Ranges and indices - C# 8.0 specification proposals | Microsoft Docs
时刻保持对代码的敬畏之心,我是一个游戏码农,一个追梦的人。
喜欢就点个关注吧。