はじめに
どうも、レガシー組込みエンジニアの@yagisawaです。
Rustを勉強しているとCでは見かけない言語仕様(主に記号の使い方)に出くわすことがあるので、チートシート的な感じでまとめてみました。
勉強が進み次第都度更新していく予定です。
「RustではXXX
等に使います」と曖昧な表現をしていますが、私の勉強が足りていないだけで本記事で紹介しているもの以外の使い方も存在する可能性がある旨、予めご承知おきください。
Cについても私が知らない使い方が存在する可能性がある旨、予めご承知おきください。仕事柄C89の仕様を主に扱っています。また本記事の主眼はRustであるため、Cの情報の充実度については本質ではありません。
RustとCの記号の一覧は以下の記事を参考にしています。
記号
!
Cでは論理否定
非等価比較
等に使います。
// 論理否定
flg = !flg;
// 非等価演算
if ( flg != false ) { ... }
Rustでは論理否定
非等価比較
の他、ビット反転
マクロ展開
never型
等に使います。
ビット反転
Rustのビット反転は~
ではなく!
です。
Cユーザがドハマりしそうなポイントで
fn main() {
let x: u32 = !0x55555555;
println!("{:X}", x);
}
の結果は0
ではなく0xAAAAAAAA
になります。
bool
型だと論理否定になるので、Cのように整数型とboot型を一緒くたに考えないように注意したいところです。
マクロ展開
Rustのマクロはめちゃくちゃ難しいのでとりあえずCユーザに馴染みがありそうな例を。
macro_rules! abs {
($e: expr) => (if $e < 0 {-$e} else {$e})
}
fn main() {
let ret = abs!(-2);
println!("{}", ret);
}
abs
というお馴染み(?)のマクロを作ってみました。そのマクロを展開するにはabs!
と書きます。
お気付きかもしれませんが、Rustは標準出力処理もマクロです。
マクロの詳細を知りたい方は以下の記事をご参考ください(というか、これ見て私も勉強します…)。
never型
Rustでは関数の戻り値に値を何も返さない(つまり、決してreturnしない)ことを
fn never_return() -> ! {
loop {}
}
fn main() {
never_return();
}
のように指定することができます。
:
Cでは三項演算子
ラベル
等に使います。
// 三項演算子
max = (val1 > val2) ? val1 : val2;
// ラベル
switch ( operator ) {
case ADD: result = val1 + val2; break;
case SUB: result = val1 - val2; break;
case MUL: result = val1 * val2; break;
case DIV: result = val1 / val2; break;
}
Rustではラベル
の他、型制約
構造体フィールド初期化子
等に使います。
またRustのラベル
はループラベル
に限定されており、Cのようにgoto
を使って好きなところにジャンプしたり等はできません。詳細はループラベルに記載します。
余談ですが、Rustに三項演算子はありません。
型制約
わかりやすい例だと変数宣言するときに使います。
fn main() {
let x: u32 = 1024;
println!("{}", x);
}
ちなみにRustには型推論があるため、
fn print_type_of<T>(_: T) {
println!("{}", std::any::type_name::<T>())
}
fn main() {
let x = 1024;
print_type_of(x);
}
のように型を書かなくても変数宣言できます(私の実行環境ではi32
になりました)。
余談ですがRustは賢いので、
fn main() {
let x: u8 = 1024;
println!("{}", x);
}
こんな事すると
error: literal out of range for `u8`
--> Main.rs:2:17
|
2 | let x: u8 = 1024;
| ^^^^
|
= note: `#[deny(overflowing_literals)]` on by default
= note: the literal `1024` does not fit into the type `u8` whose range is `0..=255`
error: aborting due to previous error
エラーになります。
構造体フィールド初期化子
struct Point {
x: i32,
y: i32
}
fn main() {
let origin = Point {x: 0, y: 0};
println!("x = {}, y = {}", origin.x, origin.y);
}
origin
というPoint
型の変数を初期化するときこのように書きます。
;
Cでは文の終端子
ぐらいにしか使わないと思いますが、Rustでは終端子
の他に固定長配列記法
の一部に使われます。
固定長配列記法
fn main() {
let a1: [i32; 5] = [1, 2, 3, 4, 5];
println!("{:?}", a1);
let a2 = [100; 5];
println!("{:?}", a2);
}
固定長配列の宣言及び初期化をするときこのように書きます。
a1
は型制約を使って配列の型とサイズを明示しています。
a2
は全ての要素を100で初期化しています。a2
の型は指定していませんが、右辺値より型とサイズが推論されています。
'
Cでは文字
ぐらいにしか使わないのではないでしょうか。
char c = 'a';
Rustでは文字
の他、ループラベル
名前付きのライフタイム
等に使います。
ループラベル
Rustではfor
while
loop
等のループを書くことができるのですが、それらに
fn main() {
let mut tbl = [[0; 3]; 3];
tbl[1][2] = 10;
'escape_label: for ary in tbl {
for val in ary {
println!("search!");
if val != 0 { break 'escape_label; }
}
}
println!("{:?}", tbl);
}
のようにラベルを付けることができます。そしてbreak
でラベルを指定することで多重ループを一気に抜け出すことができます。便利です。
ライフタイム
Cユーザにはよくわからない概念だと思いますのでまずCの例を。
#include <stdio.h>
int *max( int *val1, int *val2 ) {
return (*val1 > *val2) ? val1 : val2;
}
int main( void ) {
int x = 1;
int *result;
{
int y = 10;
result = max(&x, &y);
}
printf("%d\n", *result);
}
Cはこのプログラムを普通にビルドもできるし実行もできてしまいます。
出力結果はたまたま正しかったのですが、max
内では(yの値の方が大きいため)y
のポインタがreturnされresult
に代入されるので、printf
するときresult
の参照先はスコープ外のはずです。試しにprintf("%d %d\n", *result, y);
としてみるとエラーになります。
Rustでも同じようなことをしてみると
fn max( val1: &i32, val2: &i32 ) -> &i32 {
return if *val1 > *val2 { val1 } else { val2 };
}
fn main() {
let x = 1;
let result;
{
let y = 10;
result = max(&x, &y);
}
println!("{}", result);
}
error[E0106]: missing lifetime specifier
--> Main.rs:1:37
|
1 | fn max( val1: &i32, val2: &i32 ) -> &i32 {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `val1` or `val2`
help: consider introducing a named lifetime parameter
|
1 | fn max<'a>( val1: &'a i32, val2: &'a i32 ) -> &'a i32 {
| ++++ ++ ++ ++
error: aborting due to previous error
For more information about this error, try `rustc --explain E0106`.
「ライフタイム指定子の欠落」と言われエラーになります。
例にあるようにライフタイムパラメータを追加すると
fn max<'a>( val1: &'a i32, val2: &'a i32 ) -> &'a i32 {
return if *val1 > *val2 { val1 } else { val2 };
}
fn main() {
let x = 1;
let result;
{
let y = 10;
result = max(&x, &y);
}
println!("{}", result);
}
error[E0597]: `y` does not live long enough
--> Main.rs:11:26
|
11 | result = max(&x, &y);
| ^^ borrowed value does not live long enough
12 | }
| - `y` dropped here while still borrowed
13 |
14 | println!("{}", result);
| ------ borrow later used here
error: aborting due to previous error
For more information about this error, try `rustc --explain E0597`.
今度は「`y`は長生きできない」というエラーに変わりました。
全ての引数と戻り値に'a
をつけましたが、これは「全ての引数と戻り値は同じライフタイム'a
を持つ」という意味になります。そのため2つの引数のうち短い方のライフタイム(この例ではy
のライフタイム)が戻り値にも適用され、エラーとなったわけです。
補足ですがネット上の例ではライフタイムに大体'a
'b
'static
辺りが使われていると思います。'static
は「プログラムが実行されている限り有効な値への参照」つまり最大のライフタイムを表す特別な意味を持っていますが、それ以外についてはどんな名前でも問題ないようです。
つまり
fn max<'shortest_lifetime>( val1: &'shortest_lifetime i32, val2: &'shortest_lifetime i32 ) -> &'shortest_lifetime i32 {
return if *val1 > *val2 { val1 } else { val2 };
}
これでも大丈夫です。
流石にキーワードはダメでしたが、'i32
とかはいけました。
_
Cでは変数・関数名等をスネークケースで書くときぐらいにしか使わないのではないでしょうか。
int some_val = 123;
Rustでは変数・関数名
等の他に、「無視」パターン束縛
整数リテラル
等に使います。
「無視」パターン束縛
色々なパターンがあるのですが
fn main() {
let x = 10;
match x {
0 => println!("x is 0."),
1 => println!("x is 1."),
_ => println!("x is other.")
}
}
まずmatch式
に使われます。switch文
のdefault
のような感じです。
fn foo(_: i32) {
println!("繰り返すだけ");
}
fn main() {
let x = [1, 2, 3, 4, 5];
for _ in x {
foo(0);
}
}
次にこんな感じでforや引数の値全体を無視することもできます。
fn main() {
let x = 10;
let warn = 20;
let _not_warn = 30;
println!("{}", x);
}
それからRustは未使用変数に対してワーニングを出すのですが、変数名の先頭に_
を付けていると未使用でもワーニングが出ません。
整数リテラル
fn main() {
let a = 1_000_000;
let b: u32 = 0x8000_FFFA;
println!("{}, {:X}", a, b);
}
のように整数リテラルを_
で区切って見やすくすることができます。
@
Cではそもそも使わないと思いますが、Rustではパターン束縛に使います。
パターン束縛
なんでこんな仕様にしたのか…私がスッキリしていないのでうまく説明できませんが。
まずこれを理解するためにはRustのenum
とmatch式
についてざっくり理解する必要があります。
Rustのenum
は
enum IpAddr {
V4 { a1: u8, a2: u8, a3: u8, a4: u8 },
V6 { a: String }
}
let ip = IpAddr::V4 { a1: 192, a2: 168, a3: 0, a4: 1 };
のように列挙子にデータを持たせることができます。
そして
fn main() {
enum IpAddr {
V4 { a1: u8, a2: u8, a3: u8, a4: u8 },
V6 { a: String }
}
let ip = IpAddr::V4 { a1: 192, a2: 168, a3: 0, a4: 1 };
match ip {
IpAddr::V4 { a1: 0..=127, a2, a3, a4 } => {
println!("クラスAです")
},
IpAddr::V4 { a1: n @ 192..=223, a2, a3, a4 } => {
println!("{}.{}.{}.{}はクラスCのV4アドレスです", n, a2, a3, a4)
},
IpAddr::V4 { a1, a2, a3, a4 } => {
println!("{}.{}.{}.{}はV4アドレスです", a1, a2, a3, a4)
},
IpAddr::V6 { a } => {
println!("{}はV6アドレスです", a)
},
}
}
のようにmatch式でip
がどのパターンにマッチするかによって処理を変えることができます。
このとき
IpAddr::V6 { a } => {
println!("{}はV6アドレスです", a)
},
のように書くとIpAddr::V6
の中にあるデータをローカル変数a
に代入し使用することができますが、中にあるデータに関係なく全てのIpAddr::V6
がマッチします。
中にあるデータも考慮してマッチさせたいときは
IpAddr::V4 { a1: 0..=127, a2, a3, a4 } => {
println!("クラスAです")
},
のようにa1
の範囲書いてあげるのですが、a1: 0..=127
の部分は「a1の範囲が0~127のもの」という意味だけでa1
というローカル変数を宣言したことにはなっていません。後続のa2
- a4
はローカル変数として使用できるのですが…
そのため「a1の値も変数に代入して使いたいよ!」という場合は@
を使って
IpAddr::V4 { a1: n @ 192..=223, a2, a3, a4 } => {
println!("{}.{}.{}.{}はクラスCのV4アドレスです", n, a2, a3, a4)
},
と書くようです。
ちなみにわかりやすいようにa1の値をn
に代入しましたが、a1: a1 @ 192..=223
と書いても大丈夫のようです。
|
Cでは論理OR
ビットOR
等に使います。
// 論理OR
if ( a || b ) { ... }
// ビットOR
a = b | c;
Rustでは論理OR
ビットOR
の他、パターンOR
クロージャ
等に使います。
パターンOR
match式
のパターンで
fn main() {
let x = 0;
match x {
0 | 1 => println!("x is 0 or 1."),
_ => println!("x is other.")
}
}
のように使います。
クロージャ
モダンな言語の似たような仕様に無名関数
やラムダ式
といったものがありますがCにはない仕様です。
The Rust Programming Languageによると
Rustのクロージャは、変数に保存したり、引数として他の関数に渡すことのできる匿名関数
だそうです。
例えばベクタをクロージャで指定したアルゴリズムでソートするsort_by
という関数があり
use std::cmp::Ordering;
fn main() {
let mut v = vec![9, 5, 0, 7, 2, 1, 6, 4, 3, 8];
// 昇順
v.sort_by(|a, b| a.cmp(b));
println!("{:?}", v);
// 降順
v.sort_by(|a, b| b.cmp(a));
println!("{:?}", v);
// 偶数のほうが小さい。偶奇が同じ場合は昇順
v.sort_by(|a, b| match (a % 2).cmp(&(b % 2)) {
Ordering::Less => Ordering::Less,
Ordering::Greater => Ordering::Greater,
Ordering::Equal => a.cmp(b)
});
println!("{:?}", v);
}
こんな感じで使います。
無理やり組込みっぽい例を出してみると、よくタイマーのコールバックとかで
void foo( void )
{
timer_start(500, cb_tmout);
// なんか色々な処理
}
// 色々な関数があって
// 必ずしもコールバック関数を
// 近くに置ける訳では無い
void cb_tmout( void )
{
led_blink();
}
こんなコードを書いて、「あれ、このタイマータイムアウトしたとき何するんだっけ?」みたいなことになることがある…と思いますが、クロージャを使うことで
fn foo() {
blink_timer.start(500, || led.blink());
// なんか色々な処理
}
と書けるので、「あー、こいつタイムアウトしたらLEDブリンクするのね」とすぐにわかります。
?
TBD
#
TBD
$
TBD
()
TBD
<>
TBD
..
TBD
->
TBD
=>
TBD
...
TBD
おわりに
何か新しい言語を学ぶときに未知の記号に戸惑うことがあります。記号ってなかなか検索でヒットさせるのが難しいですよね、名前もわからないですし。
この記事がそんなRust初学者の道標…は言い過ぎか、検索のきっかけぐらいにはなることを願います。