前回の記事で System.Index を紹介しました。C# 8.0(.NET Core 3.0)ではあわせて System.Range 型と ..(範囲演算子)も追加されました。配列や文字列の部分範囲を、Array.Copy や Substring を使わず簡潔に取得できます。
従来の部分取得
配列の一部を切り出すには、従来は Array.Copy や LINQ の Skip / Take を使う必要がありました。
int[] numbers = { 10, 20, 30, 40, 50 };
// インデックス 1〜3 を取得(従来)
int[] sub = numbers.Skip(1).Take(3).ToArray(); // { 20, 30, 40 }
.. 演算子(範囲演算子)
C# 8.0 からは start..end の形で部分範囲を指定できます。
int[] numbers = { 10, 20, 30, 40, 50 };
// [0] [1] [2] [3] [4]
int[] sub = numbers[1..4]; // { 20, 30, 40 }
範囲の指定は 開始インデックスを含み、終了インデックスを含まない(左閉右開)です。1..4 は「インデックス 1 以上 4 未満」= インデックス 1、2、3 の要素です。
^ 演算子との組み合わせ
.. には System.Index を使えるため、^ と組み合わせた末尾基準の指定も書けます。
int[] numbers = { 10, 20, 30, 40, 50 };
int[] last3 = numbers[^3..]; // { 30, 40, 50 }(末尾 3 つ)
int[] first3 = numbers[..3]; // { 10, 20, 30 }(先頭 3 つ)
int[] middle = numbers[1..^1]; // { 20, 30, 40 }(先頭・末尾を除く)
int[] all = numbers[..]; // { 10, 20, 30, 40, 50 }(全要素)
| 書き方 | 意味 |
|---|---|
[start..end] |
インデックス start 以上 end 未満 |
[..end] |
先頭から end の手前まで |
[start..] |
start から末尾まで |
[..] |
全要素 |
[^n..] |
末尾 n 個 |
[..^n] |
末尾 n 個を除いた部分 |
System.Range とは
.. 演算子はコンパイル時に System.Range 型に変換されます。System.Range は start(含む)と end(含まない)の 2 つの Index を持つ読み取り専用の構造体です。
Range r1 = 1..4; // Index 1 〜 Index 4(4 は含まない)
Range r2 = ^3..^1; // 末尾から 3 番目〜末尾から 1 番目(1 は含まない)
Console.WriteLine(r1.Start); // 1
Console.WriteLine(r1.End); // 4
静的ファクトリメソッドでも作成できます。
Range r = Range.StartAt(new Index(2)); // 2..(インデックス 2 から末尾まで)
Range r2 = Range.EndAt(new Index(3)); // ..3(先頭からインデックス 3 の手前)
Range all = Range.All; // ..(全体)
文字列への適用
string も .. に対応しています。
string text = "Hello, World!";
string hello = text[..5]; // "Hello"
string world = text[7..12]; // "World"
string last5 = text[^5..]; // "orld!"
従来の Substring より簡潔に書けます。
Span と ReadOnlySpan への適用
.. 演算子は Span<T> / ReadOnlySpan<T> でも使えます。こちらはコピーを発生させずにスライスを返します。
int[] numbers = { 10, 20, 30, 40, 50 };
Span<int> span = numbers.AsSpan();
Span<int> sub = span[1..4]; // コピーなしで部分参照
ヒープ割り当てなしで部分参照できるため、パフォーマンスが要求される処理で有効です。
GetOffsetAndLength メソッド
Range を実際のオフセットと長さに変換するには GetOffsetAndLength(length) を使います。
Range r = 1..^1;
var (offset, length) = r.GetOffsetAndLength(5);
// offset = 1, length = 3 (コレクション長 5 で 1 〜 末尾の 1 つ手前 = 3 要素)
カスタム型に対応させる
独自のコレクション型で .. を使えるようにするには、int 型の Length または Count プロパティと int 型のインデクサーに加えて、Slice(int start, int length) メソッドを定義します。
class MyList
{
private int[] _data = { 1, 2, 3, 4, 5 };
public int Length => _data.Length;
public int this[int index] => _data[index];
public MyList Slice(int start, int length) => /* 省略 */;
}
まとめ
| 書き方 | 説明 |
|---|---|
a..b |
インデックス a 以上 b 未満 |
..b |
先頭から b の手前まで |
a.. |
a から末尾まで |
.. |
全体 |
^n.. |
末尾 n 個 |
..^n |
末尾 n 個を除いた部分 |
Range.All |
.. と同義 |
r.GetOffsetAndLength(len) |
Range を offset と length に変換 |
System.Index(^)と System.Range(..)を組み合わせることで、配列・文字列・Span<T> のスライス操作を読みやすく書けます。