Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

復習 #4

Open
iterable-company opened this issue Sep 3, 2023 · 8 comments
Open

復習 #4

iterable-company opened this issue Sep 3, 2023 · 8 comments

Comments

@iterable-company
Copy link
Owner

iterable-company commented Sep 3, 2023

Rust 勉強会の開催に合わせて復習していく
https://iot-security-center.connpass.com/event/294154/

@iterable-company
Copy link
Owner Author

chap4 所有権

所有権のチェックはコンパイル時に行われ、実行時には実行速度に影響しない。

スタックとヒープ

スタック上のデータは全て「既知」の「固定サイズ」でなければならない。
 => コンパイル時にサイズがわからないもの、サイズが可変のものはヒープに置かれる

ヒープにメモリを確保するとき、OSがヒープ上に十分な大きさの空き領域を見つけて、使用中にして、ポインタを返す。
ポインタ自体は既知の固定サイズのため、スタックに積むことができるが、実データが必要な時はポインタを追いかける必要がある。
ヒープへのアクセスはスタックに比べると低速。
連続領域のアクセスの方が高速。

所有権規則

  • Rustの各値は、所有者と呼ばれる変数と対応している
  • いかなる時も所有者は1つである
  • 所有者がスコープを外れたら、値は破棄される

String型

文字列リテラルはプログラムにハードコードされる。
 => 不変である
 
サイズがわからない場合の文字列(例えばユーザー入力を受け付ける場合など)はString型を使用する。
String型はヒープに確保される。

メモリと確保

文字列リテラルはコンパイル時にサイズが判明しているため、ハードコードされる。
 => 高速で効率的

String型の場合

  • メモリは実行時にOSに要求される
  • String型を使用し終わったら、OSにメモリを返還する方法が必要

Rustは変数がスコープを抜ける時に、 drop を呼ぶ。

変数とデータの相互作用法: ムーブ

String型の構成要素

  • ポインタ
  • 長さ
  • 許容量

長さは文字列のバイト数、許容量はOSから受け取ったメモリ領域のバイト数

let s1 = String::from("hoge");
let s2 = s1;

Copyトレイトはコピーにオーバーヘッドがかからないプリミティブ型に実装されていることが多い。
ヒープにメモリを確保するString型はコピーしない。コピーに時間がかかるためRustではそのようなことはしない。
 => つまりs1, s2で別々のメモリ領域を持っているわけではない

s1, s2 が同じメモリ領域を指している場合、スコープから抜けてdropが呼び出された時に二重解放のエラーになってしまう。
上記のコードで実際に起こることは、s2 = s1 の時に、s1は有効でなくなったと考え、スコープを抜けた際に何も解放しない。
s1 は s2に代入された時点で有効ではなくなったので、ここ以降のコードで使用しようとするとコンパイルエラーになる。
 => ムーブ

Rust では自動的にデータが deep copy されることはない。

変数とデータの相互作用法: クローン

let s1 = String::from("hoge");
let s2 = s1.clone();

とすると、ヒープのデータが実際にコピーされる。
 => clone()はCopy トレイトを実装していないデータのコピーが走るのでコストが高いと考えることができる

let x1 = 1;
let x2 = x1;

はx2の代入でコピーされる。
スタックに確保される値は既知のサイズでコピーも動作的に軽いから。

型自身やその一部が Drop トレイトを実装している場合、Copy トレイトを実装させてくれない。
Copyトレイトを実装しているものの例

  • 整数型、論理値型、浮動小数点型
  • 文字型 char
  • タプル: ただし、構成要素が全てCopyトレイトを実装していること

所有権と関数

関数に値を渡すことと、値を変数に代入することは似ている。
関数に値を渡すと、ムーブやコピーが発生する。

戻り値とスコープ

値を返すことでも、所有権は移動する。

@iterable-company
Copy link
Owner Author

Chap4 所有権

4.2 参照と借用

let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("{} length = {}", s1, len);

fn calculate_length(s: &String) -> usize {
  s.len()
}

Stringはポインタ、サイズ、キャパシティを持っているため、s1 は左の3つを持っている。
ポインタはヒープに確保された実体のアドレス。
calculate_length 関数が呼ばれたとき、引数 s は s1へのポインタとなっている

参照は所有権を持っていないため、変数がスコープを抜けても drop されない。
関数の引数に参照をとることを「借用」という

可変な参照

特定のスコープで、特定のデータに対しては、1つしか可変な参照を持てない。

競合状態

  • 2つ以上のポインタが同じデータに同時にアクセスする
  • 少なくとも1つのポインタがデータに書き込みを行っている
  • データへのアクセスを同期する機構が使用されていない

スコープが違えばOK

let mut s = String::from("hello");

{
    let r1 = &mut s;

} // r1はここでスコープを抜けるので、問題なく新しい参照を作ることができる

let r2 = &mut s;

不変参照があるスコープでは可変参照を持てない。
 => println! の引数にするときも immutable borrow になる

ダングリングポインタ

他人に渡されてしまった可能性のあるメモリを指すポインタ。
参照がスコープを抜けるまで、データの実体がスコープを抜ける(dropされる)ことのないようにコンパイラが監視する

要点

  • 参照は常に有効でなければならない
  • 任意のタイミングで、1つの可変参照か、複数の不変参照かのどちらかが有効である

@iterable-company
Copy link
Owner Author

chap4.3 スライス型

所有権のない別のデータ型 => スライス
(もう1つは参照)

コレクション全体ではなく、そのうちの一連の要素を参照する。

文字列スライス

let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

スライスデータ構造

  • 開始地点
  • スライスの長さ
fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s); // word will get the value 5
                               // wordの中身は、値5になる

    s.clear(); // this empties the String, making it equal to ""
               // Stringを空にする。つまり、""と等しくする

    // word still has the value 5 here, but there's no more string that
    // we could meaningfully use the value 5 with. word is now totally invalid!
    // wordはまだ値5を保持しているが、もうこの値を正しい意味で使用できる文字列は存在しない。
    // wordは今や完全に無効なのだ!
    println!("{}", s);
    println!("{}", word);
}

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn first_word2(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

first_word2の時は、wordのライフタイムがsのライフタイムと一致しているので、println!("{}", word)がコンパイルエラーになる。
first_wordの時は、コンパイルエラーにならず、表示される。

文字列リテラルはスライス

let s = "Hello, world!";

でsの型は &str
これはスライスで、&str は不変参照である。

&str を関数の引数にすると、&String, &str どちらでも渡すことができる。

@iterable-company
Copy link
Owner Author

iterable-company commented Oct 21, 2023

https://chat.openai.com/c/eaa24dc4-9b8c-49d1-a946-5194a5f45b01

15. スマートポインタ

参照はデータを借用するだけのポインタなのに対して、スマートポインタは指しているデータを所有している。

  • String
  • Vec
    も実はスマートポインタ
    => 所有者を複数にすることできなくないですか???

スマートポインタは Deref と Drop を実装している。

本章で取り扱うスマートポインタ

  • Box: ヒープに値を確保する
  • Rc: 複数の所有権を可能にする
  • RefCell, Ref, RefMut: コンパイル時ではなく、実行時に借用規則を強制する

15.1 Box

スタック: ヒープへのポインタ
ヒープ: 実際の中身の領域を確保

以下のような場面でよく使う

  • コンパイル時にはサイズを知ることができない型があり、正確なサイズを要求する文脈でその型の値を使用する時 
  • 多くのデータがあり、その所有権を移したいが、その際にデータがコピーされないようにしたい時
    => 関数に渡して、その中で別の構造体に詰め込んだりするときにコピーを発生させないようにする
    => クロージャに大きなデータをキャプチャして渡したい時にコピーが発生しないようにする
  • 値を所有する必要があり、特定の型であることではなく、特定のトレイトを実装する型であることのみ気にかけている時
fn main() {
    let b = Box::new(5);
    println!("b = {}", b);
}

実際に確保されるのはヒープ上だが、スタックにあるのと同じように操作できる。
スコープを抜けると、スタック上のポインタとヒープ上の実体の両方が解放される。

Boxにより再帰的な型定義ができるようになる

再起的な型はコンパイル時に大きさがわからない。
Box自体のサイズ(ポインタのサイズは指している型に影響を受けない)はわかるので、Boxを挟むことでコンパイル可能になる。

非再帰的な型のサイズを計算する

#![allow(unused)]
fn main() {
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
  }
}

variantの中で一番大きなサイズを必要とするものをMessage型のサイズとする。
これをConsリストに適応すると、無限に続くConsの部分のサイズを確定できない。

@iterable-company
Copy link
Owner Author

15.2 Deref

Deref トレイトを実装することで、参照外し演算子の * の振る舞いをカスタマイズできる。
参照外し型強制があるおかげ、参照、スマートポインタがうまく使える。

参照外し演算子で値までポインタを追いかける

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

とすると、アサートがとおる

assert_eq!(5, y);

とすると、Integerと&{Integer} のPartialEqは実装されていないため、コンパイルエラーとなる。

Box を参照のように使う

fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

参照外し演算子によってボックスのポインタを辿ることができる。

Deref トレイトを実装して型を参照のように扱う

use std::ops::Deref;

struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
  }

selfを借用し、内部のデータへの参照を返すメソッドを実装する。

*y 
は
*(y.deref())

を内部的に行っている。

参照型外し強制

コンパイラが関数、メソッドの実引数に行うもの。

fn hello(name: &str) {
    println!("Hello, {}!", name);
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}

引数として &m を渡している。
MyBox値への参照。
MyBox に Deref を実装したので、コンパイラは Deref を呼び出すことで &MyBox を &String に変換できる。

m = Stringのポインタへのポインタ(スタック) -> Stringへのポインタ(ヒープ) -> Stringの実体(ヒープ)
&m = Stringのポインタへのポインタ(スタック)の参照
=> Deref することで、 Stringへのポインタ(ヒープ)の参照 に変わる
コンパイラは更にDerefを呼び出し、Stringの実体(ヒープ)の参照 => &str (スライス)に変わる

参照外し型強制がない場合は以下のように書かなければいけない。

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);
}

引数の型に一致するまでDerefするが、Derefされる回数はコンパイル時に決定する。

参照外し型強制が可変性と相互作用する方法

以下の場合に参照外し型強制する

  • T: Deref<Target=U> の時、&T から U
  • T: DerefMut<Target=U>の時、&mut T から &mut U
  • T: Deref<Target=U>の時、&mut T から &U
struct BoxString(String);

impl Deref for BoxString {
    type Target = String;

    fn deref(&self) -> &String {
        &self.0
    }
}

impl DerefMut for BoxString {
    fn deref_mut(&mut self) -> &mut String {
        &mut self.0
    }
}

fn main() {
    let mut my_str = BoxString(String::from("Hello"));

    // Derefを使った参照外し型強制
    // &BoxStringから&Stringへの変換
    println!("Length: {}", my_str.len());

    // DerefMutを使った参照外し型強制
    // &mut BoxStringから&mut Stringへの変換
    my_str.push_str(", World!");
    println!("{}", my_str);
}

fn make_uppercase(s: &mut BoxString) {
    let uppercased = s.to_uppercase();
    s.0 = uppercased;
}

fn main() {
    let mut my_str = BoxString(String::from("Hello"));

    // Convert the BoxString to uppercase
    make_uppercase(&mut my_str);

    // Now, we take a reference to my_str and get its length.
    // Here, &mut BoxString is coerced to &String due to the Deref trait.
    let length: usize = my_str.len();

    println!("Uppercased: {}", my_str);
    println!("Length: {}", length);
}

@iterable-company
Copy link
Owner Author

https://chat.openai.com/c/711e99ac-22ef-4bb6-9747-3fca85e020f5

15.3 Drop

スコープを抜けたときに起きることをカスタマイズできる。
ファイルやネットワーク接続などのリソースの解放。

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        // CustomSmartPointerをデータ`{}`とともにドロップするよ
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

std::mem::drop を呼べば、スコープを抜けてDropが自動で呼ばれる前に手動で呼べる。
Dropトレイトのdropを呼べなくしているのは二重解放を防ぐため。
std::mem::drop を呼べば、Dropトレイトのdropが実装されている場合は再帰的に呼び出される。
これを呼んで以後は、渡した変数は使えなくなる。

@iterable-company
Copy link
Owner Author

15.4 Rc

複数の所有者を持つスマートポインタ。Referencing count の略。

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

fn main() {
    let a = Cons(5,
        Box::new(Cons(10,
            Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}

bを宣言するときにaの所有権が移る。その後、cを宣言するところでも所有権の移転の問題が発生してコンパイルエラーとなる。(value used after move)
参照を持つようにすれば、所有権の問題が解決するが、ライフタイム指定子でリストの要素が、少なくともリスト全体よりも長く生存する必要がある。

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

Rc::cloneのたびに参照カウントが増える。

@iterable-company
Copy link
Owner Author

15.5 RefCell

Rc と異なり、RefCell 型は、保持するデータに対して単独の所有権を表す。
では、Boxとはどう違うのか?

4章で学んだ借用規則

  • いかなる時も1つの可変参照かいくつもの不変参照のどちらかが可能になる
  • 参照は常に有効でなければならない
    => 「使用される時に」をつけた方がよくない??

Boxは、借用規則の普遍条件はコンパイル時に強制される。
RefCell は実行時に強制される。
=> RefCellでこれを破るとpanicになる。 (Boxの場合はコンパイルエラー)

コンパイル時の精査では許容できない特定のメモリ安全な筋書きが許される。
コードが借用規則に沿っていることをプログラマは確証を持っているが、コンパイラがそれを理解し保証できない場合に、RefCell型は有用。

内部可変性: 不変値への可変借用

モックオブジェクト

pub trait Messenger {
    fn send(&self, msg: &str);
}

pub struct LimitTracker<'a, T: 'a + Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}

impl<'a, T> LimitTracker<'a, T>
    where T: Messenger {
    pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }

    pub fn set_value(&mut self, value: usize) {
        self.value = value;

        let percentage_of_max = self.value as f64 / self.max as f64;

        if percentage_of_max >= 0.75 && percentage_of_max < 0.9 {
            // 警告: 割り当ての75%以上を使用してしまいました
            self.messenger.send("Warning: You've used up over 75% of your quota!");
        } else if percentage_of_max >= 0.9 && percentage_of_max < 1.0 {
            // 切迫した警告: 割り当ての90%以上を使用してしまいました
            self.messenger.send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 1.0 {
            // エラー: 割り当てを超えています
            self.messenger.send("Error: You are over your quota!");
        }
    }
}

before

#[cfg(test)]
mod tests {
    use super::*;

    struct MockMessenger {
        sent_messages: Vec<String>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger { sent_messages: vec![] }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        let mock_messenger = MockMessenger::new();
        let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);

        limit_tracker.set_value(80);

        assert_eq!(mock_messenger.sent_messages.len(), 1);
    }
}

after: MockMessanger: sent_messages を Vec => RefCell<Vec> にした。

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::RefCell;

    struct MockMessenger {
        sent_messages: RefCell<Vec<String>>,
    }

    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger { sent_messages: RefCell::new(vec![]) }
        }
    }

    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.borrow_mut().push(String::from(message));
        }
    }

    #[test]
    fn it_sends_an_over_75_percent_warning_message() {
        // --snip--

        assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
    }
}

RefCell で実行時に借用を追いかける

不変参照、可変参照を作成するとき、それぞれ & と &mut 記法を使用する。
RefCell では borrow, borrow_mut メソッドを使用し、それぞれ Ref, RefMut を返す。
どちらもDerefを実装していて、普通の参照のように扱える。

RefCell は現在活動中の Ref、RefMut の数を追いかける。
borrowを呼ぶと RefCellは不変参照の数を増やし、Refがスコープを抜けると数を減らす。
この原理で、いかなる瞬間も複数の不変借用 or 1つの可変借用を持つことを許す。
これが破られると panic を起こす。

Rc と RefCellを組み合わせることで可変なデータに複数の所有者を持たせる

RefCell を抱える Rcがあれば、複数の所有者を持ち、そして可変化できる値を得られる。

#[derive(Debug)]
enum List {
    Cons(Rc<RefCell<i32>>, Rc<List>),
    Nil,
}

use List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let value = Rc::new(RefCell::new(5));

    let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));

    let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
    let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));

    *value.borrow_mut() += 10;
    match &b {
        Cons(v1, list1) => match &**list1 {
            Cons(v2, list2) => {
                *v2.borrow_mut() += 10;
            },
            Nil => (),
        }
        Nil => (),
    }

    println!("a after = {:?}", a);
    println!("b after = {:?}", b);
    println!("c after = {:?}", c);
}
a after = Cons(RefCell { value: 25 }, Nil)
b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 25 }, Nil))
c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 25 }, Nil))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant