安全性、速度、並行性を兼ね備えた言語と、巷でうわさの「Rust」を覗いてみる(その5:所有権と関数)
今回も「The Rust Programming Language」を読みながら、所有権と関数を覗いていきたいと思います。
関数の戻り値と所有権
前回は関数の引数の所有権について確認できたので、今回は戻り値について見ていきます。
fn main() { let s1 = gives_ownership(); println!("{}", s1); } fn gives_ownership() -> String { let some_string = String::from("hello"); some_string }gives_ownership 関数内でヒープに確保された some_string は、関数のreturn時にムーブされるようです。
なので、 main 関数内の変数 s1 が所有権を持ちます。
そして、 main 関数の終了時に s1 はドロップされます。
次は、引数と戻り値の複合パターンを確認していきます。
fn main() { let s2 = String::from("hello"); let s3 = takes_and_gives_back(s2); println!("{}", s3) } fn takes_and_gives_back(a_string: String) -> String { a_string }
変数 s2 は、 takes_and_gives_back 関数にムーブされます。
takes_and_gives_back 関数は、 a_string を返すだけなので、 main 関数にムーブされて、
戻り値は変数 s3 にムーブされます。
そして最終的にスコープを抜けると s3 はドロップされます。
関数を受け取る関数の場合のムーブ
関数に関数を渡す場合のムーブが気になりました。
fn main() { let s1 = String::from("hello"); let s2 = wrap(s1, moge); // println!("{}", s1); // error println!("{}", s2); } fn wrap<F: Fn(String) -> String> (string: String, f: F) -> String { f(string) } fn moge(string: String) -> String { string }
ジェネリックは雰囲気で書きましたが、こんなコードでコンパイルは通りました。
wrap 関数は、2つの引数をとります。一つ目は、String型で、二つ目はF型です。
F型は、Fn(String) -> Stringになり、 moge 関数はこの境界を満たします。
コメントアウトしている部分を動かすと、「value borrowed here after move」が出力されるので、
変数 s1 から s2 へ関数を通してムーブされていることが分かりました。
しかし、残念ながら今回も私のRust力では理解できない部分があります。
まず、関数を渡すのになぜジェネリック型を使用しないといけないのかです。
参考にしたサイトによると、クロージャの仕様が関係してそうです。
When a closure is defined, the compiler implicitly creates a new anonymous structure to store the captured variables inside, meanwhile implementing the functionality via one of the traits: Fn, FnMut, or FnOnce for this unknown type. This type is assigned to the variable which is stored until calling.
Since this new type is of unknown type, any usage in a function will require generics. However, an unbounded type parameter <T> would still be ambiguous and not be allowed. Thus, bounding by one of the traits: Fn, FnMut, or FnOnce (which it implements) is sufficient to specify its type.
クロージャを定義すると、コンパイラは匿名の構造体を作って、キャプチャされた変数をその中に保存する。
その時、この不明な型(unknown type)のために、トレイトのFn,FnMut,FnOnceの一つを経由して機能を実装する。
この型は、呼び出すまで保存される変数にアサインされる。
この新しい型は不明な型(unknown type)なので、関数で使用するにはジェネリックが必要になる。
しかしながら、境界が決まっていない<T>型だと曖昧すぎるので許されない。
なので、トレイトのFn,FnMut,FnOnceの一つの境界を実装する。
みたいなことが書かれています。
関数を渡すときもクロージャとして処理され、その型が不明なので型を明示する必要があるという事でしょうか。
ここまでの落穂ひろい
タプル型
タプルは、いろんな型を値を1つの複合型にまとめる一般的な方法で、固定長のようです。
宣言されると、サイズを増やしたり減らしたりできません。
fn main() { let tup: (i32, f64, u8) = (500, 6,4, 1); }
この例では、i32(32ビット符号付整数型)とf64(64ビット浮動小数点型)、u8(8ビット符号無し整数型)を混在しています。
fn main() { let tup = (500, 6.4, 1); let (x, y, z) = tup; println!("The value of y is: {}", y); }
タプルの値は、パターンマッチングを利用して取り出します。 変数 tup の内容をletと (x, t, x) と書くことで分割(もしくは分配、destructuring)できるようです。 JSの分割代入(Destructuring assignment)と似たような感じでしょうか。 そうなると「…」の扱いなどが気になってきます。
ちょっと調べてみると、「..」を使えばできそうです。
fn main() { let tup = (500, 6.4, 1); let (x, ..) = tup; println!("The value of x is: {}", x); //500 }x 以外の残りの2つの値が無視されて500が出力されます。
fn main() { let tup = (500, 6.4, 1); let (..) = tup; //println!("The value of y is: {}", z); }
すべての値を無視してもコンパイルエラーは出ませんでした。
fn main() { let tup = (500, 6.4, 1); let (.., x, ..) = tup; println!("The value of x is: {}", x); }
こちらはエラーになります。
.. は一回しか使えないようです。
fn main() { let tup = (500, 6.4, 1); let (_, y, ..) = tup; println!("The value of y is: {}", y); }
「_」を使うことで、無視することができるようです。
「_x」と書くこともできるようですが、こちらは x に値が束縛されるのでムーブの対象になるらしいです。
この記事を書いた人
-
2008年にアーティスへ入社。
システムエンジニアとして、SI案件のシステム開発に携わる。
その後、事業開発部の立ち上げから自社サービスの開発、保守をメインに従事。
ドメイン駆動設計(DDD)を中心にドメインを重視しながら、保守可能なソフトウェア開発を探求している。
この執筆者の最新記事
関連記事
最新記事
FOLLOW US
最新の情報をお届けします
- facebookでフォロー
- Twitterでフォロー
- Feedlyでフォロー