THINK

Rust覚書7: 共通コレクション

2/21/2020

Rustの標準ライブラリには、コレクションと呼ばれる非常に便利なデータ構造体が多数含まれている

ほとんどのデータ型は1つの特定の値を表すが、コレクションには複数の値を含めることができる

配列やタプルとは異なり、これらのコレクションが指すデータはヒープに保存される

つまり、コンパイル時にデータ量を知る必要はなく、プログラムの実行に応じてデータの量を増減できる

コレクションの種類ごとに異なる機能とコストがある

現在の状況に適したコレクションを選択することは、時間の経過とともに向上するスキル

Rustでは、次の3つのコレクションがよく使われる

これはマップと呼ばれる一般的なデータ構造の特定の実装

標準ライブラリが提供する他の種類のコレクションについては ドキュメント を参照すること

ベクターで値のリストを保存する

最初のコレクション型は 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の要素に対して 実行される動作でエラーを引き起こす可能性がある

enummatch式を使うことで、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などの他の多くの文字列型も存在する

ライブラリクレートは、文字列データを保存するための多くのオプションを提供できる

それらは名前の最後がStringStrで終わっている これらは所有及び借用した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

StringVec<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メソッドを使用して、 blue10がペアになっているタプルのベクターを作成できる

次に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);

Entryor_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耐性とは... 総当りしても元の値に戻せないよというあれかな?

参考