Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Collections in Rust: The Essentials

2025年09月29日に「Dev.to」が公開したITニュース「Collections in Rust: The Essentials」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

Rustのコレクションは、動的に変化するデータを効率的に扱うための機能だ。`Vec`(可変長配列)、`HashMap`(キーと値)、`HashSet`(重複なし)が主要なコレクション。その他、ソート済みデータや優先度付きデータ用など多様な種類があり、用途に応じた選択が効率的なプログラム作成に繋がる。

出典: Collections in Rust: The Essentials | Dev.to公開日:

ITニュース解説

ソフトウェア開発において、ただの数値や文字といった基本的なデータ型だけでは、複雑なアプリケーションを作ることはできない。プログラムが実行中にデータの数が変わったり、データを効率的に整理したりする必要がある場合、特別なデータ構造が必要となる。それが「コレクション」である。Rustのコレクションは、複数の値を効率的に管理し、必要に応じてサイズを動的に変更できるデータ構造であり、すべてヒープというメモリ領域に確保される。Rustの標準ライブラリには、さまざまな用途に最適化された強力なコレクションが用意されている。

最も頻繁に使われる主要なコレクションは、「Vec(ベック)」、「HashMap(ハッシュマップ)」、「HashSet(ハッシュセット)」の三つである。

Vecは「動的な配列」と考えるとわかりやすい。これは、要素の順序が保たれ、同じ値を複数持てて、特定の場所にある要素に素早くアクセスできる。また、必要に応じて自動的にサイズが大きくなったり小さくなったりする。例えば、ゲームのスコアリストや、ユーザーが入力した数値の並びなどを管理するのに適している。Vec::new()で空のVecを作成したり、vec![値1, 値2, ...]という便利なマクロで初期値を設定したりできる。push()メソッドで末尾に新しい要素を追加し、extend()を使えば複数の要素を一度に追加できる。特定の要素にアクセスするにはscores[0]のようにインデックスを指定するが、存在しないインデックスにアクセスするとプログラムが異常終了(パニック)するので注意が必要だ。安全にアクセスしたい場合はget()メソッドを使い、要素があればそれを取り出し、なければ何もないことを示すOption型で結果を受け取るのが一般的である。forループを使って全ての要素を順番に処理することもできる。また、len()で現在の要素数、capacity()で現在確保されているメモリの容量を確認でき、reserve()を使ってあらかじめ多くのメモリを確保しておくことで、要素の追加による頻繁なメモリ再確保を防ぐことも可能だ。Vecは、スタック(後入れ先出し)や、キュー(先入れ先出し)のように使うこともでき、ソート機能も持っているため、まさにコレクションの「万能ナイフ」と言える。

HashMapは「キーと値を紐付ける」データ構造である。例えば、生徒の名前をキーとして成績を値として保存したり、設定の名前をキーとしてその値を保存したりするのに使う。キーから対応する値を非常に高速に見つけることができるのが特徴で、平均して一定の時間(O(1))で挿入、削除、検索が可能である。これはキャッシュ、インデックス、検索テーブルといった用途に非常に適している。HashMap::new()で作成し、insert("キー", 値)でデータを追加する。初期値を持たせる場合はHashMap::from([("キー1", 値1), ("キー2", 値2)])のように作成もできる。get("キー")で値を取得するが、そのキーが存在しない可能性もあるため、Option型で結果が返される。特に便利なのがentry()APIである。これは、特定のキーが存在しない場合は新しい値を挿入し、存在する場合は既存の値を更新するという処理を、複数回キーを検索することなく効率的に行える機能である。

HashSetは、キーと値を紐付けるHashMapとは異なり、値そのものを保持する。最大の特徴は、格納する値が「一意」であること、つまり重複する値は一切含まれないことだ。例えば、登録済みのユーザーIDのリストや、記事に付けられたユニークなタグのリストなどを管理するのに適している。値がコレクションに含まれているかどうかの判定が高速である。HashSet::new()で作成し、insert(値)で要素を追加するが、同じ値を再度挿入してもコレクションは変化しない。into()を使って既存の配列などからHashSetを簡単に作成することもできる。さらに、二つのHashSet間で和集合、積集合、差集合といった集合演算を行う機能や、contains(値)を使って特定の要素が含まれているかを確認する機能も備わっている。

これらの主要なコレクションの他にも、特定の用途に特化したコレクションがいくつか存在する。

VecDeque(ベックデック)は「両端キュー」であり、リストの両端から要素を効率的に追加したり削除したりする必要がある場合に利用する。例えば、新しいチャットメッセージが最後に追加され、画面に表示しきれなくなった古いメッセージが先頭から削除されるような履歴管理に使うことができる。これはリングバッファとして実装されており、push_front()で先頭に、push_back()で末尾に要素を追加し、pop_front()で先頭から、pop_back()で末尾から要素を取り出すことができる。

BTreeMap(ビーツリーマップ)とBTreeSet(ビーツリーセット)は、HashMapやHashSetと同様にそれぞれキーバリューと一意な値を保持するが、格納されたデータを常に「ソートされた順序」で保持するのが最大の特徴である。これにより、キーのアルファベット順や数値順でデータを繰り返し処理したり、特定の範囲内のデータを検索したりするのに便利だ。例えば、リーダーボードのスコアを高い順に表示したり、特定の期間内のイベントを検索したりする場合に有効である。操作にかかる時間はHashMapよりは少し遅く、O(log n)となる。

BinaryHeap(バイナリヒープ)は「優先度キュー」を実装する。これは、コレクション内の要素に優先順位があり、常に最も優先度の高い要素を素早く取り出したい場合に利用する。RustのBinaryHeapはmax-heapとして実装されており、常に最大の要素を先頭に保持する。例えば、タスクの緊急度に基づいて処理順序を決定したり、最も近い経路を探索するアルゴリズムで次に訪れるべきノードを選択したりするのに使われる。push()で要素を追加し、pop()で最大の要素を順番に取り出すことができる。

どのコレクションを選ぶかは、プログラムの要件に大きく依存する。特定の場所へのアクセスが必要で、末尾への追加が主で、たまの再確保を許容できるならVecを選ぶ。両端での効率的な追加・削除が必要ならVecDeque。順序は不要でキーと値を関連付けたいならHashMap。ソートされたキーと値が必要ならBTreeMap。重複のない値を効率的に管理したいならHashSet。ソートされた重複のない値が必要ならBTreeSet。優先度に従って要素を取り出したいならBinaryHeapがそれぞれ適している。

Rustのコレクションも、他のすべてのデータと同様に「所有権」のルールに従う。コレクションに格納されたデータは、そのコレクションが所有しているか、あるいはコレクションが参照を保持しているかのどちらかである。例えば、Vecからremove(0)で要素を取り出すと、その要素の所有権はVecから取り出した変数へと移動する。一方、&data[0]のように参照を渡す場合、Vecは所有権を保持したまま、他のコードがそのデータを利用できるように「貸し出す」形となる。アプリケーションの設計では、コレクションがデータを所有すべきか、それとも単に参照を保持すべきかを慎重に検討する必要がある。所有権を持つデータは扱いが単純だがメモリ使用量が多くなる傾向があり、参照を持つデータはメモリ効率が良いが、Rustの「ライフタイム」という参照の有効期間に関する制約が生じる。

Rustには、コレクションを扱う上で便利な「パターン」や「イディオム」がいくつか存在する。collect()パターンは、イテレータを使って変換やフィルタリングを行った結果を、新しいコレクションとして集めるための強力な方法である。例えば、数値のリストを2倍にして新しいVecにしたり、偶数だけを抽出してVecにしたり、重複を除去してHashSetにしたり、文字列の文字数を数えてHashMapにしたりできる。また、into_iter().collect()を使うことで、VecからHashSetへ、HashSetからVecへといったコレクション間の変換を簡単に行える。さらに、HashMapのキーとしてカスタムの型を使いたい場合など、デフォルトのハッシュや比較動作が望ましくない場合は、HashEqトレイトを実装することで、例えば大文字小文字を区別しない文字列としてキーを扱うようなカスタムロジックを定義できる。

パフォーマンスを最適化するためのヒントもいくつかある。もし、コレクションがどれくらいのデータを格納することになるかおおよそ予測できるのであれば、Vec::with_capacity()HashMap::with_capacity()のように初期容量をあらかじめ指定しておくことで、要素の追加に伴うメモリの再確保(再割り当て)の回数を減らし、パフォーマンスを向上させることができる。イテレーションの方法にも注意が必要である。for item in collectionはコレクションの所有権を移動または消費し、for item in &collectionは各要素を借りて読み取り専用でアクセスし、for item in &mut collectionは各要素を可変参照として借りて変更可能にする。HashMapのキーとして使う型は、HashトレイトとEqトレイトの両方を実装している必要がある。ほとんどの組み込み型はこれらを実装しているが、カスタム型を使う場合は自分で実装するか、#[derive(Hash, Eq, PartialEq)]のような属性を付ける必要がある。また、Vecのインデックスアクセスvec[0]は、指定したインデックスが範囲外だとプログラムがパニックする可能性があるため、特にユーザー入力など信頼できないデータに基づいてアクセスする場合は、安全なget()メソッドを使うことを推奨する。

Rustのコレクションは、性能とメモリ安全性を両最優先に設計されている。これらのコレクションを適切に選択し利用することで、メモリリークやバッファオーバーフローといった他のシステムプログラミング言語でよく見られる問題を心配することなく、効率的で正確なプログラムを構築できる。最も重要なのは、まず自分のニーズに合う最もシンプルなコレクションから使い始めることだ。多くの場合はVecやHashMapで十分である。そして、もしプロファイリングの結果、性能ボトルネックが見つかった場合にのみ、より特殊なコレクションへの切り替えを検討するのが良い。Rustの標準ライブラリにあるコレクションはすべて高度に最適化されているため、多くの場合は個々のコレクション内部の微細な最適化よりも、適切なコレクションを選ぶこと自体がプログラムの性能に大きな影響を与える。

関連コンテンツ

関連IT用語