Rustの標準ライブラリには、コレクションと呼ばれる非常に便利なデータ構造体が多数含まれている
ほとんどのデータ型は1つの特定の値を表すが、コレクションには複数の値を含めることができる
配列やタプルとは異なり、これらのコレクションが指すデータはヒープに保存される
つまり、コンパイル時にデータ量を知る必要はなく、プログラムの実行に応じてデータの量を増減できる
コレクションの種類ごとに異なる機能とコストがある
現在の状況に適したコレクションを選択することは、時間の経過とともに向上するスキル
Rustでは、次の3つのコレクションがよく使われる
- ベクター(ベクター?: vector): 可変の値を隣接して保存できる(可変長配列?)
- 文字列(string): 文字のコレクション
- ハッシュマップ(hash map): 値を特定のキーに関連付けることができる
これはマップと呼ばれる一般的なデータ構造の特定の実装
標準ライブラリが提供する他の種類のコレクションについては ドキュメント を参照すること
ベクターで値のリストを保存する
最初のコレクション型は Vec<T>
であり、ベクターとも呼ばれる
ベクターを使用すると、1つのデータ構造に複数の値を保存して、メモリないの全ての値を並べて保存できる
ベクターは同じ型のみを保存できる
ファイル内のテキスト行やショッピングカートないのアイテムの価格など、アイテムのリストがある場合に便利である
新しいベクターの作成: creating a new vector
let v: Vec<i32> = Vec::new();
型を明記していることに注意
でなければvector
に値を入れていないため、Rustがどのような要素を入れるかを知ることが出来ない
これは重要なポイントで、vector
はジェネリックを使って実装されている
実際のコードでは、Rustは値を挿入すると値の型を推測できることが多いため、 型注釈を付ける必要はほとんど無い
初期値を持つVec<T>
を作成するのがより一般的で、 Rustはvec!
マクロを提供する
let v = vec![1, 2, 3];
初期値にi32
を指定したため、 Vec<i32>
と推測されるため型注釈はは不要になる
Vectorの更新: updating a vector
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
この場合、内部に配置している値は全てi32
であり、Rustはこれをデータから推測する
そのため、型注釈が不要になる
Vectorがドロップする場合、要素もドロップする: dropping a vector drops its elements
{
let v = vec![1, 2, 3, 4];
// do stuff with v
} // <- v goes out of scope and is freed here
ベクターが失われると、その内容も全て失われる
これは簡単な点に見えるかもしれないが、Vectorの要素への参照を導入し始めると 少し複雑になる可能性がある
ベクターの要素を読む: reading elements of vector
ベクターに保存された値を参照するには、2つの方法がある
インデックス構文あたはgetメソッドを使った方法を示す
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."),
}
1つ目はインデックスを利用して3番目の要素を取得している
ベクターは00から始まる番号でインデックス付される
2つ目はget
メソッドを利用して、Option<&T>
を取得している
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
最初の &v[100]
は存在しない要素を参照しているため、プログラムをパニックさせる
この方法は、ベクターの終わりを超えて要素にアクセスしようとした場合にプログラムをクラッシュさせたい場合に最適(皮肉?)
get
メソッドは、ベクター外のインデックスが渡されると、パニックを起こさずNone
を返す
通常の状況下でベクターの範囲を超えることがある場合、このメソッドを使用する
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {}", first);
これは、不変参照と可変参照が同時にあるため、動作しない
ベクターの値の反復処理: iterating over the values in a vector
ベクターの値全てにアクセスする時 for
ループを使用して不変参照を取得する
let v = vec![100, 32, 57];
for i in &v {
println!("{}", i);
}
要素に変更を加える場合、可変参照にもできる
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
可変参照が参照する値を変更するには、+=
演算子を使用する前に、参照解除演算子(*
)を使用し、 i
の値を取得する必要がある
Using an Enum to Store Multiple Types
enum
を使って複数の型を保存する: using an enum to store multiple types
ベクターには同じ型の値しか入れられないが、 列挙型を使用すれば、異なる型を入れることができる (列挙型自体は1つの型)
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
Rustはコンパイル時にvector
に含まれる型を知る必要があるため、 ヒープ上のメモリ量を正確に知ることができる
また、この性質により、vector
の型を明示できる
もし、Rustがvector
に任意の型を保持することを許可した場合、1つ以上の方がvector
の要素に対して 実行される動作でエラーを引き起こす可能性がある
enum
とmatch
式を使うことで、Rustはコンパイル時に全ての可能性が処理されることを保証する
文字列を使ったUTF-8エンコードされたテキストの保存: storing utf-8 encoded text with string
文字列について、更に詳しく...
文字列はbytes
のコレクションとして実装されている
文字列とは: what is a string?
Rustには、コア言語の文字列型が1つしか無い
これは、借用形式の&str
で見られる文字列スライスstr
である
文字列型はコア言語にコード化されるのではなく、Rustの標準ライブラリによって提供さるUTF-8でエンコードされた文字列型である
Rustでstring
を指す場合、String
と&str
を意味する
ここではString
について説明するが、両方ともRustの標準ライブラリで頻繁に使用され、両方ともUTF-8エンコードされる
Rustの標準ライブラリには、OsString
などの他の多くの文字列型も存在する
ライブラリクレートは、文字列データを保存するための多くのオプションを提供できる
それらは名前の最後がString
かStr
で終わっている これらは所有及び借用したvariant
を参照する
これらの文字列型は異なるエンコーディングでテキストを保存したり、異なる方法でメモリに表現できる
新しい文字列の作成: creating a new string
Vec<T>
のようにnew
メソッドをString
も使用できる
let mut s = String::new();
Stringに初期値を入れることがある これは文字列リテラルのto_string
メソッドを使って実現できる
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
を使っても同様の操作を行うことができる
let s = String::from("initial contents");
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");
文字列の更新: updating a string
Vec<T>
のコンテンツのように、文字列のサイズは大きくなり、 コンテンツは変化する
文字列に追記: appending to a string with push_str and push
push_str
メソッドを使って文字列スライスを追加することで、文字列を拡大できる
push_str
メソッドは、所有権を取得しない
もし、所有権を取得していた場合、下記のコードは動かないことになる
let mut s = String::from("foo");
s.push_str("bar");
push
メソッドは、パラメーターとして単一の文字を受け取り、それを文字列に追加する
let mut s = String::from("lo");
s.push('l');
format!
マクロまたは+
演算子との結合
しばしば、2つの文字列を結合する必要がある
1つの方法として、+
演算子がある
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
+
演算子はadd
メソッドを使用している
fn add(self, s: &str) -> String {
s1
から所有権を取得し、s2
の内容をコピーしてから結果の所有権を返している
複数の文字列を連結する必要がある場合、 +
演算子の動作は扱いにくい (結果がわかりにくいため)
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!
は読みやすく、そのいずれの値の所有権も取得しない
文字列内のインデックス付け: indexing into strings
Rustではインデックス構文を使用して文字列の一部にアクセスしようとするとエラーが発生する
let s1 = String::from("hello");
let h = s1[0];
内部表現: internal representation
String
はVec<u8>
のラッパーである
let len = String::from("Hola").len();
これらの各文字はUTF-8でエンコードされた場合、1バイトなため、let len
は4となる
let len = String::from("Здравствуйте").len();
これは、12ではなく24になる (キリル文字は2バイト文字)
バイトとスカラ値と書記素クラスター: bytes and scalar values and grapheme clusters! oh my!
(タイトルよくわかんない)
UTF-8のもう一つのポイントは、実際にRustの観点から文字列を見るための3つの関連する方法があるということ
バイトとスカラ値と書記素クラスター(文字と呼ぶものに近いもの)
ヒンディー語「नमस्ते」を見ると、次のようなu8
値のベクターとして保存されている
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]
これらをUnicodeのスカラ値とみなすと、Rustのchar
型は次のようになる
['न', 'म', 'स', '्', 'त', 'े']
6つのchar
があるが、4番目と6番目は文字ではなく、それ自体では意味をなさない発音区別記号である
最後に、書記素クラスターとして見るとヒンディー語を構成する4文字と人が呼ぶものが得られる
["न", "म", "स्", "ते"]
Rustが文字を主著区するために文字列にインデックスを作成することを許可しない最後の理由は、 インデックス作成操作には常に一定の時間(O(1))がかかることが予想されることである
しかし、Rustは有効な文字がいくつあるかを判断するために、最初からインデックスまでコンテンツを調べなければならないため、 文字列でそのパフォーマンスを保証することは出来ない
文字列のスライス: slicing string
文字列へのインデックス付けは、文字列インデックス付け操作の戻り地の型をが明確でないため、良くない考えである
したがって、実際にインデックスを使用して文字列スライスを作成する必要がある場合、 Rustはより具体的になるように求める
インデックスをより具体的にして、単一の数字で[]
を使用するのではなく、 文字列スライスが必要であることを示すには、範囲[]
を使用して特定のバイトを含む文字列スライスを作成できる
let hello = "Здравствуйте";
let s = &hello[0..4];
では、 &hello[0..1]
を使用するとどうなるか...
答えは無効なインデックスがベクターでアクセスされた場合と同じようにRustは実行時にパニックになる
範囲を使用すると、プログラムをクラッシュさせる可能性があるため、文字列スライスの作成には注意が必要
文字列を反復処理するメソッド: methods for iterating over strings
個々のUnicodeスカラ値に対して操作を実行する必要がある場合、 最善の方法は、chars
メソッドを使用することである
for c in "नमस्ते".chars() {
println!("{}", c);
}
バイトメソッドは、各生バイトを返す
これは、ドメインに適している場合がある
for b in "नमस्ते".bytes() {
println!("{}", b);
}
文字列から書記素クラスターを取得することは複雑であるため、 この機能は標準ライブラリでは提供されていない
この機能が必要な場合、crates.io
を利用できる
文字列はそれほど単純ではない: strings are not so simple
Rustは文字列データの正しい処理を書くことをデフォルトの動作にした
プログラマーはUTF-8データの処理を事前に検討する必要がある
このトレードオフは、他のプログラミング言語で明らかな文字列の複雑さの多くを明らかにする
しかし、開発ライフサイクルの後半で非ASCII文字に関連するエラー処理をする必要がなくなる
ハッシュマップにキーと関連付けられた値の保存: storing keys with associated values in hash maps
HashMap<K, V>
型は、K
のキーとV
の値のマッピングを格納する
これは、これらのキーと値をメモリに配置する方法を決定するハッシュ関数を介して行われる
多くのプログラミング言語はこの種のデータ構造を別の名前でサポートしている
ハッシュマップはベクターのようにインデックスを使用するのではなく、任意の型のキーを使用してデータを検索する場合に役立つ
新しいハッシュマップの作成: creating anew hash map
new
で空のハッシュマップを作成し、inser
で要素を追加できる
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
3つの一般的なコレクションのうち、 これは最も使用頻度が低いため、自動的にスコープに入れられる機能には含まれていない
また、Vect<T>
のように構築用のマクロも存在しない
ベクター同様、ハッシュマップはデータをヒープに保存する
ハッシュマップを構築する別の方法は、タプルのベクターで collect
メソッドを使用する
collect
メソッドは、HashMapを含むいくつかのコレクション型にデータを収集する
例として、2つの別々のベクターにチーム名と書紀スコアがある場合、zipメソッドを使用して、 blue
と10
がペアになっているタプルのベクターを作成できる
次にcollect
メソッドを使用してタプルのベクターはハッシュマップに変換できる
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がどれを望むかわからないからである
ただし、キーと値のパラメーターにはアンダースコアを使用している (Rustがベクター内のデータ型に基づいて型を推測できるため)
ハッシュマップと所有権
i32
のようなコピー特性を実装するタイプの場合、値はハッシュマップにコピーされる
String
などの所有値の場合、所有権は譲渡され、ハッシュマップがそれらの値の所有者になる
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!
値への参照をハッシュマップに挿入すると所有権はハッシュマップに移動しない
ただし、参照が指す値は少なくともハッシュマップが有効である限り有効である必要がある
ハッシュマップの値へのアクセス: accessing values in a hash map
キーをget
メソッドに提供することで、ハッシュマップから値を取得できる
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);
ここでのscore
の結果はSome(&10)
になる
get
メソッドは、Option<&V>
を返すため、Some
でラップされている
for
ループを使用することで、ベクターと同様の方法でハッシュマップ内の各キー/ペアを反復処理できる
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);
}
パッシュマップの更新: updating a hash map
キーと値の数は増えるが、各キーは一度に1つの値しか関連付けることが出来ない
ハッシュマップのデータを変更する場合、 キーにすでに値が割り当てられている場合の処理方法を決定する必要がある
値を更新するか、それとも値がない場合のみ追加するか... それとも値を組み合わせるか...
値の上書き: overwriting a value
キーと値をハッシュマップに挿入してから、同じキーに異なる値を挿入すると そのキーに関連付けられた値が置き換えられる
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
println!("{:?}", scores);
キーに値がない場合にのみ値を挿入する: only inserting a value if the key has no value
特定のキーに値があるかどうかを確認し、値がない場合は値を挿入するのが一般的
ハッシュマップには、entry
と呼ばれるチェックするキーをパラメーターとして受け取れる特別なAPIがある
entry
メソッドの戻り値は、存在する場合と存在しない場合がある値を表すEntry
という列挙型
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);
Entry
のor_insert
メソッドは、そのキーが存在する場合、対応するキーへの可変参照を返すように定義され、存在しない場合、 このキーの新しい値tとしてパラメーターを挿入し、可変参照を返す
この手法は、ロジックを自分で記述するよりも遥かに借用チェッカーでよりうまく機能する
古い値に基づいて値を更新する: updating a value based on the old value
ハッシュマップのもう一つの一般的な使用例は、キーの値を検索し、古い値に基づいて更新すること
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);
or_insert
メソッドは、可変参照(&mut V
)を返している
ここで、可変参照をcount
変数に格納している
割り当てるには、最初にアスタリスク(*
)を使用してcount
を逆参照する必要がある
可変参照は、ループの終わりに範囲外になるため、これらの変更は全て安全でり、 借用ルールによって許可される
ハッシュ関数: hashing functions
デフォルトでは、ハッシュマップは「暗号的に強力な」ハッシュ関数を使用し、DoS攻撃に対する耐性を提供できる
これは、利用可能な最速のハッシュアルゴリズムではないが、パフォーマンスの低下にともなるセキュリティの向上とのトレードオフに値する
コードのプロファイルを作成し、デフォルトのハッシュ関数が目的に対しておそすぎる場合、別のハッシュ関数を指定して別の関数に切り替えることができる
ハッシュはBuildHasher
トレイトを実装する型
crates.io
には多くの一般的なハッシュアルゴリズムを実装するハッシングを提供する他のRustユーザーが共有するライブラリがある
感想
ベクトルとベクターどっち??
ベクターらしい...たまにベクトルって書いてる記事も見つけた
Goと違ってRustは文字列の扱いが大変なのか... 誰かがRustと違ってGoはメモリが潤沢で有ることを前提にしているって言っていたけど、 こういうところとかのことなんだろうなぁと思った次第
自動的に逆参照付けるとか言っていたけど、なんで逆参照?って思ったけど、コピー特性というか、 値の完全上書きは逆参照必要って話みたい
ハッシュ関数のDoS耐性とは... 総当りしても元の値に戻せないよというあれかな?