ベクトルを使った値のリストの保存

RustRustBeginner
今すぐ練習

This tutorial is from open-source community. Access the source code

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

ベクトルを使った値のリストの保存へようこそ。この実験は、Rust Bookの一部です。あなたはLabExでRustのスキルを練習することができます。

「この実験では、Vec<T>コレクション型(ベクトルとも呼ばれます)を調べます。これは、同じ型の値のリストを単一のデータ構造に保存することができます。」


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/ControlStructuresGroup(["Control Structures"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/floating_types("Floating-point Types") rust/ControlStructuresGroup -.-> rust/for_loop("for Loop") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100406{{"ベクトルを使った値のリストの保存"}} rust/mutable_variables -.-> lab-100406{{"ベクトルを使った値のリストの保存"}} rust/integer_types -.-> lab-100406{{"ベクトルを使った値のリストの保存"}} rust/floating_types -.-> lab-100406{{"ベクトルを使った値のリストの保存"}} rust/for_loop -.-> lab-100406{{"ベクトルを使った値のリストの保存"}} rust/expressions_statements -.-> lab-100406{{"ベクトルを使った値のリストの保存"}} rust/method_syntax -.-> lab-100406{{"ベクトルを使った値のリストの保存"}} rust/operator_overloading -.-> lab-100406{{"ベクトルを使った値のリストの保存"}} end

ベクトルを使った値のリストの保存

最初に見てみるコレクション型は、Vec<T>(ベクトルとも呼ばれます)です。ベクトルを使うと、メモリ上ですべての値を隣接させた単一のデータ構造に複数の値を保存できます。ベクトルは同じ型の値のみを保存できます。ファイルのテキスト行やショッピングカートの商品価格など、項目のリストが必要な場合に便利です。

新しいベクトルの作成

新しい空のベクトルを作成するには、Vec::new関数を呼び出します。これは、リスト8-1に示すようになります。

let v: Vec<i32> = Vec::new();

リスト8-1:i32型の値を保持する新しい空のベクトルの作成

ここで型注釈を追加しました。このベクトルに値を挿入していないため、Rustはどの種類の要素を格納する予定かを知りません。これは重要なポイントです。ベクトルはジェネリクスを使って実装されています。第10章で独自の型とジェネリクスを使う方法について説明します。今のところ、標準ライブラリによって提供されるVec<T>型は任意の型を保持できることを知っておきましょう。特定の型を保持するベクトルを作成するとき、角括弧内に型を指定できます。リスト8-1では、vVec<T>i32型の要素を保持することをRustに伝えています。

より一般的には、初期値を持つVec<T>を作成し、Rustが格納したい値の型を推論します。そのため、この型注釈はめったに必要ありません。Rustは便利にもvec!マクロを提供しており、与えた値を保持する新しいベクトルを作成します。リスト8-2は、値12、および3を保持する新しいVec<i32>を作成します。整数型はi32です。これは、「データ型」で説明した通り、デフォルトの整数型だからです。

let v = vec![1, 2, 3];

リスト8-2:値を含む新しいベクトルの作成

初期のi32値を与えているため、Rustはvの型がVec<i32>であることを推論でき、型注釈は必要ありません。次に、ベクトルを変更する方法を見てみましょう。

ベクトルの更新

ベクトルを作成してから要素を追加するには、pushメソッドを使用できます。これは、リスト8-3に示すようになります。

let mut v = Vec::new();

v.push(5);
v.push(6);
v.push(7);
v.push(8);

リスト8-3:pushメソッドを使用してベクトルに値を追加する

他の変数と同様に、値を変更できるようにするには、第3章で説明したmutキーワードを使用して可変にする必要があります。入れる数字はすべてi32型で、Rustはデータからこれを推論するので、Vec<i32>の注釈は必要ありません。

ベクトルの要素の読み取り

ベクトルに格納された値を参照する方法は2つあります。インデックスを使う方法とgetメソッドを使う方法です。以下の例では、これらの関数から返される値の型を注釈付けして、より明確にしています。

リスト8-4は、インデックス構文とgetメソッドの両方を使ってベクトルの値にアクセスする方法を示しています。

let v = vec![1, 2, 3, 4, 5];

1 let third: &i32 = &v[2];
println!("The third element is {third}");

2 let third: Option<&i32> = v.get(2);
match third  {
    Some(third) => println!("The third element is {third}"),
    None => println!("There is no third element."),
}

リスト8-4:インデックス構文とgetメソッドを使ってベクトルの項目にアクセスする

ここでいくつかの詳細について注意してください。ベクトルは0から始まる番号でインデックス付けされるため、3番目の要素を取得するにはインデックス値2を使います[1]。&[]を使うことで、インデックス値の要素への参照が得られます。引数として渡されたインデックスを使ってgetメソッドを使うとき[2]、Option<&T>が得られ、これをmatchで使うことができます。

Rustはこの2つの方法を提供しています。既存の要素の範囲外のインデックス値を使おうとしたときのプログラムの動作を選ぶことができます。例えば、要素数が5のベクトルがあり、それぞれの手法でインデックス100の要素にアクセスしようとするときに何が起こるか見てみましょう。これは、リスト8-5に示すようになります。

let v = vec![1, 2, 3, 4, 5];

let does_not_exist = &v[100];
let does_not_exist = v.get(100);

リスト8-5:要素数が5のベクトルのインデックス100の要素にアクセスしようとする

このコードを実行すると、最初の[]メソッドは存在しない要素を参照するため、プログラムがパニックになります。この方法は、ベクトルの末尾を超えて要素にアクセスしようとした場合にプログラムをクラッシュさせたいときに最適です。

getメソッドにベクトルの範囲外のインデックスが渡されると、パニックにならずにNoneを返します。通常の状況下でベクトルの範囲外の要素にアクセスすることが時々起こる場合には、この方法を使います。その後、コードは第6章で説明したように、Some(&element)またはNoneのいずれかを持つ論理を持つことになります。たとえば、インデックスは人が数字を入力することから来る場合があります。彼らが偶然大きすぎる数字を入力し、プログラムがNone値を取得した場合、現在のベクトルに何個の項目があるかをユーザーに伝え、有効な値を入力する機会をもう一度与えることができます。これは、誤字によってプログラムをクラッシュさせるよりもユーザーにやさしいでしょう!

プログラムが有効な参照を持っている場合、借用チェッカーは所有権と借用ルール(第4章で説明)を強制して、この参照とベクトルの内容への他の参照が有効なままであることを保証します。同じスコープ内に可変参照と不変参照があってはならないというルールがあることを思い出してください。このルールは、リスト8-6に当てはまります。ここでは、ベクトルの最初の要素への不変参照を保持し、末尾に要素を追加しようとしています。この関数の後半で同じ要素を参照しようとすると、このプログラムは動作しません。

let mut v = vec![1, 2, 3, 4, 5];

let first = &v[0];

v.push(6);

println!("The first element is: {first}");

リスト8-6:項目への参照を保持しながらベクトルに要素を追加しようとする

このコードをコンパイルすると、以下のエラーが発生します。

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as
immutable
 --> src/main.rs:6:5
  |
4 |     let first = &v[0];
  |                  - immutable borrow occurs here
5 |
6 |     v.push(6);
  |     ^^^^^^^^^ mutable borrow occurs here
7 |
8 |     println!("The first element is: {first}");
  |                                      ----- immutable borrow later used here

リスト8-6のコードは動作するはずのように見えます。なぜ、最初の要素への参照がベクトルの末尾の変更に関係するはずがないのでしょうか。このエラーは、ベクトルの動作方法によるものです。ベクトルはメモリ上で値を隣接させるため、ベクトルの末尾に新しい要素を追加すると、すべての要素をベクトルが現在保存されている場所で隣接させるのに十分な空間がない場合、新しいメモリを割り当てて古い要素を新しい場所にコピーする必要があるかもしれません。その場合、最初の要素への参照は解放されたメモリを指してしまうことになります。借用ルールにより、プログラムがそのような状況に陥らないようになっています。

注:Vec<T>型の実装の詳細については、https://doc.rust-lang.org/nomicon/vec/vec.htmlの「The Rustonomicon」を参照してください。

ベクトル内の値を反復処理する

ベクトル内の各要素に順番にアクセスするには、一度に1つをアクセスするためにインデックスを使うのではなく、すべての要素を反復処理します。リスト8-7は、forループを使ってi32値のベクトル内の各要素に不変参照を取得し、それらを表示する方法を示しています。

let v = vec![100, 32, 57];
for i in &v {
    println!("{i}");
}

リスト8-7:forループを使って要素を反復処理してベクトル内の各要素を表示する

可変ベクトル内の各要素の可変参照を反復処理して、すべての要素に変更を加えることもできます。リスト8-8のforループは、各要素に50を加えます。

let mut v = vec![100, 32, 57];
for i in &mut v {
    *i += 50;
}

リスト8-8:ベクトル内の要素の可変参照を反復処理する

可変参照が参照する値を変更するには、+=演算子を使う前に*参照解除演算子を使ってiの値にアクセスする必要があります。「ポインタをたどって値に到達する」で参照解除演算子についてもっと話します。

借用チェッカーのルールのおかげで、ベクトルを不変または可変で反復処理することは安全です。リスト8-7とリスト8-8のforループの本体で項目を挿入または削除しようとすると、リスト8-6のコードと同じようなコンパイラエラーが発生します。forループが保持するベクトルへの参照により、ベクトル全体の同時変更が防止されます。

複数の型を格納するための列挙型の使用

ベクトルは同じ型の値のみを格納できます。これは不便な場合があります。異なる型の項目のリストを格納する必要があるケースは確かにあります。幸いなことに、列挙型のバリアントは同じ列挙型の下で定義されているため、異なる型の要素を表すために1つの型が必要な場合、列挙型を定義して使用することができます!

たとえば、スプレッドシートの行から値を取得したいとしましょう。その行の一部の列には整数が含まれ、一部には浮動小数点数が含まれ、一部には文字列が含まれています。列挙型のバリアントが異なる値型を保持する列挙型を定義することができ、すべての列挙型のバリアントは同じ型(列挙型の型)と見なされます。そして、その列挙型を保持するベクトルを作成することができ、最終的には異なる型を保持することができます。これをリスト8-9で示しています。

enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];

リスト8-9:1つのベクトルに異なる型の値を格納するためのenumを定義する

Rustはコンパイル時にベクトルにどの型が含まれるかを知る必要があります。そうすることで、各要素を格納するためにヒープ上にどれだけのメモリが必要かを正確に把握できます。また、このベクトルに許可される型も明示する必要があります。Rustがベクトルに任意の型を保持できるようにした場合、ベクトルの要素に対して行われる操作で1つ以上の型がエラーを引き起こす可能性があります。列挙型とmatch式を使うことで、Rustはコンパイル時にすべての可能なケースが処理されることを保証します。これについては、第6章で説明しています。

実行時にベクトルに格納するための型の網羅的なセットを知らない場合、列挙型の手法は機能しません。代わりに、トレイトオブジェクトを使用することができます。これについては、第17章で説明します。

ここで、ベクトルを使用する最も一般的な方法のいくつかについて説明しましたが、標準ライブラリによってVec<T>に定義されている多くの便利なメソッドのAPIドキュメントを必ず確認してください。たとえば、pushに加えて、popメソッドは最後の要素を削除して返します。

ベクトルを破棄するとその要素も破棄される

他のstructと同様に、ベクトルはスコープ外になると解放されます。これは、リスト8-10に示すようになります。

{
    let v = vec![1, 2, 3, 4];

    // v を使って何かをする
} // <- v がスコープ外になり、ここで解放されます

リスト8-10:ベクトルとその要素が破棄される場所を示す

ベクトルが破棄されると、そのすべての内容も破棄されます。つまり、保持している整数がクリーンアップされます。借用チェッカーは、ベクトルの内容への参照がベクトル自体が有効な間のみ使用されることを保証します。

次のコレクション型であるStringに移りましょう!

まとめ

おめでとうございます!ベクトルを使った値のリストの保存に関する実験を完了しました。スキルを向上させるために、LabExでさらに多くの実験を行って練習してください。