最速で知る! プログラミング言語Rustの基本機能とメモリ管理【第二言語としてのRust】
Rustは、新しいシステムプログラミング言語です。本稿では、基本的な構文に加えて、所有権、参照と借用、ライフタイムといった特徴的な機能によるメモリ管理を解説します。
κeen(@blackenedgold)です。Rustの入門を担当することになりました。基本的な文法と使い方を説明しつつ、Rustの特徴的な機能と、なぜその機能が必要かというモチベーションを紹介していけたらと思います。
Rustは非常に高機能であり、この記事ですべてを紹介できません。興味を持った方は、ぜひ公式ドキュメントを読んでみてください。私が管理している和訳もあります。
- Rustはシステムプログラミング言語
- Rustのインストール
- Hello World
- FizzBuzz
- 偶数二乗合計
- 変数束縛
- Rustの基本的な型
- 強力な構文、match式
- Rustの式と文
- 所有権と参照とライフタイム
- 次回予告
- 執筆者プロフィール
Rustはシステムプログラミング言語
Rustは、たとえば次のような、さまざまな用途で採用されています。
- データベースやOSなどのシステムソフトウェア
- レンダリングエンジンやゲームエンジンなど、高速性が必要なコンポーネント
- ベンチマーカやテキストエディタなど、IOや描画といったイベントを効率的・即時的に扱うソフトウェア
また、C ABI(Application Binary Interface)互換性を生かしたRubyやNodeのNative Extension(Module)の作成や、最小限のランタイムを生かしてWebAssemblyへコンパイルしてWebブラウザで動かすといった形でも利用されています。
これらの利用例からわかるように、Rustは比較的新しいシステムプログラミング言語だといえます。そのため、無駄のなさや、ユーザが細かいところまで制御できることが主眼となっています。
その特徴を、公式サイトから引用してみましょう。
- ゼロコスト抽象化
- ムーブセマンティクス
- 保証されたメモリ安全性
- データ競合のないスレッド
- トレイトによるジェネリクス
- パターンマッチング
- 型推論
- 最小限のランタイム
- 効率的なCバインディング
見慣れない用語もあると思いますが、この記事を読み終わるころには、これらのRustの特徴について概要が理解できるようになるはずです。
もう一つ、公式サイトでは特に明記されていない点ですが、Rustは多くのことをコンパイル時に静的に解決する言語で、
- ランタイムのGCがないものの、コンパイル時解析のおかげで自動メモリ管理ができる
- マルチスレッドプログラミングで悩ましいデータ競合(競合状態ではない)をコンパイル時に防げる
- ポリモーフィズムをコンパイル時に解決できる
などの特徴があり、すべてを実行時に解決する動的な言語とは対極にあります。
よく、「プログラマは軽量級言語と重量級言語を1つずつくらい使えるようになっていれば困らない」などと言われることがあります。第一言語として動的な言語を使ってきた方は、静的な世界に飛び込んでみるという意味で、Rustを第二言語に選んでみるのもよいのではないでしょうか。
Rustのインストール
Rustコンパイラのインストールは簡単です。
公式ページの「インストール · プログラミング言語Rust」に従えば、コンパイラであるrustc
と、ビルドツールであるcargo
がインストールできます。
そのほか、racerやrustfmtなどのツールもあると便利ですが、この記事を読むにあたってはコンパイラとビルドツールがあれば十分です。ツールの環境構築についてはここでは触れないので、Web上の情報などを参考にセットアップしてみてください。
Hello World
さっそくRustのコードを書いてみましょう。最初はもちろん「Hello, World」です。
次のコードを、hello.rs
という名前で保存してください。
fn main() { println!("Hello, World"); }
このRustコードをコンパイルするには、次のようにrustc
コマンドを実行します。
$ rustc hello.rs
これで、同じディレクトリ内にhello
という実行可能なバイナリファイルができているはずです。
このhello
をシェルから実行すれば、Hello, World
と表示されます。
$ ./hello Hello, World $
これで皆さんもRustプログラマ(Rustaceanと呼ばれます)の仲間入りですね!
それでは、hello.rs
として保存したコードを解説していきましょう。
Rustでは、コンパイル後の実行ファイルは、main
という名前で定義されている関数から実行が開始されることになっています。そのため、このコードでもmain
関数を定義しています。
Rustでの関数定義は、一般的には次のような形になります。
fn 関数名(引数) -> 返り値の型 { 関数本体 }
返り値が何もない場合には、-> 返り値の型
は省略できて、次のように書けます。
fn 関数名(引数) { 関数本体 }
hello.rs
のmain
は返り値が不要なので、この形の関数定義を使っていました(なお、返り値がない関数の型はUnit型といいます。Unit型については後ほど説明します)。
関数定義の本体は、println!("Hello, World");
ですね。
文字列の出力に利用しているprintln!
は、関数とは少し違ったマクロの呼び出しです(Rustでは、末尾に!
がついているとマクロの呼び出しになります)。マクロについてはとりあえず気にせず、関数のようなものを呼び出していると思っておいてください。
FizzBuzz
次は、ループと条件分岐(if
)が出てくる例を見てみましょう。題材は、FizzBuzzです。
FizzBuzzにはいくつも書き方がありますが、一番愚直な方法でRustで書いてみます。
// '//' 以降はコメントとして扱われる。 // 関数の引数は`(変数名: 型名, …)`で書く。 fn fizzbuzz(n: usize) { // `for 変数 in イテレータ {…}`で繰り返しができる。 // 指定回数の繰り返しなら`m..n`のレンジリテラルが便利。m, m+1, …, (n-1)で繰り返す。 for i in 0..n { // `if 条件式 { then式 } else { else式 }`で条件分岐できる。条件の括弧は不要。 // 条件式にはbool型しか書けないので注意。 if i % 15 == 0 { println!("FizzBuzz"); // else if はこう書く。 } else if i % 3 == 0 { println!("Fizz"); } else if i % 5 == 0 { println!("Buzz"); } else { // `println!`は文字列に`{}`を使うことでフォーマッティングできる。 println!("{}", i); } } } fn main() { fizzbuzz(20); }
関数fizzbuzz
の定義では、FizzBuzzを表示したい数の上限を指定できるように、引数を設定しています。引数をとる関数をRustで定義するには、この例のようにfn 関数名 (変数名: 型名, …)
とします。
Rustには、ループの方法としてloop
やwhile
、for
がありますが、この例ではfor
を使っています。Rustにおけるfor
ループは、C言語のような
for (変数; 終了条件; ステップ) {…}
という形ではなく、
for 変数 in イテレータ {…}
という形になっています。この例のように、レンジリテラルm..n
を使うことで、指定範囲の数字を繰り返すイテレータが作れます。
if
式の書式は、ほかの言語を知っていればそれほど難しくないでしょう。注意が必要かもしれないのは、if
の条件式についてです。
真偽値以外を条件に指定できる言語もありますが、静的型付き言語であるRustでは、bool
型になる式しかif
の条件に指定できません。なお、C言語と違って条件式に()
は不要ですが、then
節とelse
節の{}
は必要です。また、else { if …
という書き方をする必要はなく、ネストせずにelse if …
と書けます。
数値を文字列としてprintln!
で出力するために、{}
によるフォーマット機能(詳しくは公式マニュアルを参照)を使っています。
最後にmain
の定義内で、引数に20
を指定してfizzbuzz
関数を呼び出しています。
偶数二乗合計
次は、0
からn
までの偶数の二乗の和を返すプログラムです。今度は、もうちょっとRust的な書き方をしてみます。
fn square_sum(n: isize) -> isize { // FizzBuzzと同じくレンジリテラル (0..n) // 高階関数の`filter`とクロージャリテラルの`|i| i % 2 == 0` .filter(|i| i % 2 == 0) // 同じく高階関数の`map` .map(|i| i * i) // イテレータへの演算`sum` .sum() // returnを書かなくても最後の値が返り値になる。 } fn main() { println!("{}", square_sum(10)); }
ここではじめて、返り値のある関数の例が登場しました。square_sum
は、isize
型(符号付き整数型の一種)の引数を1つとって、同じくisize
型の値を返す関数として定義しています。
square_sum
の定義本体は、関数型言語に慣れていないと読み難いかもしれません。やっていることは、以下の通りです。
- 0から引数で指定された値までの区間を作り(
0..n
) - そこから「偶数である(
i % 2 == 0
)」を満たす要素だけを残し(filter
) - そこから「値を二乗する(
i * i
)」という操作を各要素に施し(map
) - そこから全要素の総和を求める(
sum
)
イテレータ、クロージャ、高階関数が使えるおかげで、かなり「高級」に書けることがわかります。
しかも、このように「高級」な書き方をしても、強力なRustのコンパイラのおかげで、最適化すればループを使って書いたのと同じ速度で動きます。最適化を有効にするには、コンパイル時に-O
を指定します。
$ rustc -O sum.rs $ ./sum 120
なお、次のように--emit asm
をつけてコンパイルすれば、sum.s
にアセンブリが吐かれます。アセンブリを読めるなら、ただのループになっていることが読みとれるでしょう。
$ rustc --emit asm -O sum.rs
変数束縛
3つほどコードを見たところで、これまでの例には登場しなかった文法を説明していきます。まずは、変数束縛です。
Rustでは、let 変数名 = 値;
とすることで変数束縛が作れます。その際、変数の型は自動で推論してくれます。
fn main() { let x = 1 + 2; println!("{}", x); // => 3 }
Rustにおける変数束縛は、デフォルトでイミュータブルです。したがって、再代入はできません。
fn main() { let x = 1 + 2; x = 5; // error[E0384]: re-assignment of immutable variable `x` }
再代入できるようにするには、let mut 変数名 = 値;
のように、ミュータブルな変数であると宣言する必要があります。
fn main() { let mut x = 1 + 2; // 再代入できる。 x = 5; println!("{}", x); // => 5 }
このときに指定するmut
は、あくまでも変数につく属性なので、変数ごとに設定できます。
fn main() { // イミュータブルな変数 let x = 1 + 2; // ミュータブルな変数に束縛できる。 let mut y = x; y = 5; // さらにイミュータブルな変数に束縛できる。 let z = y; // z = 10; // これはエラーになる。 println!("{}", z); // => 5 }
先ほど、変数は型推論されると言いましたが、変数の宣言時に型注釈を書くことも可能です。型注釈は、let 変数名: 型名 = 値;
のように、:
に続けて書きます。
型注釈が必要になることは多くないですが、以下のように説明のために型を明示する目的で使われることもあります。
fn main() { // `i32`型と明示する。 let x: i32 = 1 + 2; println!("{}", x); // => 3 }
なお、変数への「再代入」はできないと言いましたが、「再束縛」はいつでも可能です。
fn main() { // 1つ目の`x`を束縛する。 let x: i32 = 1; println!("{}", x); // => 1 // 2つ目の`x`を束縛する。これは先のxとは別物。 let x: &str = "abc"; // 以後、`x`は`"abc"`を指すようになる。 println!("{}", x); // => abc }
束縛は、あくまでも「変数名と値を結び付ける関係」なので、以前の関係を忘れることさえ認めれば関係の更新はいくらでもできます。「変数は箱」という教わり方をした人にはちょっと馴染みづらいかもしれませんが、Rustではどちらかというと「変数は値につけた名前」です。
再束縛と再代入の違いを説明するため、次のようなコードを用意しました。じっくり見比べてみてください。
fn rebind() { let sum = 0; for i in 0..10 { // 新しい束縛を作っているので上の束縛には影響がない。 let sum = sum + i; } println!("{}", sum); // => 0 } fn reassign() { let mut sum = 0; for i in 0..10 { // 上の束縛の値を書き換える。 sum = sum + i; } println!("{}", sum); // => 45 } fn main() { rebind(); reassign(); }
束縛には、ほかにもいろいろ説明することがあるのですが、ひとまずこれだけ理解して次に進みましょう。
Rustの基本的な型
Rustには、さまざまな型が用意されています。そのうち、特に基本的(プリミティブ)なものを、少し多めですが、一挙に紹介します。
名前 | 説明 | リテラル例 |
---|---|---|
() |
Unit型。何もないことを表わす | ()
|
bool |
真偽値 | true , false
|
char |
文字型 | 'x' , '💕'
|
i8 , i16 , i32 , i64 |
nビット符号付き整数 | 1 , 2i8 , -3_000i32
|
u8 , u16 , u32 , u64 |
nビット符号無し整数 | 1 , 2u8 , 3_000u32
|
isize |
マシンに合わせた符号付き整数 | 1 , -3_000isize
|
usize |
マシンに合わせた符号無し整数 | 1 , 3_000usize
|
f32 |
32ビット浮動小数点数 | 1.0 , -1.0f32
|
f64 |
64ビット浮動小数点数 | 1.0 , -1.0f64
|
&T |
T型への参照型 | - |
&mut T |
T型へのミュータブルな参照型 | - |
[T; n] |
T型のn個の要素を持つ配列 | [1, 2, 3] , [-1.0; 256]
|
&[T] |
T型の要素を持つスライス | - |
str |
文字列型。通常は参照として&str の形で使われる |
"abcd"
|
(S, T, ...) |
任意個の型を並べたタプル型 | (1, 1.0, false, "abc")
|
fn (S, T, ..) -> R |
関数型 | - |
()
型は何もないことを表わす型です。唯一の値()
を持ちます。
真偽値を表すbool
型は、すでにif
式を使ったときに登場しましたね。bool
型には、true
とfalse
という2つの値があります。
数値を表す型
Rustはシステムプログラミング言語なので、ビット数ごとに整数型が用意されています。
また、isize
、 usize
という、配列などのコレクションのサイズを表わすのに十分な大きさの整数型もあります。これらの型で表される整数のビット数は、マシンによって変わります。
参照型
参照型は、C言語などのポインタ型に似た概念で、「T
型の値のありか」を表わす型です。Rustにはあとで説明する所有権の概念があるので「T
型の借用」とも呼ばれます。&値
で参照型の値が作れます。
また、&mut
のミュータブルな参照型は参照先を書き換えることができます。こちらも&mut 値
で参照型の値が作れます。変数のときと違って、こちらは型の一部です。
どちらも、*値
で参照を外し、参照先の値を取得することができますが、Rustはデータの扱いに厳しいので一定の条件を満たさないと参照外しができません。
fn main() { // イミュータブルな束縛を作っておく。 let x = 1; // `&値` で参照がとれる。 let y: &isize = &x; // ミュータブルな束縛を作っておく。 let mut a = 1; // `&mut 値`でミュータブルな参照がとれる。値もミュータブルである必要がある。 let b = &mut a; // `*参照 = 値`で代入できる。これは`&mut`型ならいつでも可能。 *b = 2; // bの参照先が書き変わっている。aは一定の条件を満たしている(Copyな)ため参照外しができる。 println!("{}", *b); // => 2 }
配列とスライス
配列型は、配列まるごとを表わす型です。
たとえば、[i64;256]
という配列は、64ビット×256=16Kビットのデータを表わします。この配列をコピーするときは、16Kビットのデータがコピーされます。扱うデータサイズもユーザ側で制御できるのが、Rustの特徴です。
一方、バイト列を表わすのに&[u8]
のような型を使うこともあります。&[]
というのは、配列への参照(ビュー)を表す型で、スライスと呼ばれます。スライスは、それ自体がデータを持っているわけではなく、データのありかを指すだけです。したがって配列に比べてデータサイズはずっと小さくなります。
fn main() { let a: [isize;3] = [1, 2 , 3]; // `&配列` でスライスが作れる。 let b: &[isize] = &a; // スライスをフォーマットするにはプレースホルダが`{:?}`になる。 println!("{:?}", b); // => [1, 2, 3] for elm in b { println!("{}", elm); } // => 1 // 2 // 3 // あるいは`(スライス/配列)[インデックス]`で要素にアクセスできる。 println!("{:?}", b[0]); // => 1 }
文字と文字列
Rustでは、ユニコード文字を扱えます。'x'
や'💕'
のようにシングルクォーテーションでくくることで、char
型の値を作れます。
文字列について、Rustでは普段、2種類の型を使います。String
と&str
です。String
は、それ自体が文字列の所有者で、文字列を伸ばすなどの操作も可能です。&str
は、スライスと同じように、文字列への参照を表わす型です。
Rustの文字列は、すべてUTF-8でエンコードされている必要があります。UTF-8以外のエンコーディングを扱いたい場合は、ライブラリに頼ることになるでしょう。
String
から&str
は低コストで作れますが、&str
からString
は文字列のコピーが必要なのでコストがかかります。
リテラルの文字列は&str
型です。つまり、変更不能です。Rubyを使っている人なら、「frozen string literal」で話題になったので理解しやすいと思います。
String
は柔軟性が高い反面、作るのにはコストがかかり、&str
は気軽に作れる反面、柔軟性に欠けます。この2つを上手く使い分けましょう。
所有権が絡むので解説は次のセクションに回しますが、リードオンリーなら&str
を、書き換えたいなら&mut String
を、そのまま値をずっと持っておきたいならString
を使うことが多いようです。
fn main() { // `&str`は`to_string()`メソッドで`String`にできる。 let mut a: String = "abc".to_string(); // 少しややこしいが、`String`に`&str`を足すと`String`ができる。 // `&str`に`String`を足したり`String`に`String`を足したりはできない。 a += "def"; println!("{}", a); // => abcdef // `.to_string()`は様々な型に用意されている。 let x = 1.0.to_string(); println!("{}", x); // 1 // `String`を`&str`にするには`as_str()`が使える。 a += x.as_str(); println!("{}", a); // => abcdef1 }
ちなみに、&str
とString
の関係と同じような関係にある型はたくさんあります。たとえば&[T]
に対応するVec<T>
という型もありますし、&T
に対するBox<T>
というのもあります。
タプル
タプルは、複数の値を組にして扱う機能です。それぞれ型が違っても問題ありません。
fn main() { // 型を混合したタプルが作れる。 let a: (isize, f64, &str) = (1, 1.0, "abc"); // `タプル.インデックス`でタプルの要素にアクセスできる。 println!("{}, {}, {}", a.0, a.1, a.2); // => 1, 1, abc }
関数
関数も、第一級の値として扱うことができます。
// 関数を定義する。 fn add(x: isize, y: isize) -> isize { x + y } fn main() { // 関数は`名前(引数)`で呼び出せる。 println!("{}", add(1, 2)); // => 3 // 関数を変数に束縛できる。 let f: fn(isize, isize) -> isize = add; // 変数に束縛した関数も`名前(引数)`呼び出せる。 let a = f(1, 2); println!("{}", a) // => 3 }
強力な構文、match式
Rustの制御構造には、match
、if let
、loop
、while
、break
、continue
、return
などがあります。if
式, for
式はすでに出てきましたね。
このうち特に便利なのは、match
式です。ほかの言語でswitch
文やcase
文と呼ばれるものに似ていますが、もう少し強力です。
fn main() { // `match 値 {パターン => 処理, ..}` の形で書く。 match 10 { // リテラルへのマッチ 0 => println!("0"), // レンジパターンが書ける。 1...10 => println!("small number"), // 変数パターンで受けると残りの可能性すべてを受け、変数をその値に束縛する。 n => println!("big number: {}", n), } match (1.0, 1) { // タプルパターンでタプルを分解できる。さらにパターンの入れ子もできる。 (0.0, 0) => println!("all zero"), // 部分的に変数パターンを使うこともできる。 (f, 0...10) => println!("float: {} with small number", f), // もちろん丸ごと変数で受け取ることもできる。 // 値を特に使わないのであれば特別なパターン`_`を使うことで無視できる。 _ => println!("other tuple"), } }
実はこのパターン、変数束縛や関数の引数でも使えるので覚えておいてください。
Rustの式と文
ここまでの説明で、「if文」ではなく「if式」という言葉を使っていたことに気がついたでしょうか。式は文とは違うものです。式は値を持ちます。たとえば、Rustのif
は「式」ですので、このような使い方ができます。
/// 階乗を計算する。 fn factorial(n: usize) -> usize { // ifは式なので関数の最後に置くと値を返せる。 if n == 0 { 1 } else { n * factorial(n - 1) } }
式ではなく、文が必要になるケースもあります。そのときは、末尾に;
をつけてあげることで文にできます。
//fn main() { // // 最後が式なので`usize`を返していることになるが、`()`でないといけない。 // factorial(10) //} //error[E0308]: mismatched types // --> /home/keen/.cargo/.cargo/script-cache/file-factorial-c98d3cd086909f54/factorial.rs:8:5 // | // 8 | factorial(10) // | ^^^^^^^^^^^^^ expected (), found usize // | // = note: expected type `()` // found type `usize` // fn main() { // `;`をつけることで文になり、コンパイルが通る。 factorial(10); }
特にif
式でif 条件 { then式; } else { else式; }
と書くか、if 条件 { then式 } else { else式 };
と書くかで意味が変わってくるので気をつけましょう。
一方、let
は文です。たとえば、let
の右辺にlet
は置けません。
fn main() { let x = let y = 1; // error: expected expression, found statement (`let`) }
所有権と参照とライフタイム
今回の記事の最後に、Rustにおいて特徴的な「所有権」「参照」「ライフタイム」を紹介します。これらはRustの中心的な機能であり、このおかげでRustではGCがなくても自動メモリ管理が可能になっています。
この記事では駆け抜ける程度にしか説明できませんが、Rustの公式ドキュメントでは「所有権」「参照と借用」「ライフタイム」それぞれに独立したセクションが割り当てられて、詳細に解説されています。本格的にRustを書いていくなら、一度は公式ドキュメントも読んでおいてください。
Rustの所有権、参照、ライフタイムは便利で強力な機能ですが、これらをRustほど全面的に取り入れた言語があまりないので、多くの人にとっては馴染みの薄い機能でしょう。それもあって、所有権、参照、ライフタイムは、Rust初心者の前に立ちはだかる大きな壁ともなっています。
とはいっても、ルール自体が難しいわけではありません。単純に新しい概念なので、馴染むのに時間がかかるだけです。コードを書いているうちに馴れてくるものなので、安心して読み進めてください。
Rustの値は資源であり、所有者が移っていく
Rustではすべての値が、資源やお金のように、一度使ったらなくなります。この性質は、マシンのリソースを効率良く使う必要があるシステムプログラミング言語にとっては、ありがたい特徴だといえます。普段扱うものの性質をそのまま表しているのですから。
さて、「値を使うとなくなる」ことを、Rustでは所有権という言葉で説明しています。関数を呼び出したり変数に格納したりすると、それらのリソースの所有権が、別の所有者に移るのです。所有権が移動することを「ムーブする」ともいいます。所有者がいなくなったリソースは自動で解放されます。
所有権が移っていくようすを、以下のコードのコメントで説明します。
fn print_string(s: String) { println!("{}", s); // sはこの関数の終わりで消滅する。 // このタイミングでsのメモリも自動で解放される。 } fn main() { let s = "this is a resource".to_string(); // 以下の行で、`s`が束縛されている文字列の所有権が`t`に移る。以後`s`は使えない。 let t = s; // 以下の行で、文字列の所有権が`t`から`print_string`に移る。以後`t`は使えない。 print_string(t); // もう一度`t`を使おうとしてもエラー。 // print_string(t); // error[E0382]: use of moved value: `t` // 同じくsを使おうとしてもエラー。 // print_string(s); // error[E0382]: use of moved value: `s` }
所有権を貸し出す
このように、所有権だけなら話は単純です。しかし、一度使ってしまうと値がなくなるばかりでは不便ですね。
そこでRustには、借用の機能があります。所有権を自分に残したまま、値を他人に貸すことができるのです。借用には、すでに出てきた参照型を使います。参照型の説明のところで、「参照型はT
型の借用とも呼ばれる」と説明したのを思い出してください。
参照には、ミュータブルなものとイミュータブルなものがありました。これらを借用という観点で直観的に言い換えると、それぞれ「使わせる」(ミュータブル)、あるいは「見せる」(イミュータブル)ような貸し方だといえます。
ミュータブルな参照とイミュータブルな参照とは共存できません。そして、ミュータブルな参照は同時に一つだけ存在できます。一方、イミュータブルな参照は同時に複数存在できます(分かる人は「コンパイル時Read-Writeロック」だと考えてもかまいません)。
イミュータブルな参照
イミュータブルな参照の例から見てみましょう。イミュータブルな参照は同時に2つ存在できるので、次のように書いても問題ありません。
fn ref_string(s: &String) { println!("{}", s); } fn main() { let s = "this is a resource".to_string(); // 参照1つめ。 let t = &s; // 参照2つめ。同時に2つ存在できる。 ref_string(&s); }
ミュータブルな参照
一方、ミュータブルな参照は2回使えず、エラーになります。
fn refmut_string(s: &mut String) { // ここでsに対して変更を加えるなどの操作も可能。 println!("{}", s); } fn main() { let mut s = "this is a resource".to_string(); // ミュータブルな参照1つめ。 let t = &mut s; // ミュータブルな参照2つめはエラー。 // refmut_string(&s); // error[E0499]: cannot borrow `s` as mutable more than once at a time }
複数箇所から同時に書き換えると、(スレッドを使っていなくても)思わぬことが起きるので、ミュータブルな参照は同時に一つしか存在できないのです。安全な言語、Rustらしい仕様ですね。
ミュータブルな参照とイミュータブルな参照は共存できない
同じく、ミュータブルな参照とイミュータブルな参照も共存できません。これも許してしまうと思わぬことが起きてしまいます。
fn main() { // ベクトルを用意する。 let mut vec = vec![1, 2, 3]; // ベクトルの要素への参照を取り出す。 // ベクトルをイミュータブルに参照する。 for i in vec.iter() { // すでにベクトルはイミュータブルに参照されているので // ここでベクトルを変更しようとするとエラー。 // 実際、これを許すと無限ループしてしまう。 vec.push(i * 2); } }
これをコンパイルすると、以下のようなエラーが出ます。
--> /path/to/mut_ref.rs:9:9 | 6 | for i in vec.iter() { | --- immutable borrow occurs here ... 9 | vec.push(i * 2); | ^^^ mutable borrow occurs here 10 | } | - immutable borrow ends here error: aborting due to previous error error: Could not compile `mut_ref`. To learn more, run the command again with --verbose. internal error: cargo failed with status 101
参照先が存在しなくなるなら、貸し出せない(ライフタイム)
値を参照することで、所有権を貸し出せることがわかりました。では、参照先がなくなってしまったらどうなるでしょう? そのような状況は、C言語などで「dangling pointer」といって防ぎようがなく、プログラマが注意してプログラミングする必要がありました。
Rustでは、そもそもそういった安全でない参照を作れない仕組みになっています。その仕組みが、ライフタイムです。
ある値のライフタイムは、その値がどこで定義されているかによって決まります。たとえば、{ }
で囲んだブロックや、関数のスコープによって、ライフタイムが区切られます。
また、値を貸している間に参照先をムーブしようとすると、コンパイル時にエラーになります。
fn main() { // 本来は`s`のライフタイムはこの関数の最後まで。 let s = "owned data".to_string(); // `{ }`で囲んだブロックはライフタイムを区切る。 { // `s`はここでムーブしてしまうのでここでライフタイムが終わる。 // `t`のライフタイムはこのブロックの終わりまで。 let t = s; } // ここでは`t`にも`s`にもアクセスできない。 // ライフタイムと参照の関係 { let s = "owned data".to_string(); // ここで`s`への参照を作る。この参照はこのブロックの最後で死ぬが、`s`のほうが長生きしないといけない。 let ref_s = &s; // たとえば以下のように`s`のライフタイムを`ref_s`より先に終わらせようとするとエラーになる。 // let t = s; // cannot move out of `s` because it is borrowed } }
このようにRustでは、ほかの言語ではどうしようもない部分、バグや脆弱性の温床になっている部分を、コンパイラの機能で解決しています。
特別な所有権を持つCopy型
最後に、所有権で特別扱いされる「Copyな値」について説明しておきます。
「Copyな型」の値については、所有権の検査で例外的に扱われます。「すべての値が、資源やお金のように一度使ったらなくなります」と説明しましたが、実際のプログラムでは、数値のように、湯水の如く扱える値も必要です。これらの値も使うたびになくなっていたのでは大変です。
そこで用意されているのが、Copyな値です。Copyな値の代表は、数値型や参照型の値です。Copyな値は、一度使ってもなくならず、ほかの言語で扱っている値のように何度でも使えます。
fn main() { let x = 1; // 下記の行で所有権がムーブしてしまいそうだが… let y = x; println!("{:?}", y); // => 1 // 数値はCopyな値なので一度使ったあともまた使える。 println!("{:?}", x); // => 1 // &strもstrへの参照なのでCopyな値 let a = "abc"; let b = a; println!("{}", a); // => abc }
ちなみに、Copy
というのは型ではなく、トレイトと呼ばれるものです。トレイトについては、この記事では説明しきれないので、回を改めて説明します。
所有権と参照とライフタイムについて、雰囲気だけでもつかんでもらえたでしょうか。繰り返しになりますが、これらの概念は特別に難しいものではなく、馴れの問題なので、実際にコードを書いて経験を積めば、すんなりと書けるようになります。
ぜひ、Rustでコードを書いてみてください。
次回予告
駆け足でRustの機能を説明してきましたが、まだまだ説明していないことがたくさんあります。
構造体と列挙型を説明していないので、クラスを説明せずにRubyを語っているようなものです。Rustを支える機能であるトレイトにも触れてません。大きなプログラムを書こうと思ったらモジュールやクレートも必要です。
次回は、これらについてお話しすることにします。それではお楽しみに。
執筆者プロフィール
κeen(blackenedgold)