博客 / 詳情

返回

C#.NET 範圍與索引(Range、Index)完全解析:語法、用法與最佳實踐

簡介

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>,它返回新的 SpanMemory

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..]);
}
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.