こんばんわ、hisayukiです。
以前、Rustの勉強をしようとおもって環境構築したものの全然進んでないので、基礎から勉強しはじめました。
環境構築は前のブログで書いたので、今回は割愛します。
あとは他の言語でもよくあるやり方は特に触れずに行こうと思います。
今回勉強に使ったサイト
こちらのサイトの日本語訳版です!
元サイトも読めなくはないのですが、日本語版があるのでこちらで進めます。
変数
Rustは標準ですべての変数がイミュータブルです。
そのため、以下のコードはコンパイルエラーとなります。
fn main() {
let x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
これはイミュータブル変数xに対して、2回代入を行おうとしたから。
もし、xに代入をしたい場合はミュータブルな変数にする必要があります。
ミュータブルな変数にしたい場合はmut
キーワードを付けます。
fn main() {
let mut x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
こうすることで、変数xは代入可能な変数となります。
シャドーイング
変数はイミュータブルですが、上書きをすることが可能です。
それがシャドーイングになります。
fn main() {
let x = 5;
let x = x + 1;
let x = x * 2;
println!("The value of x is: {}", x);
}
こちらのコードはxに代入しているように見えます。
ですが、実際にはlet
をつけることで変数xを再定義しています。
再定義なので同じ変数名でも別の型で定義することが可能です。
そこがmut
をつけたミュータブル変数との違いです。
ミュータブル変数は再代入が可能なだけで、型まで変化させることはできません。
プリミティブ型
bool型
true
と false
という2つの値
char型
Unicodeの文字型。char
はシングルクオート( '
)で作ることができます。
整数型
大きさ | 符号付き | 符号なし |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
arch | isize | usize |
符号付き(i
)と符号なし(u
)の2種類が用意されている。
どちらも8bit〜64bitまでの固定長型と、マシンポインタに依存するsize
型が存在する。
浮動小数点型
32-bit | f32 |
64-bit | f64 |
浮動小数点型は32bitと64bitの2種類が用意されている。
str型
プリミティブの文字列型。
中身はString型への参照なので、型指定は&strで定義する。
今回は説明を省きますが、このような型指定をスライスとよびます。
配列
固定長の配列を定義できる。
中身の全要素は、 同じ型でなければなりません。
タプル
固定サイズの順序ありリストです。
通常の配列との違いは、配列内の型を以下のように違うものにできます。
プリミティブ型で取り上げていますが、タプルの中にプリミティブ型以外の型を入れた場合は、プリミティブ型以外の型と同じ扱いになります。
同じ文字列ですが、こちらのコードはプリミティブ型なのでコンパイルが通ります。
{
let x: (i32, &str) = (1, "hello");
let y: (i32, &str) = x;
println!("The value of x is:(i32, String) ({},{})", x.0,x.1);
}
ですが、こちらのコードはプリミティブ型として扱われないため、コンパイルエラーになります。
{
let x: (i32, String) = (1, "hello".to_string());
let y: (i32, String) = x;
println!("The value of x is:(i32, String) ({},{})", x.0,x.1);
}
&strはプリミティブ型ですが、String型はプリミティブ型ではないためです。
これは、このあと記述する所有権が効いてるからです。
所有権
Rustの重要仕様の一つです。
所有権とはすべての変数は定義されたときから”誰か”のものであり、その誰かは複数になることはないという考え方。
つまり、1つの変数を複数で所有することはできないということです。
所有者が移動したあとに、元所有者は同じデータへのアクセスができません。
この所有者が移動することをMOVEと言います。
たとえばこのようなコード
// 変数のMOVE
// ここからスコープ開始
{
// ここではs1という所有者
let s1 = String::from("hello");
// ここで所有者がs2に移動して、s1は所有者でなくなる
let s2 = s1;
// s1とs2を呼び出しているが、
// s1は既に所有者ではないため
// そのためコンパイルエラー
println!("s1 = {}, s2 = {}", s1, s2);
}
// このスコープを抜けたらs1もs2もメモリ解放する
まず、String型の変数s1を初期化したら、Stringオブジェクトをスタックに保管します。
中身はヒープメモリにある文字列の実態”hello”に対してのポインタが保管されます。
そして新しい変数s2の初期化はs1のコピーを作成します。
ここでのコピーはあくまでポインタのコピーなので実態は増えていません。
なのでs1もs2もヒープメモリにある同じ文字列の実態”hello”に対してのポインタを保持しています。
ここまでは問題ないのですが、スコープから抜けたときヒープメモリの実態を解放します。
このヒープメモリの開放時に問題があります。
s1もs2も同じメモリを解放しようとするため、二重解放エラーになってしまいます。
それを防ぐためにRustではスタック内でポインタのコピーがされた場合には、コピー元のオブジェクトを無効にします。
今回の場合ではs1が無効化され、s2のみが有効なのでスコープを抜けた場合のメモリを解放命令はs2からしか行われません。
コメントに書いてあるとおり、これはコンパイルエラーとなります。
43 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait
44 | let s2 = s1;
| -- value moved here
45 |
46 | println!("s1 = {}, s2 = {}", s1, s2);
| ^^ value borrowed here after move
これは所有権がすでにs2に移動しているのに、s1からアクセスすることはできませんというエラーです。
さらに、Copy
traitが実装されていませんとも言われています。
最初にデータ型としてプリミティブ型を紹介したのはこのためです。
プリミティブ型はCOPY
プリミティブ型はすべて、MOVEではなくCOPYが行われます。
これはプリミティブ型は実態もスタックにあるためです。
なので、スタック内で実態ごとコピーされるため所有権の移動が発生しません。
そのため、このようなコードでも動作します。
{
let x:i8 = 5;
let y:i8 = x;
println!("The value of x is:i8 {}", x);
println!("The value of y is:i8 {}", y);
}
今度は変数xをi8の整数プリミティブ型で初期化しているので、実態ごとスタックに入ります。
次にyもi8の整数プリミティブ型なので、ここでMOVEではなくCOPYが行われます。
そのため、そのあとのprintln!ではどちらもスタックに実態があるので呼び出すことができます。
まとめ
今回は所有権についてまとめました。
大まかな内容は以前もみたのですが、プリミティブ型はMOVEが発生しないこと。
スタックやヒープメモリについても改めて勉強することができました!
今まで高級言語を触ることのが多かったので、メモリ管理などのシステム側に近い言語で考える必要のあることを知ることができました。
新しいことやってる感あって、すごい楽しいです!
ここからWebAssemblyのことやCargoの凄さなども理解していきたいと思います!
コメント