簡単な答え:最大限の柔軟性を実現するために、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();
}
このコードを拡張し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
は単一のコールバックタイプのみを処理できます。各クロージャーが異なるタイプを持っていることを考えると、ジェネリックProcessor
はproc.set_callback(|| println!("hello"))
その後に続くを処理できませんproc.set_callback(|| println!("world"))
。2つのコールバックフィールドをサポートするように構造体を拡張するには、構造体全体を2つのタイプにパラメーター化する必要があり、コールバックの数が増えるとすぐに扱いにくくなります。コールバックの数を動的にする必要がある場合、たとえばadd_callback
、異なるコールバックのベクトルを維持する関数を実装する場合など、型パラメーターを追加しても機能しません。
typeパラメーターを削除するために、トレイトオブジェクトを利用できます。これは、トレイトに基づいて動的インターフェイスを自動的に作成できるRustの機能です。これは型消去と呼ばれることもあり、C ++ [1] [2]で一般的な手法であり、Java言語とFP言語の多少異なる用語の使用法と混同しないでください。C ++に精通している読者は、実装されていることを閉鎖との間の区別を理解するであろうFn
し、Fn
一般的な関数オブジェクトとの区別に相当するような形質オブジェクトstd::function
C ++の値。
トレイトオブジェクトは、&
オペレーターと一緒にオブジェクトを借用し、特定のトレイトへの参照にキャストまたは強制することによって作成されます。この場合、Processor
コールバックオブジェクトを所有する必要があるため、借用を使用することはできませんが、機能的に特性オブジェクトと同等のヒープ割り当てBox<dyn Trait>
(Rustと同等std::unique_ptr
)にコールバックを格納する必要があります。
をProcessor
格納する場合Box<dyn FnMut()>
、ジェネリックである必要はなくなりましたが、set_callback
メソッドc
はimpl 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
種類にバインド寿命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ことを確実にします。
CB
でなければならない'static
最後の例では?