概要
最近、単方向リストを試しに実装してみたが、Rc や RefCell の理解度が大いに試された。それもあって、腕試しになりそうな題材を乗せる。
環境
- rustc 1.57.0 (Rust 2021)
- WSL1 (Ubuntu 18.04 LTS)
問題の紹介
次のプログラムが Rust で書けると Rc, RefCell の基本的な理解ができていると思います。自信のあるひとは解答をみないで書いてみてください。
実力チェック (Go, Julia の例)
以下の配列で、競合する要素 b[0] を XXX
に書き換えてみるというものです。
index | names | a=names[0:2] | b=names[1:3] | 期待値 |
---|---|---|---|---|
0 | John | John | John | |
1 | Paul | Paul | Paul <= XXX に書き換え | XXX |
2 | George | George | George | |
3 | Ringo | Ringo |
Go と Julia だとこんな感じです。ざっと調べた感じだと、Pythonあたりは標準ではできなさそうですね。
Goでの実装例
package main
import "fmt"
func main() {
names := [4]string{
"John", "Paul", "George", "Ringo",
}
fmt.Println(names)
a := names[0:2]
b := names[1:3]
fmt.Println(a, b)
b[0] = "XXX"
fmt.Println(a, b)
fmt.Println(names)
}
実行例
[John Paul George Ringo]
[John Paul] [Paul George]
[John XXX] [XXX George]
[John XXX George Ringo]
Juliaでの実装例
@view
を使います。
names = ["John", "Paul", "George", "Ringo"]
println("names=$(names)")
a = @view names[1:2]
b = @view names[2:3]
println("names[1:2]=$(a) names[2:3]=$(b)")
println("names[2:3][1]=XXX")
b[1] = "XXX"
println("names[1:2]=$(a) names[2:3]=$(b)")
println("names=$(names)")
実行例
names=["John", "Paul", "George", "Ringo"]
names[1:2]=["John", "Paul"] names[2:3]=["Paul", "George"]
names[2:3][1]=XXX
names[1:2]=["John", "XXX"] names[2:3]=["XXX", "George"]
names=["John", "XXX", "George", "Ringo"]
Pythonでの実装(NG)
listではできない模様
names = ["John", "Paul", "George", "Ringo"]
print(f"names={names}")
a = names[0:2]
b = names[1:3]
print(f"names[1:2]={a} names[2:3]={b}")
print("names[2:3][1]=XXX")
b[1] = "XXX"
print(f"names[1:2]={a} names[2:3]={b}")
print(f"names={names}")
names=['John', 'Paul', 'George', 'Ringo']
names[1:2]=['John', 'Paul'] names[2:3]=['Paul', 'George']
names[2:3][1]=XXX
names[1:2]=['John', 'Paul'] names[2:3]=['Paul', 'XXX']
^^^^^^ NG
names=['John', 'Paul', 'George', 'Ringo']
^^^^^^ NG
Rustでの実装例 (コンパイルエラー)
まずは、RustのスライスでGoやJuliaと同じように実装すると怒られます。
fn main() {
let mut names = [
String::from("John"),
String::from("Paul"),
String::from("George"),
String::from("Ringo")
];
println!("{:?}", names);
let a = &names[0..2];
let b = &mut names[1..3]; // ここ!
println!("{:?} {:?}", a, b);
b[0] = String::from("XXX");
println!("{:?} {:?}", a, b);
println!("{:?}", names);
}
Immutable(変更不可)なスライス同士ならいいのですが、すでに Immutable のスライスがあると、規則違反で怒られます。
error[E0502]: cannot borrow `names` as mutable because it is also borrowed as immutable
--> slice_pointers.rs:11:18
|
10 | let a = &names[0..2];
| ----- immutable borrow occurs here
11 | let b = &mut names[1..3];
| ^^^^^ mutable borrow occurs here
12 | println!("{:?} {:?}", a, b);
| - immutable borrow later used here
error: aborting due to previous error
For more information about this error, try `rustc --explain E0502`.
Rustでの実装例
他の言語と異なり、Rustでは次の操作で実装指針が異なる。
- 参照先の値を変更できるようにする
- 値を置換する
Rc<RefCell<String>>版
もともとの Go のサンプルだと値も書き換えられるので、まずは Rc
と RefCell
を使います。上位互換的存在ですね。
複数の所有者 vec![..]
とスライス b
が居るので Rc を通じて共有ができます。Rcを使わないケースでは、コピーなりの動作で別物です。(ポインタを出力すると違いが分かります)
RefCell では一時的に更新ができるようになります。これにより、コンパイルでは規則違反にならずに済みます。*b[0].borrow_mut() = String::from("XXX")
の部分は、今回の例だと b[0].replace(String::from("XXX"));
でもよい。
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let names = vec![
Rc::new(RefCell::new(String::from("John"))),
Rc::new(RefCell::new(String::from("Paul"))),
Rc::new(RefCell::new(String::from("George"))),
Rc::new(RefCell::new(String::from("Ringo")))
];
println!("names={:?}", names);
let a = &names[0..2];
let b = &names[1..3]; // mut 指定は不要で
println!("a={:?} b={:?}", a, b);
*b[0].borrow_mut() = String::from("XXX");
println!("a={:?} b={:?}", a, b);
println!(
"names={:?}",
names.iter().map(
|x| x.borrow().clone()
).collect::<Vec<String>>()
);
}
RefCell<String>版
実はスライスを使っている関係で Rc は必要ないという…ことに気づく。
b[0].borrow_mut().push_str("a");
のようなこともできる。
use std::cell::RefCell;
fn main() {
let names = [
RefCell::new(String::from("John")),
RefCell::new(String::from("Paul")),
RefCell::new(String::from("George")),
RefCell::new(String::from("Ringo")),
];
println!("names={:?}", names);
let a = &names[0..2];
let b = &names[1..3];
println!("a={:?} b={:?}", a, b);
b[0].replace(String::from("XXX"));
println!("a={:?} b={:?}", a, b);
println!(
"names={:?}",
names.iter().map(
|s| s.borrow().clone()
).collect::<Vec<_>>()
);
}
RefCell<&str>版
&str
でも実装できる。不変オブジェクトなので borrow()
は使えても borrow_mut()
は使えないことに注意する。
use std::cell::RefCell;
fn main() {
let names = [
RefCell::new("John"),
RefCell::new("Paul"),
RefCell::new("George"),
RefCell::new("George"),
];
println!("names={:?}", names);
let a = &names[0..2];
let b = &names[1..3];
println!("a={:?} b={:?}", a, b);
b[0].replace("XXX");
println!("a={:?} b={:?}", a, b);
println!(
"names={:?}",
names.iter().map(
|s| s.borrow().clone()
).collect::<Vec<_>>()
);
}
Cell<&str>版
std::cell::Cellとstrを使った例です。
Cellは内部可変性を持ちスライスの宣言が Immutable でも値の置き換えができます。なお、 Cell
の get()
は Copyトレイトを要求するので、Cell<String>
版は実装できない。
set(...)
の代わりに replace(...)
も使えるが復帰値を使わない場合は set(...)
がよりシンプルである。
use std::cell::Cell;
fn main() {
let names = [
Cell::new("John"),
Cell::new("Paul"),
Cell::new("George"),
Cell::new("Ringo")
];
println!("names={:?}", names);
let a = &names[0..2];
let b = &names[1..3];
println!("a={:?} b={:?}", a, b);
b[0].set("XXX");
println!("a={:?} b={:?}", a, b);
println!(
"names={:?}",
names.iter().map(
|s| s.get()
).collect::<Vec<_>>()
);
}
実行結果
names=[Cell { value: "John" }, Cell { value: "Paul" }, Cell { value: "George" }, Cell { value: "Ringo" }]
a=[Cell { value: "John" }, Cell { value: "Paul" }] b=[Cell { value: "Paul" }, Cell { value: "George" }]
a=[Cell { value: "John" }, Cell { value: "XXX" }] b=[Cell { value: "XXX" }, Cell { value: "George" }]
names=["John", "XXX", "George", "Ringo"]
まとめ
スライス周りは、言語による思想の違いが出て興味深いですね。
Rustは競合の要因をプログラマー以上にコンパイラーが知っていて、それに気づかせてくれます。大抵、コンパイラーを解消していると競合の問題の大半は消滅しています。
この例は単純ですが、リストを自作して見ると同じようなエラーではまります(それは別途)。