はじめに
最近、流行りのRustを勉強し始めました。学んだ中で、今回はRustの基本構文についてまとめました。
Rustとは?
Rustは、CやC++変わるものとして、実行時パフォーマンスはCと同程度を目指しつつむも、Cよりも楽にコーディングできることを目指している言語です。例えば、次のような特徴があります。
- 静的型付け言語・型推論のサポート
- コンパイル言語
- traitを用いたポリモーフィズム
- ジェネリクス
- ガベージコレクションなしに確保されるメモリ安全性
最近では、Linuxカーネルの開発言語として採用されたり(長らくCが使用されていた)、最近ではJavaScriptランタイムのdenoがRustで実装されていたりしていて、話題になっていました。
また、私はPythonも書くのですが、使用しているライブラリ「cryptography」の実装がRustであるなど、ハイパフォーマンスが求められる場面で、Rustが使われることが増えてきていることを体感しています。
Rustの特徴的な基本構文
変数と定数
Rustの変数は、標準では不変であり、再代入することができません。そのため、変数に値を代入することを「値を束縛する」と言います。
let
キーワードで変数を宣言できます。
1 2 3 4 5 6 7 8 9 10 11 | fn main() { // 宣言と同時に値を変数に束縛 let x = 1; println!("x = {}", x); // x = 1 x = 2; // 再代入はできない // 宣言と束縛を別々に行う let y: i32; // この場合は型アノテーション y = 3; println!("y = {}", y); // y = 3 } |
また、変数を可変にするには、
mut
キーワードを使用します。
1 2 3 4 5 6 | fn main() { let mut z = 2; z = 3; // 再代入できる println!("z = {}", z); // z = 3 z = "hello"; // 型が違うのでエラー } |
Rustは静的型付け言語なので、宣言時と異なる型での再代入はできません。
mutを避ける方法として、シャドーイングがあります。
プログラムが長くなってくると、同じ変数名を使いたくなってくることがありますが、Rustでは同じ変数名で再宣言することができます。
1 2 3 4 5 6 7 8 | fn main() { let x = 1; println!("x = {}", x); // x = 1 let x = 2; println!("x = {}", x); // x = 2 let x = "hello!"; println!("x = {}", x); // x = hello! } |
この場合、新たな変数を宣言していることになるため、再代入とは異なり、別の型にすることもできます。
所有権
最近のプログラミング言語では、GCにより、参照されていないメモリ上の領域を開放する機能が備わっていると思います。RustにはGCは無く、代わりに所有権という概念により、適切にメモリ領域が破棄されます。
今回は、所有権を理解するところから始めます。
例えば、変数に値を束縛する例を見てみましょう。
1 2 3 4 5 6 7 8 9 | fn main() { let x = 1; let y = x; print!("x = {}, y = {}", x, y); // x = 1, y = 1 let s1 = String::from("hello"); let s2 = s1; println!("{}, world!", s1); // ERROR: s2に所有権がmoveしたため、s1は使用できない } |
前者のi32型はエラーとなりませんが、後者のString型はエラーとなります。その違いは、i32にはCopyトレイトが実装されていますが、Stringには実装されていないことにあります。
変数に値が束縛される際、Copyトレイトが実装された型である場合のみ値のCopyが行われ、そうでない場合はポインタのみがコピーされ、所有権が移ります。
それでは、なぜi32はコピートレイトが実装されているのでしょうか。その理由は、i32は固定長であり、値をコピーする際のコストが少ないからだそうです。
Copyトレイトが実装されている型は、基本的にはスカラー値のみで、それ以外の型はmoveが発生すると思っておくとよさそうです。
以下に、Bookからの引用で、Copyが行われるケースを例示します。
- あらゆる整数型。
u32
など。- 論理値型である
bool
。true
とfalse
という値がある。- あらゆる浮動小数点型、
f64
など。- 文字型である
char
。- タプル。ただ、
Copy
の型だけを含む場合。例えば、(i32, i32)
はCopy
だが、(i32, String)
は違う。
次に、関数に引数を渡す例を見てみます。
1 2 3 4 5 6 7 8 | fn main() { let name = String::from("Taro Yamada"); greeting(name); println!("name: {}", name); // greeting() で name がムーブされているので、ここでは name は使用できない } fn greeting(name: String) { println!("Hello {}!", name); } |
関数の引数に変数を渡した場合も、所有権が移るため、元の変数を使用することはできません。
nameを引き続き使用したい場合には、greeting関数で引数の対をreturnして、所有権を返してもらう必要があります。ですが、毎回そのような実装をするのは面倒なので、そのようなケースでは所有権の借用をすることになります。
最終的に、所有権を持った変数は、スコープを抜けたタイミングでドロップされます。ドロップされると、メモリ上の領域から消去されます。
このように、所有権に反するコードがあると、コンパイルエラーとなるため、GCが無くても適切にメモリ上の領域が破棄されます。
所有権の借用
所有権をムーブすることなく、関数に変数の値を渡すためには、「参照渡し」をする必要があります。先ほどのコードを次のように修正すると、エラーが解消します。
1 2 3 4 5 6 7 8 | fn main() { let name = String::from("Taro Yamada"); greeting(&name); println!("name: {}", name); // 所有権は移っていないので、エラーにならない } fn greeting(name: &str) { println!("Hello {}!", name); } |
値の参照を取得するには、
&
を付けることで取得できます。また、参照で受ける場合には、型名にも&を付けます。
なお、参照を外すには
*
演算子を使用します。
関数
既に何度も登場していますが、Rustの関数の基本形は次のようになります。
1 2 3 4 5 6 7 8 | fn main() { let result = addition(1, 2); println!("Result: {}", result); } fn addition(a: i32, b: i32) -> i32 { a + b } |
静的型付け言語を使っている人にとってはお馴染みだと思いますが、関数の仮引数には型指定が必要で、戻り値の型指定も必要です。
また、関数内で一番最後に実行される部分に「式」を書くことで、returnを省略することができます。
ちなみに、式は文末にセミコロンを書くと文として扱われてしまうため、注意が必要です。
エラーハンドリング
Rustでのエラーハンドリングは大きく分けて2つあります。
回復不能なエラー(panic!)
panic!()を使用すると、その時点でプログラムは終了します。panic!は回復不能なエラーであるため、よほどの場合でない限り、使用することはないと思います。
1 2 3 | fn main() { panic!("Crashed!!!") } |
回復可能なエラー(Result)
RustにはJavaやPythonなどのように、例外を投げるという機能はありません。代わりに、回復可能なエラーとしてResultをReturnする方法があります。実際のところ、Resultは次のようなEnumです。
1 2 3 4 | pub enum Result<T, E> { Ok(T), Err(E), } |
扱いはEnumと同じなので、Resultをmatch式で受け、エラーハンドリングすることができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | use std::{fs::File, io::Read}; fn main() { read_file("hello.txt"); } fn read_file(path: &str) { let mut f = match File::open(path) { Ok(file) => file, Err(e) => panic!("Problem opening the file: {:?}", e), }; let mut s = String::new(); // read_to_stringの第一引数は &mut selfとなっているためfは可変参照(mut)である必要がある match f.read_to_string(&mut s) { Ok(_) => println!("File contents: {}", s), Err(e) => panic!("Problem reading file: {:?}", e), } } |
上記のコードにはまだ問題があります。それは、せっかくエラーハンドリングしているのに、panic!を起こしてしまっている点です。それでは、エラーが発生した場合に、呼び元にエラーを返す(移譲する)ようにしたいと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | use std::{ fs::File, io::{self, Read}, }; fn main() { match read_file("hello.txt") { Ok(s) => println!("{}", s), Err(e) => println!("Error: {}", e), } } fn read_file(path: &str) -> Result<String, io::Error> { let mut f = File::open(path)?; let mut s = String::new(); f.read_to_string(&mut s)?; Ok(s) } |
まず、main関数でエラーハンドリングが行われるようになったため、read_file()関数の結果をmatch式で評価するようになりました。
また、read_fileの返り値の型が
Result<String, io::Error>
となりました。これは、read_fileで使用している関数のResult型が返すエラーがio::Error型であるためです。
そして、read_file()関数の中身がかなりスッキリしました。文末に
?
演算子が付いています。これは、Result型を返す関数に対してのみ使える書き方で、エラーが返ってきた場合に、呼び出し元にそのままエラーをreturnすることができる書き方です。
?を付けた場合、返り値の型はResultのT型となるため、続けて関数を呼び出すこともできます。
1 2 3 4 5 | fn read_file(path: &str) -> Result<String, io::Error> { let mut s = String::new(); File::open(path)?.read_to_string(&mut s)?; Ok(s) } |
さいごに
Rustで一番初めに躓きそうなポイントを紹介できたと思います。今後は、さらに深掘りしたいと思います!