てくメモ

trivial な notes

【C#】範囲の概念のある処理の引数に範囲構文を導入する

スライシングでお世話になる範囲構文。

var span = new int[] { 1, 2, 3, 4, 5 }.AsSpan();

var a = span[..];   // 1, 2, 3, 4, 5
var b = span[3..];  // 4, 5
var c = span[..3];  // 1, 2, 3
var d = span[2..4]; // 3, 4
var e = span[^1..]; // 5
var f = span[..^1]; // 1, 2, 3, 4

その実体はRange型(とそれが持つIndex型)。


なので、引数にRangeを取るだけで範囲構文を受けることが可能。
処理でスライシングを使うなら、利用には引数で受けたRangeを使うだけ。

以下は指定された範囲の文字列を反転するメソッドの例。

public static string Reverse(this string str) => str.Reverse(Range.All);
public static string Reverse(this string str, Range range)
{
    return string.Create(str.Length, (range, str), static (buf, state) =>
    {
        var (range, str) = state;
        str.AsSpan().CopyTo(buf);
        buf[range].Reverse();
    });
}

以下のように範囲構文を受けることができる。

var str = "イーハトーヴォ";

Console.WriteLine(str.Reverse());
Console.WriteLine(str.Reverse(2..5));
Console.WriteLine(str.Reverse(..^1));
// ォヴートハーイ
// イーートハヴォ
// ヴートハーイォ


ところで、RangeにはGetOffsetAndLength(int)という、コレクションの長さを利用してオフセット(開始インデックス)と長さを計算してくれるメソッドがある。
これを利用すると、スライシングを直接行わない範囲を表す処理に、Rangeを簡単に導入できる。

以下のような累積和を表すクラスを考えてみる。

    public class CumulativeSum<T>
        where T : struct, IAdditionOperators<T, T, T>, ISubtractionOperators<T, T, T>
    {
        private readonly T[] sums;
        public int Length => sums.Length - 1;

        public CumulativeSum(scoped ReadOnlySpan<T> span)
        {
            sums = new T[span.Length + 1];
            sums[0] = default;
            for (int i = 0; i < span.Length; i++)
            {
                sums[i + 1] = sums[i] + span[i];
            }
        }
    }

[left, right) 範囲をRangeで指定して区間和を取るインデクサは次のように書ける。

public T this[Range range]
{
    get
    {
        var (offset, length) = range.GetOffsetAndLength(Length);
        return sums[offset + length] - sums[offset];
    }
}

GetOffsetAndLength(int)のおかげでRangeからの変換を意識しなくて済む。


使用例。
参考:累積和を何も考えずに書けるようにする! - Qiita

var array = new int[] { 2, 5, -4, 10, 3 };
var sum = new CumulativeSum<int>(array);
int N = 5;
int K = 3;
int result = int.MinValue;
for (int i = 0; i < N - K; i++)
{
    var current = sum[i..(i + K)];

    if (result < current) result = current;
}

Console.WriteLine(result);
// 11


ちなみに、Rangereadonly structなので引数においては値の参照渡し(in引数)が浮かぶが、サイズがintふたつぶんということで値渡しでよさそう。
参考:参照渡し - C# によるプログラミング入門 | ++C++; // 未確認飛行 C
なお、Enumerable.Take(Range)では値渡しだった。