สำนวนการโทรกลับใน 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;
};

วิธีสำนวนในการทำสนิมคืออะไร? โดยเฉพาะsetCallback()ฟังก์ชันของฉันควรใช้ประเภทใดและควรmCallbackเป็นประเภทใด ควรใช้เวลาFn? อาจจะFnMut? ฉันบันทึกไว้Boxedไหม ตัวอย่างจะน่าทึ่ง

คำตอบ:


195

คำตอบสั้น ๆ : เพื่อความยืดหยุ่นสูงสุดคุณสามารถจัดเก็บการโทรกลับเป็น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(); // hello world!
}

รหัสนี้สามารถขยายเพื่อรวม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เพื่อให้แน่ใจว่าสตริงอยู่ได้นานกว่าโปรเซสเซอร์


15
ว้าวฉันคิดว่านี่เป็นคำตอบที่ดีที่สุดที่ฉันเคยมีสำหรับคำถาม SO! ขอบคุณ! อธิบายได้อย่างสมบูรณ์แบบ สิ่งเล็กน้อยที่ฉันไม่เข้าใจ - ทำไมCBถึงต้อง'staticอยู่ในตัวอย่างสุดท้าย?
Timmmm

9
ใช้ในวิธีที่ข้อมูลBox<FnMut()> struct Box<FnMut() + 'static>ประมาณ "วัตถุลักษณะแบบบรรจุกล่องไม่มีการอ้างอิง / การอ้างอิงใด ๆ ที่มีอายุยืน (หรือเท่ากัน) 'static" จะป้องกันไม่ให้โทรกลับจับคนในพื้นที่โดยการอ้างอิง
bluss

ฉันเห็นฉันคิดว่า!
Timmmm

1
@Timmmm รายละเอียดเพิ่มเติมเกี่ยวกับการ'staticที่ถูกผูกไว้ในบล็อกโพสต์ที่แยกจากกัน
user4815162342

3
นี่เป็นคำตอบที่ยอดเยี่ยมขอบคุณที่ให้ @ user4815162342
Dash83
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.