Rust Traits Go Interfaces से कैसे अलग हैं?


64

मैं गो से अपेक्षाकृत परिचित हूं, इसमें कई छोटे कार्यक्रम लिखे हैं। जंग, ज़ाहिर है, मैं कम परिचित हूं, लेकिन नज़र रखता हूं।

हाल ही में पढ़े गए http://yager.io/programming/go.html पर , मुझे लगा कि मैं व्यक्तिगत रूप से उन दो तरीकों की जांच करूंगा, जिन्हें जेनरिक ने संभाला है क्योंकि लेख को गलत तरीके से आलोचना करना प्रतीत होता है, जब व्यवहार में जाएं, तो ऐसा नहीं था कि इंटरफेसेस सुरुचिपूर्ण ढंग से पूरा नहीं कर सका। मैं प्रचार के बारे में सुनता रहा कि रस्ट के लक्षण कितने शक्तिशाली थे और गो के बारे में लोगों की आलोचना के अलावा कुछ नहीं। गो में कुछ अनुभव होने के बाद, मैं सोचता था कि यह कितना सच था और आखिरकार क्या अंतर था। मैंने पाया कि ट्रेट्स और इंटरफेस बहुत समान हैं! अंत में, मुझे यकीन नहीं है कि अगर मुझे कुछ याद आ रहा है, तो यहां उनकी समानता का एक त्वरित शैक्षिक ठहरनेवाला है ताकि आप मुझे बता सकें कि मैंने क्या याद किया!

अब, आइए उनके दस्तावेज़ों से Go Interfaces पर एक नज़र डालें :

गो में इंटरफेस किसी वस्तु के व्यवहार को निर्दिष्ट करने का एक तरीका प्रदान करता है: यदि कोई ऐसा कर सकता है, तो इसका उपयोग यहां किया जा सकता है।

अब तक सबसे आम इंटरफ़ेस है Stringerजो ऑब्जेक्ट का प्रतिनिधित्व करने वाला एक स्ट्रिंग देता है।

type Stringer interface {
    String() string
}

तो, इस String()पर परिभाषित किसी भी वस्तु एक Stringerवस्तु है। इसका उपयोग प्रकार के हस्ताक्षर में किया जा सकता है जो func (s Stringer) print()लगभग सभी वस्तुओं को ले जाता है और उन्हें प्रिंट करता है।

हमारे पास भी interface{}कोई वस्तु है। हमें तब प्रतिबिंब के माध्यम से रनटाइम पर प्रकार का निर्धारण करना चाहिए।


अब, चलो उनके प्रलेखन से जंग के लक्षणों पर एक नज़र डालें :

अपने सरलतम पर, एक लक्षण शून्य या अधिक विधि हस्ताक्षर का एक सेट है। उदाहरण के लिए, हम उन चीजों के लिए प्रिंट करने योग्य घोषित कर सकते हैं, जिन्हें कंसोल में मुद्रित किया जा सकता है, एक एकल विधि हस्ताक्षर के साथ:

trait Printable {
    fn print(&self);
}

यह तुरंत हमारे गो इंटरफेस के समान दिखता है। एकमात्र अंतर यह है कि हम केवल तरीकों को परिभाषित करने के बजाय लक्षणों के 'कार्यान्वयन' को परिभाषित करते हैं। तो, हम करते हैं

impl Printable for int {
    fn print(&self) { println!("{}", *self) }
}

के बजाय

fn print(a: int) { ... }

बोनस प्रश्न: यदि आप किसी ऐसे फ़ंक्शन को परिभाषित करते हैं जो एक विशेषता को लागू करता है, लेकिन क्या आप उपयोग नहीं करते हैं तो क्या होता है impl? यह सिर्फ काम नहीं करता है?

गो के इंटरफेस के विपरीत, रस्ट के सिस्टम में टाइप पैरामीटर होते हैं जो आपको उचित जेनरिक और चीजें जैसे interface{}कि कंपाइलर और रनटाइम को वास्तव में टाइप करते हैं। उदाहरण के लिए,

trait Seq<T> {
    fn length(&self) -> uint;
}

किसी भी प्रकार पर काम करता है और संकलक जानता है कि प्रतिबिंब का उपयोग करने के बजाय संकलन समय पर अनुक्रम तत्वों का प्रकार।


अब, वास्तविक प्रश्न: क्या मुझे यहाँ कोई अंतर याद आ रहा है? वे वास्तव में कर रहे हैं कि इसी तरह की? क्या कुछ और मूलभूत अंतर नहीं है जो मुझे यहाँ याद आ रहा है? (उपयोग में। कार्यान्वयन विवरण दिलचस्प हैं, लेकिन अंततः महत्वपूर्ण नहीं हैं यदि वे समान कार्य करते हैं।)

वाक्यात्मक अंतर के अलावा, मेरे द्वारा देखे जाने वाले वास्तविक अंतर इस प्रकार हैं:

  1. गो में स्वचालित विधि प्रेषण है। अनुगमन को implलागू करने के लिए जंग (?) की आवश्यकता होती है
    • सुरुचिपूर्ण बनाम स्पष्ट
  2. जंग में प्रकार के पैरामीटर होते हैं जो प्रतिबिंब के बिना उचित जेनरिक के लिए अनुमति देते हैं।
    • गो वास्तव में यहाँ कोई प्रतिक्रिया नहीं है। यह एकमात्र ऐसी चीज है जो काफी अधिक शक्तिशाली है और यह अंततः विभिन्न प्रकार के हस्ताक्षर के साथ तरीकों को कॉपी करने और चिपकाने के लिए एक प्रतिस्थापन है।

क्या ये केवल गैर-तुच्छ अंतर हैं? यदि ऐसा है, तो यह गो का इंटरफेस / टाइप सिस्टम दिखाई देगा, व्यवहार में, जैसा कि कमजोर नहीं है।

जवाबों:


59

अगर आप किसी फंक्शन को लागू करते हैं, तो रस्ट में क्या होता है, जो एक गुण को लागू करता है, लेकिन आप इम्प्लांट का उपयोग नहीं करते हैं? यह सिर्फ काम नहीं करता है?

आपको स्पष्ट रूप से विशेषता को लागू करने की आवश्यकता है; मिलान / हस्ताक्षर के साथ एक विधि का होना Rust के लिए अर्थहीन है।

जेनेरिक कॉल प्रेषण

क्या ये केवल गैर-तुच्छ अंतर हैं? यदि ऐसा है, तो यह गो का इंटरफेस / टाइप सिस्टम दिखाई देगा, व्यवहार में, जैसा कि कमजोर नहीं है।

स्थिर प्रेषण प्रदान नहीं करना कुछ मामलों के लिए एक महत्वपूर्ण प्रदर्शन हिट हो सकता है (जैसे कि Iteratorमैं नीचे उल्लेख करता हूं)। मुझे लगता है कि आपका यही मतलब है

गो वास्तव में यहाँ कोई प्रतिक्रिया नहीं है। यह एकमात्र ऐसी चीज है जो काफी अधिक शक्तिशाली है और यह अंततः विभिन्न प्रकार के हस्ताक्षर के साथ तरीकों को कॉपी करने और चिपकाने के लिए एक प्रतिस्थापन है।

लेकिन मैं इसे और अधिक विस्तार से कवर करूंगा, क्योंकि यह अंतर को गहराई से समझने लायक है।

जंग में

रस्ट का दृष्टिकोण उपयोगकर्ता को स्थिर प्रेषण और गतिशील प्रेषण के बीच चयन करने की अनुमति देता है । एक उदाहरण के रूप में, यदि आपके पास है

trait Foo { fn bar(&self); }

impl Foo for int { fn bar(&self) {} }
impl Foo for String { fn bar(&self) {} }

fn call_bar<T: Foo>(value: T) { value.bar() }

fn main() {
    call_bar(1i);
    call_bar("foo".to_string());
}

फिर call_barऊपर दिए गए दो कॉल क्रमशः कॉल करने के लिए संकलित करेंगे,

fn call_bar_int(value: int) { value.bar() }
fn call_bar_string(value: String) { value.bar() }

जहाँ वे .bar()मेथड कॉल्स स्टैटिक फंक्शन कॉल हैं, यानी मेमोरी में एक निश्चित फंक्शन एड्रेस पर। यह इनलाइनिंग जैसे अनुकूलन की अनुमति देता है, क्योंकि कंपाइलर जानता है कि वास्तव में किस फ़ंक्शन को कहा जा रहा है। (यह वही है जो C ++ भी करता है, जिसे कभी-कभी "मोनोमोर्फिसेशन" भी कहा जाता है।)

गो में

गो केवल "जेनेरिक" कार्यों के लिए गतिशील प्रेषण की अनुमति देता है, अर्थात, विधि पते को मूल्य से लोड किया जाता है और फिर वहां से बुलाया जाता है, इसलिए सटीक फ़ंक्शन केवल रनटाइम पर जाना जाता है। ऊपर दिए गए उदाहरण का उपयोग करना

type Foo interface { bar() }

func call_bar(value Foo) { value.bar() }

type X int;
type Y string;
func (X) bar() {}
func (Y) bar() {}

func main() {
    call_bar(X(1))
    call_bar(Y("foo"))
}

अब, वे दो call_barएस हमेशा उपर्युक्त कॉलिंग करेंगे call_bar, barइंटरफ़ेस के वाइबेट से लोड के पते के साथ ।

निम्न स्तर

सी संकेतन में उपरोक्त को फिर से लिखना। जंग का संस्करण बनाता है

/* "implementing" the trait */
void bar_int(...) { ... }
void bar_string(...) { ... }

/* the monomorphised `call_bar` function */
void call_bar_int(int value) {
    bar_int(value);
}
void call_bar_string(string value) {
    bar_string(value);
}

int main() {
    call_bar_int(1);
    call_bar_string("foo");
    // pretend that is the (hypothetical) `string` type, not a `char*`
    return 1;
}

गो के लिए, यह कुछ अधिक है:

/* implementing the interface */
void bar_int(...) { ... }
void bar_string(...) { ... }

// the Foo interface type
struct Foo {
    void* data;
    struct FooVTable* vtable;
}
struct FooVTable {
    void (*bar)(void*);
}

void call_bar(struct Foo value) {
    value.vtable.bar(value.data);
}

static struct FooVTable int_vtable = { bar_int };
static struct FooVTable string_vtable = { bar_string };

int main() {
    int* i = malloc(sizeof *i);
    *i = 1;
    struct Foo int_data = { i, &int_vtable };
    call_bar(int_data);

    string* s = malloc(sizeof *s);
    *s = "foo"; // again, pretend the types work
    struct Foo string_data = { s, &string_vtable };
    call_bar(string_data);
}

(यह बिलकुल सही नहीं है --- वीटेबल में अधिक जानकारी होनी चाहिए --- लेकिन डायनेमिक फंक्शन पॉइंटर होने का तरीका यहाँ प्रासंगिक है।)

जंग पसंद प्रदान करता है

को वापस जा रहा

रस्ट का दृष्टिकोण उपयोगकर्ता को स्थिर प्रेषण और गतिशील प्रेषण के बीच चयन करने की अनुमति देता है।

अब तक मैंने केवल Rust का प्रदर्शन सांख्यिकीय रूप से प्रचलित जेनेरिकों के साथ किया है, लेकिन Rust, ट्रिट ऑब्जेक्ट्स के माध्यम से Go (जैसे अनिवार्य रूप से समान कार्यान्वयन) के साथ डायनेमिक विकल्प चुन सकता है। पसंद नहीं है &Foo, जो अज्ञात प्रकार का उधार लिया गया संदर्भ है जो Fooविशेषता को लागू करता है। इन मूल्यों में गो इंटरफ़ेस ऑब्जेक्ट के समान / बहुत समान व्यवहार्य प्रतिनिधित्व है। (एक विशेषता वस्तु एक "अस्तित्वगत प्रकार" का एक उदाहरण है ।)

ऐसे उदाहरण हैं जहां डायनेमिक डिस्पैच वास्तव में सहायक है (और कभी-कभी अधिक परफॉर्मेंट, जैसे, कोड ब्लोट / दोहराव को कम करना), लेकिन स्टैटिक डिस्पैच कॉलर्स को इनलाइन करने और उनके सभी अनुकूलन को लागू करने की अनुमति देता है, जिसका अर्थ है कि यह सामान्य रूप से तेज है। यह विशेष रूप से रस्ट के पुनरावृत्ति प्रोटोकॉल जैसी चीजों के लिए महत्वपूर्ण है , जहां स्थिर प्रेषण विधि कॉल, उन पुनरावृत्तियों को सी समकक्षों के रूप में तेजी से होने की अनुमति देता है, जबकि अभी भी उच्च-स्तरीय और अभिव्यंजक लग रहा है

; जाओ केवल गतिशील प्रेषण के लिए अनुमति देता है।

पैरामीट्रिक बहुरूपता

इसके अलावा, लक्षणों पर जोर देने और परावर्तन को प्रतिबिंबित करने से रस्ट को अधिक मजबूत पैरामीट्रिक बहुरूपता मिलती है : प्रोग्रामर को ठीक से पता है कि एक फ़ंक्शन अपने तर्कों के साथ क्या कर सकता है, क्योंकि उसे फ़ंक्शन हस्ताक्षर में जेनेरिक प्रकार के कार्यान्वयन के लक्षणों को घोषित करना होगा।

गो का दृष्टिकोण बहुत लचीला है, लेकिन कॉल करने वालों के लिए इसकी कम गारंटी है (इसे प्रोग्रामर के लिए कुछ हद तक कठिन बना सकता है), क्योंकि अतिरिक्त प्रकार की जानकारी के लिए एक फ़ंक्शन के इंटर्नल क्वेरी (और कर सकते हैं) (गो में एक बग था) मानक पुस्तकालय जहाँ, iirc, एक फंक्शन ले रहा है एक लेखक Flushकुछ इनपुट पर कॉल करने के लिए प्रतिबिंब का उपयोग करेगा , लेकिन अन्य नहीं)।

बिल्डिंग एब्स्ट्रैक्ट

यह कुछ हद तक एक व्यथा बिंदु है, इसलिए मैं केवल संक्षेप में बात करूंगा, लेकिन "उचित" जेनेरिक जैसे रुस्ट के पास निम्न स्तर के डेटा प्रकारों के लिए अनुमति देता है जैसे कि गो का mapऔर []वास्तव में मानक पुस्तकालय में सीधे एक मजबूत प्रकार से लागू किया जा सकता है, और रस्ट ( HashMapऔर Vecक्रमशः) में लिखा गया है ।

और इसके न केवल प्रकार, आप उनमें से शीर्ष पर टाइप-सुरक्षित जेनेरिक संरचनाएं बना सकते हैं, उदाहरण LruCacheके लिए एक हैशमैप के ऊपर एक सामान्य कैशिंग परत है। इसका मतलब यह है कि लोग डेटा स्टोरेज को सीधे मानक लाइब्रेरी से सीधे इस्तेमाल कर सकते हैं, बिना डेटा को स्टोर किए interface{}या टाइप करते समय डेटा का उपयोग करते समय या बाहर निकालते समय। यही है, यदि आपके पास एक है, तो आपको LruCache<int, String>गारंटी दी जाती है कि चाबियाँ हमेशा intएस हैं और मान हमेशा Stringएस हैं: गलती से गलत मान डालने का कोई तरीका नहीं है (या गैर निकालने का प्रयास करें String)।


मेरा खुद AnyMapमें रस्ट की ताकत का एक अच्छा प्रदर्शन है, जो नाजुक वस्तुओं का एक सुरक्षित और अभिव्यंजक अमूर्त प्रदान करने के लिए जेनेटिक्स के साथ विशेषता वस्तुओं का संयोजन करता है, जिसमें गो की आवश्यकता लिखी जाएगी map[string]interface{}
क्रिस मॉर्गन

जैसा कि मुझे उम्मीद थी, रस्ट अधिक शक्तिशाली है और मूल रूप से / सुरुचिपूर्ण ढंग से अधिक विकल्प प्रदान करता है, लेकिन गो की प्रणाली काफी करीब है कि ज्यादातर चीजें जो याद आती हैं, जैसे छोटे हैक के साथ पूरा किया जा सकता है interface{}। जबकि रस्ट तकनीकी रूप से बेहतर लगता है, मुझे अभी भी लगता है कि गो ... की आलोचना थोड़ी कठोर है। 99% कार्यों के लिए प्रोग्रामर की शक्ति बहुत अधिक है।
लोगन

22
@Logan, निम्न-स्तरीय / उच्च-प्रदर्शन डोमेन के लिए Rust का लक्ष्य है (जैसे ऑपरेटिंग सिस्टम, वेब ब्राउज़र ... कोर "सिस्टम" प्रोग्रामिंग सामान), स्टैटिक प्रेषण का विकल्प न होने पर (और यह प्रदर्शन / अनुकूलन देता है) यह अनुमति देता है) अस्वीकार्य है। यह एक कारण है कि गो उन अनुप्रयोगों के लिए जंग के समान उपयुक्त नहीं है। किसी भी मामले में, प्रोग्रामर की शक्ति वास्तव में बराबर नहीं है, आप किसी भी पुन: प्रयोज्य और गैर-अंतर्निहित डेटा संरचना के लिए सुरक्षा (संकलित समय) प्रकार को खो देते हैं, रनटाइम प्रकार के दावे पर वापस गिरते हैं।
हून जूल

10
यह बिल्कुल सही है - जंग आपको बहुत अधिक शक्ति प्रदान करता है। मुझे लगता है कि रस्ट एक सुरक्षित सी ++ के रूप में है, और एक तेज़ पायथन (या एक बहुत ही सरलीकृत जावा) के रूप में जाओ। उन कार्यों के बड़े प्रतिशत के लिए जहां डेवलपर उत्पादकता सबसे अधिक मायने रखती है (और रनटाइम और कचरा संग्रह जैसी चीजें समस्याग्रस्त नहीं हैं), गो चुनें (जैसे, वेब सर्वर, समवर्ती प्रणाली, कमांड लाइन उपयोगिताओं, उपयोगकर्ता अनुप्रयोग, आदि)। यदि आपको हर अंतिम प्रदर्शन की आवश्यकता है (और डेवलपर उत्पादकता शापित है), तो जंग चुनें (उदाहरण के लिए, ब्राउज़र, ऑपरेटिंग सिस्टम, संसाधन-विवश एम्बेडेड सिस्टम)।
weberc2
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.