Rust の慣用的なコールバック 質問する

Rust の慣用的なコールバック 質問する

C/C++ では通常、単純な関数ポインターを使用してコールバックを実行し、場合によってはパラメーターvoid* userdataも渡します。次のようになります。

typedef void (*Callback)();

class Processor
{
public:
    void setCallback(Callback c)
    {
        mCallback = c;
    }
    
    void processEvents()
    {
        //...
        mCallback();
    }
private:
    Callback mCallback;
};

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

ベストアンサー1

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

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

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

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++ と同じです。クロージャは ではないためfnset_callback他の種類の関数オブジェクトを受け入れる必要があります。

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

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

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

struct Processor<CB> {
    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 };
    p.process_events();
}

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

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

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

多少直感に反しますが、クロージャを受け入れるオブジェクトの型の特性境界を指定する場合、 はFnOnce実際には最も許容度の高い境界です。ジェネリック コールバック型がFnOnce特性を満たす必要があると宣言することは、文字通りあらゆるクロージャを受け入れることを意味します。ただし、それには代償が伴います。つまり、所有者はそれを 1 回しか呼び出せません。 はprocess_events()コールバックを複数回呼び出すことを選択し、メソッド自体も複数回呼び出される可能性があるため、次に許容度の高い境界は です。 をmutating としてFnMutマークする必要があることに注意してください。process_eventsself

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

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

型パラメータを削除するには、特性オブジェクトは、特性に基づいて動的インターフェースを自動的に作成できるRustの機能です。これは、型消去C++では人気のテクニックです[1][2]Java や FP 言語ではこの用語の使い方が多少異なるため、混同しないように注意してください。C++ に精通している読者は、実装するクロージャFnFn特性オブジェクトの違いが、C++ における一般的な関数オブジェクトと値の違いと同じであることに気付くでしょうstd::function

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

Processor店舗の場合はBox<dyn FnMut()>、もはやジェネリックである必要はありませんが、set_callback 方法ジェネリックはcimpl Trait口論そのため、状態を持つクロージャを含むあらゆる種類の呼び出し可能オブジェクトを受け入れ、それを に格納する前に適切にボックス化することができますProcessor。 の汎用引数は、set_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の型の寿命の境界は、コンパイラに次のことを納得させる簡単な方法です。cset_callback参照に含まれる はc、その環境を参照するクロージャである可能性があり、グローバル値のみを参照するため、コールバックの使用中は有効なままです。ただし、静的境界も非常に厳格です。オブジェクトを所有するクロージャは問題なく受け入れられますが (これは、上記でクロージャ を作成することで保証されていますmove)、ローカル環境を参照するクロージャは拒否されます。たとえ、それがプロセッサよりも長く存続する値のみを参照し、実際には安全であっても拒否されます。

プロセッサが動作している間だけコールバックが有効であればよいので、その有効期間をプロセッサの有効期間に結び付けるようにする必要があります。これは、 よりも緩い境界です。しかし、から有効期間の境界'staticを削除するだけでは、コンパイルされなくなります。これは、 が新しいボックスを作成し、として定義されたフィールドにそれを割り当てるためです。定義ではボックス化された特性オブジェクトの有効期間が指定されていないため、が暗黙的に指定され、割り当てによって有効期間が実質的に拡張されます (コールバックの名前のない任意の有効期間から へ)。これは許可されません。修正するには、プロセッサの明示的な有効期間を提供し、その有効期間をボックス内の参照と が受け取るコールバック内の参照の両方に結び付けます。'staticset_callbackset_callbackcallbackBox<dyn FnMut()>'static'staticset_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文字列がプロセッサよりも長く存続することが保証される必要があります。

おすすめ記事