Rustの慣用的なコールバック


100

C / C ++では、通常、プレーンな関数ポインターを使用してコールバックを実行し、void* userdataパラメーターも渡す可能性があります。このようなもの:

typedef void (*Callback)();

class Processor
{
public:
    void setCallback(Callback c)
    {
        mCallback = c;
    }

    void processEvents()
    {
        for (...)
        {
            ...
            mCallback();
        }
    }
private:
    Callback mCallback;
};

Rustでこれを行う慣用的な方法は何ですか?具体的には、setCallback()関数はどのタイプを取る必要mCallbackがあり、どのタイプにする必要がありますか?それはかかるべきFnですか?多分FnMut?保存しBoxedますか?例は素晴らしいでしょう。

回答:


195

簡単な答え:最大限の柔軟性を実現するために、FnMutコールバックタイプに汎用のコールバックセッターを使用して、コールバックをボックス化されたオブジェクトとして格納できます。このためのコードは、回答の最後の例に示されています。より詳細な説明については、以下をお読みください。

「関数ポインタ」:としてのコールバック fn

質問のC ++コードに最も近いのは、コールバックをfn型として宣言することです。C ++の関数ポインタのようにfnfnキーワードで定義された関数をカプセル化します。

type Callback = fn();

struct Processor {
    callback: Callback,
}

impl Processor {
    fn set_callback(&mut self, c: Callback) {
        self.callback = c;
    }

    fn process_events(&self) {
        (self.callback)();
    }
}

fn simple_callback() {
    println!("hello world!");
}

fn main() {
    let p = Processor {
        callback: simple_callback,
    };
    p.process_events(); // hello world!
}

このコードを拡張しOption<Box<Any>>て、関数に関連付けられた「ユーザーデータ」を保持するを含めることができます。それでも、慣用的なRustではありません。データを関数に関連付けるRustの方法は、最新のC ++の場合と同様に、匿名のクロージャでデータをキャプチャすることです。クロージャはそうfnではないので、set_callback他の種類の関数オブジェクトを受け入れる必要があります。

ジェネリック関数オブジェクトとしてのコールバック

RustとC ++の両方で、同じ呼び出しシグネチャを持つクロージャは、キャプチャする可能性のあるさまざまな値に対応するためにさまざまなサイズで提供されます。さらに、各クロージャ定義は、クロージャの値に対して一意の匿名型を生成します。これらの制約のため、構造体はそのcallbackフィールドのタイプに名前を付けることも、エイリアスを使用することもできません。

具体的な型を参照せずに構造体フィールドにクロージャを埋め込む1つの方法は、構造体をジェネリックにすることです。構造体は、そのサイズと、渡された具象関数またはクロージャのコールバックのタイプを自動的に調整します。

struct Processor<CB>
where
    CB: FnMut(),
{
    callback: CB,
}

impl<CB> Processor<CB>
where
    CB: FnMut(),
{
    fn set_callback(&mut self, c: CB) {
        self.callback = c;
    }

    fn process_events(&mut self) {
        (self.callback)();
    }
}

fn main() {
    let s = "world!".to_string();
    let callback = || println!("hello {}", s);
    let mut p = Processor { callback: callback };
    p.process_events();
}

以前と同様に、コールバックの新しい定義はfn、で定義された最上位の関数を受け入れることができますが、これは、|| println!("hello world!")としてのクロージャ、およびなどの値をキャプチャするクロージャも受け入れます|| println!("{}", somevar)。このため、プロセッサはuserdataコールバックを伴う必要はありません。の呼び出し元によって提供されるクロージャはset_callback、環境から必要なデータを自動的にキャプチャし、呼び出されたときに利用できるようにします。

しかし、どうしたのFnMutか、なぜだけではないのFnですか?クロージャはキャプチャされた値を保持するため、クロージャを呼び出すときはRustの通常のミューテーションルールを適用する必要があります。クロージャが保持する値をどのように処理するかに応じて、クロージャは3つのファミリにグループ化され、それぞれに特性が付けられます。

  • Fnデータを読み取るだけのクロージャであり、場合によっては複数のスレッドから複数回安全に呼び出される可能性があります。上記の両方のクロージャはFnです。
  • FnMutキャプチャされたmut変数に書き込むなどして、データを変更するクロージャです。それらは複数回呼び出されることもありますが、並行して呼び出されることはありません。(FnMut複数のスレッドからクロージャを呼び出すと、データの競合が発生するため、ミューテックスを保護してのみ実行できます。)クロージャオブジェクトは、呼び出し元が可変であると宣言する必要があります。
  • FnOnceは、キャプチャした値を所有権を取得する関数に移動するなどして、キャプチャしたデータの一部を消費するクロージャです。名前が示すように、これらは1回だけ呼び出すことができ、呼び出し元はそれらを所有する必要があります。

クロージャを受け入れるオブジェクトのタイプにバインドされた特性を指定する場合、直感に反して、FnOnce実際には最も寛容なものです。ジェネリックコールバックタイプがFnOnce特性を満たさなければならないことを宣言することは、文字通りすべてのクロージャを受け入れることを意味します。しかし、それには代償が伴います。つまり、所有者は一度だけ電話をかけることができます。以来はprocess_events()、コールバックを複数回呼び出すために選ぶことができ、および方法としての地位は、次の最も許容限界があり、複数回呼び出すことができFnMut。変更process_eventsとしてマークする必要があることに注意してくださいself

非ジェネリックコールバック:関数トレイトオブジェクト

コールバックの一般的な実装は非常に効率的ですが、インターフェースに重大な制限があります。各Processorインスタンスを具体的なコールバックタイプでパラメータ化する必要があります。つまり、単一のインスタンスProcessorは単一のコールバックタイプのみを処理できます。各クロージャーが異なるタイプを持っていることを考えると、ジェネリックProcessorproc.set_callback(|| println!("hello"))その後に続くを処理できませんproc.set_callback(|| println!("world"))。2つのコールバックフィールドをサポートするように構造体を拡張するには、構造体全体を2つのタイプにパラメーター化する必要があり、コールバックの数が増えるとすぐに扱いにくくなります。コールバックの数を動的にする必要がある場合、たとえばadd_callback、異なるコールバックのベクトルを維持する関数を実装する場合など、型パラメーターを追加しても機能しません。

typeパラメーターを削除するために、トレイトオブジェクトを利用できます。これは、トレイトに基づいて動的インターフェイスを自動的に作成できるRustの機能です。これは型消去と呼ばれることもあり、C ++ [1] [2]で一般的な手法であり、Java言語とFP言語の多少異なる用語の使用法と混同しないでください。C ++に精通している読者は、実装されていることを閉鎖との間の区別を理解するであろうFnし、Fn一般的な関数オブジェクトとの区別に相当するような形質オブジェクトstd::functionC ++の値。

トレイトオブジェクトは、&オペレーターと一緒にオブジェクトを借用し、特定のトレイトへの参照にキャストまたは強制することによって作成されます。この場合、Processorコールバックオブジェクトを所有する必要があるため、借用を使用することはできませんが、機能的に特性オブジェクトと同等のヒープ割り当てBox<dyn Trait>(Rustと同等std::unique_ptr)にコールバックを格納する必要があります。

Processor格納する場合Box<dyn FnMut()>、ジェネリックである必要はなくなりましたが、set_callback メソッドcimpl Trait引数を介してジェネリックを受け入れるようになりました。そのため、状態のあるクロージャを含む、あらゆる種類の呼び出し可能オブジェクトを受け入れ、に格納する前に適切にボックス化できますProcessorset_callback受け入れられるコールバックのタイプはProcessor構造体に格納されているタイプから切り離されているため、への一般的な引数は、プロセッサが受け入れるコールバックの種類を制限しません。

struct Processor {
    callback: Box<dyn FnMut()>,
}

impl Processor {
    fn set_callback(&mut self, c: impl FnMut() + 'static) {
        self.callback = Box::new(c);
    }

    fn process_events(&mut self) {
        (self.callback)();
    }
}

fn simple_callback() {
    println!("hello");
}

fn main() {
    let mut p = Processor {
        callback: Box::new(simple_callback),
    };
    p.process_events();
    let s = "world!".to_string();
    let callback2 = move || println!("hello {}", s);
    p.set_callback(callback2);
    p.process_events();
}

ボックスクロージャ内の参照の存続期間

'static種類にバインド寿命cが受け付ける引数は、set_callbackというコンパイラを説得するための簡単な方法です参照が中に含まれているcその環境を指し閉鎖であるかもしれないが、唯一のグローバル値を参照し、そのための使用を通じて有効なままになります折り返し電話。ただし、静的境界も非常に手間がかかります。オブジェクトを適切に所有するクロージャーを受け入れますが(クロージャーを作成することで上記で確認しましたmove)、ローカル環境を参照するクロージャーは、次の値のみを参照する場合でも拒否します。プロセッサよりも長持ちし、実際には安全です。

プロセッサが稼働している限り、コールバックが稼働している必要があるだけなので、コールバックの有効期間をプロセッサの有効期間に関連付ける必要があります。これは、よりも厳密ではありません'static。しかし、'staticからライフタイムバウンドを削除するだけではset_callback、コンパイルされなくなります。これはset_callback、新しいボックスを作成し、それをcallbackとして定義されたフィールドに割り当てるためBox<dyn FnMut()>です。定義ではボックス化されたトレイトオブジェクトの有効期間が指定されていないため、'staticが暗示され、割り当てによって有効期間が(コールバックの名前のない任意の有効期間から'static)に効果的に拡張されますが、これは許可されていません。修正は、プロセッサに明示的な有効期間を提供し、その有効期間をボックス内の参照と、以下が受信するコールバック内の参照の両方に関連付けることset_callbackです。

struct Processor<'a> {
    callback: Box<dyn FnMut() + 'a>,
}

impl<'a> Processor<'a> {
    fn set_callback(&mut self, c: impl FnMut() + 'a) {
        self.callback = Box::new(c);
    }
    // ...
}

これらの有効期間が明示されると、を使用する必要がなくなります'static。閉鎖は今ローカルに参照できるsオブジェクト、すなわち、もはやすることがないmoveの定義があること提供、sの定義の前に置かれpた文字列は、プロセッサをoutlivesことを確実にします。


15
うわー、これは私が今までにSOの質問に答えた中で最高の答えだと思います!ありがとうございました!完全に説明されています。一つのマイナーな事は、私はかかわら得ることはありません-なぜんCBでなければならない'static最後の例では?
Timmmm 2016

9
Box<FnMut()>構造体のフィールドの意味で使用されますBox<FnMut() + 'static>。大まかに言って、「ボックス化されたトレイトオブジェクトには参照が含まれていません/含まれている参照は長生きします(または等しい)'static」。コールバックが参照によってローカルをキャプチャするのを防ぎます。
bluss 2016

ああ、なるほど!
Timmmm 2016

1
@Timmmm別のブログ投稿で'static境界の詳細を確認してください。
user4815162342 2017

3
これは素晴らしい答えです。@ user4815162342を提供していただきありがとうございます。
dash83 2018
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.