###### tags: `Rust` `Drop` `if let` `while let` `trait object` `動的ディスパッチ` `クロージャ` # Rust テキスト学習 6 # Dropトレイト いわゆる "デストラクタ" Rustから提供されているトレイト structがスコープ外になり、メモリから開放されるときに実行されるメソッドである .drop() を定義するTrait ```rust= struct FireWork{ strength: i32, } impl Drop for HasDrop { fn drop(&mut self){ println!("Boom time {} !!!", self.strength); } } fn main(){ let tnt = Firework{strength: 100} } ``` ## Dropトレイトの使いみち スコープが切れると呼ばれるという特性から、 リソースのクリーンアップに利用するのがベスト Arc\<T>型は参照カウント型 参照している変数がスコープを抜けるとカウントが減り、0になるとリソースを開放する。 # if let if と let を一体化して使用する。 Golangでも、 `if x:= val; condition {}` のような構文があった。 ある値の入ったOption型変数の option があるとする。 もし、if let を用いないとすると、二種類のパターンマッチがかける。 ```rust= match option { Some(x) => { foo(x) }, None => {}, } if option.is_some() { let x = option.unwrap(); foo(x); } ``` しかし、if let を使うと更にスマートに書ける 書式は `if let パターン = 値 {code}` ```rust= if let Some(x) = option { foo(x); } else { bar(); } ``` = の左のパターンに右の値が当てはまるなら、代入してif文を実行 matchしないなら実行しない or else を実行 **Rustでは代入文は式ではない**。よって、 if (let x = option) でtrueの値で分岐するのではなく **if let** (パターン = 値) でマッチする/しない という構文である。 # while let あるベクターから値がなくなるまでpop()し続ける処理があるとする。 Vec\<T>.pop() の戻り値は enum Option型なので、 loop と match を組み合わせてかける。 ```rust= let mut v = vec![1, 3, 5, 7, 11]; loop { match v.pop() { Some(x) => println!("{}", x), None => break, } } ``` ただし、while let を使うと更に簡潔に書ける。 ```rust= while let Some(x) = v.pop() { println!("{}", x); } ``` # トレイトオブジェクト トレイトオブジェクトを語る前に、ポリモーフィズムなコードが 実際に実行するときにバージョンを決めるメカニズムであるディスパッチについて 理解しなければならない。 トレイトオブジェクトは、動的ディスパッチである。 - 静的ディスパッチ : コンパイル時にすべて型を決定する(今までのやつ) - 動的ディスパッチ : &Trait 、 または Box\<Trait> のように、ポインタの形でtraitを扱う ## 静的ディスパッチ コンパイル時にポリモーフィズムなジェネリックなどの型が決定され、 それぞれの実行時の型の関数、Structなどを作成する。(これを関数・構造体の特殊化という) ジェネリック(一般的) <=> スペシャル(特別) ほぼすべての場合で、動的ディスパッチよりも優れた最適化ができる。 なぜなら、コンパイル時に呼び出される関数がわかるので、 関数のインライン化(関数call しているコードの部分で、関数の処理を展開する)により 関数呼び出しの処理コストを軽減できるためである。 ただし、インライン化した関数は命令キャッシュを膨張させる。 地獄の沙汰もキャッシュ次第。 メモリが貧弱な状況やめちゃめちゃコード量の多い関数を多用する場合は要注意。 #[inline], #[inline(always)] アトリビュートにより 関数をインライン関数に変換できるが、慎重に行ったほうが良い。 しかし、上記のようなデメリットの可能性もあるがほぼ一般的なすべての状況で 静的ディスパッチのほうが効率的、かつ静的ディスパッチを動的ディスパッチでラップする ことは可能なものの不可逆であることから、Rustは静的ディスパッチを推奨する ## 動的ディスパッチ **トレイトオブジェクト**機能により提供される。 ### トレイトオブジェクト 指定されたトレイトを実装する型の値を、型によらずすべて代入できる。 正確な型は、プログラム実行時に初めて判明する。 トレイトオブジェクトの表記 - x: &Trait - y: Box\<Trait> 型強制 or キャストにより使える この2つの操作は同一である - キャスト : as キーワードを使って、元の引数を異なる型に変換すること ```rust= fn do_something(x: &Foo) { x.method(); } fn main() { let x = 5u8; do_something(&x as &Foo); } ``` - 型強制 : 関数の仮引数の方に、実引数が自動的に合わせられること ```rust= fn do_something(x: &Foo) { x.method(); } fn main() { let x = "Hello".to_string(); do_something(&x); } ``` ### トレイトオブジェクトの内部表現 これはコンパイラが作成・管理するものでありプログラマの目に触れることのない コードである ```rust= pub struct TraitObject { pub data: *mut (), pub vtable: *mut (), } ``` vtable : トレイトが持つメソッドの一覧表(関数ポインタのレコード) data : トレイトオブジェクトが保存している(何らかのT型)のデータ 更に、call_method_on_u8 や call_method_on_string といった関数が用意されたりするが、 複雑なので無視する。 ## オブジェクト安全性 すべてのトレイトがトレイトオブジェクトとしては扱えない。 ルールが有る - トレイトが Self: Sized を要求しない - トレイトのメソッドすべてがオブジェクト安全であること - オブジェクト安全とは: - どのような型パラメータも持たない - Selfを使ってはならない 特別な状況を覗いて、トレイトメソッドでSelfを使うとオブジェクト安全でなくなる 要するに、オブジェクトの値を参照したり変更したりすることはオブジェクトに変化をもたらす 可能性のある操作なので、それを行うとオブジェクト安全ではないということ # クロージャ ```rust= //1行 let fun = |x: i32| x + 1; //複数行 let fun2 = |x| { x += 1; x += 1; x += 1; x } // 外部のスコープの変数を使う let mut num = 10; let fun3 = |x| { x + num }; // 注意: fun3 は num に対する参照 &num を借用している // 変更可能な借用 &mut numと両立できないので、下はエラー let y = &mut num; //error ``` クロージャとは - 関数と自由変数を1つにまとめる機能(コードの明確さ、再利用性の向上) - 自由変数 = クロージャーの外部のスコープにある変数 - 自由変数がクロージャー内で使われるとき、閉じ込められる(クローズ、閉める) - そういうまとまりをクロージャーという Rustにおいては - インスタントな関数として使える - 無名関数、ラムダのような扱い - 引数は | | 2本のパイプの間に書く - 引数や戻り値の型は書いても書かなくてもいい - 使いやすさのため - 特性上、定義した場所ですぐ使われる - 名前付き関数が引き起こす、定義と離れた場所で発生するエラー要因とならないから ## moveクロージャ move : このキーワードをクロージャー定義に付け足すと、moveセマンティクスに従うようになる。 すなわち、外部の変数をクロージャ内で使うと、 クロージャ内同名変数 = 外部変数; で初期化されることになる。 ```rust= let num = 5; let owns_num = move |x: i32| x + num; ``` 上記のコードでクロージャーは、numについて &num ではなく、コピーの所有権を持つ。 これは、move キーワードにより、クロージャ自身の専用のスタックフレームを持つことになるため。 ### moveクロージャーのさらなる例 クロージャーの生存範囲をブロックで制限して、numの値を変更しようとする クロージャーを作成して実行する。 moveなし : 変更可能参照を取得して、num = 10 になる ```rust= let mut num = 5; { let mut add_num = |x: i32| num += x; add_num(5); } assert_eq!(10, num); ``` moveあり : move semanticsに従う。 i32は **Copy** トレイトを実装しているので、変更可能コピーする。 ```rust= let mut num = 5; { let mut add_num = move |x: i32| num += x; add_num(5); } assert_eq!(5, num); ``` ## クロージャー実装 Rustにおけるクロージャは実質的にトレイトのシンタックスシュガー クロージャー構文 : || {} 関数を呼び出す : () <= このカッコはオーバーロード可能な演算子 Fn, FnMut, FnOnce というトレイトが存在し、 関数、mut関数、1回だけ呼び出す関数 に該当する。 ||{} を使ったところで、 - Rustの環境用の構造体を作成 - 適切なトレイトをimpl することでクロージャーは実装される。 ## クロージャーを引数にする クロージャーが実際はトレイトであることを学んだので、 クロージャーを引数(コールバック)とするような関数を書くことができる whereも使う ```rust= fn call_with_one<F>(some_closure: F) -> i32 where F: Fn(i32) -> i32 { some_closure(1) } ``` ここでは、where節により - ジェネリックパラメータ F は Fn(i32) -> i32 を実装している。 - つまり、i32を引数にしてi32を返すクロージャーを引数にできる(トレイト境界 = ジェネリック制限) - トレイト境界の指定により、コンパイル時に静的ディスパッチされる - 他の言語とはクロージャの扱いが異なる 多くの言語だと、クロージャはヒープに動的配置され、常に動的ディスパッチ。 Rustでは常に静的ディスパッチ。パフォーマンスに寄与。 ## クロージャ環境 クロージャ内で使われる変数の初期化状態のこと move キーワードを使うか使わないかで挙動が異なる。 ## クロージャと関数ポインタの互換性 クロージャを引数として期待する関数に、関数ポインタを渡すことができる。 関数ポインタは環境を持たないクロージャとみなせる。 (環境を持たない = 外部変数を閉じ込めて使わない = 外部変数に依存しない) ## 関数の戻り値としてクロージャを返す 超面倒なので、完成形だけ ```rust= fn factory() -> Box<Fn(i32) -> i32> { let num = 5; Box::new(move |x| x + num) } let f = factory(); let answer = f(1); assert_eq!(6, answer); ``` factory() 関数は、クロージャ Fn(i32)->i32 のBox(ヒープアロケート)を返す。 1. Rustは関数の返り値のサイズを知る必要がある。 -> サイズを与えるため&Fn()の参照を返す 2. 引数がないので、参照戻り値のライフタイムの省略が聞かない -> 'staticにしてみる 3. closure@\<anon>を使ってしまっている -> Box<Fn()>でトレイトオブジェクトの参照として返す - クロージャはそれぞれの環境用structを作成し、Fnを実装する - 型は匿名であり、クロージャのためだけに存在する。 - 戻り値がFn()の参照になることを期待されているが、closure専用のstructが帰ってきている 4. クロージャの借用しているnumの元のオブジェクトのスコープが先に切れてしまう -> moveキーワードを付けてコピーの所有権を取得