導入
皆さんこんにちは。趣味でRustを使ってコンパイラを作ってるらいパン粉です。
プログラミング言語Rustの良さを雑にあっぴるしていきます。
では、早速cargo run!(このコマンドでRustのプログラムが実行される)
C,C++のコードが出てくるので覚悟してください。
Rustとは
- Mozillaが応援している言語
- Microsoftも注目している
- 2006年から開発が始まった新しめの言語
- 2016年、2017年、2018年のStack Overflow Developer Surveyで「最も愛されているプログラミング言語」で一位を獲得している
- C/C++と同等の処理速度
- C/C++の代替えを目指している
- 静的に型が付く、コンパイラ言語
- 静的に変数の寿命もわかり、自動でメモリを解放(GCより速い!)
- 関数内部限定での極めて賢い型推論
- C/C++と比べて極めて安全
- オブジェクト指向ではないし関数型言語でもない新たな概念を持つ
- デフォルトでスレッドセーフが保証された変数達
- C++のムーブセマンティクスを言語レベルでサポート
- C++と違い、制約付きのテンプレートがある
- DSL作成可能で健全なマクロ
- タプル、代数的データ型とマッチ式もあります
- 標準でテスト機能が付いている
- 標準でパッケージマネージャーがある
ざっとこんな感じ。
では、早速Rustを語っていきます。
インストールがありえないほど簡単!
基本的にはこのページに従うだけでインストールは終わります。
https://www.rust-lang.org/ja-JP/install.html
Windowsならインストーラを起動するだけ。
MacやLinuxはターミナルにコマンドをコピペしてEnterするだけです。
簡単!
爆速でうごくHelloWorld!
Rustをインストールすると三つのcommandが使えるようになります。
Rustのインストールをするrustup。
Rustのコンパイラrustc。
パッケージマネージャーのcargo。
しかし、実際に触るのはcargoだけです。他二つはめったに触りません。
そして、以下のcommandでプロジェクトが生成されます。
cargo new プロジェクト名
そしてcd コマンドでできたプロジェクトの中に入り、以下のcommandを打ちます。
cargo run
そうすると画面にHelloWorldが出力されるでしょう。
Rustではプロジェクトを作成すると、main.rs
というファイルが生成される。
これがいわゆるRustのソースファイルになり、main.rs
の中身は以下のようになっています。
fn main() {
println!("Hello, world!");
}
そう!RustではもはやHelloWorldは書く必要はありません。
わーお
公式チュートリアルが日本語に翻訳されている
チュートリアルが日本語であるのはうれしいですね。
すこし、翻訳ががばがばな気がしますが、全然読めます。
https://doc.rust-jp.rs/book-ja/index.html
ほとんど全ての機能が説明されています。
また、Rustの日本コミュニティのslackもあります。
Rustの達人がたくさんいるので知恵袋やスタックオーバーフローで聞くより良いです。
https://rust-jp.herokuapp.com/
所有権&ライフタイムによる、安全で優れた設計の強制
Rustは所有権と借用システムにより、安全で優れた設計を強制します。
このことより、Rustを学ぶことは、プログラマーとしての設計能力をいっそう成長させると思います。
ではまず、以下のくそみたいなCのコードを見てみましょう。
#include <stdio.h>
int* hoge(int a, int b){
int c = a + b;
return &c;
}
int main(void)
{
printf("%d", *hoge(8, 2));
return 0;
}
こちらで実行できます。
https://wandbox.org/permlink/nhTU8h2gJnIKJtFQ
hoge
関数は8
と2
を受け取り、足した結果を変数c
に入れ、そのcのポインタを返しています。
main
では変数c
の参照をデリファレンスして画面に表示しています。
デリファレンスとはポインタが指している値を取得することです。
GCC4.9.3コンパイラでは10
が画面に表示されました。
しかしローカル変数c
の寿命はhoge
内だけなので、c
のポインタをhoge
のスコープ外に渡すのは未定義動作です。
未定義動作とは、何が起きるかわからないという意味です。
この例では運よく8+2
が表示されましたが、次はどうでしょうか。
#include <stdio.h>
int* hoge(int a, int b){
int c = a + b;
return &c;
}
void fuga(int* d){
int s = 777;
printf("%d", *d);
}
int main(void)
{
fuga(hoge(8, 2));
return 0;
}
こちらで実行できます
https://wandbox.org/permlink/qVQkNsJOlkFQcxje
私が実行したときは画面には10
ではなく777
が表示されました。
これは望む結果ではありません。
C言語ではこのような危険なことができますが、Rustではポインタの寿命を厳格に管理するため、コンパイル時にエラーしてくれます。
まあ、といっても最近のCコンパイラは賢いのでこの類の未定義動作は警告してくれるのですぐに気が付きますが。
一応Rustで書くとこんな感じです。これはコンパイルエラーになります。
fn hoge<'a>(a:i32, b:i32) -> &'a i32{
let c= a + b;
return &c;
}
fn main(){
println!("{}", hoge(8, 2));
}
コンパイルエラーの内容は、変数c
の寿命が短いので持ち出せないよ!といったエラーでした。
Rustではポインタが無効な場所を指さないように、寿命(ライフタイム)が有効かどうかを静的に検査します。
次により深刻的なケースをC++で紹介します。
これはC++だけでなくC#やJavaにも起こる可能性があります。
【例題】
あるところに、二人の若者、a
とb
がいた。
ある日、二人は体力勝負をするために、どれだけジャンプできるかを競うことにした。
そのためにカウンターを用意して、ジャンプするたびにカウントして、カウント数が多い方が勝ちとした。
では早速これをC++で実装してみる。
運が悪く、これを実装したC++プログラマは初心者であった。
//数値をカウントする機械
class Counter{
int cnt = 0;
public:
//カウントする
void count(){
cnt++;
}
};
//ジャンプする人
class Jumper{
Counter* c;
public:
Jumper(Counter* c){
this->c = c;
}
//ジャンプする。ジャンプしたらカウントする
void jump(){
c->count();
}
~Jumper(){
delete c;
}
};
int main()
{
Counter* c = new Counter();
Jumper a = Jumper(c);
Jumper b = Jumper(c);//おおっと!間違えて二人に同じカウンターを配布してしまった!
a.jump();
b.jump();
}
このコードには3つの問題がある。
1つは同じカウンターを二人に渡してしまったので計測ができないことである。
もう1つはわかりずらいが、Counter
をJumper
デストラクタで二回delete
しているところである。
そして最後はこの、a
とb
が非同期で実行される時に起きる。
もしa
とb
が非同期で独立して動くプロセスだとして、二人が同時にjump
したらどうなるだろう?
そう!たったこれだけのコードでもたくさん考慮しないといけないことがある!!
値がどこで書き換わるのか、
その値はもう削除されたのか、
それはスレッドセーフなのか、
これはJavaでもC#でも変わらない。
設計を慎重に行わなければ、可読性の低く、バグを含んだ、状態まみれのクソコードの完成だ!
Rustでは所有権システムがこのようなクソコードを許さない。
まずRustでは、値を書き換えれる責任は一つのオブジェクトしか持てない。
つまり、a
とb
に同じカウンターを渡すことは不可能である。
なので値がどこで書き換わるかはあまり意識しなくてよくなる。
さらにRustでは値の削除は全て自動的に行われる(これについてはあとで)
なので、値の削除について考えることはない。
そして、値は必ず一か所でしか変更されないため、非同期でも問題なく動く。
これでRustがいかに安全か分かったと思います。
null安全
Rustの参照またはポインタにはnull
が存在しない。(一応存在はするがunsafe内でのみ使用可能)
なので、間違ってnull
の値にアクセスしたりできない。
null
の代わりにOption
という構造が用意されています。
以下のCのコードを見てください。
#include <stdio.h>
//intポインタが指す値をカウントする
//nullかもしれないのでちゃんとnullチェックしないとだめ
void count(int* a){
if(a != NULL)
*a = *a + 1;
}
int main(){
int a = 5;
count(&a);
printf("%d", a);
return 0;
}
実行はこちら
https://wandbox.org/permlink/xgH6Mu17gGOdvKfL
nullチェック忘れたら大変ですよね。
Rustではこうなります。
fn count(a: &mut Option<i32>){
match a.as_mut(){
Some(x)=> *x += 1,
None=> ()
};
}
fn main()
{
let mut a = Some(5);
count(&mut a);
println!("{}", a.unwrap())
}
Option
は値がないかもしれないを表す型で、値を取り出すときに値があるかどうかを必ずチェックする必要があります。
これによって、チェックのし忘れは起きません。
コードの解説をします。
まずはmain
関数から。
Rustでは、変数宣言にlet
が必要です。
let 変数名
で宣言された変数は値を書き換えることができないため、
今回はlet mut a
として、変数が書き換え可能であることを宣言します。
mut
はミュータブルの略であり、変更可能を意味します。
続いて、Some(5)
は整数値5
をOption
で包んでいます。
変数宣言に型を指定する必要はありません。コンパイラが型を推論してくれます。
続いてa
をカウントしたいので、count
関数に渡します。
この時&mut
修飾子を付けます。これは変更可能な参照を渡しますよとコンパイラに指示しています。
これによってプログラマは、あ、このcount
関数ではa
を書き換えるんだなということが明確にわかります。
あとはcount
内でOption
の中身である5
を取り出すためにmatch
式を用います。ここの詳しい説明はしません。
エラー処理
Rustは関数が失敗したかどうかをキャッチする特別な機構はありません。
関数の返り値としてエラーかどうかを判別します。
これにより、例外のcatchミスによる、停止を防げます。
例としてC#の文字列を数値に変換するプログラムを見てみましょう。
using System;
namespace Wandbox
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(int.Parse("4593"));
Console.WriteLine(int.Parse("hao123"));//数字じゃないので例外を投げる
}
}
}
https://wandbox.org/permlink/VFc5pSAzozxN6YxO
このプログラムはint.Parse
関数を使って文字列を数値に変換してそれを画面に表示するプログラムです。
int.Parse
は数字に変換できない文字列が与えられると例外を吐くのでしっかりチェックをしないといけないのですが、それを忘れてしまい、プログラムが停止しています。
本来はこうするべきです。
using System;
namespace Wandbox
{
class Program
{
static void Main(string[] args)
{
try{
Console.WriteLine(int.Parse("4593"));
Console.WriteLine(int.Parse("hao123"));//数字じゃないので例外を投げる
}
catch{
Console.WriteLine("変換できませんでした");
}
}
}
}
ではRustでの例を見てみましょう。
fn main()
{
let a : Result<i32,_> = "4693".parse();
let b : Result<i32,_> = "hao123".parse();
match a{
Ok(x) => println!("{}", x),
Err(_) => println!("変換に失敗")
};
match b{
Ok(x) => println!("{}", x),
Err(_) => println!("変換に失敗")
};
}
https://play.rust-lang.org/?version=stable&mode=debug&edition=2015&gist=dca2726190250e98b59d297eb319d2ee
注目すべきはparse
の結果がResult
に包まれていることです。
これは成功か失敗を表す型です。
RustではただResult
値が返ってくるだけなので、別にプログラムは停止しません。
Result
値もOption
同様match
式で成功か失敗かをチェックしないと値は取り出せません。
少し冗長なコードに見えるかもしれないですが、これはわざとそうしているだけで実はもっとスマートに書けます。(?
演算子の活用)
メモリ管理
メモリの管理は一歩間違えれば、足を打ち抜くことになります。
Rustほど素晴らしいメモリ管理はないと思います。
C/C++は早いですがメモリ管理を手動で管理しないといけないので大変です。
スマートポインタも全然スマートじゃありません。
C#/JavaはGCでメモリを自動で管理しますが、メモリ破棄のタイミングは分かりません。
場合によってはバグの発見に遅れるし、デストラクタが使い物にならないので設計がしづらいときがあります。
Rustではコンパイル時に全ての変数の寿命が分かるため、自動でメモリを破棄してくれます。
GCを使っていないのでC/C++と同等に高速です。素晴らしい!
テスト
他の言語では単体テストや結合テストを行うのに、テストフレームワークを別途インストールしたりしますが、Rustではそんな面倒なことはしません。
テストは言語レベルで組み込まれています。
試しに足し算の関数を作って、それの単体テストを作成してみます。
fn plus(a:i32,b:i32)->i32{
a+b
}
#[test]
fn plus_test(){
assert_eq!(plus(4, 5), 9);
assert_eq!(plus(100, -1), 99);
assert_eq!(plus(114000, 514), 114514);
}
ね、簡単でしょ?
#[test]
を付けた関数は通常コンパイル時には無視されます。
以下のcommandを打つとRustは#[test]
が付いた関数を実行しマルチスレッドでテストを開始します。
cargo test
このテストはソースコードのどこにでも書くことができます。
マクロ
C言語のマクロをご存じでしょうか?
マクロとは、コンパイルする際にコードを指定したルールで置き換えてくれる機能のことです。
Rustにもマクロはあります。
RustのマクロはC言語のマクロと比べて、極めて強力で安全であります。
標準で用意されているマクロの利用
標準で用意されているマクロについてはコチラをご覧いただくか、もっと素晴らしい記事やドキュメントを参照ください。
https://qiita.com/elipmoc101/items/f76a47385b2669ec6db3
ちょっとしたショートカットにマクロの利用
マクロを使うと文法を拡張できますので、関数分割でも短くできそうにない煩雑な個所をマクロで短く置き換えることができます。
ドメイン特化言語としてのマクロの利用
Rustのマクロを使えば独自の文法でコードを書けるようになります。
これを利用し、可読性と効率を高めることができます。
(やろうと思えば、Haskellのdo構文をある程度は模倣できるようです。)
終わりに
Rustは本当に最高の言語です。
もしより成長を望むのであれば、この言語を極めるべきだと感じています。
ここに書かれていることはRustのほんの一部でしかないので、公式のチュートリアルを読むことをお勧めします。