Rustにおけるプログラミングにおいて、範囲演算子は日常的に利用される非常に強力なツールです。
データの集合から特定の部分を切り出したり、ループ処理の回数を指定したりする際に、簡潔かつ安全な記述を可能にします。
Rustには主に .. (半開範囲) と ..= (閉範囲) という2種類の演算子が存在し、それぞれ異なる挙動を示します。
本記事では、2026年現在の最新の仕様を踏まえ、これらの範囲演算子の基本的な使い方から実践的な応用例、さらには注意点までを詳しく解説します。
初心者の方から中級者の方まで、Rustの表現力を最大限に引き出すための知識を深めていきましょう。
Rustにおける範囲演算子の基本概念
Rustの範囲演算子は、「どこからどこまでの値が含まれるか」を定義するための構文です。
これらは単なる構文糖衣ではなく、標準ライブラリの std::ops モジュールで定義されている構造体に対応しています。
.. (半開範囲) の特徴
start..end という形式で記述される演算子は、「開始値は含むが終了値は含まない」という性質を持っています。
数学的な表記では [start, end) と表される範囲です。
インデックスが0から始まるプログラミング言語の特性上、配列の長さを用いた指定などと非常に相性が良いのが特徴です。
..= (閉範囲) の特徴
一方、start..=end と記述する演算子は、「開始値も終了値も両方含む」という性質を持ちます。
数学的な表記では [start, end] となります。
最大値まで確実に処理を含めたい場合や、数値の範囲を直感的に指定したい場合に適しています。
演算子と比較表
それぞれの演算子が生成する型と性質を以下の表にまとめました。
| 構文 | 名称 | 数学的表現 | 対応する型 (std::ops) |
|---|---|---|---|
start..end | 半開範囲 (Exclusive) | start ≤ x < end | Range |
start..=end | 閉範囲 (Inclusive) | start ≤ x ≤ end | RangeInclusive |
範囲演算子の具体的な使い方
範囲演算子の最も一般的な用途は、反復処理、スライスの取得、およびパターンマッチングの3点です。
forループでの活用
特定の回数だけ処理を繰り返したい場合、範囲演算子を for 文と組み合わせるのが一般的です。
fn main() {
// 1から4までを表示する (5は含まない)
println!("半開範囲 (..):");
for i in 1..5 {
println!("値: {}", i);
}
// 1から5までを表示する (5を含む)
println!("\n閉範囲 (..=):");
for i in 1..=5 {
println!("値: {}", i);
}
}
半開範囲 (..):
値: 1
値: 2
値: 3
値: 4
閉範囲 (..=):
値: 1
値: 2
値: 3
値: 4
値: 5
このように、終了値を含めるか否かで出力結果が変わる点に注意してください。
多くのアルゴリズムでは、0から要素数未満までを回す 0..n の形式が多用されます。
スライス (Slice) の取得
配列や Vec 、文字列の一部を参照する際にも範囲演算子は欠かせません。
スライスを作成する場合、元のデータの所有権を持たずに特定の範囲だけを効率的に扱うことができます。
fn main() {
let numbers = [10, 20, 30, 40, 50];
// インデックス1から3未満まで (20, 30)
let slice1 = &numbers[1..3];
println!("slice1: {:?}", slice1);
// インデックス1から3まで (20, 30, 40)
let slice2 = &numbers[1..=3];
println!("slice2: {:?}", slice2);
}
slice1: [20, 30]
slice2: [20, 30, 40]
スライスの範囲指定で境界を越えると実行時にパニックが発生するため、インデックスの指定には注意が必要です。
パターンマッチングでの利用
match 式や if let 文において、値が特定の範囲に収まっているかを判定する際にも利用されます。
特に match においては、条件を簡潔に記述できるため非常に便利です。
fn check_grade(score: u32) {
match score {
90..=100 => println!("秀"),
80..90 => println!("優"),
70..80 => println!("良"),
60..70 => println!("可"),
_ => println!("不可"),
}
}
fn main() {
check_grade(95);
check_grade(80);
check_grade(50);
}
秀
優
不可
パターンマッチングでは、網羅性 (Exhaustiveness) が求められるため、デフォルトケース _ と組み合わせることが推奨されます。
特殊な範囲演算子と拡張記法
Rustには start や end を省略した特殊な範囲記法も用意されています。
これらは主にスライスの操作において、「端まで」を表現するために使用されます。
開始値の省略 (..end / ..=end)
開始値を省略すると、自動的に 「0番目から」 あるいは 「最小値から」 と解釈されます。
let data = [0, 1, 2, 3, 4, 5];
let first_three = &data[..3]; // &data[0..3] と同等
終了値の省略 (start..)
終了値を省略すると、「最後まで」を意味します。
これは std::ops::RangeFrom 型に対応しており、イテレータとして無限に値を生成する場合にも利用されます。
let data = [0, 1, 2, 3, 4, 5];
let from_two = &data[2..]; // &data[2..6] と同等
全範囲の指定 (..)
両方の値を省略した .. は、データ構造のすべての範囲を指します。
Vec をスライスに変換したい場合や、すべての要素を対象にする関数に渡す際に便利です。
let data = vec![1, 2, 3];
let all = &data[..]; // すべての要素を参照するスライス
範囲演算子を使用する際の注意点とベストプラクティス
範囲演算子は便利ですが、Rustの安全性やパフォーマンスの観点から知っておくべき重要なポイントがいくつかあります。
境界条件とパニックのリスク
スライスのインデックス指定に範囲演算子を用いる場合、境界外へのアクセスは実行時エラー (panic) を引き起こします。
&data[0..10] と記述した際に、data の長さが5しかなければ、プログラムは直ちに停止します。
安全性が最優先される場面では、get メソッドを使用して Option<&[T]> を受け取ることを検討してください。
Range型とイテレータの性質
Range 型 (..) は Iterator トレイトを実装しているため、そのままループや map, filter などのメソッドチェーンに利用できます。
しかし、閉範囲の RangeInclusive (..=) は、半開範囲に比べてわずかに複雑な内部構造を持っています。
これは、終了値が含まれるかどうかを判定するためのフラグが必要になる場合があるためです。
極限のパフォーマンスを追求するホットループ内では、可能であれば .. を使用する方が最適化されやすい傾向にあります。
空の範囲の挙動
開始値が終了値以上の場、例えば 5..2 のような指定をしても、コンパイルエラーにはなりませんが空の範囲として扱われます。
この範囲に対して for ループを実行しても、一度もループ内は実行されません。
意図しない空の範囲が生成されないよう、変数を範囲に用いる際は大小関係を意識する必要があります。
2026年現在のRustにおける範囲演算子の動向
Rustの進化に伴い、範囲演算子はより抽象的なコンテキストでも利用されるようになっています。
例えば、impl RangeBounds トレイトを活用することで、自作のデータ構造に対しても範囲演算子を使ったアクセスを簡単に提供できるようになりました。
また、コンパイラの最適化能力が向上したことで、RangeInclusive の境界チェックに関するオーバーヘッドも、現代の環境ではほとんど無視できるレベルにまで洗練されています。
そのため、「コードの読みやすさ」を最優先に選択するのが2026年における標準的な開発スタイルと言えるでしょう。
まとめ
Rustの範囲演算子 .. と ..= は、シンプルながらも非常に奥が深い機能です。
「終了値を含まない ..」 は配列のインデックス操作や一般的なループに最適です。
一方、「終了値を含む ..=」 はパターンマッチングや明確な範囲定義においてその真価を発揮します。
省略記法を組み合わせることで、スライスの操作を直感的かつ簡潔に記述できる点も大きな魅力です。
それぞれの特性を正しく理解し、適切に使い分けることで、バグが少なくメンテナンス性の高いRustコードを記述することができます。
本記事で解説したテクニックを、ぜひ日々の開発に役立ててください。
