閉じる

【C#】Range(範囲演算子)とIndex(^)の使い方!配列の切り出しを解説

C#において、配列やコレクションの特定の部分を抽出する操作は、データ処理の基本と言えます。

かつてはSubstringメソッドや、ループ処理による手動のコピー、あるいはLINQのSkipTakeを組み合わせて実現していましたが、C# 8.0以降、Index(インデックス)Range(範囲)という新しい型と演算子が導入されました。

これにより、直感的かつ簡潔に配列の切り出し(スライシング)が可能になっています。

本記事では、末尾からのインデックス指定を行う^演算子や、範囲を定義する..演算子の詳細な使い方から、内部的な挙動、パフォーマンス上のメリットまで徹底的に解説します。

Index(末尾からのインデックス)の基本

C#で配列の要素にアクセスする場合、通常は「0番目」から始まる整数を使用します。

しかし、「最後からn番目」という指定をしたい場合、これまではarray.Length - nという計算が必要でした。

これを解決するのが、Index構造体^演算子です。

^演算子による末尾指定の仕組み

^演算子は、末尾からの相対的な位置を表します。

最も重要な点は、末尾の要素は「^1」で表現されるということです。

  • ^1:最後の要素(length - 1
  • ^2:最後から2番目の要素(length - 2
  • ^0:配列の長さそのもの(length

^0は、配列の有効なインデックス範囲外(長さと同じ値)を指すため、array[^0]のように直接要素にアクセスしようとするとIndexOutOfRangeExceptionが発生します。

しかし、後述する「範囲」の指定においては、「末尾まで」を表現するために非常に重要な役割を果たします。

System.Index型の実体

この^演算子によって生成される値は、System.Indexという構造体のインスタンスです。

内部的には整数値を持ち、「先頭からか、末尾からか」を判別するフラグを保持しています。

C#
using System;

class Program
{
    static void Main()
    {
        // 文字列配列の作成
        string[] fruits = { "Apple", "Banana", "Cherry", "Dragonfruit", "Elderberry" };

        // 従来のアクセス方法(最後から1番目)
        string lastOld = fruits[fruits.Length - 1];

        // Index型(^1)を使用したアクセス
        string lastNew = fruits[^1];

        // 最後から3番目の要素
        string thirdFromEnd = fruits[^3];

        Console.WriteLine($"従来の最後: {lastOld}");
        Console.WriteLine($"新しい最後: {lastNew}");
        Console.WriteLine($"最後から3番目: {thirdFromEnd}");
        
        // Index型の変数として保持することも可能
        Index idx = ^2;
        Console.WriteLine($"最後から2番目: {fruits[idx]}");
    }
}
実行結果
従来の最後: Elderberry
新しい最後: Elderberry
最後から3番目: Cherry
最後から2番目: Dragonfruit

このように、コードが非常に短くなり、「意図した場所がどこか」がひと目で理解できるようになります。

Range(範囲演算子)による配列の切り出し

次に、複数の要素をまとめて抽出するRange(範囲)について解説します。

範囲を指定するには、..演算子を使用します。

..演算子の基本ルール

Range演算子start..endには、非常に重要なルールがあります。

それは、開始インデックスは「含む(inclusive)」、終了インデックスは「含まない(exclusive)」という点です。数学の半開区間 [start, end) と同じ考え方です。

記述例意味
1..4インデックス1から、4の直前(3)までを抽出
0..2インデックス0から、2の直前(1)までを抽出
^3..^1最後から3番目から、最後から1番目の直前までを抽出

この「終了を含まない」という仕様は、一見不便に見えるかもしれませんが、プログラムにおいては「終了 - 開始 = 要素数」になるため、計算が非常に楽になるというメリットがあります。

例えば1..4なら、要素数は 4 - 1 = 3 個であることが保証されます。

範囲の省略記法

Range演算子は、開始または終了、あるいはその両方を省略することができます。

  • start..:指定したインデックスから末尾まで。
  • ..end:先頭から、指定したインデックスの直前まで。
  • ..:全範囲(コピーを作成する際に便利)。
C#
using System;

class Program
{
    static void Main()
    {
        int[] numbers = { 0, 10, 20, 30, 40, 50, 60 };

        // インデックス1から4の直前まで(10, 20, 30)
        int[] sub1 = numbers[1..4];
        
        // インデックス3から末尾まで(30, 40, 50, 60)
        int[] sub2 = numbers[3..];
        
        // 先頭から最後から2番目の直前まで(0, 10, 20, 30, 40)
        int[] sub3 = numbers[..^2];

        // 全範囲(配列全体のコピー)
        int[] sub4 = numbers[..];

        Console.WriteLine($"sub1: {string.Join(", ", sub1)}");
        Console.WriteLine($"sub2: {string.Join(", ", sub2)}");
        Console.WriteLine($"sub3: {string.Join(", ", sub3)}");
        Console.WriteLine($"要素数 (sub1): {sub1.Length}");
    }
}
実行結果
sub1: 10, 20, 30
sub2: 30, 40, 50, 60
sub3: 0, 10, 20, 30, 40
要素数 (sub1): 3

System.Range型の変数利用

Range演算子の結果は、System.Range型の値として変数に代入できます。

特定の範囲を「フィルタ」のように定義して、複数の配列に対して使い回すことが可能です。

C#
Range middleRange = 1..^1; // 先頭と末尾を除外する範囲

int[] data1 = { 1, 2, 3, 4, 5 };
int[] data2 = { 100, 200, 300, 400 };

int[] result1 = data1[middleRange]; // { 2, 3, 4 }
int[] result2 = data2[middleRange]; // { 200, 300 }

このように、ロジックとデータを分離できるため、コードの再利用性が高まります。

文字列(string)への応用

Range演算子は、配列だけでなく文字列に対しても非常に強力に機能します。

従来のSubstringメソッドよりも直感的に記述でき、読みやすさが劇的に向上します。

Substringとの違い

従来のstring.Substring(int startIndex, int length)は、第2引数が「長さ」でした。

これに対し、Range演算子は「終了位置」を指定します。

C#
string text = "C# Programming is fun!";

// 従来のSubstring (15文字目から3文字分)
string subOld = text.Substring(15, 3);

// Range演算子 (15文字目から18文字目の直前まで)
string subNew = text[15..18];

// 末尾から4文字を切り出し
string lastFour = text[^4..];

Console.WriteLine($"Old: {subOld}"); // "fun"
Console.WriteLine($"New: {subNew}"); // "fun"
Console.WriteLine($"Last: {lastFour}"); // "fun!"

text[^4..]のような記述は、末尾の文字数が動的に変わる可能性がある処理(例えばファイルの拡張子チェックなど)において、計算ミスによるバグを劇的に減らす効果があります。

文字列操作におけるパフォーマンス

文字列に対してRange演算子を使用すると、内部的にはSubstringが呼び出されます。

そのため、新しい文字列オブジェクトがヒープに割り当てられる(アロケーションが発生する)という点に注意が必要です。

大量の文字列を切り出すループ処理などでは、後述するReadOnlySpan<char>との組み合わせが推奨されます。

Span<T> との組み合わせによる最適化

Range演算子の真価を発揮するのが、Span<T>ReadOnlySpan<char>といった「ビュー」を提供する型との併用です。

メモリコピーを発生させないスライシング

通常、配列に対してarray[1..4]と記述すると、その範囲の要素が新しい配列としてコピーされます。

これは安全ですが、巨大な配列や高頻度の処理ではメモリの消費が問題になります。

これに対し、Span<T>を介してRangeを使用すると、コピーを作成せずに元のメモリ領域を指し示す「窓」のようなものが作成されます。

C#
using System;

class Program
{
    static void Main()
    {
        int[] heavyData = new int[10000];
        // 配列のスライス(新しい配列が生成され、メモリを消費する)
        int[] copy = heavyData[1000..2000];

        // Spanのスライス(メモリコピーが発生せず、高速)
        Span<int> view = heavyData.AsSpan()[1000..2000];

        // viewの値を書き換えると、元のheavyDataも書き換わる
        view[0] = 999;
        Console.WriteLine($"元の配列の値: {heavyData[1000]}"); // 999
    }
}

このように、Span<T>に対してRangeを使うことで、パフォーマンスと記述の簡潔さを両立させることができます。

文字列解析の高速化

文字列の解析(パーサ)を作成する場合も、ReadOnlySpan<char>を活用することで、アロケーションフリーな処理が可能になります。

C#
string log = "2026-01-16:ERROR:Database connection failed";
ReadOnlySpan<char> logSpan = log.AsSpan();

// コピーを発生させずに部分文字列を取得
ReadOnlySpan<char> datePart = logSpan[..10];
ReadOnlySpan<char> levelPart = logSpan[11..16];

Console.WriteLine(datePart.ToString());
Console.WriteLine(levelPart.ToString());

ToString()を呼び出すまでは、新しい文字列は生成されません。

これにより、大規模なログファイルの解析などにおいて、ガベージコレクション(GC)の負荷を大幅に軽減できます。

実践的な活用シーン

RangeとIndexは、単なるシンタックスシュガー(書き方の工夫)に留まらず、実際の開発において多くのメリットをもたらします。

1. CSVや固定長データの解析

固定のフォーマットを持つデータを扱う際、IndexとRangeを使うとコードが宣言的になります。

C#
string csvRow = "ID12345,TaroYamada,25,Tokyo";
// カンマの位置を探す手間を省き、固定位置なら以下のように書ける
// (実際にはSplitやSpanでの検索を併用しますが、固定幅なら非常に強力)

2. 配列の「最後以外」を処理する

例えば、リストを表示する際に「最後の要素だけカンマを付けない」といった処理を行う場合、items[..^1]で最後以外の全要素を回すことができます。

C#
string[] tags = { "C#", "DotNet", "Programming" };

// 最後以外の要素に対してループ
foreach (var tag in tags[..^1])
{
    Console.Write(tag + ", ");
}
// 最後の要素だけ個別に処理
Console.WriteLine(tags[^1]);

3. アルゴリズムの実装(二分探索など)

探索範囲を狭めていくアルゴリズムでは、Range型を変数として管理することで、今どの範囲を探索しているのかを明確にコードで表現できます。

RangeとIndexの注意点と限界

非常に便利な機能ですが、使用する際に気を付けるべきポイントがいくつかあります。

多次元配列には直接使えない

残念ながら、int[,] array = new int[5, 5];のような多次元配列に対して、array[1..3, 1..3]のように範囲指定することはできません。

ジャグ配列(配列の配列 int[][])であれば、各次元に対して適用可能です。

負の数は指定できない

Indexの^演算子は「末尾から」を意味する正の整数を受け取ります。

^-1のような記述はコンパイルエラーとなります。

実行時の例外

指定した範囲が配列のサイズを超えている場合、実行時にArgumentOutOfRangeExceptionが発生します。

動的に範囲を指定する場合は、事前にIndexRangeの値が有効な範囲に収まっているかチェックする必要があります。

状況結果
開始位置 > 終了位置例外発生
インデックスが負コンパイルエラー
範囲が配列長を超える例外発生

カスタムクラスでのRange対応

もし自分で作成したクラスで..^を使えるようにしたい場合、特別なインターフェースを実装する必要はありません。

特定のパターンに従ったメソッドを定義するだけで、コンパイラが自動的に認識してくれます。

  1. LengthまたはCountプロパティ(int型)を持っていること。
  2. int型を引数に取るインデクサを持っていること。
  3. Slice(int start, int length)メソッドを持っていること。

これらの条件を満たすと、そのクラスに対してRange演算子による切り出しができるようになります。

これは「パターンベース」の機能と呼ばれます。

C#
public class MyDataList
{
    private int[] _data = { 1, 2, 3, 4, 5 };

    public int Length => _data.Length;

    public int this[int index] => _data[index];

    // Range演算子を使ったときに呼ばれるメソッド
    public int[] Slice(int start, int length)
    {
        int[] result = new int[length];
        Array.Copy(_data, start, result, 0, length);
        return result;
    }
}

// 利用例
var myDoc = new MyDataList();
var sub = myDoc[1..3]; // Slice(1, 2) が呼ばれる

このように、自作のコレクションクラスでもC#の標準的な構文を利用可能にすることで、ライブラリの利用者に一貫したコーディング体験を提供できます。

まとめ

C#のRange(範囲演算子)とIndex(末尾からのインデックス)は、モダンなC#プログラミングにおいて欠かせない要素です。

これらを利用することで、配列や文字列の切り出しが非常に簡潔になり、array.Length - 1のような冗長でミスを誘発しやすい記述を排除できます。

特に^1による末尾アクセスや、start..endによる半開区間での切り出しは、コードの意図を明確にする効果があります。

さらに、Span<T>と組み合わせることで、パフォーマンスを犠牲にすることなく高度なメモリ操作も実現可能です。

最初は「終了インデックスを含まない」という仕様に戸惑うかもしれませんが、慣れてしまえばこれほど合理的な記法はありません。

ぜひ日々の開発に取り入れ、より安全で読みやすいクリーンなコードを目指してください。

基本操作

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!