Closed15

Rust 勉強メモ その 2

nukopynukopy

下の続き

https://zenn.dev/pyteyon/scraps/4e04a66ce38074

References

https://doc.rust-jp.rs/rust-by-example-ja/index.html

fmt

https://doc.rust-jp.rs/rust-by-example-ja/hello/print.html

std::fmt はいくつものトレイトを持ち、それによってどのようにディスプレイに表示されるかが決まる。 特に大事な形式は以下の2つ。

  • fmt::Debug: は、{:?} というマーカーを使用し、デバッギング目的に使われる。
  • fmt::Display: は {} というマーカーを使用し、より美しく、ユーザフレンドリーに表示する。

カスタム型をテキストとして表示する場合は、さらに手順が必要。fmt::Display トレイトを実装すると、自動的に ToString トレイトが実装される。これにより String 型への型変換ができるようになる。

nukopynukopy

The Book: chap 7. 肥大化していくプロジェクトをパッケージ、クレート、モジュールを利用して管理する

https://doc.rust-jp.rs/book-ja/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html

Rustには、どの詳細を公開するか、どの詳細を非公開にするか、どの名前がプログラムのそれぞれのスコープにあるか、といったコードのまとまりを保つためのたくさんの機能があります。 これらの機能は、まとめて「モジュールシステム」と呼ばれることがあり、以下のようなものが含まれます。

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

この章では、これらの機能をすべて学び、これらがどう相互作用するかについて議論し、これらをどう使ってスコープを制御するのかについて説明します。 この章を読み終わる頃には、モジュールシステムをしっかりと理解し、熟練者のごとくスコープを扱うことができるようになっているでしょう!

nukopynukopy

Derive マクロ

よく使われる derive 属性の種類

  • Copy:所有権の移動をせずに、複製を作成するマーカートレイト
  • Clone:オブジェクトの複製(ディープコピー)を作成できる
  • Debug{:?} で出力できる
  • PartialEqEq==!= が使える。Eq はマーカートレイト。
  • PartialOrdOrd<><=>= が使える。Ord は順序付けができる。

下記より

https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/497a21#📌-derive属性

使用例:serde

https://serde.rs/derive.html

nukopynukopy

トレイトオブジェクトの実験

use serde::{Deserialize, Serialize};

trait Geometry<T>: std::fmt::Debug {
    fn area(&self) -> T;

    // name のデフォルト実装
    fn name(&self) -> &str {
        "Geometry"
    }
}

/*
trait Geometry<T>: std::fmt::Debug {
    fn area(&self) -> T;

    // name のデフォルト実装
    fn name(&self) -> &str {
        "Geometry"
    }
}
*/

// Rectangle
#[derive(Serialize, Deserialize, Debug)]
struct Rectangle {
    x: f64,
    y: f64,
}

impl Rectangle {
    fn new(x: f64, y: f64) -> Self {
        Self { x, y }
    }
}

impl Geometry<f64> for Rectangle {
    fn area(&self) -> f64 {
        self.x * self.y
    }

    fn name(&self) -> &str {
        "Point"
    }
}

// Triangle
#[derive(Serialize, Deserialize, Debug)]
struct Triangle {
    height: f32,
    base: f32,
}

impl Triangle {
    fn new(height: f32, base: f32) -> Self {
        Self { height, base }
    }
}

impl Geometry<f32> for Triangle {
    fn area(&self) -> f32 {
        self.height * self.base * 0.5
    }

    fn name(&self) -> &str {
        "Triangle"
    }
}

pub fn main() {
    println!("\n===== trait_practice::main() =====");

    // Rectangle
    let r = Rectangle::new(3.0, 4.0);
    println!("r.area(): {:.2}", r.area());
    println!("r.name(): {:?}", r.name());
    println!("r: {:?}", r);

    let serialized = serde_json::to_string(&r).unwrap();
    println!("serialized = {}", serialized);
    let deserialized: Rectangle = serde_json::from_str(&serialized).unwrap();
    println!("deserialized = {:?}", deserialized);

    // Triangle
    let t = Triangle::new(3.0, 4.0);
    println!("t.area(): {:.2}", t.area());
    println!("t.name(): {:?}", t.name());
    println!("t: {:?}", t);

    let serialized = serde_json::to_string(&t).unwrap();
    println!("serialized = {}", serialized);
    let deserialized: Triangle = serde_json::from_str(&serialized).unwrap();
    println!("deserialized = {:?}", deserialized);

    // trait object
    let r: Box<dyn Geometry<f64>> = Box::new(Rectangle::new(3.0, 4.0));
    let t: Box<dyn Geometry<f32>> = Box::new(Triangle::new(3.0, 4.0));
    println!("r: {:?}", r);
    println!("t: {:?}", t);
}

出力

===== trait_practice::main() =====
r.area(): 12.00
r.name(): "Point"
r: Rectangle { x: 3.0, y: 4.0 }
serialized = {"x":3.0,"y":4.0}
deserialized = Rectangle { x: 3.0, y: 4.0 }
t.area(): 6.00
t.name(): "Triangle"
t: Triangle { height: 3.0, base: 4.0 }
serialized = {"height":3.0,"base":4.0}
deserialized = Triangle { height: 3.0, base: 4.0 }
r: Rectangle { x: 3.0, y: 4.0 }
t: Triangle { height: 3.0, base: 4.0 }
nukopynukopy

上記の実装では、下記のパターン 1 だとコンパイルが通り、パターン 2 だとコンパイルが通らない。

パターン 1:Geometry に対して明示的に Debug トレイトを要求する(Rectangle、Triangle では derive 属性で Debug を実装)

trait Geometry<T>: std::fmt::Debug {
    fn area(&self) -> T;

    // name のデフォルト実装
    fn name(&self) -> &str {
        "Geometry"
    }
}

パターン 2:Geometry に対して Debug トレイトを要求しない(Rectangle、Triangle では derive 属性で Debug を実装)

trait Geometry<T> {
    fn area(&self) -> T;

    // name のデフォルト実装
    fn name(&self) -> &str {
        "Geometry"
    }
}

コンパイルエラー

➜ cargo run
   Compiling rust-playground v0.1.0 (/Users/nukopy/Projects/Rust/rust-playground)
error[E0277]: `dyn Geometry<f64>` doesn't implement `Debug`
  --> src/trait_practice.rs:97:25
   |
97 |     println!("r: {:?}", r);
   |                         ^ `dyn Geometry<f64>` cannot be formatted using `{:?}` because it doesn't implement `Debug`
   |
   = help: the trait `Debug` is not implemented for `dyn Geometry<f64>`
   = help: the following other types implement trait `Debug`:
             (dyn Any + 'static)
             (dyn Any + Send + 'static)
             (dyn Any + Send + Sync + 'static)
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0277]: `dyn Geometry<f32>` doesn't implement `Debug`
  --> src/trait_practice.rs:98:25
   |
98 |     println!("t: {:?}", t);
   |                         ^ `dyn Geometry<f32>` cannot be formatted using `{:?}` because it doesn't implement `Debug`
   |
   = help: the trait `Debug` is not implemented for `dyn Geometry<f32>`
   = help: the following other types implement trait `Debug`:
             (dyn Any + 'static)
             (dyn Any + Send + 'static)
             (dyn Any + Send + Sync + 'static)
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.
error: could not compile `rust-playground` due to 2 previous errors
nukopynukopy

トレイトを実装している引数を受け取る方法

fn draw<T>(geometry: &impl Geometry<T>) {
    println!("draw: {:?}", geometry);
}

fn draw1<T: Geometry<f64>>(geometry: &T) {
    println!("draw1: {:?}", geometry);
}

fn draw2<T>(geometry: &T)
where
    T: Geometry<f64>,
{
    println!("draw2: {:?}", geometry);
}
nukopynukopy

RAII: Resource Acquisition Is Initialization

https://zenn.dev/mebiusbox/books/22d4c1ed9b0003/viewer/063df5#📌-raiiとは

RAII とは?

リソースの確保をオブジェクトの初期化時に行い,リソースの開放をオブジェクトの破棄と同時に行う手法

Rust の参照は RAII と考えられる。参照は仮の所有権を保持し、スコープから外れると仮の所有権を破棄する。

  • 参照の機能
    • 参照先のオブジェクトを操作できる(read / write)
    • 参照先のオブジェクトの仮の所有権を破棄する

Rust における参照の実現:参照先のオブジェクトを操作するためのトレイト DerefDerefMut

参照先のオブジェクトを操作する、つまり参照外しを実現するためのトレイトに DerefDerefMut がある。

  • Deref トレイト
    • 不変参照で参照外しを実現しているトレイト。不変参照に対して Deref トレイトを実装することで不変参照の RAII を実現している。
  • DerefMut
    • 可変参照で参照外しを実現しているトレイト。可変参照に対して DerefMut トレイトを実装することで可変参照の RAII を実現している。

Rust における参照の実現:参照先のオブジェクトの仮の所有権を破棄するためのトレイト Drop

参照先のオブジェクトの仮の所有権を破棄するためのトレイトに Drop トレイトがある。Drop トレイトを実装した型のオブジェクトは、自身が破棄されるとき(スコープを抜けるときなど)に drop メソッドが呼ばれる。このメソッドでリソースの開放処理を行う。drop メソッドは可変参照を引数に取る。

  • Drop トレイト
impl Drop for Resource {
    fn drop(&mut self) {
        ...
    }
}

drop メソッドは基本オブジェクトが破棄される時に自動で呼ばれるが、明示的に呼びたい場合は std::mem::drop 関数を呼んで強制的にオブジェクトを破棄できる(非推奨)。

ChatGPT に聞いてみた:Rust でオブジェクトが破棄されるときに自動で drop を呼ぶ仕組みはどうやって実現しているのですか?

Rustのコンパイラは、特にDropトレイトを持つオブジェクトについて、オブジェクトがスコープから外れるときにdropメソッドを自動的に呼び出すようにコードを生成します。これは、コンパイラがオブジェクトの生存期間を追跡し、その生存期間が終了するとき(スコープを外れるとき)に自動的にデストラクタ(Dropトレイトのdropメソッド)を呼び出す仕組みです。

Rustはスタックベースの言語で、ローカル変数はスコープに入るときにスタックにプッシュされ、スコープを出るときにスタックからポップされます。このとき、Dropトレイトを持つ型の値はそのdropメソッドが呼び出され、必要なクリーンアップ操作が行われます。この動作は、コンパイラが生成したコードによって自動的に行われます。

なお、所有権の移動が行われるとき、Dropトレイトの対象となるのは新しい所有者となる変数で、元の変数はドロップされません。これも所有権システムとライフタイム(生存期間)の仕組みによって保証されています。所有権が移動するとき、コンパイラは新しい所有者のスコープが終了するときにdropメソッドを呼び出すようにコードを生成します。

以上のように、Rustの所有権システム、ライフタイムの仕組み、そしてDropトレイトは、メモリ安全性とリソースのクリーンアップを自動的に行う仕組みを提供しています。これらの仕組みはコンパイラが自動的にコードを生成することによって実現されています。

なるほど、所有権が移動すると移動元は drop メソッド呼ばれなくなるんだ

nukopynukopy

Rust の参照 - 不変参照、可変参照

参照は基本 move(ある変数から別の変数へ所有権が移動すること)は起きない。参照外しによって move させることはできる。

nukopynukopy

不変参照 immutable reference

  • 不変参照の特徴
    • immutable
    • readonly
    • move は起きない
  • 不変参照の使い所
    • 例えば、ある関数がデータを読み取るだけでそのデータを変更しないとき、不変参照を使うのが良い。ただし、値を使用するには参照外しを行う必要がある。
fn main() {
    let x = 10;
    let y = &x;

    println!("x: {}", x);
    print_val(y);
}

fn print_val(x: &i32) {
    println!("print_val: {}", x);
}

暗黙に不変参照を指して参照と呼ばれることもあるが、ここでは可変参照と対比してわかりやすいように不変参照(immutable reference)と呼んだ。

nukopynukopy

可変参照 mutable reference

  • 可変参照の特徴
    • mutable
    • read / write が可能
    • move は起きない
  • 可変参照の使い所
    • 例えば、ある関数が渡された引数(変数)の値を書き換えたいとき、可変参照を使うのが良い。ただし、値を書き換えるには参照外しを行う必要がある。

可変参照を理解する上で重要なのが Rust の所有権、借用に関する以下のルール:

このルールの解釈において、以下を理解しておくことが大事。

  • 同じスコープ内であっても、1 つの可変参照が終了した後(生存期間が終了した後)には、新たな可変参照または不変参照を作ることができる
  • また、生存期間が重ならなければ、同じスコープ内で可変参照と不変参照を交互に作成することも可能

このルールは、同時アクセスによるデータ競合を防ぐためのもので、Rustのメモリ安全性を保証するための重要な特性である。

可変参照周りでよく起きるコンパイルエラー

fn test_mutable_reference() {
    let mut x = 10;
    let y = &mut x;

    println!("x: {}", x);
}

コンパイルエラー

error[E0502]: cannot borrow `x` as immutable because it is also borrowed as mutable
  --> src/raii.rs:26:23
   |
24 |     let y = &mut x;
   |             ------ mutable borrow occurs here
25 |
26 |     println!("x: {}", x);
   |                       ^ immutable borrow occurs here
27 |     print_val_mut(y);
   |                   - mutable borrow later used here
   |
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

上記のコードでは、y という可変参照がすでに x を借用している。そして、その後で、println! マクロの中で再度 x を不変参照として借用しようとしている。これは Rust の参照のルールに違反するため、コンパイルエラーが発生する。Rust では不変参照と可変参照が同時に存在してはいけない(なぜ?)

問題を解決するためには、y のスコープ(生存期間)が終わったあとに println! を実行するか、y を使用する前に println! を実行すればよい。つまり、不変参照と可変参照が同時に存在しないようにする必要がある。

  • y のスコープが終わったあとに println! を実行する
fn test_mutable_reference() {
    let mut x = 10;
    {
        let y = &mut x;
        // y の生存期間はこのスコープ内に限られます。
    }
    println!("x: {}", x);  // y のスコープ(生存期間)が終わった後なのでエラーは発生しない
}
  • y を使用する前に println! を実行する
fn test_mutable_reference() {
    let mut x = 10;
    println!("x: {}", x);  // y が借用する前なのでエラーは発生しない
    let y = &mut x;
}
nukopynukopy

参照外し

Rust では、参照や可変参照を通して値を操作するために、* 演算子を使って参照外しを行う。これにより、参照が指し示す実際の値にアクセスできる。参照外しは主に可変参照を通してオブジェクトの値を変更するときに使用される。

参照外しを行ったときの所有権の挙動は?

参照外しを行ったとき、所有権が移動するわけではない。参照外しは、参照やポインタが指す値にアクセスするための操作であり、所有権には影響しない。

  • 参照外しの例
let x = 5;
let y = &x;
println!("{}", *y); // ここで参照外しを行なっているが、所有権は移動しません
println!("{}", x); // 所有権は移動しないためここで x を使うことができる

参照外しの結果を使用した、少しややこしい所有権の移動の例を以下で説明する。

参照外しを行った結果を別の変数にバインド(束縛)し、その型が Copy トレイトを実装していない場合、所有権はその新しい変数に移動する。このような場合、参照外し自体が所有権を移動させるわけではなく、代入操作が所有権の移動を引き起こす

以下の例では、Copy トレイトを実装していない構造体 Foo を定義し、変数 x にそのオブジェクトを代入している。そして、変数 x の不変参照 y に対して参照外しを行い、参照外しの結果を変数 z に代入している。ここでは、変数 z に変数 x の所有権が移動している。これは let z = *ylet z = x と同等の挙動であるためであり、*y は所有権へ影響を与えているわけではない。代入操作が所有権の移動を引き起こしている

  • 参照外しの結果を使って所有権の移動を起こす例
#[derive(Debug)]
struct Foo(i32);

let x = Foo(5);
let y = &x;
let z = *y; // この場合、所有権が z に移動します。
// ここで x を使おうとするとコンパイルエラーになります

不変参照、可変参照における参照外し


fn test_deref() {
    let mut x: i32 = 10;
    println!("x: {}", x); // 不変参照

    let y: &mut i32 = &mut x; // 可変参照

    deref_of_immutable_ref(y);
    println!("y: {}", y);
    deref_of_mutable_ref(y);
    println!("y: {}", y);
}

fn deref_of_immutable_ref(x: &i32) {
    println!("x: {}", *x); // 参照外し
}

fn deref_of_mutable_ref(x: &mut i32) {
    *x += 1; // 参照外し
}
nukopynukopy

Zenn の「Rust 入門」読み終わった!長かった。理解できてないとこブログかなんかにまとめる。

目次

  • Chapter 01 はじめに
  • Chapter 02 基本
  • Chapter 03 コピートレイト
  • Chapter 04 データ型
  • Chapter 05 関数
  • Chapter 06 制御構文
  • Chapter 07 構造体
  • Chapter 08 列挙型
  • Chapter 09 ジェネリクス
  • Chapter 10 Option型
  • Chapter 11 パターンマッチング
  • Chapter 12 エラー処理
  • Chapter 13 トレイト
  • Chapter 14 RAII
  • Chapter 15 内部可変性
  • Chapter 16 クロージャ
  • Chapter 17 ライフタイム
  • Chapter 18 並列処理
  • Chapter 19 所有権のまとめ
  • Chapter 20 マクロ
  • Chapter 21 イテレータ
  • Chapter 22 コレクション
  • Chapter 23 Cargo
  • Chapter 24 モジュール
  • Chapter 25 ユニットテスト
  • Chapter 26 Tips
  • Chapter 27 さいごに
  • Chapter 28 書籍「動かして学ぶ!Rust入門」

所感

一通り読み終わった段階での所感:

  • Rust の概念にインデックスを張れた(調べたいときにどういう用語で調べれば良いか大方分かった)
  • ライフタイムをゴリゴリ使ったコードはまだ読めないけど、初見で「何これ??」ってなった部分の大半は読めるようになった(Rust でよく使われる書き方。エラーハンドリング、トレイト、クロージャなど。)

まだ理解できていない概念:

  • 内部可変性(Rc、RefCell)
  • 並列処理、並列処理における所有権の挙動(サンプルコードは理解できたが実践的なコードはまだ少ししか読めてない、実装できていないので本質は掴めてない)
  • 非同期処理(これは tokio のチュートリアルやる)
  • ライフタイム

所有権の移動が発生するのはコンパイラに怒られながら見えるようになってきた。振り返ってみるとコピートレイトを初めの方に持ってくる構成はなるほどなと思った。別の言語だと変数を別の変数に代入するとき「コピー or 参照が作成される」ってのが起きるだけで元の変数が使えなくなるってことは基本ない。Rust だとその段階でコンパイラに怒られる。早めにその感覚の説明はしてくれたのが分かりやすかった。

Rc ~ 内部可変性 (RefCell) にかけては初見きつかった…特に内部可変性の章は後半流し読みした。じっくりサンプルコード動かしながらもうちょい粘る。

ライフタイムも並列処理も実践的な例見ないとまだまだ。

このスクラップは2023/06/20にクローズされました