dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

Tour of Rustをやった

もう一度Rustを勉強したくなったが The Rust Programming Language をやる気力はなかったので、Tour of Rust をやってみた。

tourofrust.com

大部分が日本語に翻訳されている。ありがたい。

ざっとやってみた感想としては、

  • 基本的な文法、概念は一通りカバーできていて良い
  • 文字列だけでまるまる1章使ってるのは良い(第6章)
  • 所有権、借用、ライフタイムについては難しさに対して説明はさらっとしててこれだけでの完全理解は無理だった
  • 第8章 スマートポインタは何もわからない

Tour of Rust をやってもまだ理解が不十分なところとしては

  • 所有権と借用
    • 構造体にメソッド生やすとき、どういうときに参照を受け取るような設計にして、どういうときに所有権を移動させるべきなのか。参照で済む場合は極力参照?
  • ライフタイム
  • 動的ディスパッチ

以下、各章の簡単なメモ。

第1章 - 基礎

  • 基本的な型
    • 配列型 - コンパイル時に長さが決まる同じ要素のコレクション
    • スライス型 - 実行時に長さが決まる同じ要素のコレクション
    • str(string slice) - 実行時に長さが決まるテキスト

第2章 - 基本制御フロー

  • for文は “イテレータとして評価される式を反復処理します”
  • .. 演算子は、開始番号から終了番号の手前までの数値を生成するイテレータを作成します”
  • match
    • matched_num @ 10..=100 で変数に束縛できるの知らなかった
  • loop から値を返す
  • ブロック式から値を返す
      let v = if x < 42 { -1 } else { 1 };
    

第 3 章 - 基本的なデータ構造体

  • メモリ:Rustでは次の3種類のメモリ空間を持っている
    • データメモリ:固定長もしくはスタティックなデータ
    • スタックメモリ:関数内で宣言された変数
    • ヒープメモリ:プログラムの実行時に作られるデータ
      • このメモリにあるデータは追加、移動、削除、サイズの調節などの操作が許されている
      • データをヒープメモリに入れることをアロケーション(allocation)、ヒープメモリから削除することをディアロケーション(deallocation)という
  • 列挙型にはデータを持たせることもできる

第 4 章 - ジェネリック型

  • ジェネリック
  • Option
  • Result
  • ? 演算子:Result 型が Ok なら値を取り出し、 Err 型ならその場で return
  • unwrap やっつけな Option / Result 処理
  • ベクタ型: Vec<T>
    • iter() を使うとベクタからイテレータを生成できる。for ループに使える
      let string_vec = vec![String::from("Hello"), String::from("World")];
    
      for word in string_vec.iter() {
          println!("{}", word);
      }
    

第 5 章 - データの所有権と借用

  • 所有権

    型のインスタンスを作成して変数に束縛するとメモリリソースが作成され、そのすべてのライフタイムに渡って Rust コンパイラが検証します。 束縛された変数はリソースの所有者と呼ばれます。

  • スコープベースのリソース管理
    • スコープが終わるとリソースのデストラクトと解放を行う

    C++ では Resource Acquisition Is Initialization (RAII)「リソース取得は初期化である」とも呼ばれています。****

  • 参照による所有権の可変な借用

      struct Foo {
          x: i32,
      }
    
      fn do_something(f: Foo) {
          println!("{}", f.x);
          // f はここでドロップ
      }
    
      fn main() {
          let mut foo = Foo { x: 42 };
          let f = &mut foo;
    
          // 失敗: do_something(foo) はここでエラー
          // foo は可変に借用されており移動できないため
    
          // 失敗: foo.x = 13; はここでエラー
          // foo は可変に借用されている間は変更できないため
    
          f.x = 13;
          // f はここから先では使用されないため、ここでドロップ
    
          println!("{}", foo.x);
    
          // 可変な借用はドロップされているため変更可能
          foo.x = 7;
    
          // foo の所有権を関数に移動
          do_something(foo);
      }
    
    • 💬 f は暗黙的にこれ以上使われてる箇所がないと判断されて途中でドロップするんだな
  • 借用したデータの受け渡し

    • Rust では、可変な参照が 1 つだけか、不変な参照が複数かのどちらかが許可されます。両方を同時には使用できません
    • 参照は所有者よりも長く存在してはなりません。
      • 前者はデータ競合を防ぐ
      • 後者は存在しないデータへの参照(ダングリングポインタ)を防ぐ
  • 明示的なライフタイム

    関数は、どの引数と戻り値とがライフタイムを共有しているかを、識別のための指定子で明示的に指定できます。

      // 引数 foo と戻り値はライフタイムを共有
      fn do_something<'a>(foo: &'a Foo) -> &'a i32 {
          return &foo.x;
      }
    
  • 複数のライフタイム

  • スタティックライフタイム
    • 'static という特別なライフタイム指定子
    • 文字列リテラル'static

第6章 - テキスト

  • 文字列リテラル&'static str

    str は常に有効なutf-8であるバイト列を指していることを意味しています。

  • utf-8 とは
  • 文字列スライス

    文字列スライスは、常に有効な utf-8 でなければならないメモリ内のバイト列への参照です。

    • 常に有効な utf-8 でなければならないとは? → 絵文字の途中とかでスライスするとパニックする
  • Chars

    • utf-8バイトのシーケンスを char 型のベクトルとして取得する
      let chars = "hi 🦀".chars().collect::<Vec<char>>();
    
  • String

    • メモリがヒープ上にあるので拡張や修正が可能
  • 関数パラメータとしてのテキスト

    文字列リテラルや文字列は、一般的に文字列スライスとして関数に渡されます。 これによって、実際には所有権を渡す必要がないほとんど場合において柔軟性が増します。****

  • 文字列の構築
    • concatjoin
  • 文字列のフォーマット
    • format!マクロ
  • 文字列変換
    • 多くの型は to_string でStringに変換可能
    • 文字列や文字列リテラルからparse で型付きの値に変換できる。失敗する可能性があるので Result を返す

第7章 - オブジェクト指向プログラミング

  • オブジェクト指向プログラミングとは?
  • Rustはオブジェクト指向プログラミング言語ではない
    • データと動作の継承機能を意図的に持っていない
  • 選択的な公開による抽象化
    • pub キーワードで構造体のフィールドやメソッドをモジュール外に公開
    • フィールドとメソッドは、デフォルトではそれらが属しているモジュールにのみアクセス可能
  • トレイトを用いたポリモーフィズム
    • trait はinterfaceみたいなもの
  • トレイトに実装されたメソッド
    • トレイトは実装されたメソッドを持つこともできる
  • 動的ディスパッチと静的ディスパッチ
    • 静的ディスパッチ - インスタンスの型がわかっている場合、どの関数を呼び出せばよいかを直接知ることができる
    • 動的ディスパッチ - インスタンスタイプが不明な場合、正しい関数を呼び出す方法を見つけなければならない
    • 要は、関数やメソッドの引数に構造体ではなくトレイト(&dyn MyTrait)を受け取るように書かれているもの
      • fn dynamic_make_noise(noise_maker: &dyn NoiseMaker) {
  • トレイトオブジェクト
    • &dyn MyTrait型のパラメータに渡すとき、トレイトオブジェクトと呼ばれるものを渡す
    • 🤔

    インスタンスの正しいメソッドを間接的に呼び出すことを可能にするのがトレイトオブジェクトです。 トレイトオブジェクトは、インスタンスのポインタと、インスタンスのメソッドへの関数ポインタのリストを保持する構造体です。

  • サイズの合わないデータの処理
    • 🤔

    トレイトは元の構造体を難読化するため、元のサイズも難読化されます。

    • 構造体に格納されるサイズの合わない値は、2つの方法で処理される
  • ジェネリック関数
  • ジェネリクス関数の省略記法
    • where のかわりに、引数に impl Trait と書くこともできる
    • 「実践Rust入門」だともう1つ、型パラメータに <P: Trait> と書く方法も紹介されていた
  • ボックス(Box)
    • データをスタックからヒープに移動させるためのデータ構造
    • スマートポインタと呼ばれる構造体で、ヒープ上のデータへのポインタを保持する
    • たとえば Vec<dyn NoiseMaker>>dyn NoiseMaker のサイズがコンパイル時に決まらないためエラーになる。Boxで囲むことでコンパイルエラーを解決できる
  • ジェネリクス構造体

[未翻訳] Chapter 8 - Smart Pointers

  • References Revisited
    • 参照 (reference) とは、単にメモリ上のバイトの開始位置を示す数字でしかない
    • 何が参照とただの数字を異なるものにしているかというと、Rustは参照のライフタイムが参照先(所有者)より長く存在していないことを検証するという点
  • Raw Pointers
    • 参照は生ポインタ型(raw pointer)というよりプリミティブな型にコンバートできる
    • 数値とよく似て、多少の制約はあるもののコピーやムーブができる
    • Rustは生ポインタが指し示す先のメモリ位置についてなんの保証もしない
    • 2つの生ポインタがある
      • *const T 不変
      • *mut T可変
  • Dereferencing 参照外し
  • The * Operator
  • The . Operator
    • 勝手に dereference するので * つけなくていい
  • Smart Pointers
  • Smart Unsafe Code
    • 数値を生ポインタとみなして参照外しをする場合、Rustはそのメモリ位置が妥当であることを何も保証できないので unsafe ブロックで囲む必要がある
  • Familiar Friends
    • Vec<T>String もスマートポインタ
    • サンプルコードの alloc とか Layout は理解するの諦めた
  • Heap Allocated Memory
    • Box 型
    • データはヒープに置かれ、そのポインタをスタックに保存する。が、あたかもスタックにデータがあるかのように値に直接アクセスできる
  • Failable Main Revisited
    • fn main() -> Result<(), Box<dyn std::error:Error>> とするとmainですべてのErrorを捕捉できる****
    • Errorはトレイトなのでdynにする必要あり
  • Referencing Counting
  • Sharing Access
  • Sharing Across Threads
    • Mutex
    • マルチスレッドで使う?

[未翻訳] Chapter 9 - Project Organization and Structure

モジュールの基本的な考え方は昔まとめた:

Rustのモジュールシステムの基本 - dackdive's blog

  • Modules
    • すべてのRustプログラムあるいはライブラリはクレート
    • すべてのクレートはモジュールの階層構造でできている
    • すべてのクレートはルートとなるモジュールを持つ
    • モジュールは、グローバル変数や関数、構造体、トレイトや他のモジュールを保持できる
  • Writing a Program
    • プログラムは main.rs というルートモジュールを持つ
  • Writing a Library
    • ライブラリは lib.rs というルートモジュールを持つ
  • Referencing Other Modules and Crates
    • モジュール内の各要素を利用する方法。フルパス指定か、use キーワード
  • Referencing Multiple Items
    • use std::f64::consts::{PI,TAU}
  • Creating Modules
    • foo というモジュールを定義する方法は2つ
      1. foo.rs
      2. foo/mod.rs
  • Module Hierarchy
  • Inline Module
  • Internal Module Referencing
    • use で使える特別なキーワード:crate , super, self
  • Exporting
    • pub で他モジュールからも使えるよう公開できる
  • Structure Visibility
    • 構造体のフィールドも個別に pub つける必要がある
  • Prelude
    • 何も use してないのに VecBox が使えるのはなぜか?
    • std::prelude::* というモジュールは自動的に読み込まれているから
  • Your Own Prelude
    • 独自のライブラリを作ったときも、そのライブラリを使用するための最も一般的なデータ構造を prelude として公開すると良い