Help us understand the problem. What is going on with this article?

RUST事始め:所有権とイミュータブル/ミュータブル(mut),コピーセマンティクス

概要

ちょっとしたツールを作るために,RUSTをやり始めた.RUST初心者の備忘録です.

変数宣言のミュータブルとイミュータブル

RUSTでは,値が一度変数に束縛されると変更できない.これをイミュータブルという.

fn main() {
    let v1 = 1;
    v1 = 2; // コンパイルエラー
}

変更するためには,変数宣言時にmutをつけて,ミュータブルにする.

fn main() {
    let mut v1 = 1
    v1 = 2;
    println!("v1 = {}", v1); // -> "v1 = 2"と出力
}

このとき,mutをつけるのは,変数の左側.RUSTは強力な型推論機構を持っているので,変数の型を書かなくても勝手に推論してくれる.でも,書いてもいい.よって,上記に型を書くと

fn main() {
    let mut v1:i32 = 1
    v1 = 2;
    println!("v1 = {}", v1); // -> "v1 = 2"と出力
}

こんな風にかける.ここで,変数の左側に書くとは,let v1:mul i32 = 1;とは書かない,ということ.これだとコンパイルエラーになる.

関数定義のミュータブルとイミュータブル

RUSTでは,変数宣言のときには基本的にコンパイラに型を推論させるため,型は省略するのが普通らしい.逆に,型推論のヒントとして関数の引数には型をきっちり書く.

fn test(v:i32) {
    println!("v = {}", v);
}

fn main() {
    let v1 = 10;
    test(v1);
}

これで,v=1と出力される.ここで,testの中でvの値を書き換えたいとする.しかし,testの引数vはイミュータブルなので変更できない.そこで,変数宣言にmulをつける.ここでも,変数名の前(左側)につける.

fn test(mul v:i32) {
    v += 1
    println!("v test = {}", v); // "v = 11"と出力
}

fn main() {
    let mul v1 = 10;
    test(v1);
    println!("v main = {}", v); // "v = 10"と出力
}

ここで,呼び出し側にもlet mul v1 = 10と,mulをつけなければいけないことに注意する.
そして,このプログラムを実行すると,

v test = 11
v main = 10

このように,正常に動作する.この点にはまだ戻って再考する.

参照

説明の都合上,構造体を用意する.

struct Test {
    iv: i32,    
}
fn t2(mut arg: Test) {
    arg.iv = 5;
    println!("iv : {}", arg.iv);
}

fn stest() {
    let mut sv = Test {
        iv: 20,        
    };
    println!("iv : {}", sv.iv);
    t2(sv);    
}

mulに関しては上記の例と同じなので,これを実行すると

iv : 20
iv : 5

となる.

ここで,stestを以下のように一行加えて書き換える

fn stest() {
    let mut sv = Test {
        iv: 20,        
    };
    println!("iv : {}", sv.iv);
    t2(sv);    
    println!("iv : {}", sv.iv); // compile error
}

コメントのところでコンパイルエラーとなる.先程の例ではコンパイルできていたはず.これに関しては後述する.

この問題の解決には,所有権という考え方が必要になる.このあたりの説明はたくさんあるだろうから,結論だけ書くと,所有権を渡さずに借用を使え,ということになる.馴染みやすい言い方だと,参照を使え,ということになる.

参照を使った関数定義を,イミュータブルの場合,ミュータブルの場合それぞれ示す.

// イミュータブル
fn t3(v:&Test){
    println!("iv t3 = {}", v.iv)
}
// ミュータブル
fn t4(v:&mut Test){
    v.iv += 1;
    println!("iv t4 = {}", v.iv)
}

参照を使うときには,関数の引数の型の前に'&'をつけることで表現する.また,ミュータブルのときには型の前&mutをつける.参照を使わないときは変数の前に付けていた.こうすると,変数の前には何もつけなくて良い.

fn stest() {
    let mut sv = Test {
        iv: 20,        
    };

    println!("iv : {}", sv.iv);

    t3(&sv); // 参照を渡す
    println!("iv : {}", sv.iv); // コンパイルできる    
}

今度は,t3の呼び出しで参照を渡している(svの前に&をつけてる).こうすると,コンパイルエラーとならない.
実行すると

iv t3 = 20
iv : 20

を得る.また,ミュータブルの関数定義も同様で,型の前&mutをつけることに注意する.

fn stest() {
    let mut sv = Test {
        iv: 20,        
    };
    println!("iv : {}", sv.iv);
    t4(&mut sv); // &mutをつける
    println!("iv : {}", sv.iv);    
}

t4を呼び出すと,以下の結果を得る.

iv t4 = 21
iv : 21

t4の中でivの値が1加えられたことにより,stestの出力も変わっている.
ここで,関数定義の仕方をまとめると,以下のようになる.

fn f(v:&Test) {}
fn f(v:&mut Test) {}
//fn f(v:mut Test) {} // NG
fn f(v: Test) {}
fn f(mut v: Test) {}
//fn f(&mut v: Test) {} // NG

つまり,参照型を使うかどうかでmulを付ける位置が変わることに注意する.初心者の混乱の元..

所有権

RUSTには,所有権,という概念があり,変数や値の所有権を放棄したあと,その変数にアクセスすることができない.そして,所有権の移動は変数の代入等によって起こる.

t2(sv);
println!("iv : {}", sv.iv); // compile error

そのため,関数呼び出しの時,t2(sv)ここでsvの所有権を放棄したことになる.その後,sv.ivで構造体にアクセスしようとしているのでコンパイルエラーになった.しかし,i32の場合にはコンパイルでき,Testの場合にはコンパイルエラーとなった,なぜ振る舞いが違うのかを理解するには,次に述べるコピーセマンティクスを理解する必要がある.

コピーセマンティクス

i32でコンパイルできた時の実行結果をみると

v test = 11
v main = 10

となっている.つまり,test関数の中のvと,mainのvは別のものとして扱われている.しかし,Testでは別のものと扱われず,所有権を放棄したためにコンパイルエラーとなっていた.なぜこんな違いが出るかと言うと,代入文の意味(セマンティクス)が異なるからである.i32の時,代入文とは値のコピーであり,Testのときには所有権の移動,である.RUSTの基本は後者だが,意味を変えてコピーにする方法がある.詳しい説明は他に譲るが(というか,そこまで詳しくない),Copyトレイト,Cloneトレイトを実装することでこのセマンティクスを変更することができる.
具体的には,Testの定義にアノテーションを加える.

#[derive(Copy, Clone)]
struct Test {
    iv: i32,    
}

fn t2(mut arg: Test) {
    arg.iv = 5;
    println!("iv : {}", arg.iv);
}

fn stest() {
    let mut sv = Test {
        iv: 20,        
    };
    println!("iv : {}", sv.iv);
    t2(sv);
    println!("iv : {}", sv.iv); // compile OK

}

こうすると,このコードは無事にコンパイルでき,以下の実行結果を得る.

iv : 20
iv : 5
iv : 20

今度は,代入の意味がコピーに変わった,ということがわかる.ではなぜi32のときには最初からこのような振る舞いだったのか,といえば,RUSTではプリミティブ型は予めCopyとCloneトレイトを実装している,からである.紛らわしい...

しかし,これはJavaなどの言語がプリミティブ型とオブジェクト型でコピーセマンティクスと参照セマンティクスを使い分けているのと同じなので,自然なのかな?とも思う.

間違いなどあればご指摘ください

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away