わかる!?Rustの所有権システム

6 min read

「Rustの所有権が難しい」という話しがときどきありますが、所有権システムの理解を助ける記事にしてみたいと思います。

所有権とはリソースを開放する権利のこと

所有権とは関数がリソース(資源)を開放する権利のことです。所有するというより消費されてしまうイメージに近いです。

以下のように変数の束縛での解説が多いですが、たぶんつまずくのは関数のほうかなと。

let a = String::from("abc");
let b = a;
println!("{}", a); // bに所有権が移動するのでコンパイルエラー

let c: u32 = 1;
let d: u32 = c;
println!("c = {}, d = {}", c, d); // 基本データ型はCopyトレイトを実装しているのでコンパイル可能

というわけで、ここから関数の例で解説します…。

以下のfoo関数はb: Barを所有します。関数が終了するときにbを所有しているので破棄(Drop)されます。関数が終了するときにbを所有していないなら破棄されません(あとで詳しく解説します)。

fn foo(b: Bar) {
  /* 直接的にbを所有して利用 */
} // この関数がbの所有権を持っているのでDropされる

let a = Bar::new();
foo(a);
// a は fooに所有されてしまうので、使えなくなる

そのリソースの所有権は複数ヵ所で持てません。所有者が最後にそのリソースを開放するため、常に一ヵ所です。これに違反するコードを書くとコンパイルできません。

所有権の移動(ムーブ)とは

所有権を奪ったうえで、ほかの関数に所有権を譲渡するとどうなるか。以下の例ではself.bar_list.pushに所有権が渡るのでfooの最後でbは破棄されません。このように所有権が移動することをムーブと呼びます。ちなみに、実体型に定義したからといって、コピーコストは掛かりません。 ムーブでは(セマンティクス上の)コピーが発生しません。

fn foo(&mut self, b: Bar) {
  self.bar_list.push(b); // ムーブ
} // この関数がbの所有権を持っていないのでDropしない

破棄されるタイミングは変わりますが、所有権を奪われることは変わりありません。

let a = Bar::new();
x.foo(a); // ムーブ
// a は fooに所有(消費)されてしまうので、使えなくなる

どこで破棄されるかいちいち解説していますが、実際のコードを書くときは気にしていません。それはrustcに任せるべきだからです。

ちなみに、ムーブが起こる操作は以下(実践Rust入門より引用)。

  • パターンマッチ(match式だけではなくlet文による変数の束縛も含む)
  • 関数呼び出し
  • 関数やブロックからのリターン
  • コンストラクタ
  • moveクロージャ

https://amzn.to/2WYAimY

所有権の借用(ボロー)とは

あるリソースの所有権を奪う関数を呼び出したあとも、そのリソースを利用したい場合複製を渡す以外には、その関数の戻り値でリソースの所有権を戻すしかありません。不便!

fn foo(b: Bar) -> Bar {
  // ... 
  b // 関数からのリターンで所有権をムーブできる
}

let a = Bar::new();
let a = foo(a.clone()); // 複製をムーブする
println!("{:?}", a);

そういうときに使うのが参照を使った借用(ボロー)です。引数の型は参照型になります。借用した場合はその名の通り破棄されません。その関数がリソースを所有していないからです。

fn foo(b: &Bar) {
  /* bを借用して利用 */
} // 参照のbはDropされない

let a = Bar::new();
foo(&a); // 借用
println!("{:?}", a);

借用(参照)を使う典型例としては、メンバーの参照を返すケースですね。

pub fn name(&self) -> &str {
  &self.name
}

移動(ムーブ)/借用(ボロー)の好ましくない例

以下の公式ドキュメントが参考になるかもしれません。補足を入れながら考えて見ます。

https://sinkuu.github.io/api-guidelines/flexibility.html#呼び出し側がデータをコピーするタイミングを決める-c-caller-control

参照を受け取るが、関数内部でわざわざ複製を作る例

引数の所有権を要する関数は、借用して複製するのではなく所有権を受け取るべきです。

// 良い例:
fn foo(b: Bar) {
  /* 直接的にbを所有して利用 */
} // この関数がbの所有権を持っているのでDropされる

// 悪い例:
fn foo(b: &Bar) {
  let b = b.clone(); // わざわざcloneする…。
  /* 複製後にbを所有して利用 */
} // 複製されたbがDropされるが、参照のbは借用なのでDropされない

悪い例はかなり恣意的ですが、、、無意味なcloneをするぐらいなら最初から所有権を奪うほうがよいでしょう。ムーブされてしまうと困るという話なら以下のようにcloneすればよいです。悪い例と何が違うのかというと、呼び出し元に複製の決定権があり設計に柔軟性があるということです。

let a = Bar::new();
foo(a.clone()); // 呼び出し元が複製を作るかを決める
println!("{:?}", a);

所有権を奪うが、関数内部で所有権を利用しない例

もし関数が引数の所有権を必要としないのなら、 所有権を取って最終的にdropする代わりに可変あるいは非可変借用を受け取るべきです。

// 良い例:
fn foo(b: &Bar) {
  /* bを借用して利用 */
} // 参照のbはDropされない

// 悪い例:
fn foo(b: Bar) {
  /* 実質的にbは所有されず借用のみ。関数の最後にDropされる */
} // この関数がbの所有権を持っているのでDropされる

どのようなシグニチャがよいか

前述のことを踏まえて、selfのメンバーにname: Nameというメンバーがあるとして、その値を読むケース、書くケースでどういうシグニチャがよいのか簡単にまとめてみます。

値を読む

※Tは任意の型

  • (&self) -> &T

値を読むだけであれば、このように参照を取得するだけでよい。

pub fn name(&self) -> &Name {
  &self.name
}
  • (&self) -> T

パフォーマンスの最適化の面で考えると、cloneはできる遅延させるべき。本当に複製がほしいかは呼び出し元で判断したい。

pub fn name(&self) -> Name {
  self.name.clone() // 実体が必要かどうかわからない状況で 先に複製を作る…
}
  • (self) -> &T

selfの所有権を奪っているのでこのメソッドの最後で破棄されます。当然nameも破棄されます。そのnameの参照は返せないのでコンパイルエラーです。

pub fn name(self) -> &Name {
  &self.name // コンパイルできない
} // selfがDropされる
  • (self) -> T

selfは破棄されますが、nameがムーブします。つまりnameだけ残してほかは捨てることになります。名前を読みたいだけなのにselfが破棄されるのは都合が悪そうです。

pub fn name(self) -> Name {
  self.name // nameだけムーブする
} // selfがDropされる

このパターンはどういうときに使いやすいかというと、selfから別のインスタンスを生み出すときに使えます。たとえばビルダのbuildメソッドなどです。

pub fn build(self) -> Request {
  Request::new(self.name, self.group_id)    
}

selfで受け取るときって、そのまま何もしなければ破棄されてしまいます。ですので、一般的には読み込みというよりselfからほかの値への変換になるでしょう。

値を書く

関数内部でコレクションに追加したり、メンバーに代入する際は所有権が必要なります。

  • (&mut self, &T)

参照だとcloneすることになります。最初から所有権を奪ったほうがよいでしょう。

pub fn add_name(&mut self, name: &Name) {
  self.names.push(name.clone());
}
  • (&mut self, T)

ということでこちらのほうがよいです。

pub fn add_name(&mut self, name: Name) {
  self.names.push(name);
}

Nameを構造体のメンバーに持つ場合は、インスタンスを作成するファクトリ関数で所有権を奪う必要があります。

pub fn new(name: T) -> Person {
  Person {
    name
  }
}
  • (mut self, &T)

読み込みの例でも言及しましたが、mut selfでも戻り値を返さないとselfが破棄されるだけです。ほとんど意味がありません。

pub fn add_name(mut self, name: &Name) {
  self.names.push(name.clone());
} // selfがDropされる…
  • (mut self, T)

上記と同様です。

pub fn add_name(mut self, name: Name) {
  self.names.push(name);
} // selfがDropされる…

mut selfを取る場合も変換のユースケースになるでしょう。

可変参照のときだけ呼べるようにしたいなら、build0のようになりますがbuild1のように内部で可変にできます。

pub fn build0(mut self) -> Request {
  self.name = "(" + self.name + ")";
  Request::new(self.name, self.group_id)    
}

pub fn build1(self) -> Request {
  let mut temp = self;
  temp.name = "(" + self.name + ")";
  Request::new(self.name, self.group_id)    
}

まとめると、だいたいパターン化されます。Rustの場合は型に関する情報が多いので、シグニチャを見ただけ内部で何が起きるか分かります。

読み込む際は(&self) -> &T

pub fn name(&self) -> &Name {
  &self.name
}

書き込む際は(&mut self, T)

pub fn add_name(&mut self, name: Name) {
  self.names.push(name);
}

ファクトリを書く場合

pub fn new(name: Name) -> Person {
  Person {
    name
  }
}

selfから何かに変換する場合

pub fn build(self) -> Request {
  Request::new(self.name, self.group_id)    
}

Discussion

ログインするとコメントできます