คำตอบสั้น ๆ : เพื่อความยืดหยุ่นสูงสุดคุณสามารถจัดเก็บการโทรกลับเป็น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
เป็น self
mutating
การเรียกกลับที่ไม่ใช่แบบทั่วไป: ฟังก์ชันลักษณะวัตถุ
แม้ว่าการใช้งานการโทรกลับโดยทั่วไปจะมีประสิทธิภาพสูงมาก แต่ก็มีข้อ จำกัด ของอินเทอร์เฟซที่ร้ายแรง ต้องกำหนดให้แต่ละ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
ไม่ จำกัด ชนิดของการเรียกกลับโปรเซสเซอร์ยอมรับเป็นประเภทของการเรียกกลับได้รับการยอมรับจะหลุดพ้นจากชนิดที่เก็บไว้ในProcessor
struct
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
อยู่ในตัวอย่างสุดท้าย?