0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rustなんもわからん部~チュートリアルを読むPart4/n~

Posted at

Rustなんもわからん

Ruistなんも分かりません。わからないのでおててを動かしていきます。

今回もRustのチュートリアル(bookなるもの)を見ていきます。
英語版とその日本語訳版がありますが、今回は日本語訳版を見ていきたいと思います。
(今週は省エネ記事です。)

他の参考資料は以下の通りです。

チュートリアルを読む会

チュートリアルをひたすら読んで、内容をまとめていきます。
今回は8章までです。

7.モジュール化

この章では、例えばPythonでライブラリを自作し、それを別のコードでimportして使うような、モジュール化について説明しています。
コンテンツとしては以下の通りです。

  • パッケージ: クレートをビルドし、テストし、共有することができるCargoの機能
  • クレート: ライブラリか実行可能ファイルを生成する、木構造をしたモジュール群
  • モジュール と use: これを使うことで、パスの構成、スコープ、公開するか否かを決定できます
  • パス: 要素(例えば構造体や関数やモジュール)に名前をつける方法

パッケージとクレート(未解決)

パッケージとクレートに関して、まずは以下の説明を見てみます。

最初に学ぶモジュールシステムの要素は、パッケージとクレートです。 クレートはバイナリかライブラリのどちらかです。 クレートルート (crate root) とは、Rustコンパイラの開始点となり、クレートのルートモジュールを作るソースファイルのことです(モジュールについて詳しくは「モジュールを定義して、スコープとプライバシーを制御する」のセクションで説明します)。 パッケージ はある機能群を提供する1つ以上のクレートです。 パッケージは Cargo.toml という、それらのクレートをどのようにビルドするかを説明するファイルを持っています。

さすがに意味が分からないので、gptに聞いてみました。

✅ まとめ

  • クレート = コンパイル単位。バイナリ or ライブラリ。
  • パッケージ = Cargo管理の配布単位。1つ以上のクレートを含む。最大1つのライブラリ+複数のバイナリ。
  • 実務では大部分は1パッケージ1クレート構成なので、用語が混ざることもしばしばですが、区別して理解するとCargoの構造やワークスペース設計がクリアになります。

また、追加で調べたところ、以下のように説明しているサイトがありました。

Rustのクレートはコンパイル単位を示す概念であり、Rustプログラムを構成する最小ユニットです。
クレートはプログラム全体やライブラリ、モジュールの集まりとして機能するため、クレートを使ってプログラムの構造を整理し、外部のコードやライブラリと連携することが可能です。

また、クレートにはバイナリクレートとライブラリクレートの二種類が存在し、バイナリクレートは実行可能なプログラムを生成、ライブラリクレートは他クレートからインポートして使用するためのコードやモジュールの提供を行う、との説明もありました。

結局のところ、クレートが一つの単位であり、パッケージはクレートとCargo.tmolによるビルド方法指定がまとまっているものである、という以上のことはわかりませんでした。

モジュール

モジュールは、クレート内のコードをグループ化して可読性と再利用性を向上させたもので、要素の公開/非公開も設定できます。実際のコードは以下の通りです。

// module(mod)の定義
mod front_of_house {
    // mod中のmodの定義
    mod hosting {
        fn add_to_waitlist() {}

        fn seat_at_table() {}
    }

    // mod中のmodの定義(二つ目)
    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}

// このコードによるmoduleツリー
crate // 全部このcrate下にある
 └── front_of_house
     ├── hosting
        ├── add_to_waitlist
        └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

このようにして、module間での関係性を定義することが出来ます。
ちなみに、モジュール間で親子関係、兄弟関係があり、今回であれば、front_of_househostingは親子関係(前者が親、後者が子)であり、add_to_waitlistseat_at_tableは兄弟となっています。親子関係に関しては、Pythonでのclassにおける継承よろしく、 子は親の機能に全てアクセスできますが、親は子の機能にアクセスすることが(一部除いて)できません。 次はこの話をします。

モジュールツリーの要素を示すパス

先ほどモジュールツリーを確認しましたが、その内部要素の指定方法について説明されていました。
パスの形態としては、ディレクトリと同様に、絶対パスと相対パスの二つがあるそうです。

  • 絶対パス:クレートの名前かcrateという文字列を使うことで、クレートルートからスタートします
  • 相対パス:selfsuperまたは今のモジュール内の識別子を使うことで、現在のモジュールからスタートします。

実際のコードでは以下のようになります。

// moduleの定義
mod front_of_house {
    // publicの形でmoduleを定義
    pub mod hosting {
        // publicの形で関数を定義
        pub fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    // 絶対パス
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    // 相対パス
    front_of_house::hosting::add_to_waitlist();
}

このように、要素::要素とすることでパスを構成しています。
ここで、pubというワードが出ましたが、これは要素を公開する、という宣言です。Rustでは基本的にモジュールの中身は非公開になり、アクセスすることが出来ません。(モジュール自体を公開してもその中身自体はデフォルトでは非公開)なので、pubをつけて公開することを明示しています。

また、今回front_of_housepubを宣言していませんが、これらは同じモジュール内部で定義されているので、お互いにアクセス可能になっています。

相対パスについては、superを使って始めることもでき、以下のように記述することもできます。

fn serve_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::serve_order();
        // superはback_of_houseの親モジュールを指す
    }

    fn cook_order() {}
}

構造体とenumのpubについて

pubに関連してですが、構造体とenumではpubを付けた際の挙動が若干異なります。

mod back_of_house {
    // 構造体自体は公開される
    pub struct Breakfast {
        // toastフィールドのみ公開される
        pub toast: String,
        seasonal_fruit: String,
    }

    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    // 夏 (summer) にライ麦 (Rye) パン付き朝食を注文
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // やっぱり別のパンにする
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // 下の行のコメントを外すとコンパイルできない。食事についてくる
    // 季節のフルーツを知ることも修正することも許されていないので
    // meal.seasonal_fruit = String::from("blueberries");

    // meal.seasonal_fruitへのアクセスは許可されていない
}

mod back_of_house {
    // enumはこれで全て公開される
    pub enum Appetizer {
        Soup,
        Salad,
    }
}

pub fn eat_at_restaurant() {
    // enumの中身も自動で公開されるので、これらのコードで問題なくアクセス可能
    let order1 = back_of_house::Appetizer::Soup;
    let order2 = back_of_house::Appetizer::Salad;
}

このように、structはフィールドごとに個別公開、enumは全公開、という違いがあります。

useによるパス記法

useによって、モジュールの呼び出しをより短い記述で行うことが出来るようになります。

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;
// 以下のように相対パスでもOK
use self::front_of_house::hosting;

pub fn eat_at_restaurant() {
    // use定義をしておいたので、hostingだけで使える
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

このuseに関して、関数については親モジュールを持ち込み、構造体やenumその他要素についてはフルパスで書くのが慣習になっているようです。

// 親モジュールを明示することで、
// 関数がどこのモジュールに属するかをわかりやすくする
use crate::front_of_house::hosting;

// Hashmapは構造体
// 構造体についてはフルパスで記述する
use std::collections::HashMap;

また、useでの呼び出しは、pythonでのimport asのように、名前を付けて呼び出すことも可能であり、その呼び出しを再公開することが出来ます。

pub use std::io::Result as IoResult;

こうすることで、IoResultという名前で呼び出すことができ、その呼び出し方を別のコードでも使えるようになりました。

外部パッケージの呼び出し

外部パッケージを使う場合、Cargo.tomlにパッケージを追加する必要があります。
例えば、randパッケージを使う場合、以下のようになります。

Cargo.toml
rand = "0.8.3"
use rand::Rng;

fn main() {
    let secret_number = rand::thread_rng().gen_range(1..101);
}

このようにして外部パッケージを使うことが出来ます。

巨大なuseのリストをネストしたパスを使って整理する

同じクレートかモジュールで定義されている複数要素を使う場合、以下のようにまとめることが出来ます。

// まとめないバージョン
use std::cmp::Ordering;
use std::io;

// まとめるバージョン
use std::{cmp::Ordering, io};

// まとめないバージョン
use std::io;
use std::io::Write;

// まとめるバージョン
use std::io::{self, Write};

// 全部呼ぶ
use std::collections::*;

モジュールの複数ファイルへの分割

これまでモジュールの定義はmodの直下で行っていましたが、これを別ファイルで実行することが出来ます。
次のモジュールを分割することを考えます。

src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

これは、以下のような3ファイルに分割することが出来ます。

src/lib.rs
// セミコロンで締めることで、モジュール中身をモジュール名のファイルから読み込むように指示
mod front_of_house;
src/front_of_house.rs
// src/lib.rs内部のfront_of_houseをここで定義

// さらに別ファイルでhostingモジュールを定義
pub mod hosting;
src/front_of_house/hosting.rs
// front_of_house::hostingの定義
pub fn add_to_waitlist() {}

このように、モジュール同士の関係性をファイルのディレクトリ構成に置き換えることで、モジュール定義を分割することが出来ます。

8.一般的なコレクション

Rustの標準ライブラリには、コレクションと呼ばれる有益なデータ構造が含まれます。
一般的なデータ型(int, strなど)では、一つの値しか含めないのに対して、コレクションは複数の値を含むことが出来ます。また、データがメモリのヒープ領域に保存されるため、データ量を後からいじることが可能となっています。
頻繁に利用されるものとして、以下の三つがあるそうです。

  • ベクタ型 :可変長の値を並べて保持
  • 文字列 :文字のコレクション
  • ハッシュマップ :値と特定のキーとの紐づけ

ベクタ型

ベクタ型(Vec<T>)は、以下のように定義されます。

    // 具体的要素を宣言しない場合は型注釈が必須
    let v: Vec<i32> = Vec::new();

    // vsc!というマクロによって、i32型の1,2,3からなるベクタを生成
    let v = vec![1, 2, 3];

ベクタに要素を追加する場合は、pushメソッドを使用します。

    // 変更可能な形で定義
    let mut v = Vec::new();

    /// ベクタvに5を追加
    v.push(5);

中身にアクセスする場合は以下のようにします。

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

    let third: &i32 = &v[2]; // スライスを利用してアクセス可能
    println!("The third element is {}", third);

    match v.get(2) { // .get(位置)でもアクセス可能
        //                      "3つ目の要素は{}です"
        Some(third) => println!("The third element is {}", third),
        //               "3つ目の要素はありません。"
        None => println!("There is no third element."),
    }

要素アクセスの方法として、スライスとgetの二つがありますが、これらは要素外の位置を指定された場合の挙動が異なります。

    // 要素長は5
    let v = vec![1, 2, 3, 4, 5];

    // スライスで100番目を指定するとクラッシュ
    let does_not_exist = &v[100];
    // getで100番目を指定するとNoneを返す(Option<&T>を得ているので)
    let does_not_exist = v.get(100);

part2で読んだ部分に、同一スコープ上では可変と不変な参照を同時に存在させられない、という話がありましたので、以下のようなコードではエラーになります。

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

    let first = &v[0];

    v.push(6);

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

>    Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
(エラー: 不変としても借用されているので、`v`を可変で借用できません)
 --> 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
  |                                               (その後不変借用はここで使われています)

このコードでは、ベクタがヒープ領域に存在するため、要素を追加するだけのスペースがない場合に、割り当てられるメモリ領域が更新されることとなり、結果としてfirstがデータの存在しない領域へのポインタになることを防ぐため、コンパイルエラーとなります。

ベクタ内部の要素の順々処理

以下では、ベクタの個別要素に順々にアクセスしています。

    // vec!マクロでのベクトル定義
    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の前にある*は、参照外し演算子といわれるものだそうです。詳しくは15章で扱うそうですが、ざっくり見たところ、参照では通常ポインタを保持しますが、参照外し演算子をつけることで、ポインタで指定されている値そのものにアクセスすることが出来るそうです。

Enumを使っての複数型の保持

ベクタは通常同じデータ型の値しか持てませんが、enumの列挙子は同じenum型の中に定義されるので、enumを使うことで、無理やり複数のデータ型を一つのベクタにまとめることが出来るそうです。

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

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

文字列の概要(未解決)

Rustにおいて、言語の核として1種類しか文字列型が存在せず、それは文字列スライスのstrだそうです。通常は借用された形態&strだそうですが、これは、別の場所に格納されたUTF-8エンコードされた文字列データへの参照だそうです。
String型は、Rustの標準ライブラリで提供される、伸長可能、 可変、所有権のあるUTF-8エンコードされた文字列型なんだそうです。
いまいち理解できないので、まずはサンプルコードを見ていきます。

文字列の生成

文字列の生成は以下のようにして行えます。

// 空の文字列を生成
let mut s = String::new();

// to_string()を使っての文字列の生成
let data = "initial contents";
let s = data.to_string();

// 直接も可能
let s = "initial contents".to_string();

// 文字列リテラルからの生成
let s = String::from("initial contents");

// utf-8ゆえ、以下の言語に対応
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");

文字列の更新

文字列はサイズ変更を行うことが出来ます。

let mut s = String::from("foo");
s.push_str("bar"); 
// s が"foobar"に変化

// .push_strは所有権を必要としないので、以下のコードでもOK
let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2); // スライスとして渡している?らしい
println!("s2 is {}", s2);

// 文字列の結合
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1はムーブされ、もう使用できないことに注意

// 文字列の結合(3つ)
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = s1 + "-" + &s2 + "-" + &s3;
let s = format!("{}-{}-{}", s1, s2, s3);
補足:文字列の結合について

文字列の結合部分は、かなり込み入った形になっているそうです。
いまいち理解しきれなかったので、ドキュメントをそのまま置いておきます。

+演算子は、addメソッドを使用し、そのしぐにちゃは以下のような感じです:

let s = format!("{}-{}-{}", s1, s2, s3);

これは、標準ライブラリにあるシグニチャそのものではありません: 標準ライブラリでは、addはジェネリクスで定義されています。 ここでは、ジェネリックな型を具体的な型に置き換えたaddのシグニチャを見ており、これは、 このメソッドをString値とともに呼び出した時に起こることです。ジェネリクスについては、第10章で議論します。 このシグニチャが、+演算子の巧妙な部分を理解するのに必要な手がかりになるのです。

まず、s2には&がついてます。つまり、add関数のs引数のために最初の文字列に2番目の文字列の参照を追加するということです: Stringには&strを追加することしかできません。要するに2つのString値を追加することはできないのです。 でも待ってください。addの第2引数で指定されているように、&s2の型は、&strではなく、 &Stringではないですか。では、なぜ、リスト8-18は、コンパイルできるのでしょうか?
add呼び出しで&s2を使える理由は、コンパイラが&String引数を&strに型強制してくれるためです。 addメソッド呼び出しの際、コンパイラは、参照外し型強制というものを使用し、ここでは、 &s2を&s2[..]に変えるものと考えることができます。参照外し型強制について詳しくは、第15章で議論します。 addがs引数の所有権を奪わないので、この処理後もs2が有効なStringになるわけです。
2番目に、シグニチャからaddはselfの所有権をもらうことがわかります。selfには&がついていないからです。 これはつまり、リスト8-18においてs1はadd呼び出しにムーブされ、その後は有効ではなくなるということです。 故に、s3 = s1 + &s2;は両文字列をコピーして新しいものを作るように見えますが、 この文は実際にはs1の所有権を奪い、s2の中身のコピーを追記し、結果の所有権を返すのです。言い換えると、 たくさんのコピーをしているように見えますが、違います; 実装は、コピーよりも効率的です。

https://doc.rust-jp.rs/book-ja/ch08-02-strings.html

文字列への添え字アクセス

Rustにおいて、添え字記法でStringの一部にアクセスしようとするとエラーになります。

let s1 = String::from("hello");
let h = s1[0];

error[E0277]: the trait bound `std::string::String: std::ops::Index<{Integer}>` is not satisfied
(エラー: トレイト境界`std::string::String: std::ops::Index<{Integer}>`が満たされていません)
  |>
3 |>     let h = s1[0];
  |>             ^^^^^ the type `std::string::String` cannot be indexed by `{Integer}`
  |>                   (`std::string::String``{Integer}`で添え字アクセスできません)
  = help: the trait `std::ops::Index<{Integer}>` is not implemented for `std::string::String`
  (ヘルプ: `std::ops::Index<{Integer}>`というトレイトが`std::string::String`に対して実装されていません)

つまり、添え字アクセスをサポートしていません。これを深堀します。

まず初めに、以下のコードを見てください。

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

このコードでは、文字列"Hola"を保持するベクタの長さが4バイトであることを意味します。(つまり、各文字はUTF-8エンコードで1バイト)
しかし、以下のコードではそうはいきません。

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

この場合だと、各文字は2バイトでエンコードされています。?????
そのため、以下のようなコードではエラーになります。

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

let s = &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
('main'スレッドはバイト添え字1は文字の境界ではありません; `Здравствуйте`'З'(バイト番号0から2)の中ですでパニックしました)

なので、文字列に対してはスライスを利用してのアクセスではなく、以下のような方法が紹介されていました。

for c in "नमस्ते".chars() {
    println!("{}", c);
}








// for b in "नमस्ते".bytes()だと、18バイトになる
// この文字列は、UTF-8で18バイトでエンコードされる

まとめると、Rustにおいては、一文字1バイトじゃない場合があるため、スライスをそのまま活用するとエラーになることがあります。怖すぎますね。

キーとバリュー格納するハッシュマップ

HashMap<K, V>は、K型のキーとV型の値の対応関係の保持を、ハッシュ関数を介して行います。(キーと値のメモリ配置方法を決定する)

まずは最初のサンプルです。以下のようにしてハッシュマップを生成することが出来ます。

// HashMapはuseする必要あり
use std::collections::HashMap;

let mut scores = HashMap::new();

// キーがString,値がi32型
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);

また、タプルのベクタに対して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();
// zipを使ってタプルのベクタを作成(ブルーと10がペア)
// HashMap<_, _>という型注釈が必要

i32のようなCopyトレイとを実装する型については、値はハッシュマップにコピーされるそうです。

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とfield_valueはこの時点で無効になる。試しに使ってみて
// どんなコンパイルエラーが出るか確認してみて!

ハッシュマップの値にアクセスする場合は以下のようにして行えます。

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);
// Some(&10)となる(getはOption<&V>を返すので)

ループを使っての走査も可能です。

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

ハッシュマップの更新

ハッシュマップにおいて、キーと値の数は変更できますが、各キーには一度に一つの値しか紐づけられません。ハッシュマップ内のデータを変更する場合は、すでに紐づいている値をどうするか決めないといけません。
たとえば、上書きする場合はこのようになります。

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}

キーに値がなかった時のみ挿入する方法もあり、その場合はentry関数を使えばいいそうです。

use std::collections::HashMap;

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

// Yellowキーは存在しなかったので、挿入
scores.entry(String::from("Yellow")).or_insert(50);
// Blueキーは存在したので、次のコードはスルーされる。
scores.entry(String::from("Blue")).or_insert(50);

println!("{:?}", scores);
> {"Yellow": 50, "Blue": 10}

古い値に基づいて更新する場合は以下のようになります。

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);
> {"world": 2, "hello": 1, "wonderful": 1}

全体まとめ

今週はクレートとモジュール、一般的コレクションについて学びました。
クレートとパッケージ周りの言葉の意味と、UTF-8エンコード周り、そしてハッシュマップあたりがだいぶ難しかったです。(特にString型にスライスでうまくアクセスできなかったりするのは本当に罠になりそう。)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?