C# 使用 Index 和 Range 簡化集合操作
Intro
有的語言陣列的索引值是支援負數的,表示從後向前索引,比如:arr[-1]
從 C# 8 開始,C# 支援了陣列的反向 Index
,和 Range
操作,反向 Index
類似於其他語言中的負索引值,但其實是由編譯器幫我們做了一個轉換,Range
使得我們對陣列擷取某一部分的操作會非常簡單,下面來看一下如何使用吧
Sample
使用 ^
可以從集合的最後開始索引元素,如果從陣列的最後開始索引元素,最後一個元素應該是 1 而不是0如: arr[^1]
使用 ..
可以基於某個陣列擷取集合中的某一段建立一個新的陣列,比如 var newArray = array[1..^1]
,再來看一下下面的示例吧
int[] someArray = new int[5] { 1, 2, 3, 4, 5 };
int lastElement = someArray[^1]; // lastElement = 5
lastElement.Dump();
someArray[3..5].Dump();
someArray[1..^1].Dump();
someArray[1..].Dump();
someArray[..^1].Dump();
someArray[..2].Dump();
輸出結果如下:
Index
那麼它是如何實現的呢,索引值引入了一個新的資料結構 System.Index
,當你使用 ^
運算子的時候,實際轉換成了 Index
。
Index
:
public readonly struct Index : IEquatable<Index>
{
public Index(int value, bool fromEnd = false);
/// <summary>Create an Index pointing at first element.</summary>
public static Index Start => new Index(0);
/// <summary>Create an Index pointing at beyond last element.</summary>
public static Index End => new Index(~0);
//
// Summary:
// Gets a value that indicates whether the index is from the start or the end.
//
// Returns:
// true if the Index is from the end; otherwise, false.
public bool IsFromEnd { get; }
//
// Summary:
// Gets the index value.
//
// Returns:
// The index value.
public int Value { get; }
//
// Summary:
// Creates an System.Index from the end of a collection at a specified index position.
//
// Parameters:
// value:
// The index value from the end of a collection.
//
// Returns:
// The Index value.
public static Index FromEnd(int value);
//
// Summary:
// Create an System.Index from the specified index at the start of a collection.
//
// Parameters:
// value:
// The index position from the start of a collection.
//
// Returns:
// The Index value.
public static Index FromStart(int value);
//
// Summary:
// Returns a value that indicates whether the current object is equal to another
// System.Index object.
//
// Parameters:
// other:
// The object to compare with this instance.
//
// Returns:
// true if the current Index object is equal to other; false otherwise.
public bool Equals(Index other);
//
// Summary:
// Calculates the offset from the start of the collection using the given collection length.
//
// Parameters:
// length:
// The length of the collection that the Index will be used with. Must be a positive value.
//
// Returns:
// The offset.
public int GetOffset(int length);
//
// Summary:
// Converts integer number to an Index.
//
// Parameters:
// value:
// The integer to convert.
//
// Returns:
// An Index representing the integer.
public static implicit operator Index(int value);
}
如果想要自己自定義的集合支援 Index
這種從陣列最後索引的特性,只需要加一個型別是 Index
的索引器就可以了,正向索引也是支援的,int 會自動隱式轉換為 Index
,除了顯示的增加 Index
索引器之外,還可以隱式支援,實現一個 int Count {get;}
的屬性(屬性名叫 Length
也可以),再實現一個 int
型別的索引器就可以了
寫一個簡單的小示例:
private class TestCollection
{
public IList<int> Data { get; init; }
public int Count => Data.Count;
public int this[int index] => Data[index];
//public int this[Index index] => Data[index.GetOffset(Data.Count)];
}
var array = new TestCollection()
{
Data = new[] { 1, 2, 3 }
};
Console.WriteLine(array[^1]);
Console.WriteLine(array[1]);
Range
Range
是在 Index
的基礎上實現的,Range
需要兩個 Index
來指定開始和結束
public readonly struct Range : IEquatable<Range>
{
/// <summary>Represent the inclusive start index of the Range.</summary>
public Index Start { get; }
/// <summary>Represent the exclusive end index of the Range.</summary>
public Index End { get; }
/// <summary>Construct a Range object using the start and end indexes.</summary>
/// <param name="start">Represent the inclusive start index of the range.</param>
/// <param name="end">Represent the exclusive end index of the range.</param>
public Range(Index start, Index end)
{
Start = start;
End = end;
}
/// <summary>Create a Range object starting from start index to the end of the collection.</summary>
public static Range StartAt(Index start) => new Range(start, Index.End);
/// <summary>Create a Range object starting from first element in the collection to the end Index.</summary>
public static Range EndAt(Index end) => new Range(Index.Start, end);
/// <summary>Create a Range object starting from first element to the end.</summary>
public static Range All => new Range(Index.Start, Index.End);
/// <summary>Calculate the start offset and length of range object using a collection length.</summary>
/// <param name="length">The length of the collection that the range will be used with. length has to be a positive value.</param>
/// <remarks>
/// For performance reason, we don't validate the input length parameter against negative values.
/// It is expected Range will be used with collections which always have non negative length/count.
/// We validate the range is inside the length scope though.
/// </remarks>
public (int Offset, int Length) GetOffsetAndLength(int length);
}
如何在自己的類中支援 Range
呢?
一種方式是自己直接實現一個型別是 Range
的索引器
另外一種方式是隱式實現,在自定義類中新增一個 Count
屬性,然後實現一個 Slice
方法,Slice
方法有兩個 int
型別的引數,第一個參數列示 offset,第二個參數列示 length 或者 count
來看下面這個示例吧,還是剛才那個類,我們支援一下 Range
:
private class TestCollection
{
public IList<int> Data { get; init; }
//public int[] this[Range range]
//{
// get
// {
// var rangeInfo = range.GetOffsetAndLength(Data.Count);
// return Data.Skip(rangeInfo.Offset).Take(rangeInfo.Length).ToArray();
// }
//}
public int Count => Data.Count;
public int[] Slice(int start, int length)
{
var array = new int[length];
for (var i = start; i < length && i < Data.Count; i++)
{
array[i] = Data[i];
}
return array;
}
}
More
新的操作符 (^
and ..
) 都只是語法糖,本質上是呼叫 Index
、Range
Index
並不是支援負數索引,從最後向前索引只是編譯器幫我們做了一個轉換,轉換成從前到後的索引值,藉助於它,我們很多取集合最後一個元素的寫法就可以大大的簡化了,就可以從原來的 array[array.Length-1]
=> array[^1]
Range
在建立某個陣列的子序列的時候就非常的方便, newArray = array[1..3]
,需要注意的是,Range
是"左閉右開"的,包含左邊界的值,不包含右邊界的值
還沒使用過 Index
/Range
的快去體驗一下吧,用它們優化陣列的操作吧~~
References
- https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/ranges-indexes
- https://docs.microsoft.com/en-us/dotnet/api/system.index?view=net-5.0
- https://docs.microsoft.com/en-us/dotnet/api/system.range?view=net-5.0
- https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/ranges
- https://github.com/dotnet/runtime/blob/master/src/libraries/System.Private.CoreLib/src/System/Index.cs
- https://github.com/dotnet/runtime/blob/master/src/libraries/System.Private.CoreLib/src/System/Range.cs
- https://github.com/dotnet/runtime/blob/master/src/libraries/System.Private.CoreLib/src/System/ArraySegment.cs