LoginSignup
1
0

More than 3 years have passed since last update.

The Rust Programming Languageの浅いまとめ

Last updated at Posted at 2019-05-12

概要

最近Rustを勉強し始め、とりあえずThe Rust Programming Languageをだらだら読むようにしているので内容を順番にピックアップしてみます。自分用ノートでもあります。詳細は書きません。オリジナルを読んでください。
(この記事は読み進めるごとに追記していきます。)

注意事項

私はRustを下記の興味と目的でとりあえず使っているので、その観点でまとめてあります。

  • テスト駆動開発
  • プログラミングコンテスト

また本文中の注釈は私の感想みたいなものです。

内容

事始め

cargo というツールの話が後半になると出てきます。
cargo run するとビルドと実行を一緒にやってくれるのが個人的には良かった。つまりvimで
:w| :! cargo run
とやると保存とビルド、実行が一気にできるので捗ります。

数当てゲーム

Hello Worldより少し複雑な内容でRustでのコーディングが紹介されます。

  • io::stdin().read_line(&mut guess) でとりあえず入力を得られる
    • expect()とかつける必要があるけど
  • なんで(可変)変数宣言が let の一言ではないのかという説明
  • match文の登場 1

普遍的なプログラミング概念

  • (ほかのプログラミングにおける多くの場面と異なり)「式」と「文」は区別されます。
    • x = y = 5 みたいな記述が許されないのはこれが理由
    • 「関数呼び出しは式です」
    • 「ブロック({})も式です」
    • だから x = { y = 5; y }はできます。
  • if
    • 同様に条件2であるため、()で区切ると警告される3が、{}で区切るぶんには何も言われません。すなわち次のような書き方ができます。
main.rs
    if { y == 5 } { // 違和感はありますが……
        println!("y = 5");
    }
  • 「ifは式なので、let文の右辺に持ってくることができます。」
  • forループ4in が使えます

所有権を理解する

  • 基本的にヒープに確保されたメモリのアドレスを2つ以上の変数が共有することはありません。
    • 例えばこのポインタを s1 と s2 とします。
    • s1とs2が生きるスコープを抜けるときにRustは GC drop() するが、s1もs2も同じところを指してたら二重解放になってしまいます。
    • よって例えば s2=s1 するなら、その瞬間にs1は参照を無効にすることでこの問題を回避するようです。
  • 一方で整数などのデータはスタックに格納されるので、このようにはなりません5
  • 上記の s1 と s2 のような関係、すなわち所有権の移動は、関数の内外をまたぐときも起きます。
    • なのである関数 ownership() に s2 を引数で渡してしまったら返してもらわなければなりません。
    • というのは面倒なので特に必要がない場合は参照を生成し渡すようにします
      • 参照を受け取ることを借用というようです。借用したものは変更できません。
      • というよりは、ヒープを使用(内容を書き換え)することそのものが借用なのかもしれません。
      • 可変な状態で参照させることも可能です。ただし1回まで。
    • 関数の外で指定したヒープを関数の中で解放してしまうことはできるが、関数の中で指定したヒープを関数の外に持ち出すのはコンパイル時に阻止される6

上記を理解するために次のようなコードを書いてみました。

main.rs
fn main() {
    let mut s = String::from("Hello");
    //let r1 = &mut s; // ここで参照させてしまうと下のブロックで参照させられない。
    {
        let r1 = &mut s;
        //let r1_2 = &mut s; // 2重で参照させることはできない
        //let r1_2 = &mut r1; // 又貸しもできない
        r1.push_str(", hello");
        // s.push_str(", hello"); // 可変で参照させている間は変更できない
        println!("r1 : {}", r1);
    }
    {
        let r2 = &s;
        //s.push_str(", hello"); // 可変・不変にかかわらず参照させている間に
                                 // 値を書き換えることはできない。
        println!("r2 : {}", r2);
    }
    {
        let r3 = &s[0..4];
        //s.push_str(", hello"); // スライスに関しても同様
        println!("r3 : {}", r3);
    }
    s.push_str(", hello"); // もう誰にも参照させていないので書き換えられる
    println!("s  : {}", s);
}

これらのエラーは、時としてイライラするものではありますが、

慣れないと本当に……

構造体を使用して関係のあるデータを構造化する

structまたは、構造体は、意味のあるグループを形成する複数の関連した値をまとめ、名前付けできる独自のデータ型です

  • タプル構造体: 構造体の名前は持つがフィールドの名前は持たない
  • ユニット様構造体
    • トレイトは実装するけど
    • データは持たない構造体の場合に使えるらしい
  • 構造体は基本的にデータの所有権を持つ
  • 構造体のprintln!()
    • {:#?}が使える
      • #[derive(Debug)]を書く。Debugトレイルを継承する、らしい
  • 構造体上にメソッドを実装する ⇒ impl
  • Rustには「自動参照および参照外し7」機能があるのでC言語みたいな->がない
  • 関連関数。コンストラクタのように使える。static関数みたいな感じ。
  • Rustにはいわゆるクラスと呼ばれるものは無いそうです

Enumとパターンマッチング

  • 有用なenumであるOptionは、値が何かであるか、何でもないことを表現できます
  • 「各列挙子に紐付けるデータの型と量は、異なってもいい」
    • 構造体やenumをenumに紐付けることもできる
  • Option<T>Tに変換する必要」
    • この仕様のためにnullの悲劇を回避できる
  • enumとパターンマッチ
    • if letでパターンマッチを省略できる
    • if letでは==ではなく=を使う。たぶんletだから。……うまく説明できない。
    • これでもmatchのようにSomeの値とかを取り出せます

モジュール

  • モジュールとは、関数や型定義を含む名前空間のこと
    • mod で定義できる
  • modであることだけ宣言して内容はほかのファイルで定義できます
    • 「fooという名前のモジュールにサブモジュールがなければ、fooの定義は、foo.rsというファイルに書くべきです」
    • この場合、そのファイルでは改めてmodと宣言してはいけない
  • モジュールを階層化するとき、フォルダ構成もそれに準じた形になる 8
  • extern crateでモジュールを組み込める
    • pubである必要がある
  • 非公開で未使用のモジュール関数を作っておくと警告される
  • useでモジュールの関数をモジュール名を省略して使用できます
    • enumも同じ。
    • useを使うとサブモジュールへのアクセスも相対的になるようです
      • すると横に行ったり上に行ったりに困るが、困らないようにsuperがある
  • なおこのドキュメントの部分には書いていませんが、モジュール名はsnake_caseでつけることを決められているようです。
    • 構造体やenum、あとに出てくるトレイトはUpperCamelCaseです。

一般的なコレクション

  • vectorもコレクション。データはヒープに確保されるそうです
  • ベクターの要素を得るには直接アクセスする以外にも、参照を取ったり、ベクターのgetメソッドが使える。getメソッドはoption型で返すため、範囲外アクセスで落ちることがない。
    • 借用した場合は書き換えるときに参照外しが必要です
  • enumのベクターにすることで複数の型をひとつの配列にするテクニックがある
  • 文字列型はstr、もしくは借用された&strがコアであり、Stringはそうではないらしい
    • どちらもUTF-8である
    • 文字列の演算(結合とか)では「参照外し強制」が行われる。
    • 文字列のaddないしは+は被結合の文字列(先に来る文字列)の所有権を結合後の変数に移動させてしまうが、formatを使えば所有権は変わらずメモリがコピーさせる
  • いくつかの言語にあるように文字列に添え字アクセスを許さないのも、内部でUTF-8の値で文字列を持っているから。
    • おそらくだが、Rustには、添え字アクセスはあくまでもメモリの値を返す方法であり、「?文字目」というアクセスに適用することは許さない、という美学があるのではないかと思いました
    • 文字列リテラルに対してはコンパイル時にスライスのチェックを行うとあるが、手持ちの環境ではあえなくpanickしました
fn main() {
    let str1 = String::from("hello");
    let str2 = "はろー";
    println!("{}", &str1[0..1]);
    println!("{}", &str2[0..1]); // これでpanik。ドキュメントではbuildエラーになるっぽいが……
}
  • ハッシュマップ(他の言語だと連想配列とか呼ばれるもの)の値の部分にはなんでも入るが、文字列のような所有権の伴うものは、所有権の移動が発生します
    • insertを使うことでキーと値を追加も更新もできる

エラー処理

  • panic! (マクロらしい)に到達した時点でプログラムを強制終了させられます
    • panic! はスタックトレースなどの片付け処理を含みます
    • profileを設定(詳細はドキュメント参照)することで片付け処理を省き、プログラムを小さくできます
  • Resut enum (Result型)の処理
    • マッチガード: match式におけるパターンマッチを調整できる文法(大雑把に言えば)
    • unwrap(): いちいちmatch式を書くのは冗長……というときに使えます。ResultでOkにマッチしたら値を、Errにマッチしたらpanicさせられるメソッド
    • expect(): unwrapと同等だが、panicメッセージを指定できます
    • ?: 自分で宣言した関数内での関数呼び出しにおいて、Errにマッチしたら即座に関数にErrを返させて終了できます
  • panicするべきかしないべきか
    • いろいろ書いてありますが、ここでは特にまとめません。この問題は言語に関わりなく現場の文化や製品の性質でも変わるだろうので、ここで要約した内容が正しいと断言はできないでしょう。
    • エラーチェックのテクニックについても論じられています。独自の型をつくり、その型に入れる際にチェックするという、いくつかの言語では使える技法をRustもカバーしてます。

ジェネリック型、トレイト

  • ジェネリクス
    • 「具体型や他のプロパティの抽象的な代役。」
      「具体的な型の代わりに何かジェネリックな型の引数を取ることができます。」
    • 関数に対しても、構造体や列挙子の定義に対しても、使える
      • ジェネリックな構造体に対するメソッド(impl)は、特定の型が割り当てられたときのみに限定することもできる
    • ジェネリックな定義を使用してもパフォーマンスに変化はない
      • build時に「単相化」される。つまり、バイナリでは似たような関数を何度も書いただけの状態になる。
  • とはいえすべての型に対してジェネリックになることはできない。対象とする型をある程度は特定する必要がある。そのためにトレイトを使用する。
    • たぶん正確には上記のようなことは「トレイト境界」であり、トレイト自体は「他の型と共有できる機能」みたいな感じ
      • 「共通の振る舞いを抽象的に定義できます」
      • 「他の言語でよくインターフェイスと呼ばれる機能に類似しています」らしい
    • 実装の文法は、impl for みたいな感じ
    • トレイト境界はwhereとか、山カッコ<>とか

ライフタイム

  • 「その参照が有効になるスコープのこと」
  • 暗黙的に推論される
  • ライフタイムは主にタングリング参照を避けるためにある
  • ライフタイム注釈
    • アポストロフィ'ではじまる
    • ジェネリックをTの一文字で書くようにライフタイムはaの一文字で書くのが普通
    • staticという予約されたライフタイムがあり、これはプログラムの全期間で生きることを宣言できる
  • ジェネリックな使い方により、例えば引数と戻り値のライフタイムを同じにできる
    • (そうしない場合、引数のライフタイムは関数終了時に終わる)
    • 関数の複数の引数に対して同じライフタイムを割り当てた場合、短い方に合わさせられる

恐れるな! 並行性

  • 原題は"Fearless Concurrency"でFearlessというのはニュアンス的に「恐れを知らない」という感じっぽいですが、現代日本人的には「こわくない並行性」ぐらいがちょうどいい気がしますね。
  • thread::spawnでスレッドを作れる9
    • メインスレッドがいなくなった時点でspawnされたスレッドは消される
    • joinで待つことができる
    • そういう仕様のわりに、メインスレッドの変数をサブスレッド(spawnしたスレッド)のクロージャに参照させると「クロージャの方が長生きするかもしれないのでー」 と怒られます
      • なので(?) move を使います。moveするとmove元は参照することもできなくなります。
  • 「メッセージ送信並行性を達成するためにRustに存在する一つの主な道具は、チャンネル」
    • 送信(TX)か受信(RX)のどちらかがdropするとチャンネルは閉じられる
    • インスタンスをsendするとsend先に所有権がmoveされる
    • 送信をcloneすることで1つの受信に対して複数の送信を作成・実行することができる
  • メッセージではなくメモリを共有するならmutexを使う
    • スコープなどでmutexを使う場合はmutexを解放しなくても勝手に開放します。ただしスレッドがpanickとかした場合はその限りではないです。
    • 複数のスレッドで資源を共有(もしくは奪い合う)場合、Rustではmutexだけではそれを実現できないと考えている模様
      • mutexが管理する資源には複数の所有者がいる、と考えます
      • 複数の所有者を並列して管理する場合はアトミックなスマートポインタで参照をカウントするようにします
        • すなわち、基本のスマートポインタRc<T>はスレッドセーフではない、ということを宣言しているようです
        • mutexはスレッドセーフ

  1. 筆記者はここではじめてパターンマッチという概念を知りました。 

  2. 「c 条件文」とググると、基本的には「条件式」という言葉が出てきます。少なくともC言語では「if文」であり「条件式」なのかもしれません。 

  3. 構造式?とかで丸括弧は使うからなんでしょうか? 

  4. for文という書き方はここでは出てきません。ただ、if式のようにforループの値を受け取ろうとしても何も取れないので、forループは文なのかもしれません。 

  5. というのがDrop()トレイトとかCopy()トレイトとかで実現されるようですが、詳細は省きます。GCとは言わないそうです。 

  6. ダングリングと言うそうです。 

  7. automatic referencing and dereferencing。ただしRustは参照外しをいつでも自動で行う能力がある、というわけではなく、これ以外の場合では自力で参照外しすることも多いです 

  8. って一言書いてももちろんわけわからんわけで、オリジナルを読むしかありません 

  9. どうでもいい話ですが、筆記者がはじめてディープに触れたOSがVxWorksでありVxWorksではspawnという表現をよく使っていたので、今になるととても懐かしい気分になります 

1
0
4

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
1
0