LoginSignup
22
5

More than 1 year has passed since last update.

Cloneは用法容量を守って使いましょう。

Posted at

Cloneは用法容量を守って使いましょう。

TL;DR

  • Cloneは、メモリコピーのコストもあるが、リソースの保持者が分かりにくくなるので注意が必要
  • 参照をうまく使うことでCloneを減らすことができる。
  • Cloneは悪でもない。必要なら、簡単に書けるならCloneを使うことも考えるべき。
  • Cloneする前提の形に持って行ってもいい。

Clone、それは魔法の言葉

Rustを使っているということは、ボローチェッカーとの闘いでもある

ボロー(借用)チェッカーというのは、Rust特有の所有権、借用という概念が正しく実装出来ているかをコンパイラが確認することである。
そしてそれが守られてないとコンパイラ様に、

  • 「この変数もう使われたから使わないでね」
  • 「この変数は可変で借りれないよ」

などと叱られるになる。
そして慣れないうちは、「何を言っているのだこいつは」とイラっとする。

そんなボローチェッカーへの最も簡単な答えが
clone()、もしくはto_owned()である。

「ほらCloneしてやったよ。これで静かにしてろ。」

そういわんばかりにclone,clone,cloneと唱えればキャンキャン吠える犬のごときコンパイラもさすがに沈黙してくれるだろう。

しかし、 社会の理不尽で非合理に見えるルールとは違い、 コンパイラはしっかりと訳があってその警告を出している。
そのためRustの思想を理解し、自分のプログラムの構造を理解することで、コンパイラを第三の目とすることができるだろう。
所有権、参照はRustの思想の根幹の一つである。

そのためRustの有名なアンチパターンであるCloneの多用を解消するために説明する。

Q. そもそもCloneでなんで黙らせられるの?

A 所有権が新しいリソースに対してあるから

Rustには所有権の概念がある。
簡単にいうと誰がそのリソース(文字列StringVecFileなどのメモリやコネクションなど)を扱う権限を持っているかという話だ。
Rustでは基本的に一つのリソースに対して一変数を対応させることでいろいろな問題を対処している。
例えば

  • 複数のスレッドから同じ値に書き込めたらデータレースが起こる。
  • ある変数が変わんないことを前提とするロジックに対して途中で変更が入る

などの問題をコンパイル時に抑制することでリソースの管理の明確化と実行時のチェックのコストを低減している。
また余計なデータの移動を防ぐことでの最適化もしてくれる。

ここでcloneするとどうなるか。
cloneするとリソースを新たに作り、その新しいリソースの所有権を得ることでその変数が、自分が所有権を持っていると証明することができる。そのためcloneすると所有権関係のエラーがなくなる。

現実的な例で例えると、テレビを使いたい時に他の人がそのテレビを見たいと言ったらもう一つテレビを買ってくるようなものである。

Q. それならCloneで問題ないのでは?

A. 問題がでる場合もある。

例えば変更したと思った物が反映されないという問題が起きることもある。


let mut a = "xxxx".to_string(); 
let mut b = a.clone();
b += "yyy";

assert_eq!("xxxxyyy", a); // error!

この程度の例なら簡単に思えるが、複雑になってくると見落とすこともあるのではないか?

それにクローンするということは大抵の場合deep copyすることが多い。そのため実行速度とメモリに余計なコストがかかることになる。

また見た目が悪いし、いちいち書くのが手間になってくるのではないか。

テレビの例だとある番組を見るためだけにテレビを買ってきて、見終わったらテレビを捨てるということである。

Q. ではどうやって解消するの?

A1. 基本的には借用を使う。

そもそもとして所有権自体を持っている必要な場合は少ない。
大抵の場合借用をして参照を渡せばいいだけのことが多いのである。

借用とは?

借用とは、所有権ではなく、参照する権利を渡すことである。

テレビの例だとすると所有権者はテレビを持っていれば良くて、
テレビを見る権利は渡してもいい。
同じ番組を見たい人がいるならテレビを買ってこなくても、横からのぞきこめばいいのである。

ただチャンネルを変える権利(リモコン)も渡せるが、リモコンを複数渡してもチャンネルがころころ切り替わるような状態では画面をみてはいられないだろう。
そのために可変参照は一つしかとれないようになっている。

借用を使おう

大抵cloneが必要になるのは

  • 関数に入れるときに所有権を持っていることを要求される
  • 参照として引数に渡されたものから所有権付きで返す必要がある

関数が所有権を要求する

まず一番の例だが、その関数が自分のものであるとき引数を参照にできないかを考える。


// clone()でなくてto_owned()だがほとんど同じ
let x = "aaa".to_owned();

fn f1(x : String) {
    println!("hello {x}");
}

f1(x)


//to_owned()が消えた。
let x = "aaa";

fn f2(x : &str) {
    println!("hello {x}");
}

f2(x);

それから参照で使ってから最後に所有権を手放すつもりで使えば、cloneする必要がなくなる。


fn g1(y: String){}
fn g2(y: &str){}

let y = "aaa".to_owned();
g1(y.clone());
g2(&y);

g2(&y);
g1(y);

ただこれはロジックとして許される限りである。


返り値が所有権を要求する

二つ目の例だが、本当に所有権付きで返す必要があるかを考える。
その返り値は参照で返せないか?
ただこれもロジックの意味論的に許す限りである、

#[derive(Debug)]
struct P1{
    name : String
}

fn make_p1(s : &str) -> P1{
    P1{ name : s.to_owned() }
}

let p = make_p1(x);
println!("{p:?}");


#[derive(Debug)]
struct P2<'a>{
    name: &'a str
}

fn make_p2(s : &str) -> P2 {
    P2{ name: s }
}

let p = make_p2(x);
println!("{p:?}");

借用を使うかどうか

おおよそ関数を呼ぶということはこういったことになるはず。
引数から値を加工して新しく値を作って返すとき、

  • 元の値はもう使わないならcloneせずとも消費(consume)して所有権が渡せる
  • 元の値自体を書き換えていいなら可変参照を使う(返り値で返さず変更するだけにする)
  • 元の値をそのまま使うならclone、またはそれに類する処理が必要

また参照で関数を渡していけば、必要になったときだけcloneするということもできる。

fn check(x : &str) -> Result<(),Error>{
    match x {
        "y" => Ok(()),
        err => Err(Error::CheckFail(err.to_owned()))
    }
}

慣れてくれば、どこで参照が使えてどこで所有権を渡し、どこでcloneするかが分かるようになってくる。
今のコンパイラはエラーが親切なのでそれを読むと代替解消できることが多い。
(ただそれでも分かりずらかったり、簡単には直せなかったりする。)

Cow

ちなみに変更する直前まで参照として扱い、変更の必要があれば内部でclone(厳密にはto_owned)するというCowというものもある。

const AAA : &'static str = "aaa";
let mut c : Cow<_> = AAA.into();//cowにする
let c_ref : &str = &c;// c.as_ref();でも同じ
assert_eq!(AAA.as_ptr(), c_ref.as_ptr());//cowにいれてもポインタの位置は同じ
c += "yyy";
println!("{c}");
assert_ne!(AAA.as_ptr(), c.as_ref().as_ptr());//cowに変更が入ると自分でcloneしてそこに変更する。

A2. Rc,Arcを使う

Cloneが問題になるのはRustのCloneは基本的にdeep copyをしているから。
これをshallow copyにするためにRc,Arcを使う。
Rc(参照カウンタ)はリソースを共有しながら、所有権を持たせる手法である。
基本的には機能は同じで、同期的に使うならRc,非同期に使うならArcを使うことになる。
GCのある言語のObjectに近いものである。
これによってcloneのコストを下げることができる。

(ただ参照渡しと比べて参照カウンタの変更などのコストはある。)


let a : std::rc::Rc<str> = "xxx".into();
let b = a.clone();

assert_eq!(a.as_ptr(), b.as_ptr()); // cloneしても指しているポインタは同じ

まとめ

  • Cloneは所有権の問題を解消するために使いがちだが、根本的になぜCloneが必要か考えるべきである。
  • 参照をうまく使えばCloneが減る。
  • Rc,Arcなどでshallow cloneするように変更するとコストが減る。

余談 CloneToOwned

cloneは自分自身の型にリソースを複製する。
to_ownedは自分の型とは違う型だが同じリソースを使うように複製する。

例えば<&str>::to_owned()String型を返す。

22
5
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
22
5