57
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

組込みエンジニアのためのRustのよくわからない記号まとめ

Last updated at Posted at 2022-09-14

はじめに

どうも、レガシー組込みエンジニアの@yagisawaです。
Rustを勉強しているとCでは見かけない言語仕様(主に記号の使い方)に出くわすことがあるので、チートシート的な感じでまとめてみました。
勉強が進み次第都度更新していく予定です。

「RustではXXX等に使います」と曖昧な表現をしていますが、私の勉強が足りていないだけで本記事で紹介しているもの以外の使い方も存在する可能性がある旨、予めご承知おきください。

Cについても私が知らない使い方が存在する可能性がある旨、予めご承知おきください。仕事柄C89の仕様を主に扱っています。また本記事の主眼はRustであるため、Cの情報の充実度については本質ではありません。

RustとCの記号の一覧は以下の記事を参考にしています。

記号

!

Cでは論理否定 非等価比較等に使います。

C
// 論理否定
flg = !flg;

// 非等価演算
if ( flg != false ) { ... }

Rustでは論理否定 非等価比較の他、ビット反転 マクロ展開 never型等に使います。

ビット反転

Rustのビット反転は~ではなく!です。
Cユーザがドハマりしそうなポイントで

Rust
fn main() {
    let x: u32 = !0x55555555;
    println!("{:X}", x);
}

の結果は0ではなく0xAAAAAAAAになります。
bool型だと論理否定になるので、Cのように整数型とboot型を一緒くたに考えないように注意したいところです。

マクロ展開

Rustのマクロはめちゃくちゃ難しいのでとりあえずCユーザに馴染みがありそうな例を。

Rust
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しない)ことを

Rust
fn never_return() -> ! {
    loop {}
}

fn main() {
    never_return();
}

のように指定することができます。

:

Cでは三項演算子 ラベル等に使います。

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に三項演算子はありません。

型制約

わかりやすい例だと変数宣言するときに使います。

Rust
fn main() {
    let x: u32 = 1024;
    println!("{}", x);
}

ちなみにRustには型推論があるため、

Rust
fn print_type_of<T>(_: T) {
    println!("{}", std::any::type_name::<T>())
}

fn main() {
    let x = 1024;
    print_type_of(x);
}

のように型を書かなくても変数宣言できます(私の実行環境ではi32になりました)。

余談ですがRustは賢いので、

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

エラーになります。

構造体フィールド初期化子

Rust
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では終端子の他に固定長配列記法の一部に使われます。

固定長配列記法

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では文字ぐらいにしか使わないのではないでしょうか。

C
char c = 'a';

Rustでは文字の他、ループラベル 名前付きのライフタイム等に使います。

ループラベル

Rustではfor while loop等のループを書くことができるのですが、それらに

Rust
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の例を。

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でも同じようなことをしてみると

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`.

「ライフタイム指定子の欠落」と言われエラーになります。
例にあるようにライフタイムパラメータを追加すると

Rust
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は「プログラムが実行されている限り有効な値への参照」つまり最大のライフタイムを表す特別な意味を持っていますが、それ以外についてはどんな名前でも問題ないようです。
つまり

Rust
fn max<'shortest_lifetime>( val1: &'shortest_lifetime i32, val2: &'shortest_lifetime i32 ) -> &'shortest_lifetime i32 {
    return if *val1 > *val2 { val1 } else { val2 };
}

これでも大丈夫です。
流石にキーワードはダメでしたが、'i32とかはいけました。

_

Cでは変数・関数名等をスネークケースで書くときぐらいにしか使わないのではないでしょうか。

C
int some_val = 123;

Rustでは変数・関数名等の他に、「無視」パターン束縛 整数リテラル等に使います。

「無視」パターン束縛

色々なパターンがあるのですが

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や引数の値全体を無視することもできます。

Rust
fn main() {
    let x         = 10;
    let warn      = 20;
    let _not_warn = 30;

    println!("{}", x);
}

それからRustは未使用変数に対してワーニングを出すのですが、変数名の先頭に_を付けていると未使用でもワーニングが出ません。

整数リテラル

Rust
fn main() {
    let a      = 1_000_000;
    let b: u32 = 0x8000_FFFA;

    println!("{}, {:X}", a, b);
}

のように整数リテラルを_で区切って見やすくすることができます。

@

Cではそもそも使わないと思いますが、Rustではパターン束縛に使います。

パターン束縛

なんでこんな仕様にしたのか…私がスッキリしていないのでうまく説明できませんが。
まずこれを理解するためにはRustのenummatch式についてざっくり理解する必要があります。

Rustのenum

Rust
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 };

のように列挙子にデータを持たせることができます。
そして

Rust
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がどのパターンにマッチするかによって処理を変えることができます。
このとき

Rust
IpAddr::V6 { a } => {
    println!("{}はV6アドレスです", a)
},

のように書くとIpAddr::V6の中にあるデータをローカル変数aに代入し使用することができますが、中にあるデータに関係なく全てのIpAddr::V6がマッチします。
中にあるデータも考慮してマッチさせたいときは

Rust
IpAddr::V4 { a1: 0..=127, a2, a3, a4 } => {
    println!("クラスAです")
},

のようにa1の範囲書いてあげるのですが、a1: 0..=127の部分は「a1の範囲が0~127のもの」という意味だけでa1というローカル変数を宣言したことにはなっていません。後続のa2 - a4はローカル変数として使用できるのですが…
そのため「a1の値も変数に代入して使いたいよ!」という場合は@を使って

Rust
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等に使います。

C
// 論理OR
if ( a || b ) { ... }

// ビットOR
a = b | c;

Rustでは論理OR ビットORの他、パターンOR クロージャ等に使います。

パターンOR

match式のパターンで

Rust
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という関数があり

Rust
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);
}

こんな感じで使います。

無理やり組込みっぽい例を出してみると、よくタイマーのコールバックとかで

C
void foo( void )
{
    timer_start(500, cb_tmout);

    // なんか色々な処理
}

// 色々な関数があって
// 必ずしもコールバック関数を
// 近くに置ける訳では無い

void cb_tmout( void )
{
    led_blink();
}

こんなコードを書いて、「あれ、このタイマータイムアウトしたとき何するんだっけ?」みたいなことになることがある…と思いますが、クロージャを使うことで

Rust
fn foo() {
    blink_timer.start(500, || led.blink());

    // なんか色々な処理
}

と書けるので、「あー、こいつタイムアウトしたらLEDブリンクするのね」とすぐにわかります。

?

TBD

#

TBD

$

TBD

()

TBD

<>

TBD

..

TBD

->

TBD

=>

TBD

...

TBD

おわりに

何か新しい言語を学ぶときに未知の記号に戸惑うことがあります。記号ってなかなか検索でヒットさせるのが難しいですよね、名前もわからないですし。
この記事がそんなRust初学者の道標…は言い過ぎか、検索のきっかけぐらいにはなることを願います。

57
49
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
57
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?