Rustアトリビュート活用法!コンパイラへの指示からテストまで幅広く説明

Rustのアトリビュートは、関数や構造体などの項目に追加情報を注釈することができる強力な機能です。 Rustのアトリビュートを使いこなすことで、コンパイラへの指示やテストの効率化など、幅広い分野で活用できます。
この記事では、アトリビュートの使い方や活用事例を紹介します。アトリビュートを使いこなすことで、Rustをより使いこなせるようになりましょう!

Rustのアトリビュートとは

Rustのアトリビュートは、コードの構造や振る舞いに関するメタデータを追加するためのアノテーションです。アトリビュートを使うことで、コンパイラやLinterに追加情報を提供し、コンパイル時の挙動を制御することができます。

次のように、アトリビュートを使うことで、コンパイラやLIntを制御することができます。

// testを使ってtest_foo()関数をテスト関数としてマークしている
// cargo testコマンドの実行時にテスト関数を検出してテストの実行がされる
#[test]
fn test_foo() {
    /* ... */
}

// cfgを使って条件付きコンパイルを実施
// ターゲットOSがLinuxの場合のみbarというモジュールがコンパイルされる
#[cfg(target_os = "linux")]
mod bar {
    /* ... */
}

// allowを使ってunused_variablesのLintを許可している
// unused_variables は未使用の変数の警告が無視される
fn some_unused_variables() {
  #![allow(unused_variables)]

  let x = ();
  let y = ();
  let z = ();
}

Rustのアトリビュートの主な使用目的

Rustのアトリビュートはさまざまな目的で使用されますが、主な使用目的は以下のとおりです。

  • 条件付きコンパイル: cfg
  • テスト関数のマーク: test
  • 自動的にtraitを実装する: derive
  • Lintの制御: allow, warn, deny
  • パフォーマンス最適化: inline

条件付きコンパイル: cfg, cfg!

cfgアトリビュートcfg!マクロを使うことで条件付きコンパイルを行うことができます。

cfgアトリビュートコンパイラに注釈したコードをコンパイルするかどうかを指示できます。また、cfg!マクロはbool値を返すのでコード上で条件分岐をすることができます。

// ターゲットOSがLinuxの場合にコンパイルされる
#[cfg(target_os = "linux")]
fn are_you_on_linux() {
    println!("You are running linux!");
}

// ターゲットOSがLinux以外の場合にコンパイルされる
#[cfg(not(target_os = "linux"))]
fn are_you_on_linux() {
    println!("You are *not* running linux!");
}

fn main() {
    are_you_on_linux();

    println!("Are you sure?");
    // cfg!マクロはbool値を返す
    if cfg!(target_os = "linux") {
        println!("Yes. It's definitely linux!");
    } else {
        println!("Yes. It's definitely *not* linux!");
    }
}
// 出力(※Macで実行)
// You are *not* running linux!
// Are you sure?
// Yes. It's definitely *not* linux!

テスト関数のマーク: test

testアトリビュートを使うことで、テスト関数をマークすることができます。テスト関数はテストモードのとき(rustc --testcargo testなど)のみコンパイルされ、テスト時に自動的にテスト関数が実行されます。

testアトリビュート一緒に使われるアトリビュートとして、以下のアトリビュートもあります。

  • ignore:テストを実行しないようにする。
  • should_panic:テストでパニックが発生した場合のみパスさせる
// src/lib.rs
pub fn add_two(x: i32) -> i32 {
    x + 2
}

// 条件付きコンパイラでテスト時のみtestsモジュールがコンパイルされる
#[cfg(test)]
mod tests {
    use super::*;

    // testアトリビュートにより、it_add_two()をテスト関数としてマークしている
    #[test]
    fn it_add_two() {
        let result = add_two(1);
        assert_eq!(result, 3);
    }

    // ignoreアトリビュートにより、it_ignored()を実行しないようにしている
    #[test]
    #[ignore = "not implemented yet"]
    fn it_ignored() {
        assert_eq!(5, 5);
    }

    // should_panicアトリビュートにより、it_should_panic()がpanicすることを期待している
    #[test]
    #[should_panic(expected = "something wrong happened!!")]
    fn it_should_panic() {
        panic!("something wrong happened!!");
    }
}

自動的にtraitを実装する: derive

deriveアトリビュートは、Rustの標準ライブラリに含まれる一部のトレイトの実装を自動的に生成することができます。 deriveアトリビュートは、DebugPartialEqCloneトレイトとよく使われます。

  • Debugトレイトの実装

Debugトレイトは、デバッグ情報を表示するためのトレイトです。次のようにderiveアトリビュートを使って、Debugトレイトを自動的に実装することができます。

// deriveを使用して、Point構造体にDebugトレイトを自動的に実装
#[derive(Debug)]
struct Point {
    x: f64,
    y: f64,
}

fn main() {
    let p = Point { x: 3.0, y: 4.0 };

    // {:?}でデバッグ表示
    println!("{:?}", p);
    // Point { x: 3.0, y: 4.0 }
}
  • PartialEqトレイトの実装

PartialEqトレイトは、値の比較を行うためのトレイトで、==!=演算子をサポートできます。

// deriveを使用し、Point構造体にPartialEqトレイトを自動的に実装
#[derive(PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 3, y: 4 };
    let p2 = Point { x: 3, y: 4 };
    let p3 = Point { x: 5, y: 6 };

    // == と != 演算子を利用できるようになる
    assert!(p1 == p2);
    assert!(p1 != p3);
}
  • Cloneトレイトの実装

Cloneトレイトは、オブジェクトの複製を作成するためのトレイトです。

// deriveを使用し、Point構造体にCloneトレイトとDebugトレイトを自動的に実装
#[derive(Clone, Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 3, y: 4 };
    let p2 = p1.clone(); // clone()が利用できるになる

    println!("p1: {:?}", p1); // p1: Point { x: 3, y: 4 }
    println!("p2: {:?}", p2); // p2: Point { x: 3, y: 4 }
}

Lintの制御: allow, warn, deny

Lintを使うことで、デットコードや推奨されないコーディングスタイルやコーディングパターンをチェックすることができます。Rustのallowwarndenyアトリビュートを使うことでLintチェックを制御できます。

allowはLintのwarningをださないようにすることができます。コーディングをしていて、どうしてもLintに違反してしまう書き方をするケースで使います。ないにこしたことはないですが。。

#[allow(unused_variables)]
fn main() {
    let x = 42; // この変数は使われていないが、warningは出ない
}

warnはLintのwarningをだすようにします。warningレベルなのでコンパイルは通ります。

#[warn(unused_variables)]
fn main() {
    let x = 42; // この変数は使われていないため、warningが出る
}

denyはLintに違反する場合はコンパイルを失敗させます。

#[deny(unused_variables)]
fn main() {
    let x = 42; // この変数は使われていないため、コンパイルエラーが発生する
}

パフォーマンス最適化: inline

Rustのinlineアトリビュートは、コンパイラに関数のインライン展開をするように提案するために使われます。
インライン展開とは、関数の呼び出しを、その関数の本体に置き換える最適化手法です。これにより、関数呼び出しに伴うオーバーヘッドを減らすことができますが、コードサイズが大きくなる可能性があります。

// inlineによりインライン展開を提案する
// あくまで提案なので、インライン展開をするかどうかはコンパイラが決める
#[inline]
fn fast_function(x: i32, y: i32) -> i32 {
    x + y
}

fn main() {
    let result = fast_function(1, 2);
    println!("Result: {}", result);
}

参考情報