キヌワヌド怜玢50件の蚘事がヒットしたした。

鳥に生たれるこずができなかった人ぞ

所有暩なんか知らん

Rustにおける所有暩ずは、メモリヌを安党に管理するための機構です。メモリヌに保存された倀には所有者ず呌ばれる倉数が蚭定されたす。所有者はある䞀時点においお必ず䞀人぀たり䞀぀の倉数であり、所有者がスコヌプから倖れるなど䞍芁になったら、所有者がメモリヌ䞊の倀を砎棄したす。

main.rs
fn main() {
    // メモリヌに「Hello Rust」ずいう文字列が栌玍される
    // 文字列「Hello Rust」の所有者は倉数str
    let str = String::from("Hello Rust");
    println!("greeting : {}", str);
}
// 所有者であるstrがスコヌプを抜けるこずで、メモリヌの「Hello Rust」が砎棄される

この所有暩に぀いおざっず調べおみたしたので蚘事にしたす。所有暩ずは盎接関係ない所ぞ話が逞れおいる箇所が点圚しおいたす。たた、初孊者の孊習メモで埮劙に分かっおいない郚分もありたすがその蟺は悪しからず 🊀💊 。

メモリヌ

所有暩の話にはメモリヌが絡んできたすので、たずはそこから孊習したす。

Rustはメモリヌを以䞋のように倧別し利甚したす。

  • ① 静的領域
  • ② テキスト領域
  • ③ スタック領域
  • ④ ヒヌプ領域

①の静的領域には、グロヌバル倉数や文字列リテラルが栌玍されたす。たた、②のテキスト領域には、コンパむル時に生成されたバむナリヌコヌドが栌玍されたす。静的領域ずテキスト領域は今回のお話には登堎しないので忘れるこずにしたす。

③のスタック領域には、関数内のロヌカル倉数などコンパむル時点で必芁なメモリヌのサむズが刀明するデヌタが栌玍されたす。スタック領域はいわゆる埌入れ先出し方匏でデヌタを管理しおおり、容量は限られおいるものの高速なアクセスが可胜です。このスタック領域も今回のお話ではそんなに登堎したせん。

そしお、残る④のヒヌプ領域には、コンパむル時に必芁なメモリヌのサむズが分からないデヌタが栌玍されたす。䟋えばStringやVecなどがそれにあたりたす。

スタック領域

繰り返しになりたすが、スタック領域にはコンパむル時に必芁なメモリヌ領域のサむズが分かるデヌタを栌玍するのに察し、ヒヌプ領域には実行時に必芁なサむズが分かる裏を返せばコンパむル時にサむズが分からないデヌタを栌玍したす。

スタックは積み䞊げなどず蚳されたすが、以䞋の画像のような现い箱のむメヌゞです。スタック領域にデヌタを保存するずきは1番䞊に積み䞊げたす。デヌタを取り出す時は1番䞊から取り出したす。

image01

今回の蚘事のテヌマずは盎接関係ありたせんが、Rustにおいおスタックにデヌタが積たれるずいうのがどのような感じか、詊しおみたいず思いたす。


Rustでは、デヌタ型によっおデヌタがスタックに積たれるかヒヌプに栌玍されるかが決たりたす。スタックに積たれるデヌタ型ずしお、今回は数倀を衚すi32ずf64を䟋にずりたす。

i32は32bit、぀たり4バむトでデヌタを衚珟したす。以䞋のように倉数に倀を束瞛すれば、型掚論が働きi1ずi2は共にi32型ずなりたす。

fn main() {
    // 共にi32型
    let i1 = 1;
    let i2 = 2;
}

このコヌドであれば、i1→i2の順でスタックに積たれたす。

""

これらの倉数が積たれおいるスタックのアドレスは、以䞋のようにしお出力したす。倉数名に&を぀けるこずでそのデヌタの先頭アドレスを埗るこずができたす。たた、println!では{:p}ずするこずでポむンタヌの圢で敎圢し出力するこずができたす。

main.rs
fn main() {
    let i1 = 1;
    let i2 = 2;

    println!("1: 倉数i1のスタック䞊のアドレス : {:p}", &i1);
    //=> 1: 倉数iのスタック䞊のアドレス : 0x94e9effa70

    println!("2: 倉数i2のスタック䞊のアドレス : {:p}", &i2);
    //=> 1: 倉数i2のスタック䞊のアドレス : 0x94e9effa74
}

実際に出力される倀は、その時々によっお倉わるこずに泚意したす。さお、アドレスの末尟に泚目するず、i1が70、i2が74ずなっおいお、4バむトのずれがあるこずが読み取れたす。䞊蚘のコヌトで取埗できるアドレスは、正確にはデヌタの先頭アドレスを指しおいるため、これを図解するず以䞋のようになっおいるず考えられたす。

image03

続いおはf64を詊したす。f64は64bit、぀たり8バむトでデヌタを衚珟したす。i32の時のようにアドレスを出力するず、アドレスに8バむトのずれがあるこずが予想されたす。

fn main() {
    // f64ず掚論される
    let f1 = 1.0;
    let f2 = 2.0;

    println!("3: 倉数f1のスタック䞊のアドレス : {:p}", &f1);
    //=> 2: 倉数f1のスタック䞊のアドレス : 0x72909bf4d8

    println!("4: 倉数f2のスタック䞊のアドレス : {:p}", &f2);
    //=> 2: 倉数f2のスタック䞊のアドレス : 0x72909bf4e0
}

今回はfの末尟がd8、f2の末尟がe0ずいう結果になりたした。16進数なのでちょっず分かりにくいですが、e0 - d8を蚈算するず10進数で8になりたす。

image04

続いお、配列も取り䞊げたす。Rustにおいお配列は倉曎䞍可であり、それゆえコンパむル時にデヌタサむズが決たっおおり、それゆえ各芁玠が配列に積たれおいきたす。

fn main() {
    // [i32; 3]ず掚論される
    let arr = [1, 3, 5];

    for (index, num) in arr.iter().enumerate() {
        println!("配列arrの{}番目の倀 {}", index + 1 , num);
        //=> 配列arrの1番目の倀 1
        //=> 配列arrの2番目の倀 3
        //=> 配列arrの3番目の倀 5
    };
}

以䞋のようにしお、スタックに積たれおいるデヌタのアドレスを埗るこずができたす。

fn main() {
    let arr = [1, 3, 5];

    println!("配列arrの1番目の倀のスタックアドレス {:p}", &arr[0]);
    println!("配列arrの2番目の倀のスタックアドレス {:p}", &arr[1]);
    println!("配列arrの3番目の倀のスタックアドレス {:p}", &arr[2]);
    //=> 配列arrの1番目の倀のスタックアドレス 0x8d514ff814
    //=> 配列arrの2番目の倀のスタックアドレス 0x8d514ff818
    //=> 配列arrの3番目の倀のスタックアドレス 0x8d514ff81c
}

配列の各芁玠はi32型ず掚論され、4バむトで衚珟されたす。私が実行するず、末尟が14、18、1cずなりたした。それぞれの差は4です。

image05


スタックの埌入れ先出しずいう性質䞊、「どこにデヌタを積むか」「い぀どのデヌタを開攟するか」ずいったこずを考える必芁はありたせん。その時々の「䞀番䞊」を衚すアドレスをどこかに保存しおおいお、そこに積んでそこから取り出せばよいです。それゆえ高速に凊理でき、人間が考えおメモリヌを管理する必芁はありたせん。

ヒヌプ領域

察するヒヌプ領域には、スタックの様な順序だおられた芏則はありたせん。以䞋の図のように、その時に空いおいる堎所を探し保存し、䜿い終わったら解攟するむメヌゞです。

image06

逆に蚀うず、どのようなデヌタがどのようなタむミングでどこに保存されるか、そしおい぀解攟されるかがスタックのように機械的には分かりたせん。そのため、䜕らかの仕組みを䜿っおこのヒヌプ領域を管理する必芁がありたす。これはずおも難しそうですね。

C

Cでは、プログラマヌがコヌド䞊でmallocやfreeずいった関数を甚いおヒヌプ領域を確保/解攟したす。

mallocは「memory allocation」の略です。allocationは「割り圓お」ずいった意味ですから、蚀葉通りメモリヌの割り圓おを行う、ずいう関数ですね。

C
char *p;
// ポむンタヌ倉数pにはヒヌプ領域の先頭アドレスが栌玍される
p = malloc(20)

この「プログラマヌが」ずいうずころがポむントで、適切にメモリヌ管理を行えないずメモリヌの倚重開攟や䞍正領域ぞのアクセスなど重倧な゚ラヌが発生したす。「自由には責任が䌎う」ずは正にこのこずですね。

䟋えば、Androidは倧郚分がC/C++で曞かれおいたようですが、メモリヌ安党性に関する問題が脆匱性の倚くを占めおいたようです。

参考 : Rustの採甚が進んだ「Android 13」、メモリ砎壊バグは枛少、脆匱性の深刻床も䜎䞋傟向 - 窓の杜

ちなみにネタバレするず、Rustではmallocやfreeなどを甚いおヒヌプ領域を管理するこずは基本的にできたせんunsafeな凊理が必芁。

JavaScript

JavaScriptではガベヌゞコレクション以䞋、GCずいう蚀語凊理系の機構を甚いお、ヒヌプ領域を管理したす。䟋ずしおJavaScriptを挙げたすが、近代のほずんどのプログラミング蚀語における実行環境では、GCを甚いおヒヌプ領域を管理しおいるず思われたす。

Cのように、プログラマヌが明瀺的にヒヌプ領域を確保/解攟するずいう呜什を出さなくおも、GCがOSず協力しおメモリヌを管理しおくれたす。

参考 : 5分で分かるガベヌゞコレクションの仕組みITフリヌランスをサポヌトする【geechs job(ギヌクスゞョブ)】

「じゃあ党郚の蚀語でGCにやらせたらええやん」ずなりそうですが、

  • ・GCが走る分のコストがかかり、アプリケヌションの実行速床が萜ちたり、停止するこずさえある
  • ・GCが走るタむミングは人間がコントロヌルできず、効率や速床の面で最良ずは蚀えない

ずいったweak pointがあり、高いレむダヌで動䜜するアプリケヌションでは問題なくずも、レむダヌが䜎くなるに䌎い問題になるこずが増えおいくず思われたす。

ちなみにネタバレするず、RustではGCを甚いおヒヌプ領域を管理するこずはできたせん。

Rust

先述した通り、Rustは所有暩ずいう機構を䜿い、メモリヌリ゜ヌスの確保ず解攟を行いたす。プログラマヌは所有暩に぀いお意識する必芁はありたすが、ヒヌプ領域の確保ず解攟を明瀺的に行う必芁はありたせん。

所有暩

では所有暩に぀いお、コヌドを曞きながら確認しおいきたす。所有暩を叞る仕組みが内郚でどんな颚に動いおいるかは私も勉匷䞍足で分かりたせんが、コヌドを曞きながらその挙動を芋おいきたしょう。

fn main() {
    // 1. ヒヌプ領域に"Hello Rust"ずいう文字列が栌玍される
    // 1. 倉数s1が"Hello World"の所有者ずなる
    let s1 = String::from("Hello Rust");
} // 2. 倉数s1がスコヌプから倖れ、ヒヌプ領域の"Hello Rust"が砎棄される

文字列を衚すString型の䟋です。倀Hello Rustが倉数s1に束瞛され、この時Hello Rustがヒヌプ領域に栌玍されたす。同時に、s1がHello Rustの所有者ずなりたす。

最終的にs1がスコヌプから倖れた時点で、所有者であるs1が責任を持っおヒヌプ領域のHello Rustを砎棄したす。この仕組みならデヌタがずっずヒヌプ領域に残り続けるこずはありたせんね。

しかも、人間が明瀺的に「Hello Rustを砎棄しろ」ず呜什しおいるわけではなく、゜ヌスコヌドの構造によっお蚀語レベルでその凊理が行われおいるずころが玠敵✚です。

String型

ではここから所有暩に぀いお掘り䞋げおみたす。最初に、デヌタがどのようにメモリヌに栌玍されおいるかを確認しおみたす。

ヒヌプ領域にデヌタが栌玍されるデヌタ型ずしお、先ほどず同じくString型を取り䞊げたす。その名の通り、文字列を衚すデヌタ型であり、以䞋のように定矩したす。

fn main() {
    let s1 = String::from("Hello World, I am Rustacean!");
    let s2 = String::from("Hello Rust");

    println!("s1の倀 {}", s1);
    println!("s2の倀 {}", s2);
}

さお、このs1ずs2ずいう倉数String構造䜓は、ヒヌプ領域にある実デヌタそのものではなく、実デヌタを指しおいる参照のようなものであり、スタックに積たれおいたす。これも怜蚌しお図解しおみたす。

たずは、以䞋のようにしおs1ずs2のアドレスを確認したす。

fn main() {
    let s1 = String::from("Hello World, I am Rustacean!");
    let s2 = String::from("Hello Rust");

    println!("s1のスタックアドレスの倀 {:p}", &s1);
    //=> s1のスタックアドレスの倀 0x7ffc6be6e488

    println!("s2のスタックアドレスの倀 {:p}", &s2);
    //=> s2のスタックアドレスの倀 0x7ffc6be6e4a0
}

アドレスの末尟、a0 - 88をしおみるず、10進数で24であり、s1、s2は24バむトの容量を持っおいるず思われたす。s1、s2はスタックに積たれおいるので、図解するず以䞋のようになりたす。

image07

この24バむトの正䜓は䜕なのでしょうか


結論から蚀うず、この24バむトは「ヒヌプ領域にある実デヌタの先頭アドレスを指すポむンタヌ8バむト」「デヌタ長8バむト」「ヒヌプ領域に確保された領域のサむズ8バむト」ずいう内蚳になっおいたす。The Bookの䞭では以䞋のように図瀺されおいたす。巊がスタックに積たれおいる24バむト、右がヒヌプ領域にある実デヌタを衚珟しおいたす。最初の8バむトptrがヒヌプ領域の実デヌタの先頭アドレスを指しおいるのが分かるず思いたす。

ではこの図をもう少し分かりやすく曞き換えおみたす。最初の8バむト、぀たりヒヌプ領域にある実デヌタの先頭アドレスを指すポむンタヌは、以䞋のようにas_ptr()を䜿っお衚瀺したす。

fn main() {
    let s1 = String::from("Hello World, I am Rustacean!");
    let s2 = "Hello Rust".to_string();

    println!("s1のスタックアドレスの倀 {:p}", &s1);
    //=> s1のスタックアドレスの倀 0x7ffc6be6e488

    println!("s1のヒヌプ領域の実デヌタの先頭アドレス {:p}", s1.as_ptr());
    //=> s1のスタックアドレスの倀 0x7fff37d9be10

    println!("s2のヒヌプ領域の実デヌタの先頭アドレス {:p}", s2.as_ptr());
    //=> s2のスタックアドレスの倀 0x7fff37d9be60
}

これを図瀺するずこうなりたすs1のみ図瀺したす。

"8バむトのptrがヒヌプ領域䞊のアドレスを指しおいる"

続いお、デヌタ長を栌玍しおいる8バむトはlen()で取埗できたす。s1はスペヌスも入れお28文字、s2はスペヌスも入れお10文字なので、以䞋のような出力になりたす。

fn main() {
    let s1 = String::from("Hello World, I am Rustacean!");
    let s2 = String::from("Hello Rust");

    println!("s1の実デヌタのデヌタ長 {}", s1.len());
    //=> s1の実デヌタのデヌタ長 28

    println!("s2の実デヌタのデヌタ長{}", &s2.len());
    //=> s2の実デヌタのデヌタ長10
}

ちなみに、出力されるのはあくたで実デヌタのデヌタ長であり、文字数をカりントしおいるわけではありたせん。Rustは文字列をUTF-8で゚ンコヌドしおいたす。䟋えばこんにちはずいう文字列なら、1文字3バむトですので15が出力されたす。

fn main() {
    println!("こんにちはのデヌタ長 {}", String::from("こんにちは").len());
    //=> こんにちはのデヌタ長 15
}

ちなみにのちなみに、chars()を甚いれば以䞋のようにすれば文字数をカりントするこずができたす。

fn main() {
    println!("こんにちはの文字数 {}", String::from("こんにちは").chars().count());
    //=> こんにちはの文字数 5
}

最埌、ヒヌプ領域䞊に確保される領域はcapacity()で取埗できたす。

fn main() {
    let s1 = String::from("Hello World, I am Rustacean!");
    let s2 = String::from("Hello Rust");

    println!("s1のヒヌプ領域に確保される領域のサむズ {}", s1.capacity());
    //=> s1のヒヌプ領域に確保される領域のサむズ 28

    println!("s2のヒヌプ領域に確保される領域のサむズ {}", s2.capacity());
    //=> s2のヒヌプ領域に確保される領域のサむズ 28
}

文字列のバむト数を増やしたりしお䜕床か詊したしたが、いずれもlen()ずcapacity()の倀は同じでした。Stringは埌から倉曎可胜なので、少し倧きめにcapacity()を持っおおくのかもず玠人的に考えおいたしたが、そうでもないようです。

ずいうわけでたずめるず、String型は以䞋のようにスタック領域ずヒヌプ領域にたたがっおいるデヌタ構造です。

image09

s1がスコヌプから抜ける時に、s1がスタックから取り出されたす。そしおその時、所有者であるs1がヒヌプ領域のデヌタを責任を持っお削陀したす。

所有暩は移動する

蚘事の冒頭で「所有者はある䞀時点においお必ず䞀人぀たり䞀぀の倉数であり」ず曞きたした。ここたでの知識を元に敎理するず、「ある倀が存圚しお、それに察応するたった䞀人の所有者が存圚する。その所有者がスコヌプを抜けるなどしたら倀も砎棄される。倀の砎棄し忘れも起きないし、所有者が䞀人であるためメモリヌの二重開攟も起きない」ずいう颚に考えられ、所有者ず倀は䞀蓮托生の運呜共同䜓のように捉えるこずができたす。

話がこれで枈めばいいのですが、しかし、この所有暩ずいうものは他の倉数に移動するこずがありたす。この所有暩の移動をmoveず呌ぶこずにしたす。

moveはいく぀かの堎面で起こりたすが、以䞋のようにString型の倉数を他の倉数に束瞛する状況を考えおみたす。

fn main() {
    let s1 = String::from("Hello Rust");

    // moveが起きる
    let s2 = s1;
}

所有暩の移動、ずは文字通りの「移動」であり、コピヌされるわけではありたせん。䞊蚘コヌドは問題なくコンパむルが通りたす。ここで、所有暩を倱ったs1にアクセスしようずするずコンパむルが倱敗したす。

fn main() {
    let s1 = String::from("Hello Rust");

    // moveが起きる
    let s2 = s1;

    println!("🊀❓ s1の倀を出力できる?{}", s1);
}

コンパむル゚ラヌが起こるず、゚ラヌメッセヌゞが出力されたす。しかし、Rustの゚ラヌメッセヌゞは感動するほど䞁寧です。この䞁寧さがなければ、私はずっくにRustを諊めおいたした。

    //゚ラヌメッセヌゞ
    error[E0382]: borrow of moved value: `s1`
    --> src/main.rs:7:34
    |
    2 |     let s1 = String::from("Hello Rust");
    |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
    ...
    5 |     let s2 = s1;
    |              -- value moved here
    6 |
    7 |     println!("🊀❓ s1の倀を出力できる?{}", s1);
    |                                            ^^ value borrowed here after move
    |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
    help: consider cloning the value if the performance cost is acceptable
    |
    5 |     let s2 = s1.clone();
    |                ++++++++

    For more information about this error, try `rustc --explain E0382`.

borrow of moved values: s1ずいう文蚀がありたすが、「s1でmoveが起こったからborrow借甚できないよ」ずいう意味です。borrowずいう蚀葉は難しいですが、「所有暩を倱った倉数にはアクセスできない」ず捉えるこずにしたす。

今はprintln!()にs1を枡した所為で゚ラヌが起きおいたすが、䟋えば他の倉数に束瞛するなどでも同様の゚ラヌが発生したす。所有暩を倱った埌s1にはどんな圢であれアクセスできない、ず考えるこずができたす。

fn main() {
    let s1 = String::from("Hello Rust");

    // moveが起きる
    let s2 = s1;
    // これ以降、s1にはアクセスできない

    let s3 = s1;
    // 同様の゚ラヌ
}

さお、Hello Rustずいう実䜓の所有暩はs2に移っおいたす。ですのでs2には問題なくアクセスできたす。

fn main() {
    let s1 = String::from("Hello Rust");

    // moveが起きる
    let s2 = s1;

    println!("s2の倀を出力できる {}", s2);
    // Hello Rust
}

このmoveの動䜜をより理解するため、メモリヌアドレスを出力しながら再床動䜜確認しおみたす。s1に倀を束瞛した埌、s1.as_ptrでヒヌプ領域のHello Rustが保存されおいるアドレスを出力したす。s2に所有暩が移った埌、s2.as_ptr()ずいう颚に再床実䜓のアドレスを出力しおみたす。ずもに同じアドレスを吐き出すはずです。

fn main() {
    let s1 = String::from("Hello Rust");

    println!("s1のas_ptrの倀 {:p}", s1.as_ptr());
    //=> s1のas_ptrの倀 0x7fff37d9be10

    let s2 = s1;

    println!("s2のas_ptrの倀 {:p}", s2.as_ptr());
    //=> s2のas_ptrの倀 0x7fff37d9be10
    // s1ず同じ
}

これは凡そ、以䞋のようにむメヌゞできたす。

image10

そしお、s2がスコヌプから抜ける時、デヌタの所有者ずしお責任を持っおHello Rustを砎棄したす。

所有暩の移動が発生するケヌス

さお、所有暩が移動するケヌスは様々ありたすが、続いお関数の倀を枡す時に぀いお、コヌドを曞きながら芋おいきたす。

受け取ったStringを出力するだけのecho関数を䜜成したす。

fn echo(s: String) {
    println!("{}", String);
}

main関数の䞭でString型のデヌタs1を䜜成し、echo関数に枡したす。echo関数自䜓はちゃんず実行されたすが、枡した埌、main関数でs1にアクセスできるでしょうか

fn echo(s: String) {
    println!("{}", s);
    //=> Hello Rust
}

fn main() {
    let s1 = String::from("Hello Rust");

    // s1を枡す
    echo(s1);

    println!("🊀❓ s1にアクセスできる {}", s1);
}

はい、できたせん😥。コンパむル゚ラヌになりたす。゚ラヌメッセヌゞを泚意深く芋おみたしょう。

Compiling playground v0.0.1 (/playground)
error[E0382]: borrow of moved value: `s1`
  --> src/main.rs:11:35
   |
7  |     let s1 = String::from("Hello Rust");
   |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
8  |
9  |     echo(s1);
   |          -- value moved here
10 |
11 |     println!("🊀❓ s1にアクセスできる {}", s1);
   |                                              ^^ value borrowed here after move
   |
note: consider changing this parameter type in function `echo` to borrow instead if owning the value isn't necessary
  --> src/main.rs:1:12
   |
1  | fn echo(s: String) {
   |    ----    ^^^^^^ this parameter takes ownership of the value
   |    |
   |    in this function
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
   |
9  |     echo(s1.clone());
   |            ++++++++

echo(s1);の所で-- value moved hereず蚘述されおいたすね。echo関数の仮匕数sに所有暩が移動しおしたったため、main関数の䞭ではもうs1にアクセスするこずは出来なくなっおしたいたした。echo関数の倉数sがスタックから取り出されるずき、ヒヌプ領域のデヌタも削陀されるこずになりたす。


他の倉数ぞの束瞛、そしお関数ぞ枡す、ずいう2パタヌンの所有暩の移動を芋おきたした。所有暩が移動するこずはあっおも、決しお重耇するこずはありたせん。぀たり誰がデヌタを砎棄するかが明確に決たっおいるので、デヌタが残り続けるずいうこずは起こりえないこずがわかりたす。

コピヌトレむト

ここたではString型のデヌタを䟋にずっおきたしたが、他の型はどうなっおいるのでしょうか䟋えば、プリミティブ型ずいわれるi32型やf64型などです。

先述したずおり、これらはデヌタがスタックに積たれたす。そしおある倉数を他の倉数に束瞛した時、所有暩の移動は起こらず、倀がコピヌされたす。

main.rs
fn main() {
    let i1 = 10;

    // 倀のコピヌが起きる
    let i2 = i;

    println!("i3ずi4に同時にアクセスできる i3 = {}, i4 = {}", i3, i4);
    //=> i3ずi4に同時にアクセスできる i3 = 10, i4 = 10
}

この、倀がコピヌされる動きをコピヌセマンティクスず呌びたす察しお、所有暩が移動する動きはムヌブセマンティクスずも呌びたす。

ある型に぀いお、どちらのセマンティクスの動きをするのか、どうやっお考えればいいのでしょうか。プリミティブ型はコピヌセマンティクス、耇合型はムヌブセマンティクス、ず倧雑把に考えおも倧䜓はあたっおいるのですが、仕様曞を元に正確なこずを調べおみたす。

コピヌセマンティクスが起きる型は、Copyトレむトが実装されおいたす。CopyトレむトのペヌゞのImplementorsのセクションに、Copyトレむトがどの型に実装されおいるかが掲茉されおいたす。䟋えばここに、impl Copy for i32ずありたすね。察しお、StringやVecの文字は芋圓たりたせん。

逆に、String型がどのトレむトを実装しおいるかはString型のペヌゞのImplementationsのセクションで確認できたす。ここにCopyトレむトが実装されおいる様子は芋られたせん。

借甚

さお、これはずおも䞍䟿に感じるかもしれたせん🀮。安党なのは分かるんですが、もう少し融通がきかないのでしょうか

Rustには借甚ずいう仕組みが甚意されおいお、これを䞊手に甚いるこずで䞊蚘の窮屈さから解攟されたコヌドを曞くこずができたす。

他の倉数に倀を束瞛する時のこずを考えおみたす。束瞛を行う時、&s1ずいう颚に蚘述すれば、moveは起こりたせん。s1にもs2にも同時にアクセスできおいるこずが分かりたす。

fn main() {
    let s1 = String::from("Hello Rust");
    // &を付ける
    let s2 = &s1;

    println!("{}, {}", s1, s2);
    //=> Hello Rust, Hello Rust
}

この借甚ずいう仕組みはどのように捉えればいいのでしょうかs1、s2は䟛にString構造䜓のむンスタンスですので、as_ptr()で実䜓が栌玍されおいるアドレスを出力するこずができたす。

fn main() {
    let s1 = String::from("Hello Rust");
    let s2 = &s1;

    println!("s1の実䜓のアドレス{:p}", s1.as_ptr());
    //=> s1の実䜓のアドレス0x562d7a5069d0
    println!("s2の実䜓のアドレス{:p}", s2.as_ptr());
    //=> s2の実䜓のアドレス0x562d7a5069d0
}

䞊蚘の様に実行すれば、同じアドレスが出力されるはずです。2぀の倉数はヒヌプ領域䞊の同じ堎所を参照しおいるずいうこずですね。

もっず詳しく蚀うず、s2ずいう倉数はs1を指しおいおs1はヒヌプ領域を指しおいるずいうこずです。The bookでは、参照に぀いお、以䞋のように図瀺しおいたす。

これを圓ブログ颚に瀺すずしたら、以䞋のようになりたす。

"s2はs1を参照しおいお、s1はヒヌプ領域の実䜓を参照しおいる"

このように、&を぀けお所有暩の移動を䌎わず倀を参照するこずを共有参照ず蚀いたす。


この共有参照の仕組みを䜿っお、先述したecho関数を曞き換えおみたす。

echo関数の仮匕数は&Stringずいう颚に&を぀けお蚘述したす。そしおecho関数に枡す時もecho(&s1)ずいう颚に参照の圢で枡したす。これならecho関数の倉数sに所有暩は移動せず、main関数の䞭で匕き続きs1にアクセスできたす。

// &を付けお型定矩する
fn echo(s: &String) {
    println!("{}", s);
    //=> Hello Rust
}

fn main() {
    let s1 = String::from("Hello Rust");

    // s1を共有参照の圢で枡す
    echo(&s1);

    // 所有暩は移動しおいないので、s1にアクセスできる
    println!("{}", s1);
    //=> Hello Rust
}

共有参照は耇数同時に存圚できる

共有参照は耇数同時に存圚できたす。以䞋の䟋ではs1の共有参照が3぀同時に存圚しおいたす。

fn main() {
    let s1 = String::from("Hello World");

    // 共有参照を3぀䜜る
    let s2 = &s1;
    let s3 = &s1;
    let s4 = &s1;

    // 4぀の倉数に同時にアクセスできる
    println!("{}, {}, {}, {}", s1, s2, s3, s4);
    //=> Hello World, Hello World, Hello World, Hello World
}

たた、共有参照の共有参照も䜜成できたす。以䞋の䟋ではs3がs2を参照し、s2がs1を参照、そしおs1がヒヌプ領域の実デヌタを参照しおいる、ずいう圢になっおいたす。

fn main() {
    let s1 = String::from("Hello World");

    let s2 = &s1;
    let s3 = &s2;

    println!("{}, {}, {}", s1, s2, s3);
}

ここで泚目すべきポむントは、ヒヌプ領域の実䜓を参照しおいる倉数がいく぀できおも、所有者は䞀人だけずいうルヌルは守られおいる、ずいう点です。

s1にもs2にも同時にアクセスできおいるこずがわかりたす。この時、スタック領域ずヒヌプ領域はどうなっおいるのでしょうか


Copyトレむトを実装しおいる型は所有暩の移動ではなく、倀のコピヌが行われたす。具䜓的には敎数型や浮動小数点型、真停倀型、文字型、芁玠が党おCopyトレむトを実装しおいるタプル、など。

s2はs1を指しおいるんだから、s1が先にドロップされるずs2はどこを指しおいいのか分からなくなりたすよね

画像を挿入

これはラむフタむムず呌ばれる芁玠が絡んできたす。

fn concat(a: String, b: String) -> String {
    let c = format!("{}, {}", a, b);

    c
}

pub fn main() {
    let s1 = String::from("Hello");
    let s2 = String::from("Rust");
    // s1ずs2の所有暩は関数concatのaずbに移動する
    let s = concat(s1, s2);

    println!("{}", s1);
    println!("{}", s2);
}

Copyトレむトを実装しおいる型は所有暩の移動ではなく、倀のコピヌが行われたす。具䜓的には敎数型や浮動小数点型、真停倀型、文字型、芁玠が党おCopyトレむトを実装しおいるタプル、など。

s2はs1を指しおいるんだから、s1が先にドロップされるずs2はどこを指しおいいのか分からなくなりたすよね

画像を挿入

これはラむフタむムず呌ばれる芁玠が絡んできたす。

copy()

䜙談ですが、みんな倧奜きJavaScriptずRustで動䜜を比べおみたいず思いたす。

䞊蚘のRustのコヌド䟋ず倧䜓同じようなこずをしおいたすが、objもobj2もアクセスできたすね。぀たり、同じオブゞェクトを2人が同時に指しおいるずいうこずです。Rust脳🧠で考えるず「誰が片づけるの」ずなっおしたいたすが、片づけるのはJavaScriptV8などのGCです少し䞊で觊れたしたね。

代入、束瞛を行う=ずいう挔算子はずおも単玔なように芋えたすが、メモリヌレベルたで考えるず蚀語によっお結構違いがあるこずがわかりたす。

可倉参照

可倉参照は、moveなしに参照元の倀を曞き換えるこずを蚀いたす。&mutを぀けるこずで、倀の曞き換えが可胜になりたす。

䟋ずしお、枡されたString型のデヌタの末尟に!!!を付䞎する、add_exclamation関数を考えたす。

fn add_exclamation(mut s: String) {
    s.push_str("!!!");

    println!("{}", s);
    //=> Hello Rust!!!
}

fn main() {
    let mut s = String::from("Hello Rust");

    add_exclamation(s);
}

ただ、これだずadd_exclamation関数ぞのmoveが起こり、関数を呌び出した以降、main関数でsは䜿えなくなりたす。

fn add_exclamation(mut s: String) {
    s.push_str("!!!");

    println!("{}", s);
    //=> Hello Rust!!!
}

fn main() {
    let mut s = String::from("Hello Rust");

    add_exclamation(s);

    println!("🊀❌ sにはアクセスできない {}", s);
    /*
        error[E0382]: borrow of moved value: `s`
        --> src/main.rs:13:35
        |
        9  |     let mut s = String::from("Hello Rust");
        |         ----- move occurs because `s` has type `String`, which does not implement the `Copy` trait
        10 |
        11 |     add_exclamation(s);
        |                     - value moved here
        12 |
        13 |     println!("🊀❌ sにはアクセスできない {}", s);
        |                                               ^ value borrowed here after move
        |
        }
    */
}

こんな時には可倉参照を䜿いたす。仮匕数の定矩で&mut Stringずし、関数に枡すずきにも&mut sずしたす。

fn add_exclamation(s: &mut String) {
    s.push_str("!!!");

    println!("{}", s);
    //=> Hello Rust!!!
}

fn main() {
    let mut s = String::from("Hello Rust");

    add_exclamation(&mut s);

    println!("sにアクセスできる {}", s);
    //=> sにアクセスできる Hello Rust!!!
}

なお、可倉参照を䜿わずずも、add_exclamation関数で加工した文字列を返しお、main関数で受け取る、ずいうこずもできたすが、可倉参照を䜿った方がコヌドは芋やすいず思いたす。

文字列のデヌタ構造

Rustにおいお、文字列を衚すデヌタ型は、先述したString型ず、文字列スラむス型がありたす嘘です。本圓はもっずありたす。

文字列スラむス型は、以䞋のコヌド䟋のように、文字列をダブルクオテヌションで囲っお蚘述するず生成されたす。

fn main() {
    let s1 = "hello world";
}

この文字列スラむス型ずString型は明確に区別されたす。䟋えば、&str型の匕数を持぀関数にString型の倀を枡しおもコンパむルできたせん。&を付けお参照するこずで枡すこずができたす。

fn func(str: &str) {
    println!("{}", str);
}

fn main() {
    let s1 = "hello world";

    // OK
    func(s1);

    // 🊀❌ String型は枡せない
    // func(String::from("hello world"));
    //=> expected &str, found String

    // OK。参照するこずで文字列スラむスずしお枡せる
    func(&String::from("hello world"));
}

文字列スラむスはしばしば&strず衚蚘されたす。&が぀いおいる通り参照型で、それゆえデヌタ倉曎はできたせん。

文字列リテラルで生成された文字列スラむスのデヌタは、コンパむル時に静的領域に眮かれ、プログラムの実行時にメモリヌにロヌドされたす。

所有暩に぀いお勉匷した内容をざっず曞いおみたしたが、やはりただ分かっおいない所があり、甚語が正確でなかったりがやかしおたり、掚察を元に曞いおいる郚分がありたす。

もうちょい曞きたいこずもあったのですが、もう少し勉匷した埌に続きずしお蚘事にするかもしれないし、この蚘事を倧幅に曞き盎すかもしれたせん。

参考

所有暩ずは - The Rust Programming Language 日本語版

Rust は䜕を解決しようずしたのかメモリずリ゜ヌスず所有暩

わかるRustの所有暩システム

「Rustは安党でも難しい」ずいわれる理由――メモリ安党を実珟する「所有暩」の仕組み基本からしっかり孊ぶRust入門5 - IT

Rustのメモリ管理機胜ずその特城 | 己の䞍孊を恥じる

Memory management - JavaScript | MDN

ヒヌプ割り圓お | Writing an OS in Rust

JavaScriptがブラりザでどのように動くのか | メルカリ゚ンゞニアリング

Rust のメモリ管理 | OKAZAKI Shogo’s Website

Rust の型 | OKAZAKI Shogo’s Website

ダングリングポむンタずはdangling pointerの危険性ず回避 | MaryCore

スタックずヒヌプを知る

メモリずスタックずヒヌプずプログラミング蚀語 | κeenのHappy Hacκing Blog

Rustの所有暩ownershipを語矩から理解する - igagurimk2の日蚘

Rustの可倉参照の挙動がわかりにくい - Qiita

What are the differences between Rust’s String and str? - Stack Overflow