簡介
C# 8.0 引入了範圍(Ranges)和索引(Indices)功能,提供了更簡潔、更直觀的語法來處理集合中的元素和子集。這些功能大大簡化了數組、字符串、列表等數據結構的操作。
索引(Indices)
從末尾開始的索引
使用 ^ 運算符表示從末尾開始的索引:
int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// 傳統方式獲取最後一個元素
int last1 = numbers[numbers.Length - 1]; // 9
// 使用索引運算符獲取最後一個元素
int last2 = numbers[^1]; // 9
// 獲取倒數第二個元素
int secondLast = numbers[^2]; // 8
// 獲取倒數第三個元素
int thirdLast = numbers[^3]; // 7
索引的工作原理
索引實際上是 System.Index 結構體的語法糖:
// 以下兩行代碼是等價的
int last = numbers[^1];
int last = numbers[new Index(1, fromEnd: true)];
// 從開頭開始的索引
int first = numbers[0]; // 等價於 numbers[new Index(0, fromEnd: false)]
| 表達式 | 含義 | 等同於 |
|---|---|---|
^0 |
序列結束後的位置 | array.Length |
^1 |
最後一個元素 | array[array.Length - 1] |
^2 |
倒數第二個元素 | array[array.Length - 2] |
^n |
從末尾算起的第 n 個元素 | array[array.Length - n] |
範圍(Ranges)
基本範圍操作
使用 .. 運算符指定範圍:
int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// 獲取索引1到3的元素(不包括索引3)
int[] sub1 = numbers[1..3]; // [1, 2]
// 獲取從開始到索引3的元素
int[] sub2 = numbers[..3]; // [0, 1, 2]
// 獲取從索引6到末尾的元素
int[] sub3 = numbers[6..]; // [6, 7, 8, 9]
// 獲取所有元素
int[] all = numbers[..]; // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// 使用從末尾開始的索引定義範圍
int[] sub4 = numbers[^3..^1]; // [7, 8] - 從倒數第三個到倒數第一個(不包括)
範圍的工作原理
範圍實際上是 System.Range 結構體的語法糖:
// 以下兩行代碼是等價的
int[] sub = numbers[1..4];
int[] sub = numbers[new Range(1, 4)];
// Range 包含 Start 和 End 兩個 Index
Range range = 1..4;
Console.WriteLine($"Start: {range.Start}, End: {range.End}");
// 輸出: Start: 1, End: 4
| 語法 | 含義 | 等同於 |
|---|---|---|
.. |
整個範圍 | [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。
int[] original = [1, 2, 3, 4, 5];
int[] slice = original[1..4]; // [2, 3, 4]
// 修改原始數組會影響切片
original[2] = 100;
Console.WriteLine(string.Join(", ", slice)); // 2, 100, 4
// 修改切片也會影響原始數組
slice[1] = 200;
Console.WriteLine(string.Join(", ", original)); // 1, 2, 200, 4, 5
不同類型的使用示例
數組
int[] array = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// 獲取前5個元素
int[] firstFive = array[..5]; // [0, 1, 2, 3, 4]
// 獲取最後3個元素
int[] lastThree = array[^3..]; // [7, 8, 9]
// 獲取中間部分
int[] middle = array[3..7]; // [3, 4, 5, 6]
// 獲取除第一個和最後一個之外的所有元素
int[] withoutEnds = array[1..^1]; // [1, 2, 3, 4, 5, 6, 7, 8]
字符串
string text = "Hello, World!";
// 獲取前5個字符
string hello = text[..5]; // "Hello"
// 獲取最後6個字符
string world = text[^6..]; // "World!"
// 獲取逗號後的部分(不包括逗號本身和空格)
string afterComma = text[7..^1]; // "World"
// 獲取子字符串
string sub = text[7..12]; // "World"
列表(List<T>)
List<int> list = new List<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// 獲取範圍(需要轉換為數組或使用GetRange)
int[] subArray = list.ToArray()[2..6]; // [2, 3, 4, 5]
// 或者使用List的GetRange方法(不是基於範圍的語法,但功能類似)
List<int> subList = list.GetRange(2, 4); // [2, 3, 4, 5]
Span<T> 和 Memory<T>
int[] array = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// 創建Span並使用範圍
Span<int> span = array.AsSpan();
Span<int> spanSlice = span[2..6]; // [2, 3, 4, 5]
// 創建Memory並使用範圍
Memory<int> memory = array.AsMemory();
Memory<int> memorySlice = memory[3..7]; // [3, 4, 5, 6]
高級用法和模式
與模式匹配結合使用
int[] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// 使用範圍進行模式匹配
if (numbers is [0, 1, 2, .., 8, 9])
{
Console.WriteLine("數組以0,1,2開頭,以8,9結尾");
}
// 在switch表達式中使用
string result = numbers switch
{
[0, 1, 2, ..] => "以0,1,2開頭",
[.., 7, 8, 9] => "以7,8,9結尾",
[0, .., 9] => "以0開頭,以9結尾",
_ => "其他模式"
};
自定義類型支持範圍
要使自定義類型支持範圍操作,需要實現以下方法之一:
public class MyCollection<T>
{
private T[] _items;
public MyCollection(T[] items)
{
_items = items;
}
// 方法1:實現Slice方法
public MyCollection<T> Slice(int start, int length)
{
T[] slice = new T[length];
Array.Copy(_items, start, slice, 0, length);
return new MyCollection<T>(slice);
}
// 方法2:實現索引器接受Range參數
public MyCollection<T> this[Range range]
{
get
{
var (start, length) = GetStartAndLength(range);
return Slice(start, length);
}
}
// 輔助方法:將Range轉換為(start, length)
private (int start, int length) GetStartAndLength(Range range)
{
int start = range.Start.IsFromEnd ?
_items.Length - range.Start.Value : range.Start.Value;
int end = range.End.IsFromEnd ?
_items.Length - range.End.Value : range.End.Value;
int length = end - start;
return (start, length);
}
// 其他成員...
}
// 使用自定義集合的範圍操作
var collection = new MyCollection<int>(new[] { 0, 1, 2, 3, 4, 5 });
var subCollection = collection[1..4]; // 包含元素[1, 2, 3]
實際應用場景
字符串處理
// 提取文件擴展名
string GetFileExtension(string filename)
{
int dotIndex = filename.LastIndexOf('.');
return dotIndex >= 0 ? filename[(dotIndex + 1)..] : string.Empty;
}
// 提取域名
string GetDomain(string url)
{
int protocolEnd = url.IndexOf("://");
if (protocolEnd < 0) return url;
int domainStart = protocolEnd + 3;
int pathStart = url.IndexOf('/', domainStart);
return pathStart < 0 ?
url[domainStart..] :
url[domainStart..pathStart];
}
// 處理CSV行
string[] ParseCsvLine(string line)
{
List<string> fields = new List<string>();
int start = 0;
while (start < line.Length)
{
int end = line.IndexOf(',', start);
if (end < 0) end = line.Length;
fields.Add(line[start..end].Trim());
start = end + 1;
}
return fields.ToArray();
}
數據分頁
// 使用範圍實現分頁
public IEnumerable<T> GetPage<T>(T[] data, int pageNumber, int pageSize)
{
int startIndex = (pageNumber - 1) * pageSize;
if (startIndex >= data.Length)
return Enumerable.Empty<T>();
int endIndex = Math.Min(startIndex + pageSize, data.Length);
return data[startIndex..endIndex];
}
// 使用Span<T>提高性能
public ReadOnlySpan<T> GetPageSpan<T>(T[] data, int pageNumber, int pageSize)
{
int startIndex = (pageNumber - 1) * pageSize;
if (startIndex >= data.Length)
return ReadOnlySpan<T>.Empty;
int endIndex = Math.Min(startIndex + pageSize, data.Length);
return data.AsSpan()[startIndex..endIndex];
}
數組操作
// 數組旋轉
void RotateArrayLeft<T>(T[] array, int positions)
{
positions %= array.Length;
if (positions == 0) return;
// 創建臨時數組保存前positions個元素
T[] temp = array[..positions];
// 將剩餘元素向左移動
Array.Copy(array, positions, array, 0, array.Length - positions);
// 將臨時數組中的元素放回末尾
Array.Copy(temp, 0, array, array.Length - positions, positions);
}
// 數組分割
(T[] left, T[] right) SplitArray<T>(T[] array, int splitIndex)
{
return (array[..splitIndex], array[splitIndex..]);
}