जंग में मुहावरेदार कॉलबैक


99

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? एक उदाहरण अद्भुत होगा।

जवाबों:


193

संक्षिप्त उत्तर: अधिकतम लचीलेपन के लिए, आप कॉलबैक को कॉलबैक प्रकार के रूप 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>>फ़ंक्शन से जुड़े "उपयोगकर्ता डेटा" को रखने के लिए इस कोड को बढ़ाया जा सकता है । फिर भी, यह मुहावरेदार जंग नहीं होगी। किसी फ़ंक्शन के साथ डेटा को संबद्ध करने का रस्ट तरीका आधुनिक सी ++ की तरह ही एक अनाम क्लोजर में कैप्चर करना है । चूंकि क्लोजर नहीं हैं fn, इसलिए set_callbackअन्य प्रकार के फ़ंक्शन ऑब्जेक्ट को स्वीकार करने की आवश्यकता होगी।

जेनेरिक फ़ंक्शन ऑब्जेक्ट के रूप में कॉलबैक

Rust और C ++ दोनों में एक ही कॉल सिग्नेचर के साथ क्लोजर अलग-अलग साइज में आते हैं ताकि वे अलग-अलग वैल्यू को एडजस्ट कर सकें। इसके अतिरिक्त, प्रत्येक क्लोजर परिभाषा क्लोजर के मूल्य के लिए एक अद्वितीय अनाम प्रकार उत्पन्न करता है। इन बाधाओं के कारण, संरचना अपने callbackक्षेत्र के प्रकार का नाम नहीं दे सकती है , न ही यह एक उपनाम का उपयोग कर सकती है।

एक ठोस प्रकार का जिक्र किए बिना संरचना क्षेत्र में एक बंद को एम्बेड करने का एक तरीका यह है कि संरचना को सामान्य बनाया जाए । संरचना स्वचालित रूप से अपने आकार और कंक्रीट फ़ंक्शन के लिए कॉलबैक के प्रकार या आपके द्वारा इसे पास करने के लिए बंद कर देगी:

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? चूंकि क्लोजर पकड़े गए मानों को रोकते हैं, रुस्ट के सामान्य उत्परिवर्तन नियम बंद होने पर कॉल करते समय लागू होने चाहिए। वे जो मूल्य धारण करते हैं उसके साथ क्लोजर क्या होता है, इसके आधार पर, उन्हें तीन परिवारों में बांटा गया है, जिनमें से प्रत्येक को एक निशान के साथ चिह्नित किया गया है:

  • Fnवे क्लोज़र हैं जो केवल डेटा पढ़ते हैं, और संभवतः कई बार, संभवतः कई थ्रेड्स से सुरक्षित रूप से कॉल किए जा सकते हैं। ऊपर के दोनों क्लोजर हैं Fn
  • FnMutक्लोजर हैं जो डेटा को संशोधित करते हैं, उदाहरण के लिए कैप्चर किए गए mutचर को लिखकर । उन्हें कई बार कहा जा सकता है, लेकिन समानांतर में नहीं। ( FnMutकई थ्रेड से क्लोजर कॉल करने से डेटा रेस होगी, इसलिए यह केवल म्यूटेक्स के संरक्षण के साथ किया जा सकता है।) क्लोजर ऑब्जेक्ट को कॉल करने वाले के द्वारा म्यूट घोषित किया जाना चाहिए।
  • FnOnceवे क्लोजर हैं जो कुछ ओड का उपभोग करते हैं जो वे कैप्चर करते हैं, उदाहरण के लिए एक कैप्चर किए गए वैल्यू को एक फंक्शन में ले जाकर जो उसका स्वामित्व लेता है। जैसा कि नाम से ही स्पष्ट है, ये केवल एक बार ही कहे जा सकते हैं, और कॉल करने वाले को इनका मालिक होना चाहिए।

कुछ हद तक प्रति-सहजता से, जब किसी वस्तु के प्रकार के लिए बाध्य विशेषता को निर्दिष्ट करता है जो एक क्लोजर को स्वीकार करता है, FnOnceतो वास्तव में सबसे अधिक अनुमत है। यह घोषणा करते हुए कि जेनेरिक कॉलबैक प्रकार को FnOnceविशेषता को संतुष्ट करना चाहिए, इसका मतलब है कि यह सचमुच किसी भी बंद को स्वीकार करेगा। लेकिन यह एक मूल्य के साथ आता है: इसका मतलब है कि धारक को केवल एक बार कॉल करने की अनुमति है। चूंकि process_events()कई बार कॉलबैक को लागू करने का विकल्प चुन सकते हैं, और जैसा कि विधि को एक से अधिक बार कॉल किया जा सकता है, अगली सबसे अधिक अनुमत बाध्यता है FnMut। ध्यान दें कि हमें process_eventsम्यूटिंग के रूप में चिह्नित करना था self

गैर-जेनेरिक कॉलबैक: फ़ंक्शन विशेषता ऑब्जेक्ट

भले ही कॉलबैक का सामान्य कार्यान्वयन बेहद कुशल है, लेकिन इसमें गंभीर इंटरफ़ेस सीमाएँ हैं। यह प्रत्येक Processorउदाहरण के लिए एक ठोस कॉलबैक प्रकार के साथ मानकीकृत होने की आवश्यकता है , जिसका अर्थ है कि एक एकल Processorकेवल एकल कॉलबैक प्रकार से निपट सकता है। यह देखते हुए कि प्रत्येक बंद का एक अलग प्रकार है, जेनेरिक उसके बाद Processorनहीं संभाल सकता proc.set_callback(|| println!("hello"))है proc.set_callback(|| println!("world"))। दो कॉलबैक फ़ील्ड का समर्थन करने के लिए संरचना का विस्तार करने के लिए पूरे ढांचे को दो प्रकारों के लिए मानकीकृत करने की आवश्यकता होगी, जो कॉलबैक की संख्या बढ़ने पर जल्दी से अनजान बन जाएगा। अधिक प्रकार के मापदंडों को जोड़ने से काम नहीं चलेगा यदि कॉलबैक की संख्या गतिशील होने की आवश्यकता है, उदाहरण के लिए एक add_callbackफ़ंक्शन लागू करने के लिए जो अलग-अलग कमियों के वेक्टर को बनाए रखता है।

प्रकार पैरामीटर को हटाने के लिए, हम विशेषता वस्तुओं का लाभ उठा सकते हैं , जंग की विशेषता जो लक्षणों के आधार पर गतिशील इंटरफेस के स्वत: निर्माण की अनुमति देती है। इसे कभी-कभी टाइप इरेज़र के रूप में संदर्भित किया जाता है और यह C ++ [1] [2] में एक लोकप्रिय तकनीक है , जावा और एफपी भाषाओं के शब्द के कुछ अलग उपयोग के साथ भ्रमित होने की नहीं। सी ++ के साथ परिचित पाठकों कि औजार को बंद करने के बीच के अंतर को पहचान लेगा Fnऔर एक Fnसामान्य समारोह वस्तुओं और के बीच के अंतर के बराबर विशेषता वस्तु std::functionसी में मूल्यों ++।

किसी विशेषता वस्तु को &ऑपरेटर के साथ एक वस्तु उधार लेकर और विशिष्ट विशेषता के संदर्भ में कास्टिंग या इसे बनाने के लिए बनाया जाता है । इस मामले में, चूंकि Processorकॉलबैक ऑब्जेक्ट का स्वामी होना चाहिए, इसलिए हम उधार का उपयोग नहीं कर सकते हैं, लेकिन कॉलबैक को ढेर-आवंटित Box<dyn Trait>(रस्ट समतुल्य 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कि स्ट्रिंग प्रोसेसर को आउटलाइज़ करता है।


15
वाह, मुझे लगता है कि यह सबसे अच्छा जवाब है जो मैंने कभी एसओ प्रश्न के लिए मिला है! धन्यवाद! बिल्कुल सही समझाया। एक छोटी सी बात मुझे हालांकि नहीं मिलती - अंतिम उदाहरण में क्यों होना पड़ता CBहै 'static?
टिम्मम

9
Box<FnMut()>Struct क्षेत्र साधन में इस्तेमाल किया Box<FnMut() + 'static>। मोटे तौर पर "बॉक्सिंग ट्रेट ऑब्जेक्ट में कोई संदर्भ / कोई संदर्भ नहीं होता है जिसमें यह आउटलाइव (या बराबर) होता है 'static"। यह कॉलबैक को संदर्भ द्वारा स्थानीय लोगों को कैप्चर करने से रोकता है।
दोष

आह, मुझे लगता है, मुझे लगता है!
टिम्म

1
@Timmmm 'staticएक अलग ब्लॉग पोस्ट में बाउंड पर अधिक जानकारी ।
user4815162342

3
यह एक शानदार उत्तर है, इसे @ user4815162342 पर उपलब्ध कराने के लिए धन्यवाद।
14:83 पर Dash83
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.