0
0

Rustで関数を渡したい

Last updated at Posted at 2024-08-25

Rustで関数を別の関数に渡す方法をまとめました。


目次

文字列で名前だけ渡す

Stringかstrで関数名を渡して、渡された側でmatchして実行。
一番簡単で分かりやすい。
しかし、汎用性と可読性が著しく低いため、enumの方がはるかに良い。

///関数の返り値を表すenum
#[derive(PartialEq, Debug)]
enum FuncResult<'a> {PrintHello(&'a str),}

fn execute<'a>(func_name: &'static str, arg: &'a String) -> FuncResult::<'a> 
{
    // 関数名をmatchで探す
    match func_name {
        "print_hello" => {
            let result = print_hello(arg);
            FuncResult::PrintHello(result)
            // FuncResult型に揃えてから返す
        },
        _ => panic!("Unknown name"),
    }
}

/// 名前を受け取って挨拶をし、名前の最初の一文字を返す
fn print_hello<'a>(name: &'a String) -> &'a str 
{
    println!("Hello, {}!", name);
    &name[0..1]
}

fn main() 
{
    assert_eq!(
        execute("print_hello", &"Sam".to_string()),
        FuncResult::PrintHello("S")
    )
}

結果
Hello, Sam!

enumを使う

関数に渡す引数と返り値の型を自由に設定できるという高い柔軟性がある。複数の、引数も返り値も違うような関数を取る場合は最善の選択肢。可読性も高いので、深く考えないならこれで良い。
ただし、enumを2つ使うので冗長になる。渡される全ての関数で引数と返り値が揃っているなら後述の関数ポインタを使う方法もある。

/// 関数とその引数を表すenum
enum Functions {
    Add(i32, i32),
}
/// 関数の返り値(とエラー)を表すenum
#[derive(PartialEq, Debug)]// assert_eq!用
enum FuncResult {
    Add(i32),
    /* 返り値 */
    Err, 
    /* 各種エラーを追加したりもできる */
}

fn execute(func: Functions) -> FuncResult 
{
    match func {
        Functions::Add(x, y) => FuncResult::Add(add(x, y)),
        _ => FuncResult::Err,
    }
}

fn add(a: i32, b: i32) -> i32 {a + b}

fn main() 
{
    assert_eq!(
        execute(Functions::Add(1, 2)),
        FuncResult::Add(3)
    )
}

関数名を変数のように渡す(関数ポインタ)

引数と返り値の型を明示した型注釈が必須だが、それ以外に特段難しい要素はない。普通の関数から型だけを抜き出して書いたような書き方になる。また、enumと比べて記述量が少なく簡潔になる。
ただし、引数や返り値の型が違う関数をまとめて扱うことはできない。また、単体だと元の関数名が分からない。

fn execute<'a>(
    f: fn(&'a String) -> &'a str, // 引数と型をかくだけ
    arg: &'a String  
) -> &'a str 
{
    // そのまま呼べる
    f(arg)
}

/// 名前を受け取って挨拶をし、名前の最初の一文字を返す
fn print_hello<'a>(name: &'a String) -> &'a str 
{
    println!("Hello, {}!", name);
    &name[0..1]
}

fn main() 
{
    assert_eq!(
        //関数名をそのまま渡せる!
        execute(print_hello, &"Sam".to_string()),
        "S"
    )
}

結果
Hello, Sam!

Fn系トレイトを使う

トレイト境界を使って関数を受け取る方法。enumの柔軟性と関数ポインタの簡潔さを受け継いでいる。渡された関数の戻り値をジェネリックにして使うことができる。
ただし、トレイト境界を使うのでやや複雑になる。さらに、引数はジェネリックにできない(引数をジェネリックにしたいならenumを使う。)

fn execute<'a, F, R>(f: F, arg: &'a String) -> R
where
    // 引数は固定、戻り値はジェネリック
    F: FnOnce(&'a String) -> R
{
    f(arg)
}

/// 名前を受け取って挨拶をし、名前の最初の一文字を返す
fn print_hello<'a>(name: &'a String) -> &'a str
{
    println!("Hello, {}!", name);
    &name[0..1]
}

fn main() {
    assert_eq!(
        execute(print_hello, &"Sam".to_string()), 
        "S"
    )
}

トレイト
トレイトには、FnOnceの他にFn, FnMutがある。
この記事がとても詳しく解説してくださっているのでおすすめ。

まとめ

大体こんな感じで使い分ければ良いはず。

機能 引数を注釈できる 引数を注釈できない
(複数の関数を取るなど)
返り値を注釈できる 関数ポインタ enumを使う
返り値を注釈できない Fn系トレイトを使う
or
enumを使う
enumを使う

おまけ(関数名が知りたいとき)

関数名を推論するにはstd::any::type_nameを使う。Fn系トレイトの使用が前提となる。

fn greet() {println!("Hello");}

fn get_func_name<F>(_: F) -> &'static str
where 
    F: FnOnce()
{
    //関数は個別に型が定義されるので、型名から関数名がわかる
    std::any::type_name::<F>()
}

fn main() {
    println!("{}", get_func_name(greet));
}

結果(Rust playground)
playground::greet

おまけ(メソッドを渡す)

メソッドは渡せないように思えて、実は渡せる。
このとき、「構造体のデータに紐付いたメソッド」ではなく、「構造体のデータをとるだけの関連関数」として扱うとうまくいく。

struct Something {a: i32, b: i32}
impl Something {
    fn get_a(&self) -> &i32 {&self.a} //今回渡す関数
}

fn execute_method(f: fn(&Something) -> &i32, data: &Something) -> &i32 
{
    // get_a(&self)の&selfを明示的に渡している
    f(data)
}

fn main() 
{
    let test_data = Something{a: 1, b: 2};
    assert_eq!(
    
        // Something.get_aではなく、Something::get_a
        //として関連関数のように扱っている
        execute_method(Something::get_a, &test_data),
        &1
    )
}

参考

Rustにおける関数ポインタやクロージャ

おまけ

生ポインタからの参照外しは未定義動作か強制エラー

fn sam() -> String { "Sam".to_string() }
fn exec<R>(f: *const fn() -> R) -> R {
    unsafe {
        (*f)()
    }
}

fn main() {
    println!("{}", exec(sam as *const fn() -> String));
}

結果(筆者の環境)
Exited with signal 11 (SIGSEGV): segmentation violation

0
0
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
0
0