11
7

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 1 year has passed since last update.

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

Last updated at Posted at 2019-09-11

概要

ちょっとしたツールを作るために,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はイミュータブルなので変更できない.そこで,変数宣言(変数名の前)にmutをつける.

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

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

ここで,呼び出し側にもlet mul v1 = 10と,mutをつけなければいけないことに注意する.
間違いです.testの引数は参照ではないので,呼び出し側にmutは不要です(あってもいいです).
考え方としては,変数の前のmutは,その変数(が束縛されている値)が可変かどうかを示すと考えて良いと思います.よって,関数呼び出しで値の所有権をmoveすると,呼び出し元では二度とその変数に触れないので,ミュータブルだろうとイミュータブルだろうと関係ない,ということだと思われます.そして,所有権が移動した先(test関数)で,その変数が可変なのかどうかを決める,という感じかと.呼び出し元ではすでにその変数にはアクセスできないので,どちらにしても不具合は起きない,ということになると思います.

そして,このプログラムを実行すると,

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 {  // mutはなくてもOK
        iv: 20,        
    };
    println!("iv : {}", sv.iv);
    t2(sv);    
}

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

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(mut v:&mut Test){ // これでもいい
fn t4(v:&mut Test){
    v.iv += 1;
    println!("iv t4 = {}", v.iv)
}

参照を使うときには,関数の引数の型の前に'&'をつけることで表現する.また,ミュータブルのときには型の前&mutをつける.参照を使わないときは変数の前に付けていた.こうすると,変数の前には何もつけなくて良い(&mutの場合にはつけてもいい).
考え方としては,型の指定によってこの型,&mut Testでミュータブルの参照型である,ことが確定している.よって,変数宣言にmutがなくてもミュータブルだとわかるから省略可能,と理解すればいい(?)
これは結局,

let mut hoge = 20;
let abc = &mut hoge;

と同じことをやっている.型をきちんと書くと

let mut hoge: i32 = 20;
let abc: &mut i32 = &mut hoge;

となり,let mut abcと書かなくてもabcはミュータブルだとわかる,ってこと

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); // t4の引数が&mut Testなので,&だけではなく,&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などの言語がプリミティブ型とオブジェクト型でコピーセマンティクスと参照セマンティクスを使い分けているのと同じなので,自然なのかな?とも思う.

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

11
7
1

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
11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?