C# 使用 Index 和 Range 簡化集合操作

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();

輸出結果如下:

output

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

來看下面這個示例吧,還是剛才那個類,我們支持一下 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 ..) 都只是語法糖,本質上是調用 IndexRange

Index 並不是支持負數索引,從最後向前索引只是編譯器幫我們做了一個轉換,轉換成從前到後的索引值,藉助於它,我們很多取集合最後一個元素的寫法就可以大大的簡化了,就可以從原來的 array[array.Length-1] => array[^1]

Range 在創建某個數組的子序列的時候就非常的方便, newArray = array[1..3],需要注意的是,Range 是”左閉右開”的,包含左邊界的值,不包含右邊界的值

還沒使用過 Index/Range 的快去體驗一下吧,用它們優化數組的操作吧~~

References

Tags: