はじめに
Rustは所有権など新しい概念が多く学習難易度が高いと言われています。
しかし個人的な感触としてはかなりソースコードの読みやすさに振った言語ではないかと感じているので、
そのあたりを説明したいと思います。
内容的に論理的な話というより感覚的な話が多く出てきますのでポエムということにしておきます。
ちなみに筆者がこれまで経験してきた言語はC/C++/C#/Java/Python/Ruby/Scalaあたりです。
そのためどうしても「それらの言語と比べてどうか」という話になりがちですが、
Rustが他言語より優れている/劣っているといった主張をするつもりはなく、
あくまで目指している方向が違う(と個人的に思っている)のだとご理解ください。
数値型
Rustの数値型は例えば
-
i32
: 符号付き32bit整数 -
u128
: 符号なし128bit整数 -
f64
: 64bit浮動小数点数 -
isize
: 符号付き整数(ポインタと同じサイズ)
といった感じです。int
が何ビットか、といったことを覚えておかなくてもビット数が明確ですし、アーキテクチャ依存のある型(usize
とisize
)が分離されているのも分かりやすいと思います。
また、Rustは数値型間の暗黙のキャストを許容しないので、(たとえi32
-> i64
のように明らかに情報が落ちない場合でも)型変換は必ずソースコード上に明示されることになります。
これは記述が増えるので嫌われる傾向にあると思いますが、個人的にはどこで型が変わったかソースコードを追う必要がなくなるという点で、明示されている方が読みやすく感じます。
let
とlet mut
let
は不変の変数宣言、let mut
は可変の変数宣言です。
基本的にはできるだけ不変で宣言したいので、可変の場合だけキーワードを追加するのは理にかなっていると思います。
let x = 0;
let mut y = 0;
y = 1;
例えば同じように可変と不変を宣言できるScalaの場合は以下のようになります。
こちらはこちらで縦方向に揃うのが好きなのですが、val
とvar
の区別がし辛いのでmut
の方がより可変であることが分かりやすいと思います。
(またRustの方はあえて縦方向に揃えないことで、注目を促しているようにも見えます)
val x = 0;
var y = 0;
y = 1;
シャドーイング
Rustでは以下のように同一スコープで同名の変数を再度宣言することが可能です。
(これは代入ではないので、2つのvalue
は型が違っていてもいいですし、mut
も必要ありません)
let value = "0";
let value: i32 = value.parse().unwrap();
この挙動はあまり一般的ではないので初見殺しと言われたりするようですが、変数名をシンプルに保つ効果があるのではないかと思っています。
(他にもmut
をつけたり外したりするのにも使うので、そのためだけの機能ではないですが)
例えば同じコードをC#(Parse
の字面が似ているので選びましたが他の言語でも似たような感じになると思います)で書くと以下のように型毎に変数名を変える必要があります。
この_str
や_int
は本来変数名に必要な情報ではないのではないか、ということです。
string value_str = "0";
int value_int = Int32.Parse(value_str);
初めてRustのシャドーイングの仕様を見たときは「これは想定外の箇所でシャドーイング起こしてバグの原因になるのでは?」と思ったのですが、5年ほど書いて特に困ったことも起きていないです。
型検査がしっかりしているので、予想外のシャドーイングが起こった場合すぐコンパイルエラーとして検出されるからなのかな、と思っています。
エラー処理
Rustにはいわゆる(C++やJavaにおける)例外はなく、失敗する可能性のある処理はResult
という型を返すことで表します。
fn failable() -> Result<i32, Error>;
これは特に関数呼び出し側の読みやすさに寄与していると感じます。
例えば上記のfailable
関数の呼び出しは以下のようになります。
let x = failable()?; // 失敗したらearly return
let y = failable().expect("failed at y"); // 失敗したら"failed at y"を表示して強制終了
let z = failable().unwrap(); // 失敗したら強制終了
let w = failable();
...
w.unwrap(); // 後でエラー処理する場合
このように失敗する可能性のある関数は必ず失敗したらどうなるかを呼び出し時(あるいは結果を使う前)に明示することになります。
明示しなければコンパイルエラーとなるので、忘れるということもありません。
例外の場合、各関数からどのような例外が上がってくるのかをソースコード上から知る術はなく、
いつも「必要なcatch
を網羅出来ているんだろうか?」という漠然とした不安があったのですが、
Rustの方式では(ちゃんと処理するか無視するかは別として)全てのエラーになんらかの対処をしている、と感じられるように思います。
まとめ
以上、個人的にRustが読みやすいと思っている部分についてご紹介しました。
Rustというと「所有権が~」という話になりがちですが、こういう特徴もあるよ、ということで。