简介
C# 8.0 引入了范围(Ranges)和索引(Indices)功能,提供了更简洁、更直观的语法来处理集合中的元素和子集。这些功能大大简化了数组、字符串、列表等数据结构的操作。
索引(Indices)
从末尾开始的索引
使用 ^ 运算符表示从末尾开始的索引:
1int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; 2 3// 传统方式获取最后一个元素 4int last1 = numbers[numbers.Length - 1]; // 9 5 6// 使用索引运算符获取最后一个元素 7int last2 = numbers[^1]; // 9 8 9// 获取倒数第二个元素 10int secondLast = numbers[^2]; // 8 11 12// 获取倒数第三个元素 13int thirdLast = numbers[^3]; // 7 14
索引的工作原理
索引实际上是 System.Index 结构体的语法糖:
1// 以下两行代码是等价的 2int last = numbers[^1]; 3int last = numbers[new Index(1, fromEnd: true)]; 4 5// 从开头开始的索引 6int first = numbers[0]; // 等价于 numbers[new Index(0, fromEnd: false)] 7
| 表达式 | 含义 | 等同于 |
|---|---|---|
| ^0 | 序列结束后的位置 | array.Length |
| ^1 | 最后一个元素 | array[array.Length - 1] |
| ^2 | 倒数第二个元素 | array[array.Length - 2] |
| ^n | 从末尾算起的第 n 个元素 | array[array.Length - n] |
范围(Ranges)
基本范围操作
使用 .. 运算符指定范围:
1int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; 2 3// 获取索引1到3的元素(不包括索引3) 4int[] sub1 = numbers[1..3]; // [1, 2] 5 6// 获取从开始到索引3的元素 7int[] sub2 = numbers[..3]; // [0, 1, 2] 8 9// 获取从索引6到末尾的元素 10int[] sub3 = numbers[6..]; // [6, 7, 8, 9] 11 12// 获取所有元素 13int[] all = numbers[..]; // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 14 15// 使用从末尾开始的索引定义范围 16int[] sub4 = numbers[^3..^1]; // [7, 8] - 从倒数第三个到倒数第一个(不包括) 17
范围的工作原理
范围实际上是 System.Range 结构体的语法糖:
1// 以下两行代码是等价的 2int[] sub = numbers[1..4]; 3int[] sub = numbers[new Range(1, 4)]; 4 5// Range 包含 Start 和 End 两个 Index 6Range range = 1..4; 7Console.WriteLine($"Start: {range.Start}, End: {range.End}"); 8// 输出: Start: 1, End: 4 9
| 语法 | 含义 | 等同于 |
|---|---|---|
| .. | 整个范围 | [0..^0] |
| start.. | 从 start 到序列结束 | [start..^0] |
| ..end | 从开始到 end 之前 | [0..end] |
| start..end | 从 start 到 end 之前 | [start..end] |
| ^start..^end | 使用末尾索引指定范围 | [length - start..length - end] |
范围表达式返回值
范围表达式返回的是原序列的视图(view),而不是副本。对于数组、字符串等类型,它返回的是只读视图;对于 Span<T> 和 Memory<T>,它返回新的 Span 或 Memory。
1int[] original = [1, 2, 3, 4, 5]; 2int[] slice = original[1..4]; // [2, 3, 4] 3 4// 修改原始数组会影响切片 5original[2] = 100; 6Console.WriteLine(string.Join(", ", slice)); // 2, 100, 4 7 8// 修改切片也会影响原始数组 9slice[1] = 200; 10Console.WriteLine(string.Join(", ", original)); // 1, 2, 200, 4, 5 11
不同类型的使用示例
数组
1int[] array = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; 2 3// 获取前5个元素 4int[] firstFive = array[..5]; // [0, 1, 2, 3, 4] 5 6// 获取最后3个元素 7int[] lastThree = array[^3..]; // [7, 8, 9] 8 9// 获取中间部分 10int[] middle = array[3..7]; // [3, 4, 5, 6] 11 12// 获取除第一个和最后一个之外的所有元素 13int[] withoutEnds = array[1..^1]; // [1, 2, 3, 4, 5, 6, 7, 8] 14
字符串
1string text = "Hello, World!"; 2 3// 获取前5个字符 4string hello = text[..5]; // "Hello" 5 6// 获取最后6个字符 7string world = text[^6..]; // "World!" 8 9// 获取逗号后的部分(不包括逗号本身和空格) 10string afterComma = text[7..^1]; // "World" 11 12// 获取子字符串 13string sub = text[7..12]; // "World" 14
列表(List<T>)
1List<int> list = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; 2 3// 获取范围(需要转换为数组或使用GetRange) 4int[] subArray = list.ToArray()[2..6]; // [2, 3, 4, 5] 5 6// 或者使用List的GetRange方法(不是基于范围的语法,但功能类似) 7List<int> subList = list.GetRange(2, 4); // [2, 3, 4, 5] 8
Span<T> 和 Memory<T>
1int[] array = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; 2 3// 创建Span并使用范围 4Span<int> span = array.AsSpan(); 5Span<int> spanSlice = span[2..6]; // [2, 3, 4, 5] 6 7// 创建Memory并使用范围 8Memory<int> memory = array.AsMemory(); 9Memory<int> memorySlice = memory[3..7]; // [3, 4, 5, 6] 10
高级用法和模式
与模式匹配结合使用
1int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; 2 3// 使用范围进行模式匹配 4if (numbers is [0, 1, 2, .., 8, 9]) 5{ 6 Console.WriteLine("数组以0,1,2开头,以8,9结尾"); 7} 8 9// 在switch表达式中使用 10string result = numbers switch 11{ 12 [0, 1, 2, ..] => "以0,1,2开头", 13 [.., 7, 8, 9] => "以7,8,9结尾", 14 [0, .., 9] => "以0开头,以9结尾", 15 _ => "其他模式" 16}; 17
自定义类型支持范围
要使自定义类型支持范围操作,需要实现以下方法之一:
1public class MyCollection<T> 2{ 3 private T[] _items; 4 5 public MyCollection(T[] items) 6 { 7 _items = items; 8 } 9 10 // 方法1:实现Slice方法 11 public MyCollection<T> Slice(int start, int length) 12 { 13 T[] slice = new T[length]; 14 Array.Copy(_items, start, slice, 0, length); 15 return new MyCollection<T>(slice); 16 } 17 18 // 方法2:实现索引器接受Range参数 19 public MyCollection<T> this[Range range] 20 { 21 get 22 { 23 var (start, length) = GetStartAndLength(range); 24 return Slice(start, length); 25 } 26 } 27 28 // 辅助方法:将Range转换为(start, length) 29 private (int start, int length) GetStartAndLength(Range range) 30 { 31 int start = range.Start.IsFromEnd ? 32 _items.Length - range.Start.Value : range.Start.Value; 33 34 int end = range.End.IsFromEnd ? 35 _items.Length - range.End.Value : range.End.Value; 36 37 int length = end - start; 38 return (start, length); 39 } 40 41 // 其他成员... 42} 43 44// 使用自定义集合的范围操作 45var collection = new MyCollection<int>(new[] { 0, 1, 2, 3, 4, 5 }); 46var subCollection = collection[1..4]; // 包含元素[1, 2, 3] 47
实际应用场景
字符串处理
1// 提取文件扩展名 2string GetFileExtension(string filename) 3{ 4 int dotIndex = filename.LastIndexOf('.'); 5 return dotIndex >= 0 ? filename[(dotIndex + 1)..] : string.Empty; 6} 7 8// 提取域名 9string GetDomain(string url) 10{ 11 int protocolEnd = url.IndexOf("://"); 12 if (protocolEnd < 0) return url; 13 14 int domainStart = protocolEnd + 3; 15 int pathStart = url.IndexOf('/', domainStart); 16 17 return pathStart < 0 ? 18 url[domainStart..] : 19 url[domainStart..pathStart]; 20} 21 22// 处理CSV行 23string[] ParseCsvLine(string line) 24{ 25 List<string> fields = new List<string>(); 26 int start = 0; 27 28 while (start < line.Length) 29 { 30 int end = line.IndexOf(',', start); 31 if (end < 0) end = line.Length; 32 33 fields.Add(line[start..end].Trim()); 34 start = end + 1; 35 } 36 37 return fields.ToArray(); 38} 39
数据分页
1// 使用范围实现分页 2public IEnumerable<T> GetPage<T>(T[] data, int pageNumber, int pageSize) 3{ 4 int startIndex = (pageNumber - 1) * pageSize; 5 if (startIndex >= data.Length) 6 return Enumerable.Empty<T>(); 7 8 int endIndex = Math.Min(startIndex + pageSize, data.Length); 9 return data[startIndex..endIndex]; 10} 11 12// 使用Span<T>提高性能 13public ReadOnlySpan<T> GetPageSpan<T>(T[] data, int pageNumber, int pageSize) 14{ 15 int startIndex = (pageNumber - 1) * pageSize; 16 if (startIndex >= data.Length) 17 return ReadOnlySpan<T>.Empty; 18 19 int endIndex = Math.Min(startIndex + pageSize, data.Length); 20 return data.AsSpan()[startIndex..endIndex]; 21} 22
数组操作
1// 数组旋转 2void RotateArrayLeft<T>(T[] array, int positions) 3{ 4 positions %= array.Length; 5 if (positions == 0) return; 6 7 // 创建临时数组保存前positions个元素 8 T[] temp = array[..positions]; 9 10 // 将剩余元素向左移动 11 Array.Copy(array, positions, array, 0, array.Length - positions); 12 13 // 将临时数组中的元素放回末尾 14 Array.Copy(temp, 0, array, array.Length - positions, positions); 15} 16 17// 数组分割 18(T[] left, T[] right) SplitArray<T>(T[] array, int splitIndex) 19{ 20 return (array[..splitIndex], array[splitIndex..]); 21} 22