🎷

RustのOwnershipってなんなん

2021/06/13に公開
2

概要

この前読んだ達人プログラマーには毎年新しい言語勉強しなさいって書いてあったので
WebAssemblyや組み込みでも使えるRustの勉強を最近始めた🐥

公式のドキュメント読んでくと
所有権(Ownership)がとっつきにくく混乱したので
後で振り返れるように自分用のメモを残しておく

前提

大前提としてRustには以下3つの原則がある

  1. 全ての値にはownerと呼ばれる変数が存在する
  2. ownerは必ず一つだけである
  3. ownerがスコープから外れたら値は破棄(drop)される

最初読んでも全然ピンと来なかったけど、特徴的なケースを以下に書く

変数を利用するとき

スコープ

スコープを抜けると値は破棄される
以下コードをコンパイルしようとしてもエラーになる

fn main() {
    {
        let num = 1; // numがownerだぞ!
    } // スコープ外れるのでnumはdropされる
    println!("number is {}", num); // dropされてるんで使えないぞ!
}

まぁ、これは他の言語でもスコープ外れると使えないと思うので
そんなに違和感はないなぁと思った

メモリのスタックとヒープ領域について

ownershipのこと考えるのに、スタックとヒープの話が出てきて、「なんなんや」
ってなるけど、この辺が後の話にも影響してくるんでちゃんと読んだ方が良さそう

まぁざっくりいうと、let num = 1;みたいなスカラー値をベタ書きしてるケースでは
この変数を格納するために、どれくらいのメモリ容量用意したらいいか
コンパイルするタイミングでわかるので、そいつらは取り出しが高速なスタック領域に入る

例えば、ユーザーの入力した文字列を格納する変数用意する!とかの場合
どれくらいの容量が必要か分からないので、ある程度の容量をヒープ領域に用意する。
String::new()とかがヒープ領域使うみたい

このヒープ領域は実際にデータを格納するメモリと
そのメモリの場所を示すポインターで構成されてる

こんな感じのhello文字列作ってるコードの場合

let s1 = String::from("hello");

こんな感じ

このヒープ、スタックそれぞれでownerの動きが変わるので
理解しておく必要ありそうな雰囲気

ヒープ領域変数のownerについて

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("text is {}", s1);
}

これコンパイルするとエラーになっちゃう
何故なら、こんな感じで元々ポインターになってたs1が破棄されちゃうのです

他の言語ならポインターへの参照を複製することをシャローコピーとかいうけど
Rustでは、元のポインター破棄しちゃうので、ムーブっていうみたい🐵

これのおかげで、s1, s2がスコープを抜けたときに
"hello"という文字列が格納されているメモリを2重で解放してしまう。
みたいな問題が起きないらしい!

(メモリのデータごと複製したいときはcloneメソッドとか使うらしい

let s1 = String::from("hello");
let s2 = s1.clone();

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

スタック領域変数のowner

スカラー値みたいなスタック領域使用してる変数は
別の変数に代入しても問題ないみたいっす。

fn main() {
    let x = 5;
    let y = x;

    println!("x = {}, y = {}", x, y);
}

こういうスカラー値系はCopy Traitなるものが実装されてるらしく
変数代入するとDeep Copyされ実際のデータ自体がメモリに複製されるらしい。

なので、ヒープ領域の時にあったような
ポインターが2重でメモリ解放してしまう!みたいな懸念もなく別変数に代入後も使える

関数を利用するとき

引数と戻り値

関数の引数にヒープ領域変数(s1)を渡すと
宣言してる引数の変数(a_string)にムーブしちゃう
ので、printlnする頃にはs1は破棄されちゃっててエラーになる

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

    let s2 = takes_and_gives_back(s1); // s1が関数内でdropされちゃう
    
    println!("s1 is {}, s2 is {}", s1, s2); // もうここではs1使えない・・
}

fn takes_and_gives_back(a_string: String) -> String {
   a_string
}

ReferenceとBorrow

関数に渡す変数をs: &Stringのように参照(Reference)にすると
関数の後でもヒープ領域変数(s1)が使用できる!

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

    let len = calculate_length(&s1); // referenceで渡すと所有権は渡さないのでdropされず

    println!("The length of '{}' is {}.", s1, len); // まだs1が使えるのだ
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

これを借用(Borrow)と呼ぶみたい。
つまり変数の所有権を関数の中に渡すのではなく、まさにBorrowしてるので
変数が関数内のスコープを抜けても、ドロップされず、利用できるらしい

可変なBorrow

Borrowした変数を変更するとエラーになっちゃうので
mutをつけて変数宣言すれば、変更もできる!

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

    change(&mut s);
    
    println!("s is {}", s)
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

Referenceの注意点

可変な参照はスコープ内で一度だけ

sの可変な参照を2回行うとエラーになってコンパイルできない!
いっぱい可変にしてしまうと、いろんなとこで元のデータが更新されてしまい
データ競合のリスクがあるため、Rustではこういう制約があるみたい

    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{}, {}", r1, r2);

不変な参照を可変な参照に変えることはできない

不変な参照変数はいくらでも作ることできるけど

不変な参照変数を宣言してる間に可変な参照変数を宣言することもできないみたい!

let mut s = String::from("hello");

let r1 = &s;
let r2 = &s; // 複数不変な参照作るのはOK
let r3 = &mut s; // 不変な参照変数ある時に可変な参照変数は宣言できないエラー

Dangling Referenceの回避

すでに解放されたメモリを参照してしまう不具合ダングリング参照も
Rustではコンパイル時に検知してエラーで弾いてくれる

fn main() {
  let d = dangle(); // 実際のデータはdropされてるんでDanglingになっちゃう!
}

fn dangle() -> &String {
  let s = String::new("dangle")
  &s // sのreferenceを返す
} // ここでsのメモリデータはdropされる

文字列スライス

Reference以外にも所有権を持たない概念文字列スライスがあるらしい
ある文字列の一部を表す変数はこの文字列スライス(&str)で扱える

こいつも所有権を持たないので、文字列の一部を表すのに有効に使えそう!

fn main() {
    let my_string = String::from("hello world");
    let word = first_word(&my_string[..]);
    println!("{}", word);

    let my_string_literal = "hello world";
    let word = first_word(&my_string_literal[..]);
    println!("{}", word);

    let word = first_word(my_string_literal);
    println!("{}", word);
}

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

結局・・・

頭の整理としてはこんな感じかなぁ

  1. ヒープ領域変数は代入するとMoveして、元のポインタ変数は破棄される
  2. 関数の引数にReferenceを渡すことをBorrowと言い、所有権を渡さず使ってもらえる
    1. 関数呼び出し後も渡した変数が使える!
  3. Reference利用時は可変の場合、いろいろ注意点ある
    1. 可変参照を複数作れない
    2. 不変参照宣言後に可変参照作れない

まとめ

まだまだ、公式ドキュメント読み始めたレベルだが
なんとなく読んで書き始めるより、何周かドキュメント読んでから書いたほうがいい感があるので
もう一声勉強を続けよう🤹‍♂️

Discussion

koukkouk

下記、ソースコードの
text1はs1ではないでしょうか?🧐

let s2 = text1;let s2 = s1;

ヒープ領域変数のownerについて

fn main() {
    let s1 = String::from("hello");
    let s2 = text1;
    println!("text is {}", s1);
}