hysryt / wiki

https://hysryt.github.io/wiki/
0 stars 0 forks source link

Rust #155

Open hysryt opened 3 years ago

hysryt commented 3 years ago

プロジェクトの作成

$ cargo new todo

todo プロジェクトの作成。 カレントディレクトリに todo ディレクトリが生成される。

実行

$ cargo run
hysryt commented 3 years ago

スライス

プリミティブ型。 メモリのブロックを表す。 ブロック範囲を変更することはできない。

添字を使ってブロック内のデータにアクセスできる。

let ary = [10, 20, 30, 40, 50];
println!("{:?}", ary == ary);           // true
println!("{:?}", ary == ary[..]);       // true
println!("{:?}", ary == ary[0..]);      // true
println!("{:?}", ary == ary[0..5]);     // true
println!("{:?}", ary == ary[0..3]);     // false
println!("{:?}", ary == ary[0]);        // コンパイルエラー

配列は実データ、スライスは参照?

hysryt commented 3 years ago

struct

構造体。

struct Man {
  name: String,
}

メソッド

第一引数に self を指定するとメソッドとなる。

impl Man {
  fn greet(&self) {
    println!("I'm {}.", self.name);
  }
}

関数

impl Man {
  fn new(name: Into<String>) {
    Man { name: name.into() }
  }
}
hysryt commented 3 years ago

enum

enum Animal {
  Dog,
  Cat,
}
hysryt commented 3 years ago

trait

トレイト。 インターフェイスの定義。

trait Shape {
  fn area(&self) -> f64;
}

PHPのトレイトとは全く別の概念なので注意

hysryt commented 3 years ago

Intoトレイト

https://doc.rust-lang.org/std/convert/trait.Into.html

pub trait Into<T> {
  fn into(self) -> T;
}

他の型への変換を表すトレイト。 Fromトレイトを実装すると、対となる型に自動的に実装される。

引数の型として使うと便利。

impl Man {
  fn new(name: impl Into<String>) -> Self {
    Man { name: name.into() }
  }
}
hysryt commented 3 years ago

Fromトレイト

https://doc.rust-lang.org/std/convert/trait.From.html

pub trait From<T> {
  fn from(T) -> Self;
}

他の型からの変換を表すトレイト。 Intoトレイトと相互関係にある。

基本的にIntoトレイトではなくFromトレイトを実装した方が良い。 Fromトレイトを実装すると自動的にIntoトレイトも実装される。 https://doc.rust-lang.org/src/core/convert/mod.rs.html#557

hysryt commented 3 years ago

ジェネリクスの単相化

struct Point<T> {
  x: T,
  y: T,
}
struct Point_i32 {
  x: i32,
  y: i32,
}
hysryt commented 3 years ago

ジェネリクスを使った型に対する関数の定義

struct Point<T> {
  x: T,
  y: T,
}
impl<T> Point<T> {
  fn new(x: T, y: T) -> Self {
    Point { x, y }
  }
}
impl Point<i32> {
  fn new(x: i32, y: i32) -> Self {
    Point { x, y }
  }
}
impl<T: PartialEq> Point<T> {
    fn new(x: T, y: T) -> Self {
        Point { x, y }
    }
}
hysryt commented 3 years ago

FromトレイトとIntoトレイト

impl<T, U> Into<U> for T
where
    U: From<T>,
{
    fn into(self) -> U {
        U::from(self)
    }
}

トレイト境界 std::string::String: std::convert::From<Test> が満たされていません。 std::string::Stringstd::convert::From<Test> トレイトが実装されていません。 Test に対する std::convert::Into<std::string::String> トレイトの impl による要件です。


以下のようなコードを書いたとする。

```rust
struct Test {}

fn main() {
  let t = Test {};
  let s: String = t.into();
}

TをTestに置き換えて、

impl<U> Into<U> for Test
where
    U: From<Test>,
{
    fn into(self) -> U {
        U::from(self)
    }
}

型推論により、ここで UString だが、StringFrom<Test> トレイトを実装していないため、コンパイルエラーとなる。

hysryt commented 3 years ago

クロージャ

クロージャを構造体に含める場合は以下のようにトレイトとジェネリクスを使用する。

struct Test
  where T: Fn(i32) -> i32
{
  calc: T
}

let c = || println!("hello!");

c にはクロージャの参照がバインドされる。

let mut c = || println!("hello!");

c にはクロージャの可変参照がバインドされる。

let c = move || println!("hello");

c にはクロージャがムーブされる。


クロージャを返す関数 https://doc.rust-lang.org/book/ch19-05-advanced-functions-and-closures.html#returning-closures

クロージャはトレイトによって表現される。 基本的にトレイトを返したい場合は、代わりにトレイトを実装した具象型を返す。 ただし、クロージャは返せる具象型を持たないため返すことができない。

下記のコードはコンパイルできない。

fn returns_closure() -> dyn Fn(i32) -> i32 {
    |x| x + 1
}

トレイトオブジェクトを使えば解決。

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}
hysryt commented 3 years ago

Clone トレイト

暗黙的なコピーができない型で複製を行う場合、Cloneトレイトを実装し、clone メソッドを使う。

Rustではいくつかの型は暗黙的なコピーが可能であり、それらの型では代入時や受け渡し時には所有権のムーブは発生せず、元の値を残したまま値がコピーされる。 これらの型はメモリのアロケーションを必要としないため、安全にコピーが可能。 ただしそれら以外の型では、複製を行う場合は Clone トレイトを実装し、clone メソッドで明示的にコピーを行う必要がある。

String は Clone トレイトを実装している。

let s = String::new();
let copy = s.clone();

#[derive(Clone)] で Clone の実装が可能

#[derive(Clone)]
struct Morpheus {
   blue_pill: f32,
   red_pill: i64,
}
hysryt commented 3 years ago

Copy トレイト

pub trait Copy: Clone { }

Copy トレイトは Clone トレイトのサブトレイトである。 そのため Copy トレイトを実装する場合は Clone トレイトも実装する必要がある。

Copy トレイトを実装した場合、代入時などに自動的にビット単位でコピーされる。 この際cloneメソッドは実行されず、単純にビットのコピーが行われる。

構造体に Copy トレイトを実装する場合、そのメンバー全てが Copy を実装している必要がある。

hysryt commented 3 years ago

CopyトレイトとCloneトレイトの違い

Copyトレイトは暗黙的コピーを可能にする。

let y = x;

とした時、xがCopyトレイトを実装している場合には所有権は移らず、xのコピー(複製)がyに代入される。 このコピーの振る舞いは変更できず、常にビット単位でのコピーとなる。 内部的にcloneを実行する、というようなことはできない。

Cloneトレイトはcloneを持つ単純なトレイトである。 cloneは通常のメソッドと同じであり、型ごとに定義できる。

CopyトレイトはCloneトレイトのサブトレイトであるため、実装する際はCloneトレイトの実装も必要となる。

struct MyStruct;

impl Copy for MyStruct { }

impl Clone for MyStruct {
    fn clone(&self) -> MyStruct {
        *self
    }
}

clone に特別な処理が必要ない場合は上記のように実装する。 selfを返しているが、Copyトレイトを実装しているためコピーセマンティクスによりコピーされる。

CopyトレイトがCloneトレイトのサブトレイトなのはCopyトレイトもCloneトレイトも一括してCloneトレイトとして受け取れるようにするためと思われる。 両方とも x.clone() で複製が可能。

Clone:この型は複製できる。 Copy:この型はビット単位のコピーにより複製できる。

hysryt commented 3 years ago

std::iter

イテレータに関わる諸々をまとめたモジュール。 トレイト、関数、構造体を含む。

Iteratorトレイト

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

next() は、値があるうちは Some<Item> を返す。 値がなくなったら None を返す。 値がなくなったあと、さらに next() を呼び出した時の挙動はイテレータによって異なる。 再び Some<Item> を返すものもあれば、None を返すものもある。

next() 以外のメソッドにはデフォルト実装がある。 Iterator を実装する場合は next() だけ実装すれば良い。

イテレータを生成するメソッド

コレクションからイテレータを生成するメソッドには3種類ある。

IntoIterator トレイト

IntoIterator トレイトを実装した型はイテレータに変換できる。 into_iter() でイテレータに変換する。

IntoIterator トレイトを実装した型は for で使用することもできる。

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

for x in values {
    println!("{}", x);
}

VecIntoIterator を実装しているため for で使用できる。 内部的には values のイテレータが作成され、そのイテレータでループが回っている。 https://doc.rust-lang.org/std/iter/index.html#for-loops-and-intoiterator

イテレータアダプタ

イテレータから別のイテレータを作成する関数をイテレータアダプタと呼ぶ。 具体的には map()take()filter() などを指す。

lazy

イテレータは lazy である。 作成しただけでは何も起こらず、next() を呼び出した時初めて動き出す。 map() で指定したクロージャもイテレータを使用するまで呼び出されない。 map().collect() とした時にクロージャは実行される。

hysryt commented 3 years ago

リリースプロファイル

コンパイル時のプロファイル dev プロファイルと release プロファイルが存在する。 dev プロファイルは cargo build コマンドで、 release プロファイルは cargo build --release コマンドで使用される。

hysryt commented 3 years ago

Box

Boxはデータをヒープ領域に格納する。

let b = Box::new(5);

この場合、5 はヒープ領域に格納され、b は格納箇所へのポインタを保持する。

Boxにどんなデータが入ろうとも、Boxのサイズは固定である。 Boxが持つのはポインタであり、データ自体はヒープ領域に格納されるからである。


下記のような構造体はコンパイルできない。

struct A {
  a: Option<A>,
}

構造体が再帰的に自分自身と同じ型のインスタンスを保持する場合、その構造体のサイズは固定ではなくなるからである。

Boxで保持するようにすればサイズを固定にすることができる。


struct A {
  a: Option<Box<A>>,
}
hysryt commented 3 years ago

Derefトレイト

参照外し演算子(*)の振る舞いを変更できる。 Derefトレイトを実装していない場合は & の参照外ししかできない。

ポインタからポインタの中身を取得することを「参照外し」と呼ぶ。

BoxはDerefを実装しており、* をつけた時に参照先のデータを返すようになっている。

参照外し方強制(Deref coercion) Deref coercion は、関数やメソッドの引数に対してRustが行う便利な機能です。 Deref coercion は、Derefトレイトを実装した型にのみ動作し、別の型の参照に変換します。 例えば String は参照外しの際に &str を返すようDerefトレイトを実装しているため、Deref coercion によって &String&str に変換することが可能です。

impl ops::Deref for String {
    type Target = str;

    #[inline]
    fn deref(&self) -> &str {
        unsafe { str::from_utf8_unchecked(&self.vec) }
    }
}

引数で値を受け取る時、Derefを実装した型の参照型は、参照外しをした後の型でも受け取ることが可能です。 その場合は自動的に参照外しが行われます。

fn main() {
    let x:&str = "hello";
    let y:String = String::from("hello");

    test(x);
    test(&y);  // test(&(*y)); と同義であり、test(&(*(y.deref()))); と同義。
}

fn test(s: &str) {
    println!("{}", s);
}
hysryt commented 3 years ago

Drop

https://doc.rust-lang.org/book/ch15-03-drop.html

Dropトレイトを使ったクリーンアップ時のコードの実行

スマートポインターを使う上で重要となる2つ目のトレイトは Drop です。 Drop トレイトを使うことで、値がスコープの外に出るときの挙動をカスタマイズできます。 Drop トレイトは任意の型に実装でき、実装する処理の中で、リソース(ファイルやネットワークなど)の解放を行うことができます。 Dropトレイトをスマートポインタの文脈で紹介したのは、スマートポイントを実装する場合、ほぼ必ず Drop トレイトを使用することになるからです。 例えば、Box<T> はドロップされるときにBoxが指すヒープ領域の解放を行っています。

いくつかのプログラミング言語では、スマートポインタのインスタンスを使い終わる都度、メモリ(またはリソース)を解放するコードを呼び出す必要があります。 解放の呼び出しを忘れた場合、システムは負荷がかかりすぎてしまい、クラッシュしてしまう可能性があります。 Rustでは、値がスコープを抜ける時に特定のコードが実行されるように指定できます。 結果として、クリーンアップ用コードの設置に気を使う必要がなくなり、メモリーリークも抑止できます。

Drop トレイトを実装し、値がスコープの外に出た時に実行するコードを指定します。 Drop トレイトを実装する場合、self への可変参照を受け取る drop メソッドの実装が必要です。 Rustがいつ drop を呼び出すかを確認するために、drop メソッドで println! を実行してみましょう。

リスト15-14では CustomSmartPointer 構造体を用意しています。 CustomSmartPointer 構造体はインスタンスがスコープの外に出た時に Dropping CustomSmartPointer! と出力する機能を持っています。 この例では、Rustがいつ drop メソッドを実行するかをデモンストレーションしています。

リスト15-14

struct CustomSmartPointer {
    data: String,
}

impl Drop for CustomSmartPointer {
    fn drop(&mut self) {
        println!("Dropping CustomSmartPointer with data `{}`!", self.data);
    }
}

fn main() {
    let c = CustomSmartPointer {
        data: String::from("my stuff"),
    };
    let d = CustomSmartPointer {
        data: String::from("other stuff"),
    };
    println!("CustomSmartPointers created.");
}

Drop トレイトは自動でインクルードされているため、読み込み用の記述は必要ありません。 CustomSmartPointerDrop トレイトを実装し、drop メソッドで println! を呼び出すようにしています。 drop メソッドにはインスタンスがスコープの外に出た時に実行したい処理を記述します。 Rustがいつ drop を呼び出すのかをデモンストレーションするため、drop でいくつかのテキストを出力するようにしています。

main メソッドでは CustomSmartPointer のインスタンスを2つ作り、CustomSmartPointers created. と出力しています。 main メソッドが終了すると、 CustomSmartPointer はスコープの外に出るため、drop メソッドに記述した処理がRustによって呼び出され、最後のメッセージが出力されます。 明示的に drop メソッドを呼び出す必要がないことに注目してください。

このプログラムを実行すると、次のように出力されます。

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.60s
     Running `target/debug/drop-example`
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!

Rustは自動的に drop メソッドを呼び出しています。 変数は作成とは逆の順番でドロップされるます。 そのため、dc より先にドロップされています。 この例は drop がどのように動作するかを確認するためのものです。 実際にはメッセージの出力ではなく、その型に必要なクリーンアップコード(リソースの解放など)を記述することになるでしょう。

std::mem::drop を使った早期ドロップ

残念ながら、Drop の機能を無効にすることは簡単ではありません。 Drop トレイトのメリットは自動的に処理されることであり、これを無効にする必要は通常ありません。 しかし、場合によっては値を早めにクリーンアップしたいことがあります。 例えば、ロックを管理するスマートポインタを使用する時です。 ロックを解除するときに、同じスコープで他のコードがロックを取得できるように、強制的に drop を実行したいというケースがあります。 Rustでは drop メソッドを手動で実行することを許可していません。 スコープから外に出る前に強制的に drop したい場合は、標準ライブラリが提供する std::mem::drop 関数を実行する必要があります。

リスト15-14を書き換えて main 関数の中で drop メソッドを手動で呼び出すようにすると、コンパイルエラーとなります。

リスト15-15

fn main() {
    let c = CustomSmartPointer {
        data: String::from("some data"),
    };
    println!("CustomSmartPointer created.");
    c.drop();
    println!("CustomSmartPointer dropped before the end of main.");
}

以下のエラーメッセージが表示されます。

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
error[E0040]: explicit use of destructor method
  --> src/main.rs:16:7
   |
16 |     c.drop();
   |       ^^^^ explicit destructor calls not allowed

error: aborting due to previous error

For more information about this error, try `rustc --explain E0040`.
error: could not compile `drop-example`.

To learn more, run the command again with --verbose.

このエラーメッセージは drop の明示的な呼び出しが許可されていないことを示しています。 エラーメッセージでは「デストラクタ」という用語を使っています。 デストラクタはプログラミング用語であり、インスタンスをクリーンアップする関数のことです。 Rustにおいて、drop 関数はデストラクタの1つです。

std::mem::drop 関数は Drop トレイトの drop メソッドとは異なり、引数として drop したい値を渡します。 この関数は自動的に読み込まれるため、そのまま使用できます。 リスト15-15 は以下のように書き換えることができます。

リスト15-16

fn main() {
    let c = CustomSmartPointer {
        data: String::from("some data"),
    };
    println!("CustomSmartPointer created.");
    drop(c);
    println!("CustomSmartPointer dropped before the end of main.");
}

コードを実行すると以下のように表示されます。

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/drop-example`
CustomSmartPointer created.
Dropping CustomSmartPointer with data some data!
CustomSmartPointer dropped before the end of main.

CustomSmartPointer created.CustomSmartPointer dropped before the end of main. の間で drop されていることがわかります。

hysryt commented 3 years ago

Rc\

https://doc.rust-lang.org/book/ch15-04-rc.html

参照カウントスマートポインタ

所有権をどの変数が持っているのかは基本的には明白です。 しかし、一つの値に複数の所有者が存在するケースがあります。 例えば、グラフデータ構造(ノード(頂点)とエッジ(枝)で構成されるデータ構造)です。 複数のエッジが同じ一つのノードに繋がっている場合、概念的にはそのノードは複数のエッジに所有されていることになります。 ノードはそのノードを所有するすべてのエッジが無くなるまでクリーンアップされるべきではありません。

Rustでは Rc<T> を使うことで所有権を複数の変数が持てるようになります。 RC は Reference Counting(参照カウンタ)の略です。 Rc<T> 型は変数がまだ使用されているかどうかを判断するために参照の個数を保持しています。 もし参照の個数が0であれば、クリーンアップできるということになります。

Rc<T> をリビングにあるテレビに例えてみましょう。 誰か一人がテレビを見る時、電源を入れます。 別の人がリビングに入り、テレビを見ることも可能です。 最後の一人がリビングを出る時、テレビの電源は切られます。 もし誰かがテレビを見ているのに電源を切ってしまった場合、まだテレビを見ている人から文句を言われるでしょう。

プログラム中の複数の部分で読み込みたいデータをヒープ領域に格納し、コンパイル時にはデータがどこで一番最後に使われるか判断できない場合に Rc<T> を使用します。 どこで最後に使われるか確定している場合は、その部分を所有者とし、コンパイル時に適用される通常の所有権ルールが有効となります。

注意事項として、Rc<T> はシングルスレッドでのみ使用されます。 マルチスレッドでの参照カウンタについては16章で説明します。

Rc<T> を使用したデータの共有

リスト15-5のコンスリストの例に戻りましょう。 Box<T> を使ってコンスリストを定義していました。

リスト15-5

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

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}

今回は特定のリストの所有権を共有する2つのリストを作成します。 概念的には図15-3のようになります。

図15-3:bとcはaの所有権を共有する

graph0

まず5と10を持つリスト(a)を作成します。 そしてそのリストをもとに、3から始まるリスト(b)と4から始まるリスト(c)を作成します。 bとcのリストは5と10を持つリスト(a)に繋がっています。 言い換えると、bとcのリストは最初のリストを共有していることになります。

これを Box<T> のリストを使って実装することはできません。 リスト15-17を見てください。

リスト15-17

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

use crate::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));
}

これはコンパイルエラーとなります。

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
  --> src/main.rs:11:30
   |
9  |     let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
   |         - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 |     let b = Cons(3, Box::new(a));
   |                              - value moved here
11 |     let c = Cons(4, Box::new(a));
   |                              ^ value used here after move

error: aborting due to previous error

For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list`.

To learn more, run the command again with --verbose.

これは b を作成した時点で a の所有権は b に移動しているためです。 その後 c を作ろうとしても a はすでに所有権が移動しているため、エラーとなります。

Cons の定義を所有権ではなく参照を保持するように変更することも可能ですが、そうするとライフタイムパラメータを指定する必要があります。 ライフタイムパラメータを指定するようにすると、リスト内のすべての要素がリスト全体と同じだけ長生きすることを指定することになります。 また、借用チェッカーは let a = Cons(10, &Nil) を許可しません。 一時的な Nil は参照を取得する前に drop されてしまうからです。

代わりに、Box<T>Rc<T> に変更します。 リスト15-18を見てください。 各 Cons は値と、リストを指す Rc<T> を保持します。 b を作る時、a の所有権を取るのではなく、a をクローンしています。 それによって参照カウントが1から2になり、ab で所有権を共有されます。 さらに c を作るときにも参照カウントが増え、2から3になります。 このように、 Rc::clone を呼ぶたびに参照カウンタは増加します。 そしてその参照カウンタが0になるまでクリーンアップは行われません。

リスト15-18

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

use crate::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<T> は自動的に読み込まれないため、 use で読み込む必要があります。 main 関数では5と10を持つリストを作成し、それを Rc<List> に入れたのち、a に入れています。 そして bc を作る時には Rc::clone 関数にaの参照を渡して呼び出しています。

Rc::clone(&a) ではなく a.clone() でも良いですが、慣例としてここでは Rc::clone を使っています。 これは a.clone() だけだとディープコピーを行うのかどうか判断がつかないからです。 通常の型の clone メソッドはディープコピーを行いますが、Rc::clone (及び Rc<T> のインスタンスの clone メソッド)は参照カウンタを増加させるだけで、ディープコピーは行いません。 ディープコピーは時間がかかるため、ディープコピーを行うかどうかは重要です。 参照カウンタの増加には Rc::clone を使うことで、見た目上、ディープコピーを行う clone と区別できます。 コードのパフォーマンス低下の原因を探るとき、Rc::clone は無視できるでしょう。

参照カウンタを増加させる Rc<T> のクローン

a の参照カウンタを確認できるようにリスト15-18を書き換えてみましょう。

リスト15-19では main を変更し、 c を内部スコープに入れています。 これによって c がスコープの外に出た時の参照カウンタの変化が確認できます。

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

プログラム内の参照カウンタが変化する各ポイントで参照カウンタを出力しています。 参照カウンタは Rc::strong_count 関数で取得できます。 この関数の名前が count ではなく strong_count なのは、Rc<T> 型は weak_count という関数も持っているからです。 weak_count の使い方は Preventing Reference Cycles: Turning an Rc into a Weak で確認できます。

このコードは以下のように出力します。

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished dev [unoptimized + debuginfo] target(s) in 0.45s
     Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

最初の Rc<List> の参照カウンタは1です。 その後、clone が呼び出されるたびにカウントは1ずつ上がります。 c がスコープの外に出るとカウントは1下がります。 参照カウンタを増加させるときは clone 関数を呼び出しますが、減少させるときには特に関数は呼び出しません。 Rc<T> がスコープの外に出ると Drop トレイトの実装によって自動的に参照カウンタが減少されます。

この例では ba がスコープを出たときの参照カウンタは確認できませんが、カウンタは0となり、Rc<List> はクリーンアップされます。 Rc<T> を使うことで一つの値を複数の所有者が持つことができ、カウンタによって所有者が一人でもいる限り値は有効であることが保証されます。

Rc<T> は内部で不変参照を取得するため、共有するデータは読み取りのみ可能です。 もし Rc<T> が可変参照を許した場合、借用ルール違反となり、データの競合や不整合の温床となるでしょう。

次のセクションでは「interior mutability」パターンと RefCell<T> 型について説明します。 RefCell<T>Ref<T> を合わせて使うことでこの不変性の制限に対応できます。

hysryt commented 3 years ago

RefCell\

https://doc.rust-lang.org/book/ch15-05-interior-mutability.html

RefCell<T> と Interior Mutability パターン

「Interior mutability」は、通常であれば借用ルールによって拒否される「不変参照のデータを変更する」ことを可能にするRustのデザインパターンです。 このパターンでは、データを変更するためにデータ構造の中で unsafe コードを使ってRustのルールをねじ曲げています。 unsafe については19章で説明します。 関連する unsafe コードは安全なAPIによってラップされ、外側からは不変として使用されます。

「interior mutability」パターンを可能にする RefCell<T> 型に注目し、この概念について詳しくみていきましょう。

RefCell<T> を使った、実行時の借用ルールの強制

Rc<T> とは違い、RefCell<T> 型はデータに対し単一の所有権しかありません。 では Box<T>RefCell<T> の違いはなんでしょうか? 4章で学んだ借用ルールを思い出してみましょう。

Box<T> と参照を使用する場合、借用ルールはコンパイル時に強制されます。 一方 RefCell<T> を使用する場合、借用ルールは実行時に強制されます。 参照を使用するとき、借用ルールから外れた場合はコンパイルエラーが発生します。 RefCell<T> の場合は借用ルールから外れた場合、コンパイルエラーは発生せず、実行時にパニックが発生し、終了します。

コンパイル時に借用ルールのチェックを行う利点は、エラーを早い段階で検知できることと、実行時にチェックを行わないためチェックにかかる負荷がなくなるということです。 そういった理由により、コンパイル時に借用ルールのチェックを行うことは大抵の場合はメリットとなります。

一方、実行時に借用ルールのチェックを行う利点は、コンパイル時のチェックでは禁止される特定のシナリオを許可できることです。 Rustコンパイラのような静的解析は基本的に保守的に動作します。 コードのいくつかのプロパティは分析時に検出できないことがあります。 最も有名な例は「Halting Problem」です。(https://ja.wikipedia.org/wiki/%E5%81%9C%E6%AD%A2%E6%80%A7%E5%95%8F%E9%A1%8Chttps://ja.wikipedia.org/wiki/%E3%83%A9%E3%82%A4%E3%82%B9%E3%81%AE%E5%AE%9A%E7%90%86

いくつかの分析は不可能なので、コードが所有者ルールに則っていることをRustコンパイラが確認できなかった場合、そのプログラムは拒否されます。 もしRustが正しくないプログラムを許可した場合、ユーザーはそれを信頼できなくなります。 かといって、Rustが正しいプログラムを拒否した場合、壊滅的ではありませんが、プログラマーとしては不便です。 RefCell<T> 型はコンパイラが分析出来ないコードを記述する際に役立ちます。

Rc<T> と同じように、RefCell<T> はシングルスレッド上でのみ機能し、マルチスレッド上で使用するとコンパイルエラーとなります。 RefCell<T> の機能をマルチスレッドプログラムで使用する方法は16章で説明します。

Box<T>Rc<T>RefCell<T> のそれぞれの要約は以下の通りです。

不変な値の中で値を書き換えることを「interior mutability」パターンと呼びます。 「interior mutability」がどのような場面で便利で、どのように動作しているのかをみていきましょう。

Interior Mutability:不変値への可変参照

借用ルールに則った場合、不変値を可変として借用することはできません。 例えば以下のコードはコンパイルできません。

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

これをコンパイル使用とすると以下のエラーが返ってきます。

$ cargo run
   Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
 --> src/main.rs:3:13
  |
2 |     let x = 5;
  |         - help: consider changing this to be mutable: `mut x`
3 |     let y = &mut x;
  |             ^^^^^^ cannot borrow as mutable

error: aborting due to previous error

For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing`.

To learn more, run the command again with --verbose.

しかし、メソッド内でのみ値を変更できると便利という場面が存在します。 メソッド以外の部分では値は変更できないということです。 この「interior mutability」を実現する一つの方法は RefCell<T> です。 しかし RefCell<T> は借用ルールを完全に回避するものではありません。 コンパイラの借用チェッカーは「interior mutability」を許可し、代わりに実行時に借用ルールをチェックします。 もしルールに違反した場合、コンパイルエラーではなく panic! となります。

RefCell<T> の実践的な例を見ていきましょう。

Interior Mutabilityのユースケース:モックオブジェクト

テストダブルはテスト中に使用する代用の型を表す一般的なプログラミングの概念です。 モックオブジェクトはテストダブルの一つで、テストで正しい動作が行われたことをアサートできるようにします。

Rustは他の言語でいうようなオブジェクトいうものを持っておらず、他の言語が持っているようなモックオブジェクトの機能を持っていません。 ただし、モックオブジェクトと同じ目的を持つstructを作成することは確実にできます。

ここでは、値を追跡し、どのくらい最大値に近いかに基づいてメッセージを送信するライブラリを作成し、テストします。 このライブラリを使って、例えば割り当てに対するAPIのコール数を追跡することができます。

このライブラリは最大値にどれくらい近いかやどのようなメッセージになるべきかを追跡する機能だけを提供します。 このライブラリを使用するアプリケーションはメッセージを送るメカニズムを提供する必要があります。 アプリケーションはメッセージの設置、メールの送信、テキストメッセージの送信、などができます。 ライブラリはアプリケーションの詳細について知る必要はありません。 アプリケーション側では Manager トレイトを実装します。

hysryt commented 3 years ago

Type ascription

RFC803 「式が持つべき型」を記述する機能 https://stackoverflow.com/a/36390114