質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
Rust

Rustは、MoFoが支援するプログラミング言語。高速性を維持しつつも、メモリ管理を安全に行うことが可能な言語です。同じコンパイル言語であるC言語やC++では困難だったマルチスレッドを実装しやすく、並行性という点においても優れています。

Q&A

解決済

3回答

7518閲覧

構造体 S 内の &mut T なフィールド経由の更新に &mut S を要求されるのは何故か

Eki

総合スコア429

Rust

Rustは、MoFoが支援するプログラミング言語。高速性を維持しつつも、メモリ管理を安全に行うことが可能な言語です。同じコンパイル言語であるC言語やC++では困難だったマルチスレッドを実装しやすく、並行性という点においても優れています。

2グッド

3クリップ

投稿2018/02/21 17:00

rust

1struct S<'a> { y: &'a mut i32 } 2 3fn bar() { 4 let mut x: i32 = 1; 5 let s = S { y: &mut x }; 6 *s.y = 5; 7} 8 9fn baz() { 10 let mut x: i32 = 1; 11 let s = S { y: &mut x }; 12 let z = &s; 13 *z.y = 5; 14}

上のコード片で、 bar() 関数ではエラーは起きませんが、 baz() 関数では次のようなエラーが発生します。

error[E0389]: cannot assign to data in a `&` reference --> /home/dai/workspace/daily/2018/0222/junk00-11-41.rs:15:5 | 15 | *z.y = 5; | ^^^^^^^^ assignment into an immutable reference

ここでコンパイラは z.yimmutable reference であるとしています。

疑問は、

  1. どこを見て z.yimmutable reference としているのか
    S::y の型だけ見れば &mut i32 なのに。
  2. 参照経由で *S::y を書き換えるには s を mutable にするしかないのか
    baz() 関数でも s を mutable にして z を &mut 参照とすればエラーはなくなりました。

以下、1点目の、なぜ immutable reference とされているかを理解するために考えたこと・試したことを記述します。少々長くなってしまいましたが、誤解している点などありましたら指摘していただけると嬉しいです。


以後、式 a が型 T であることを a : T と書きます (ライフタイムは省略) 。a が mutable であることを mut a と書きます。

まず、 S の定義では確かに y : &mut i32 と定義されています。*s.yy そのものに対する変更 (y が参照する先を変更するなど) ではなく y の参照先を書き換える行為なので、 smut s である必要はないと考えました。実際 foo() 関数によりこれは確かめることができています。

*s.y = 5; という処理で s の所有権がそのまま絡むような操作はないと思っています。なので s への参照 z を経由しても同じことができてほしいです。かつ、 mut s が要求されていないところでもし z : &mut S が要求されるとすると違和感があるので、 z : &S で通るはずだと考えます。ところが実際エラーが出ます。

適当に mut を付けて次の qux() 関数のように書き換えると、コンパイルが通るようになりました。

rust

1fn qux() { 2 let mut x: i32 = 1; 3 let mut s = S { y: &mut x }; // mut s に 4 let z = &mut s; // &mut 参照に 5 *z.y = 5; 6}

つまり z : &mut S が要求されています。そして s を &mut 借用するために mut s も要求されます。これはもしかして、 &mut な借用は同時に一つだけ存在するというルールに違反しないためにこのようになっているのでしょうか。確かに z : &S 経由で s.y : &mut i32 にアクセスできてしまうと、 s の immutable な参照ならいくつでも作れてしまうので、 S::y という &mut な参照が複数作成できることになります。

もし S::y : &mut i32 が、 a : S または a : &mut S のときのみ S::y : &mut i32 で、 a : &S のときは S::y : &i32 となると見做されるなら、確かに先のように複数作成できてしまうということはなくなります。

この仕組みを考えていたところで、次の関数 quux() を試してみました。

rust

1fn quux() { 2 let mut x: i32 = 1; 3 let mut s = S { y: &mut x }; 4 let z = &mut s; 5 *s.y = 5; // z -> s に変更 6}

すると、エラーが発生して :

error[E0506]: cannot assign to `*s.y` because it is borrowed --> /home/dai/workspace/daily/2018/0222/junk00-11-41.rs:29:5 | 28 | let z = &mut s; | - borrow of `*s.y` occurs here 29 | *s.y = 5; | ^^^^^^^^ assignment to borrowed `*s.y` occurs here

...つまり、エラーによると &mut s (&s も同様に) は s.y のみならず *s.y まで借用しているということになると思いますが、その認識で正しいでしょうか。それならば &s*s.y&*s.y のような形で借用し、&mut s*s.y&mut *s.y のような形で借用しているということなのでしょうか。

先の

もし S::y : &mut i32 が、 a : S または a : &mut S のときのみ S::y : &mut i32 で、 a : &S のときは S::y : &i32 となると見做される

仕組みはこういうことなのでしょうか。

非常に長くなりましたが、よろしくお願いします。

umyu, yohhoy👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答3

0

ベストアンサー

もし S::y : &mut i32 が、 a : S または a : &mut S のときのみ S::y : &mut i32 で、 a : &S のときは S::y : &i32 となると見做される

これはその通りです。「フィールドが可変(&mut TT)であること」と「構造体自体が可変(&mut SS)であること」の両方が必要です。

対応方法は状況により主に2通りあります。

  • 「その関数だけがそのデータを書き換える」という状況であれば、参照を &mut S にすればよいです。
  • 「他の関数とデータを共有しながら書き換える」という状況であれば、RefCellMutex などの内部可変性 (interior mutability) を用います。

以下、なぜこのような挙動になるのかを説明します。

原則1: 継承可変性

Rustのこのような挙動は継承可変性 (inherited mutability) といいます。フィールドにアクセスするまでの経路が全て mut であるときだけ可変とみなされるというものです。

ところで、Rustのドキュメンテーションをよく読むと、 &T のことはimmutable referenceではなくshared referenceと呼んでいる場合があります。例えばAPIドキュメントには

References, both shared and mutable.

と書いてあります。このようにRustの &mut T/&T には「可変/不変」のほかに、「排他/共有」という見方があります。この考え方を使うと、 &&mut T から &mut T を取り出せない理由が明確になります。

排他と共有による継承可変性の図

上の図で矢印を参照関係とすると、 &&T, &&mut T, &mut &T の3つは、 T が他の参照と直接的あるいは間接的に共有されているのに対し、 &mut &mut T だけは T を他の参照と共有していないことがわかります。

今回のケースは構造体がかかわっているものの、 &&mut T のケースの亜種だといえます。

原則2: 排他制御

ではこれがどうして「可変参照」「不変参照」とも呼ばれるのか。それは一般的な排他制御の規則(ファイルロックやデータベース、並行処理などで使われている)で説明がつきます。つまり、

  • データの読み取りをするときは、同時に書き込みをしている人がいないか確認する。
  • データの書き込みをするときは、同時に読み取りまたは書き込みをしている人がいないか確認する。

というものです。Rustの参照は、

  • 共有参照 &T からは、読み取りだけができる。
  • 排他参照 &mut T には、読み取りと書き込みができる。

という規則によって、静的にこの排他制御を実現していることになります。これによりRustの参照はプログラムの他の部分の干渉を受けないことが保証され、コンパイラによる最適化にとって非常に良いヒントになります。

例外1: 内部可変性

ここまでに述べた原則では、「プログラムの複数の部分でデータを共有しつつ交互に書き換える」ということができません。これらは動的に排他制御を行う仕組みによって実現されます。これらを 内部可変性 (interior mutability) といいます。

RefCellの排他制御の図

上の図で RefCell<T> から T への矢印は実際にはポインタではありませんが、これはある種の門のような役割を果たしています。 T への参照を取得するには RefCell の排他制御を通過する必要があり、同時に書き込みをしようとするとパニック(実行時エラー)になります。

これにより、Rustのエイリアス規則を破ることなく、複数の箇所から交互に書き換えを行うことができます。

例外2: ローカル変数のmut

ローカル変数の mut は上に述べたような「排他/共有」の原則よりも、「可変/不変」という文字通りの意味に近い動作をします。そもそもローカル変数は常に排他的に扱えるものなので、排他/共有の区別は意味がありません。そのためおおよそ以下のような動作をします。

  • そのローカル変数自体への &mut 参照をとるには、 let mut が必要。
  • let x : &mut T の場合は、その &mut 参照はそのまま取れる。

つまり、 let mut は「そのローカル変数自体を書き換えられる」という状況を可能な限り再現する規則になっています。しかし、 let x : Cell<T> とすれば x 自体を書き換えられるので、あくまで「可能な限り再現する」という言い方が妥当だと思います。(また逆に、 let x : Box<T> では T はヒープ上にあるにも関わらずimmutable扱いになります。)

まとめ

Rustのこのような挙動は**継承可変性 (inherited mutability)と呼ばれます。これは &T/&mut T を単に「可変」「不変」として見るのではなく、「排他」「共有」と排他制御の規則の組み合わせで理解するといいと思います。これをオプトアウトする仕組みとしてRefCellなどの内部可変性 (interior mutability)**があります。

投稿2018/02/22 03:40

編集2018/02/22 07:25
qnighy

総合スコア210

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Eki

2018/02/22 06:21

丁寧な回答をありがとうございます。排他 / 共有の考え方でスッキリしました。理解を確認させてください。 mut キーワードには参照の型としての mut と、変数の mutability の意味の mut (パターンで使われる mut) の二種類がある。これらは別物と考え、必ずしも対応しているわけではない --- z : &mut T に対して *z は mut 変数と全く同じではない (これは当初の誤解です) 。 &T だろうが &mut T だろうがデリファレンスした瞬間に T になるので、これが書き換えられるかどうかは別のチェックになる。 参照の型としての mut には「排他」の意味がある。&mut は対象を排他的に参照する。参照を通して安全に参照先を書き換えるには、その参照先を指す参照が唯一であることが必要。 &&T, &&mut T, &mut &T は共有参照の段階で他にも参照が存在しうるので (分かりやすい図をありがとうございます) 、対象となる T を排他的に参照していると保証できない。 &mut &mut T なら排他性を保証できるから、書き換えられる。 変数の mutability の意味の mut は、単に直接的な代入や &mut 参照ができる変数であることを示すのみ (そのローカル変数が可変であることとは必ずしも同値ではない : 例外2, Cell) 。 (しかし参照が唯一であることを静的に保証する継承可変性は制約が強烈なので、内部可変性が導入された。 Cell はコピーで実装されるので排他性は問題にならない。 RefCell は (UnsafeCell でポインタ演算を通して) 一旦コンパイラのチェックを無効化し、チェックを (BorrowRef(Mut)::new 等で自力実装して) 実行時に行い、参照を与える。) 総合すると、 mut s でなくても *s.y = 5; がエラーにならないのは、 値の所有権を持つ存在は唯一であり、かつ、参照されている間は所有権を持つ存在でも書き換えができないため、「s.y という &mut 参照の排他性」に影響を及ぼさない から。しかし、 *z.y = 5; がエラーになるのは、 z : &S, z.y : &mut i32 であり、 &S が複数存在し得るため、 z.y に対する参照が唯一であると保証できないから。「参照が唯一であると保証するため」に z : &mut S が必要。 そして、 mut s については、 実際の *z.y = 5; 操作自体に s が可変である必要はないが、s の &mut 参照を得る際には必要になる ため。 このような考え方で正しいでしょうか?
qnighy

2018/02/22 07:15

はい。それで良いと思います。
Eki

2018/02/22 08:01

理解できました。お付き合い頂き、ありがとうございました。
guest

0

こんにちは。先の回答に補足しますね。エラーメッセージについて。

やや意外かもしれませんが、&&mut T から &mut T は作れませんが、 &T は作れます。

rust

1struct S<'a> { 2 s: &'a mut String, 3} 4 5fn main() { 6 let s = &mut "str".to_string(); 7 let t = S { s }; 8 let t = &t; 9 10 // // cannot borrow data mutably in a `&` reference (assignment into an immutable reference) [E0389] 11 // let s: &mut String = t.s; 12 13 // こっちはOK 14 let s: &String = t.s; 15} 16

おっしゃるとおり参照はコピー可能なので &&mut T から &mut T が作れてしまうと1つしか存在できないはずの&mut T をいくらでもコピーできてしまいますね。
そして &mut T&T のサブタイプのようなものですからキャストできるのは理解できるかと思います。

1, 2の疑問ともにこれ由来のエラーメッセージかと思います。

投稿2018/02/22 03:20

blackenedgold

総合スコア468

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Eki

2018/02/22 03:29

!!なるほど。ということは、 &&mut T と &&T は実質同じもの、ということでしょうか?
Eki

2018/02/22 03:41

fn grault() { let mut x: i32; let s = S { y: &mut x }; let z = &s; *(*z).y = 5; // 明示的にデリファレンスすれば *z : S のはずなので foo() と同じ状況? } と思ったのですが、これもエラー (cannot assign to data in a `&` reference) になります。これがエラーになるべき理由は &mut T をコピーできてしまうから、ということでしょうが、型システム上は何で弾かれているのでしょうか?
qnighy

2018/02/22 03:50

借用が正当かどうかはborrowckという型検査よりも後のフェーズで検査されているので単純なシンタックスでは説明がつかないかと。
Eki

2018/02/22 06:35

確かに、 *z.y : i32 な時点で、 *z.y = 5; は型上は何かの i32 に 5 を代入する操作でしかないわけですよね。 これが書き換えられるかどうかは別のチェックになる、と。例えば i32 が変数なら mut 変数かどうか。例えば参照なら継承可変性として mutable かどうか --- この辺りが borrowck に当たる、ということなのですね。 改めて Rust の mut は型ではなく変数の属性であることを肝に銘じます。
guest

0

もし S::y : &mut i32 が、 a : S または a : &mut S のときのみ S::y : &mut i32 で、 a : &S のときは S::y : &i32 となると見做される

私もそうだと思います。struct 全体またはフィールドを借用する際、フィールドが mutable になるかは、struct のフィールド定義(y: &mut i32)ではなく、struct 自体の mutability で決まるようです。

たとえば以下のように s が immutable の時に y フィールドの mutable な参照を作ろうとしてもコンパイルできません。

rust

1fn baz2() { 2 let mut x = 1; 3 let mut v = 5; 4 let s = S { y: &mut x }; 5 let z_y = &mut s.y; // y フィールドだけ mut 借用 6 *z_y = &mut v; 7}
error[E0596]: cannot borrow field `s.y` of immutable binding as mutable --> src/main.rs:13:20 | 12 | let s = S { y: &mut x }; | - consider changing this to `mut s` 13 | let z_y = &mut s.y; | ^^^ cannot mutably borrow field of immutable binding

smut にするとコンパイルできるようになります。

理由はおっしゃってる通りだと思います。

確かに z : &S 経由で s.y : &mut i32 にアクセスできてしまうと、 s の immutable な参照ならいくつでも作れてしまうので、 S::y という &mut な参照が複数作成できることになります。

たとえば Sfn f(&self) というメソッドを実装したとき、self.y が変更できてしまうと問題があります。

  1. どこを見て z.y を immutable reference としているのか S::y の型だけ見れば &mut i32 なのに。

let z = &s により、s.y が immutable reference になっています。

  1. 参照経由で *S::y を書き換えるには s を mutable にするしかないのか

そうなります。

もしどうしても s が immutable reference の時に s.y を書き換えたかったら、コンパイル時のチェックをあきらめて、Cell などによる interior mutability を使う方法もあります(最後の手段です)

投稿2018/02/22 01:36

編集2018/02/22 02:07
tatsuya6502

総合スコア2035

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Eki

2018/02/22 03:08

丁寧に回答いただき、ありがとうございます。 mut s でない場合、 z_y = &mut s.y; というのが通らないことは納得できます。これは s.y を &mut 借用しようとしているのに s.y は immutable だからで、おっしゃる通り構造体フィールドの mutability は親のものを継承するというルールです。つまり、 s が immutable のときは s.y も immutable だから、 s.y を変更することはできないということですよね。 しかし、疑問なのは、 s.y を変更しようとしているのではなく *s.y (s.y の参照先) を変更しようとしている時にもエラーになることなのです。通常 &mut 参照先の値を変更するのに参照を束縛してある変数が mutable である必要はないはずです。実際、次のように。 let mut x: i32 = 1; let y = &mut x; // y は immutable *y = 5; // OK
tatsuya6502

2018/02/22 03:15

親が参照というのがポイントだと思います。&s であることがフィールドに伝播して & &mut s.y として扱われるのではないでしょうか。
Eki

2018/02/22 03:38

&s であることがフィールドに伝播...伝播するのは mutability だけではないということでしょうか?
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問