คำตอบสั้น ๆ : เพื่อความยืดหยุ่นสูงสุดคุณสามารถจัดเก็บการโทรกลับเป็นFnMutวัตถุชนิดบรรจุกล่องโดยมีตัวตั้งค่าการโทรกลับทั่วไปในประเภทการโทรกลับ รหัสนี้จะแสดงในตัวอย่างสุดท้ายในคำตอบ สำหรับคำอธิบายโดยละเอียดเพิ่มเติมโปรดอ่านต่อ
"ฟังก์ชันพอยน์เตอร์": เรียกกลับเป็น fn
รหัส C ++ ที่ใกล้เคียงที่สุดในคำถามคือการประกาศการเรียกกลับเป็นfnประเภท fnสรุปฟังก์ชันที่กำหนดโดยfnคำหลักเช่นเดียวกับตัวชี้ฟังก์ชันของ C ++:
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 วิธีสนิมในการเชื่อมโยงข้อมูลกับฟังก์ชั่นคือการจับข้อมูลในการปิดแบบไม่ระบุชื่อเช่นเดียวกับใน C ++ สมัยใหม่ ตั้งแต่ปิดไม่ได้fn, set_callbackจะต้องยอมรับชนิดอื่น ๆ ของวัตถุที่ฟังก์ชั่น
การเรียกกลับเป็นวัตถุฟังก์ชันทั่วไป
ในการปิดทั้ง Rust และ C ++ ที่มีลายเซ็นการโทรเดียวกันมีหลายขนาดเพื่อรองรับค่าต่างๆที่อาจจับได้ นอกจากนี้คำจำกัดความการปิดแต่ละรายการจะสร้างประเภทที่ไม่ระบุตัวตนที่ไม่ซ้ำกันสำหรับค่าของการปิด เนื่องจากข้อ จำกัด เหล่านี้ struct จึงไม่สามารถตั้งชื่อประเภทของcallbackฟิลด์ได้และไม่สามารถใช้นามแฝงได้
วิธีหนึ่งที่จะฝังปิดในด้านโครงสร้างโดยไม่ต้องหมายถึงประเภทคอนกรีตเป็นโดยการที่ struct ทั่วไป โครงสร้างจะปรับขนาดและประเภทของการเรียกกลับโดยอัตโนมัติสำหรับฟังก์ชันคอนกรีตหรือการปิดที่คุณส่งผ่านไป:
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 เมื่อเรียกการปิด ขึ้นอยู่กับสิ่งที่การปิดทำกับคุณค่าที่พวกเขายึดถือพวกเขาถูกจัดกลุ่มเป็นสามครอบครัวแต่ละตระกูลมีลักษณะ:
Fnเป็นการปิดที่อ่านข้อมูลเท่านั้นและอาจถูกเรียกอย่างปลอดภัยหลายครั้งอาจมาจากหลายเธรด Fnทั้งการปิดดังกล่าว
FnMutเป็นการปิดที่แก้ไขข้อมูลเช่นโดยการเขียนไปยังmutตัวแปรที่จับได้ นอกจากนี้ยังอาจเรียกได้หลายครั้ง แต่ไม่ได้เรียกพร้อมกัน (การเรียกการFnMutปิดจากเธรดหลายเธรดจะนำไปสู่การแย่งชิงข้อมูลดังนั้นจึงสามารถทำได้ด้วยการป้องกัน mutex เท่านั้น) อ็อบเจ็กต์การปิดต้องได้รับการประกาศว่าไม่แน่นอนโดยผู้เรียก
FnOnceเป็นการปิดที่ใช้ข้อมูลบางส่วนที่พวกเขาจับได้เช่นโดยการย้ายค่าที่จับได้ไปยังฟังก์ชันที่รับความเป็นเจ้าของ ตามความหมายของชื่ออาจเรียกได้เพียงครั้งเดียวและผู้โทรต้องเป็นเจ้าของ
ในทางตรงกันข้ามเมื่อระบุลักษณะที่ผูกไว้กับประเภทของวัตถุที่ยอมรับการปิดFnOnceเป็นสิ่งที่อนุญาตมากที่สุด การประกาศว่าประเภทการโทรกลับทั่วไปต้องเป็นไปตามFnOnceลักษณะหมายความว่าจะยอมรับการปิดอย่างแท้จริง แต่นั่นมาพร้อมกับราคานั่นหมายความว่าผู้ถือจะได้รับอนุญาตให้เรียกมันได้เพียงครั้งเดียว เนื่องจากอาจเลือกที่จะก่อให้เกิดการเรียกกลับหลายครั้งและเป็นวิธีการที่ตัวเองอาจจะเรียกได้ว่ามากกว่าหนึ่งครั้งต่อไปอนุญาตส่วนใหญ่ที่ถูกผูกไว้มีprocess_events() FnMutโปรดทราบว่าเรามีการทำเครื่องหมายprocess_eventsเป็น selfmutating
การเรียกกลับที่ไม่ใช่แบบทั่วไป: ฟังก์ชันลักษณะวัตถุ
แม้ว่าการใช้งานการโทรกลับโดยทั่วไปจะมีประสิทธิภาพสูงมาก แต่ก็มีข้อ จำกัด ของอินเทอร์เฟซที่ร้ายแรง ต้องกำหนดให้แต่ละProcessorอินสแตนซ์กำหนดพารามิเตอร์ด้วยประเภทการเรียกกลับที่เป็นรูปธรรมซึ่งหมายความว่าอินสแตนซ์Processorสามารถจัดการกับประเภทการเรียกกลับเพียงรายการเดียวเท่านั้น ระบุว่าแต่ละปิดมีประเภทที่แตกต่างกันทั่วไปProcessorไม่สามารถจัดการตามมาด้วยproc.set_callback(|| println!("hello")) proc.set_callback(|| println!("world"))การขยายโครงสร้างเพื่อรองรับฟิลด์การเรียกกลับสองช่องจะต้องทำให้โครงสร้างทั้งหมดถูกกำหนดพารามิเตอร์เป็นสองประเภทซึ่งจะกลายเป็นเรื่องที่ไม่สะดวกอย่างรวดเร็วเมื่อจำนวนการโทรกลับเพิ่มขึ้น การเพิ่มพารามิเตอร์ประเภทอื่น ๆ จะไม่ทำงานหากจำนวนการเรียกกลับที่ต้องการเป็นแบบไดนามิกเช่นการใช้add_callbackฟังก์ชันที่รักษาเวกเตอร์ของการเรียกกลับที่แตกต่างกัน
ในการลบพารามิเตอร์ type เราสามารถใช้ประโยชน์จากtrait objectsซึ่งเป็นคุณสมบัติของ Rust ที่ช่วยให้สร้างอินเทอร์เฟซแบบไดนามิกโดยอัตโนมัติตามลักษณะ บางครั้งเรียกว่าการลบประเภทและเป็นเทคนิคยอดนิยมใน C ++ [1] [2]เพื่อไม่ให้สับสนกับการใช้คำศัพท์ที่แตกต่างกันของภาษา Java และ FP ผู้อ่านที่คุ้นเคยกับ C ++ จะรับรู้ถึงความแตกต่างระหว่างการปิดที่ใช้FnและFnวัตถุลักษณะเทียบเท่ากับความแตกต่างระหว่างวัตถุฟังก์ชันทั่วไปและstd::functionค่าใน C ++
ออบเจ็กต์ลักษณะถูกสร้างขึ้นโดยการยืมอ็อบเจกต์กับตัว&ดำเนินการและหล่อหลอมหรือบีบบังคับให้อ้างอิงถึงลักษณะเฉพาะ ในกรณีนี้เนื่องจากProcessorจำเป็นต้องเป็นเจ้าของวัตถุเรียกกลับเราจึงไม่สามารถใช้การยืมได้ แต่ต้องจัดเก็บการเรียกกลับในฮีปที่จัดสรรBox<dyn Trait>(เทียบเท่าสนิมstd::unique_ptr) ซึ่งเทียบเท่ากับวัตถุลักษณะ
หากProcessorร้านค้าBox<dyn FnMut()>ก็ไม่จำเป็นที่จะทั่วไป แต่set_callback วิธีการในขณะนี้ยอมรับทั่วไปcผ่านการโต้แย้งimpl Trait เช่นนี้มันสามารถยอมรับชนิดของ callable ใด ๆ Processorรวมทั้งการปิดกับรัฐและถูกต้องกล่องมันก่อนที่จะเก็บไว้ใน อาร์กิวเมนต์ทั่วไปset_callbackไม่ จำกัด ชนิดของการเรียกกลับโปรเซสเซอร์ยอมรับเป็นประเภทของการเรียกกลับได้รับการยอมรับจะหลุดพ้นจากชนิดที่เก็บไว้ในProcessorstruct
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เพื่อให้แน่ใจว่าสตริงอยู่ได้นานกว่าโปรเซสเซอร์
CBถึงต้อง'staticอยู่ในตัวอย่างสุดท้าย?