5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

スライスの実装を通してRustのRc, RefCellの理解度が分かるコード例 (Go, Julia との比較)

Last updated at Posted at 2022-01-03

概要

最近、単方向リストを試しに実装してみたが、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 のサンプルだと値も書き換えられるので、まずは RcRefCell を使います。上位互換的存在ですね。

複数の所有者 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::Cellstrを使った例です。

Cellは内部可変性を持ちスライスの宣言が Immutable でも値の置き換えができます。なお、 Cellget() は 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は競合の要因をプログラマー以上にコンパイラーが知っていて、それに気づかせてくれます。大抵、コンパイラーを解消していると競合の問題の大半は消滅しています。

この例は単純ですが、リストを自作して見ると同じようなエラーではまります(それは別途)。

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?