この記事はwacul アドベントカレンダーの9日目の記事です。
この頃、rustやっているのですが、知っていないと中々思いつかないイディオムに遭遇したのでメモします。2ついきます。
freezing
文法的な話からはいると、https://rustbyexample.com/scope/borrow/freeze.html#freezing の技法のことです。雑に言うと、スコープ内では、変数をmutable->immutable
にfreezingでき、一旦freezeした後は、mutableに戻せない、と言っています。
一見、大したことない事項のようですが、これをうまく利用するとコードがスマートにかけたりすることもあるので、侮れない。
例えば、こういう場合を考えます1:
extern crate reqwest;
use std::io;
use std::path::{PathBuf};
fn main() {
let mut response = reqwest::get(url).unwrap();
let save_dir = PathBuf::from("./data");
// https://docs.rs/reqwest/0.8.1/reqwest/struct.Response.html#method.url
// fn url(&self) -> &Url なので、responseがimmutable borrowになる
let fname = response.url()
.path_segments()
.unwrap()
.last()
.unwrap(); // &str
let fname = save_dir.as_ref().join(fname);
// responseをmutableとしてborrowする
io::copy(&mut response, &mut File::create(fname).unwrap()).unwrap();
}
pub fn copy<R: ?Sized, W: ?Sized>(reader: &mut R, writer: &mut W)
なので、両方&mut
が要求されます。
これを実行します:
error[E0502]: cannot borrow `response` as mutable because it is also borrowed as immutable
--> src/ch03/ch03.rs:18:15
|
10 | let fname = response.url()
| -------- immutable borrow occurs here
...
18 | copy(&mut response, &mut File::create(fname).unwrap()).unwrap();
| ^^^^^^^^ mutable borrow occurs here
19 | }
| - immutable borrow ends here
原因は、一度immutableとして貸し出してしまったので、再度mutableにすることができないからです。
どうするかなんですが、同じスコープ内にあるからダメなのであって、スコープを制限してしまいます:
extern crate request;
use std::io;
use std::path::{PathBuf};
fn main() {
// define mut to use as mutable later
let mut response = reqwest::get(url).unwrap();
let save_dir = PathBuf::from("./data");
// scoped
let fname = {
// fn url(&self) -> &Url
let fname = response.url()
.path_segments()
.unwrap()
.last()
.unwrap();
save_dir.as_ref().join(fname) // セミコロンがついていないので、ここの値がfnameにsetされる
};
io::copy(&mut response, &mut File::create(fname).unwrap()).unwrap();
}
これで大丈夫なのだ!
このように、スコープ狭めるとE0502が回避できる場合があり、一つの武器として知っておくと、いざという時にちょくちょく役立つ。
素敵ですね
Struct fields that can be loaned or "taken"2
これは、fn func(&mut self)
のように、引数が借用型であって欲しいorそうせざるを得ないんだけど、funcの内部で、実体を借りたい場合に、たまーに見かけるテクニックです。
B
は適当な型、consume(self: B)
とします。結論から書くと、こんな風に解決します。
struct A {
// Box型ではなく、Option型とする。
area: Option<B>,
name: String,
}
impl A {
fn func(&mut self) {
if let Some(b) = self.area.take() {
b.consume(); // bはB型で所有権持つので、大丈夫。
}
}
}
fn take(&mut self) -> Option<T>
の中で、&mut self
のように借用型で構わないけれども、Tという実体を取り出せるところが一番のポイントです(unwrapやexpectも値を取り出しますが、self
なので、今回の要件では使用できない) 後は、Box
ではなく、Option
にする必要があることも今回のポイントです。
特に名前なさそうなので、本記事では、takeイディオムと呼びます。
これだけでは、メリットがよくわからんと思うので、実践例を見ていきましょう。
https://doc.rust-lang.org/book/second-edition/ch20-06-graceful-shutdown-and-cleanup.html#graceful-shutdown-and-cleanup での公式の例。文脈はかなり端折っているので、詳細は上記のリンクを参照のこと:
use std::thread;
type Worker = InnerHandle;
type InnerHandle = thread::JoinHandle<()>;
pub struct ThreadPool {
workers: Vec<Worker>,
}
impl Drop for ThreadPool {
fn drop(&mut self) {
// &mutまたは、&を明示的につけないと cannot move out of hereとなる
(&mut self.workers)
.into_iter()
// s: &Workerだが、joinメソッドはself, つまり、Worker型を要求するので、
// cannot move out of borrowed content となる
.for_each(|s| s.join().unwrap());
}
}
スレッドプールの実装で本記事と関係のある部分を抜き出しました。
まず、自作のThreadPool
を作っており、安全にThreadPoolを終わらせたいというモチベーションで、Dropトレイトを実装しようとしている場面です。なので、dropメソッドを実装する必要があります。
dropメソッドは、fn drop(&mut self)
を要求するので、&mut self
(借用型)ですが、内部で、スレッドがすべて終了するまで待つ必要があるので、std::thread::JoinHandle::join
メソッドを使用する必要があります。しかしながら、fn join(self) -> Result<T>
(第一引数に所有権が要求される)なので、上のコードを動かすと、
error[E0507]: cannot move out of borrowed content
--> src/main.rs:17:27
|
17 | .for_each(|s| s.join().unwrap());
| ^ cannot move out of borrowed content
というエラーになります。これを回避するためのテクニックとして、takeイディオムを用いて次のようにします:
use std::thread;
// Optionでラップする
type Worker = Option<InnerHandle>;
type InnerHandle = thread::JoinHandle<()>;
pub struct ThreadPool {
workers: Vec<Worker>,
}
impl Drop for ThreadPool {
fn drop(&mut self) {
(&mut self.workers)
.into_iter()
.for_each(|worker| {
if let Some(thread) = worker.take() {
// joinはFnOnceで、threadは所有権を持っているので大丈夫
thread.join().unwrap();
}
})
}
}
素敵ですね
他の公式の例としては、https://doc.rust-lang.org/book/second-edition/ch17-03-oo-design-patterns.html#defining-post-and-creating-a-new-instance-in-the-draft-state のrequest_review
がそうで、同じような動機でtakeイディオムを用いています。