Edited at

Rust Book 勉強会 #4 第8章「Common Collections」


この資料について

この資料は「Rust Book 勉強会 #4」用の発表資料です。



「コレクション」について


  • 一般的なデータ型は、単一の値を表現します。例: i32

  • コレクションは、複数の値を表現します。例: Vec<i32>

  • 組み込み型である「配列」や「タプル」と違いは:


    • データはスタックではなく、ヒープに置かれます。

    • コンパイル時にサイズを決定する必要がなく、実行時に伸縮できます。





この章で紹介するデータ型


  • Rustの標準ライブラリには、数々の使いやすいデータ構造が含まれています。

  • この章で紹介するのは以下の3つ:


    • Vectorベクタ

    • Stringストリング

    • Hash Mapハッシュマップ





Vector



Vector: Vec<T>


  • 最初に紹介するのはVec<T>です。

  • 同じ型Tの値を0個以上格納するデータ構造です。

  • 値はメモリの連続した領域に置かれます。



Vector: 空のVector

Vec::new関数で空のVecを作ることができます。

// Listing 8-1: Creating a new, empty vector to hold values of type i32

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

この例では、内容がない(具体的なデータがない)ため、コンパイラは型を推論することができません。そのため、型アノテーションが必要です。

Vecの実装にはGenericsが使われています。詳細については第10章を参照してください。

簡単に言えば、上記の例の<i32>の部分を別の型に書き換えることで、任意の型についての同じデータ構造を作ることができます。



Vector: 初期値を持つVector

vec!マクロを使うことで、初期値を伴うVecを作ることができます。

// Listing 8-2: Creating a new vector containing values

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

この例では、コンパイラは型を推論することできるので、型アノテーションは省略することができます。(もちろん書くこともできます)



Vector: vec!マクロ

vec!は単なるマクロなので、[]以外の括弧も使えます。ですが、[]が一般的なので、それを使いましょう。

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

let v2 = vec!{1, 2, 3};
let v3 = vec!(1, 2, 3);
assert_eq!(v1, v2);
assert_eq!(v1, v3);



Vector: 値の追加

push関数で末尾に値を追加することができます。

// Listing 8-3: Using the push method to add values to a vector

let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);

この例では、Vec::new関数で空のVectorを作っているものの、あとに続くpush関数の引数から推論できるため、型アノテーションは不要です。

また、値を変更するためmutキーワードが必要です。詳しくは第3章を参照してください。



Vector: 値のドロップ(解放)

構造体のデータなどと同様に、Vectorもスコープから外れるとドロップされます。

// Listing 8-4: Showing where the vector and its elements are dropped

{
let v = vec![1, 2, 3, 4];
// do stuff with v
} // <- v goes out of scope and is freed here

Vectorがドロップされる時、その内容についてもドロップされます。

参照(リファレンス)をVector内に保持すると、話が少しだけややこしくなります。



Vector: 値の取得

インデックス表記[]、またはget関数を使って、指定したインデックスの値を取得することができます。インデックスは0から始まり、それは1番目の要素を指します。

// Listing 8-5: Using indexing syntax or the get method to access an item in a vector

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

match v.get(2) {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}

The third element is 3

The third element is 3

get関数はOption<&T>を返します。



Vector: 値の取得(範囲外)

範囲外のインデックスにアクセスした場合、[]getで挙動が異なります。

// Listing 8-6: Attempting to access the element at index 100 in a vector containing five elements

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

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

範囲外のインデックスにアクセスした場合:



  • []はpanicを発生させます。確実に値が存在すると仮定できる場合に便利です。


  • getNoneを返します。確実に値が存在すると言えない場合に便利です。外部から入力される場合など。

Option<&T>の取り扱いについては第6章を参照してください。



Vector: 所有権

第4章で説明した通り、同一のスコープで可変参照と不変参照を同時に持つことはできません。

// Listing 8-7: Attempting to add an element to a vector while holding a reference to an item

let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {}", first);

上記のコードでは、以下のようなコンパイルエラーが発生します。

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable

--> src/main.rs:10:5
|
8 | let first = &v[0];
| - immutable borrow occurs here
9 |
10 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
11 |
12 | println!("The first element is: {}", first);
| ----- borrow later used here

firstは最初の要素の不変参照を取得しており、pushは末尾を操作しているだけなので、このコードは一見動作するように見えます。

実際には末尾への値の追加は領域の拡張(再確保とコピー)が生じる可能性があります。その場合、最初の要素への参照は妥当な領域を指さなくなり、安全ではありません。



Vector: 値の列挙 (1/2)

Vectorのすべての値について処理したい場合は、forを使うことができます。

// Listing 8-8: Printing each element in a vector by iterating over the elements using a for loop

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

100

32
57



Vector: 値の列挙 (2/2)

可変参照を得ることもできます。

// Listing 8-9: Iterating over mutable references to elements in a vector

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

*(参照解決演算子、dereference operator)については第15章で詳しく扱います。



Vector: 複数の型の保持

Vectorは1つのデータ型しか扱うことができませんが、enumを使うことで、間接的に複数の型を扱うことができます。

// Listing 8-10: Defining an enum to store values of different types in one vector

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

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

上記は、スプレッドシートのセルを扱う例です。整数i32、浮動小数点数f64、文字列Stringを1つのVectorに格納しています。

もし実行時にしか分からない複数の型を取り扱う必要がある場合、enumのテクニックは使えません。その場合、トレイトオブジェクトを使うことができます。詳しくは第17章を参照してください。



String



String

Stringについては第4章で軽く触れましたが、文字列は多くのプログラマが考えるより複雑なデータ構造です。ここではより深く見ていきましょう。

RustではStringはバイトのコレクション(バイト列)として実装されています。ただ、人とコンピュータとでは解釈が異なり、それによりどう複雑になるのかを説明します。



String: Rustにおける文字列


  • Rustのコア言語には1つの文字列型str(文字列スライス)しかありません。(第4章を参照してください)


    • 文字列スライスは、UTF-8形式でエンコードされ、どこかに格納された文字列です。(例えば、実行バイナリに含まれる文字列リテラルなど)




  • Stringは(コア言語ではなく)標準ライブラリで提供される型の1つです。



    • Stringは変更可能で、所有可能なUTF-8形式でエンコードされた文字列型です。



  • 標準ライブラリには、他にもOsStringOsStrCStringCStrなどのそれぞれの用途を持つ文字列型が存在します。


    • 詳しくはAPIドキュメントを参照してください。





String: 空の文字列の作成

Vec<T>と同じように、new関数を用いて空の文字列を作ることができます。

// Listing 8-11: Creating a new, empty String

let mut s = String::new();

Vec<T>で利用可能な操作の多くはStringでも利用可能です。



String: リテラル文字列からの作成

リテラル文字列(str)から文字列(String)を作る場合、to_string関数を使うことができます。strに限らずDisplayトレイトを実装している型で利用できます。

// Listing 8-12: Using the to_string method to create a String from a string literal

let data = "initial contents";

let s = data.to_string();

// the method also works on a literal directly:
let s = "initial contents".to_string();

別の方法として、String::from関数から作ることもできます。どちらを使うかはお好みでどうぞ。

// Listing 8-13: Using the String::from function to create a String from a string literal

let s = String::from("initial contents");



String: UTF-8

Stringは文字列をUTF-8形式で取り扱うため、Unicodeで表現できる任意の文字を含むことができます。

// Listing 8-14: Storing greetings in different languages in strings

let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שָׁלוֹם");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");

上記の文字列は、すべて有効な文字列です。



String: 文字列の追加

StringVec<T>と同様、伸縮可能です。push_str関数を使うことで、文字列の末尾に文字列を追加することができます。

// Listing 8-15: Appending a string slice to a String using the push_str method

let mut s = String::from("foo");
s.push_str("bar");

push_strは所有権を取りません。そのため以下の例のようにs2を使うことができます。

// Listing 8-16: Using a string slice after appending its contents to a String

let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2);
println!("s2 is {}", s2);



String: 文字の追加

push関数は、単一の文字を文字列の末尾に追加することができます。

// Listing 8-17: Adding one character to a String value using push

let mut s = String::from("lo");
s.push('l');



String: +演算子 (1/2)

2つの文字列を結合するのに+演算子を使うこともできます。

// Listing 8-18: Using the + operator to combine two String values into a new String value

let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used

上記の結果、s3Hello, world!となります。ただし、s1はムーブされるため、以降使うことができません。



String: +演算子 (2/2)

+演算子は以下のように定義されているadd関数を使っています。(なお、標準ライブラリ内における正確な定義ではありません。実際にはGenericsです)

fn add(self, s: &str) -> String {

上記の通り+演算子の右辺にあたるs&strであり、&Stringではありません。なのになぜ前述のコード(8-18)はコンパイルできるのでしょう?

それは「deref coercion」という機構が働くからです。詳しくは第15章を参照してください。

+演算子の左辺にあたるselfは所有権を必要としています。そのため前述のコード(8-18)においてs1はムーブされ、以降使うことができません。

let s3 = s1 + &s2;という文は一見、s1s2をコピーし、新たなs3を生成しているように見えます。しかし実際には、s1s2をコピーして追加しています。



String: format!マクロ

+演算子を使って複数の文字列を結合する場合、少々分かりづらいコードとなります。

let s1 = String::from("tic");

let s2 = String::from("tac");
let s3 = String::from("toe");
let s = s1 + "-" + &s2 + "-" + &s3;

こういった場合にはformat!マクロが便利です。

let s1 = String::from("tic");

let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3);

format!マクロはprintln!と同じように書式化しますが、結果を標準出力ではなく、文字列として出力します。



String: インデックスによるアクセス

多くのプログラミング言語では、文字列に含まれるそれぞれの文字にインデックスでアクセスできます。しかしRustはそうではありません。

例えば以下のコードはエラーになります。

let s1 = String::from("hello");

let h = s1[0];

error[E0277]: the trait bound `std::string::String: std::ops::Index<{integer}>` is not satisfied

-->
|
3 | let h = s1[0];
| ^^^^^ the type `std::string::String` cannot be indexed by `{integer}`
|
= help: the trait `std::ops::Index<{integer}>` is not implemented for `std::string::String`

このエラーを理解するには、Rustにおける文字列の内部表現について知る必要があります。



String: 内部表現

StringVec<u8>をラッピングしたものです。

let len = String::from("Hola").len();

上記の例では、len4です。Holaのそれぞれの文字はUTF-8では1バイトでエンコードされるためです。では以下の例ではどうでしょう?

let len = String::from("Здравствуйте").len();

上記の文字列は12文字に見えますが、len24となります。1文字は複数バイトで構成されており、インデックスで参照された位置にあるu8が有効な文字であるとは限りません。



String: Grapheme

(より複雑な、ヒンディー語の文字の例。割愛します)

RustのStringは効率を重視してVec<u8>として実装されていますが、有効な文字列であることを保証するために、インデックスによる文字へのアクセスはできません。



String: 文字列のスライス

文字列のスライスを得ることは、多くの場合悪いアイデアです。それは、バイト列を得たいのか、文字群を得たいのか、Grapheme群を得たいのか明かではないからです。

let hello = "Здравствуйте";

let s = &hello[0..4];

上記の例では、4バイトを含む&strが得られます。では&hello[0..1]だとどうでしょうか?結果はパニックです。

thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`', src/libcore/str/mod.rs:2188:4

クラッシュしうるので、文字列のスライスを扱う場合は十分に注意しましょう。



String: 文字列の列挙(文字単位)

幸い、Rustの文字列には何を列挙するのか明確な関数があります。chars関数を使えば、文字単位(正確にはUnicodeスカラ値単位)で列挙できます。

for c in "नमस्ते".chars() {

println!("{}", c);
}

結果は以下の通りです。









String: 文字列の列挙(バイト単位)

bytes関数を使えば、バイト単位で列挙できます。

for b in "नमस्ते".bytes() {

println!("{}", b);
}

結果は以下の通りです。

224

164
// --snip--
165
135



String: 文字列の取り扱いは単純じゃない

要するに、文字列の取り扱いは難しいものです。それぞれのプログラミング言語は、それぞれ異なった方法で複雑な「文字列」という存在にアプローチしています。

Rustの場合、すべてのプログラマが安全にUTF-8を扱える道を選びました。これにはいくらかのトレードオフもありますが、文字列の取り扱いに関するエラーに比べたらはるかにマシです。



Hash Map



Hash Map: 「Hash Map」とは?


  • 「キー」と、それに関連する「値」を格納するためのデータ構造です。

  • RustではHashMap<K, V>と表現されます。

  • 効率よくアクセスするために「ハッシュ関数」を用いて生成した「ハッシュ値」が用いられます。

  • 他のプログラミング言語では「map」「object」「hash table」「dictionary」「associative array」などと呼ばれることがあります。

  • Vectorとは異なり、任意の型をキーとして用いることができます。

  • 使用例: チーム名(文字列)をキーとして、チームのスコア(数値)を格納するデータ構造。



Hash Map: 作成と要素の追加

new関数で空のデータを作成し、insertで要素を追加することができます。

// Listing 8-20: Creating a new hash map and inserting some keys and values

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

HashMapを使うためにはuseする必要があります。他の2つのコレクション(Vector、String)は自動的に取り込まれる機能がありますが、HashMapはそうではありません。

Vectorと同様、Hash Mapのデータもヒープに置かれます。上記の例の場合、キーはStringで、値はi32です。

Vectorと同じように、キー、値には任意の型を用いることができますが、すべてのキー、すべての値のそれぞれは同じ型である必要があります。



Hash Map: collectによる変換

別の方法として、キーのVector、値のVectorからHash Mapを作成する方法があります。

// Listing 8-21: Creating a hash map from a list of teams and a list of scores

use std::collections::HashMap;

let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];

let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();

ここでは型アノテーションHashMap<_, _>が必要です。collect関数は様々なデータ構造を生成することができるため、型アノテーションを行わないと、生成するデータ構造を特定できません。

ここでの_は、Rustに型推論を行わせることを意味します。



Hash Map: 所有権

i32のようなCopyトレイトを実装している型の場合、その値はHash Mapにコピーされます。

一方、Stringのようなownedな値の場合、その値はHash Mapにムーブされます。

// Listing 8-22: Showing that keys and values are owned by the hash map once they’re inserted

use std::collections::HashMap;

let field_name = String::from("Favorite color");
let field_value = String::from("Blue");

let mut map = HashMap::new();
map.insert(field_name, field_value);
// field_name and field_value are invalid at this point, try using them and
// see what compiler error you get!

field_namefield_valueの値はムーブされるため、insertを呼び出したあとは使うことができません。

参照をinsertした場合、値はHash Mapにムーブされません。その代わり、Hash Mapが有効である限り、参照先が有効である必要があります。

これらについては第10章の「Validating References with Lifetimes」で詳しく説明します。



Hash Map: 値へのアクセス

get関数で値を取得することができます。

// Listing 8-23: Accessing the score for the Blue team stored in the hash map

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

let team_name = String::from("Blue");
let score = scores.get(&team_name);

get関数はOption<&V>を返します。キーに関連付けられた値が存在する場合はSome(&V)が、存在しない場合はNoneが返されます。



Hash Map: キーと値の列挙

キーと値の組について、Vectorなどと同じように列挙することができます。

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

for (key, value) in &scores {
println!("{}: {}", key, value);
}

実行結果は以下の通りです。なお、列挙の順序は一定ではありません。

Yellow: 50

Blue: 10



Hash Map: 更新

各キーには、1つの値だけを関連付けることができます。そのため、キーが既に存在する場合の挙動を決める必要があります。

例えば、キーが既に存在した場合:


  • 古い値を新しい値で置き換え、古い値を削除する。

  • 新しい値を無視し、古い値を維持する。ただし、キーが存在しない場合は新しい値を追加する。

  • 古い値を新しい値を組み合わせる。



Hash Map: 値を上書きする

既に存在するキーを追加した場合、古い値は新しい値で置き換えられます。

// Listing 8-24: Replacing a value stored with a particular key

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);

println!("{:?}", scores);

実行結果は以下の通りです。

{"Blue": 25}



Hash Map: キーが存在しない場合に追加する

「特定のキーが存在するかどうか確認し、無い場合は値を追加する」というのは、Hash Mapにおいてよく用いられる操作です。

Hash Mapにはentry関数というAPIがあり、or_insert関数を持つEntry列挙型を返します。

// Listing 8-25: Using the entry method to only insert if the key does not already have a value

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);

scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);

println!("{:?}", scores);

実行結果は以下の通りです。

{"Yellow": 50, "Blue": 10}



Hash Map: 古い値と新しい値を組み合わせる

他のよくあるケースとして「古い値と新しい値を組み合わせる」というのがあります。具体例として「単語を数える」例を以下に示します。

use std::collections::HashMap;

let text = "hello world wonderful world";

let mut map = HashMap::new();

for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}

println!("{:?}", map);

実行結果は以下の通りです。

{"wonderful": 1, "hello": 1, "world": 2}

or_insert関数は値への可変参照(&mut V)を返します。そのため*を用いることで値を更新することができます。



Hash Map: ハッシュ関数

デフォルトでは、HashMapは「暗号学的に強度のある」ハッシュ関数を用います。これはDoS(サービス不能攻撃)を防ぐためです。

ただし、このハッシュ関数は遅いというトレードオフがあります。

もしパフォーマンスを改善したい場合、他のハッシュ関数に切り替えることもできます。

crates.ioには多くのハッシュ関数が登録されているため、それらを利用できるかも知れません。



まとめ



まとめ


  • Vector、String、そしてHash Mapという3つのコレクション(データ構造)について説明しました。

  • これらはデータを操作(保存、読み込み、変更)するプログラムが必要とする多くの機能を提供します。

  • 演習問題:


    • 複数の整数を渡し、その平均値、中央値、頻出値を返す関数を書きましょう。

    • 文字列を「pig latin」に変換する関数を書きましょう。(詳細は割愛)

    • Hash MapとVectorを使って、従業員名と部署名を管理するCLIを書きましょう。(詳細は割愛)