ลักษณะของ Rust แตกต่างจาก Go Interfaces อย่างไร


64

ฉันค่อนข้างคุ้นเคยกับ Go โดยเขียนโปรแกรมขนาดเล็กจำนวนมากในนั้น แน่นอนว่าสนิมฉันไม่คุ้นเคย แต่จับตาดู

เมื่อเร็ว ๆ นี้อ่านhttp://yager.io/programming/go.htmlฉันคิดว่าฉันควรตรวจสอบสองวิธีที่ Generics ได้รับการจัดการเพราะบทความดูเหมือนจะวิพากษ์วิจารณ์อย่างไม่เป็นธรรมไปเมื่อในทางปฏิบัติมีอะไรมากที่อินเตอร์เฟส ไม่สามารถทำได้อย่างสวยงาม ฉันยังคงได้ยินโฆษณาต่อไปเกี่ยวกับลักษณะของ Rust ที่ทรงพลังและไม่มีอะไรนอกจากการวิจารณ์จากคนเกี่ยวกับ Go มีประสบการณ์ใน Go ฉันสงสัยว่าความจริงมันเป็นอย่างไรและอะไรคือความแตกต่างในท้ายที่สุด สิ่งที่ฉันพบคือลักษณะและการเชื่อมต่อมีความคล้ายคลึงกัน! ท้ายที่สุดฉันไม่แน่ใจว่าฉันขาดอะไรบางอย่างดังนั้นนี่คือบทสรุปการศึกษาที่รวดเร็วเกี่ยวกับความคล้ายคลึงกันของพวกเขาเพื่อให้คุณสามารถบอกฉันว่าฉันพลาดอะไรไป!

ตอนนี้ขอใช้เวลาดูไปเชื่อมต่อจากพวกเขาเอกสาร :

อินเทอร์เฟซในตัวไปมีวิธีในการระบุพฤติกรรมของวัตถุ: หากมีสิ่งใดสามารถทำได้ก็สามารถใช้ที่นี่ได้

ส่วนใหญ่แล้วอินเตอร์เฟสจะ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? มันไม่ทำงานเหรอ?

ซึ่งแตกต่างจาก Go's Interfaces ระบบชนิดของ Rust มีพารามิเตอร์ชนิดที่ให้คุณทำ generics และสิ่งที่เหมาะสมเช่นinterface{}ในขณะที่คอมไพเลอร์และรันไทม์รู้ประเภท ตัวอย่างเช่น,

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

ทำงานกับประเภทใดก็ได้และคอมไพเลอร์รู้ว่าประเภทขององค์ประกอบลำดับที่เวลารวบรวมมากกว่าการใช้การสะท้อน


ตอนนี้คำถามจริง: ฉันไม่มีความแตกต่างที่นี่หรือไม่? พวกเขาเป็นจริงที่คล้ายกัน? ไม่มีความแตกต่างพื้นฐานที่ฉันขาดหายไปที่นี่ใช่หรือไม่ (ในการใช้งานรายละเอียดการใช้งานมีความน่าสนใจ แต่ท้ายที่สุดก็ไม่สำคัญว่าจะทำงานเหมือนกันหรือไม่)

นอกจากความแตกต่างทางไวยากรณ์ความแตกต่างที่แท้จริงที่ฉันเห็นคือ:

  1. Go มีวิธีการจัดส่งแบบอัตโนมัติกับ Rust ต้องการ (?) implเพื่อสร้างคุณลักษณะ
    • สง่างามและชัดเจน
  2. สนิมมีพารามิเตอร์ประเภทที่อนุญาตให้ใช้ยาชื่อสามัญที่เหมาะสมโดยไม่มีการสะท้อนกลับ
    • ไปจริงๆไม่มีการตอบสนองที่นี่ นี่เป็นสิ่งเดียวที่มีประสิทธิภาพมากขึ้นอย่างมากและในที่สุดก็เป็นเพียงวิธีการคัดลอกและวางที่มีลายเซ็นประเภทที่แตกต่างกัน

สิ่งเหล่านี้เป็นความแตกต่างที่ไม่สำคัญหรือไม่? ถ้าเป็นเช่นนั้นก็จะปรากฏว่าระบบอินเตอร์เฟส / ประเภทของโกในทางปฏิบัติไม่อ่อนแอเท่าที่รับรู้

คำตอบ:


59

จะเกิดอะไรขึ้นใน Rust หากคุณกำหนดฟังก์ชั่นที่ใช้คุณสมบัติ แต่คุณไม่ได้ใช้งาน impl? มันไม่ทำงานเหรอ?

คุณจำเป็นต้องใช้คุณลักษณะนี้อย่างชัดเจน การมีวิธีที่มีชื่อ / ลายเซ็นตรงกันนั้นไม่มีความหมายสำหรับ 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 ++ ทำเช่นกันบางครั้งเรียกว่า "monomorphisation")

ในระหว่างการเดินทาง

อนุญาตให้ใช้งานการส่งแบบไดนามิกสำหรับฟังก์ชัน "ทั่วไป" เท่านั้นนั่นคือวิธีการโหลดที่อยู่จากค่าจากนั้นเรียกใช้จากที่นั่นดังนั้นฟังก์ชันที่แน่นอนจึงรู้ได้เฉพาะที่รันไทม์เท่านั้น ใช้ตัวอย่างด้านบน

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_bars จะถูกเรียกร้องดังกล่าวข้างต้นcall_barมีอยู่ของbarโหลดจากอินเตอร์เฟซของvtable

ระดับต่ำ

ในการใช้ถ้อยคำใหม่ข้างต้นในรูปแบบ C รุ่นสนิมสร้างขึ้น

/* "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;
}

สำหรับ Go มันเป็นอะไรที่มากกว่า:

/* 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);
}

(สิ่งนี้ไม่ถูกต้อง --- ต้องมีข้อมูลเพิ่มเติมใน vtable --- แต่การเรียกเมธอดเป็นตัวชี้ฟังก์ชันไดนามิกเป็นสิ่งที่เกี่ยวข้องที่นี่)

สนิมเสนอทางเลือก

กลับไปที่

วิธีการของ Rust ช่วยให้ผู้ใช้สามารถเลือกระหว่างการจัดส่งแบบคงที่และการจัดส่งแบบไดนามิก

จนถึงตอนนี้ฉันได้แสดงให้เห็นว่า Rust มีการจัดส่งยาชื่อสามัญไปแล้วเท่านั้น แต่ Rust สามารถเลือกที่จะเป็นแบบไดนามิกเช่น Go (ด้วยการใช้งานเดียวกัน) ผ่านวัตถุลักษณะ Notated like &Fooซึ่งเป็นการอ้างอิงถึงชนิดที่ไม่รู้จักซึ่งใช้Fooลักษณะนี้ ค่าเหล่านี้มีการแสดง vtable เหมือนกัน / คล้ายกันมากกับวัตถุอินเตอร์เฟส Go (วัตถุลักษณะเป็นตัวอย่างของ"ประเภทที่มีอยู่" )

มีบางกรณีที่การกระจายแบบไดนามิกมีประโยชน์จริง ๆ (และบางครั้งมีประสิทธิภาพมากกว่าเช่นลดการขยายตัวของรหัส / การทำซ้ำ) แต่การส่งแบบคงที่ช่วยให้คอมไพเลอร์อินไลน์ไซต์และใช้การปรับให้เหมาะสมทั้งหมด นี้เป็นสิ่งสำคัญโดยเฉพาะอย่างยิ่งสำหรับสิ่งที่ต้องการโปรโตคอลซ้ำสนิมของที่คงลักษณะการฝึกอบรมวิธีการสายช่วยให้ iterators ผู้ที่จะให้เร็วที่สุดเท่าเทียบเท่า C ขณะที่ยังคงดูเหมือนระดับสูงและการแสดงออก

Tl; dr: วิธีการของ Rust นำเสนอทั้งการส่งแบบคงที่และแบบไดนามิกในข้อมูลทั่วไปตามดุลยพินิจของโปรแกรมเมอร์; อนุญาตเฉพาะการส่งแบบไดนามิกเท่านั้น

ตัวแปรหลายตัวแปร

ยิ่งไปกว่านั้นการเน้นคุณลักษณะและการสะท้อนความคิดทำให้เกิดสนิมแบบพาราเมตริกตัวแปร : โปรแกรมรู้ดีว่าฟังก์ชันสามารถทำอะไรกับอาร์กิวเมนต์ได้เพราะมันต้องประกาศลักษณะทั่วไปที่ใช้ในฟังก์ชันลายเซ็น

วิธีการทำงานของ Go นั้นยืดหยุ่นมาก แต่มีการรับประกันน้อยกว่าสำหรับผู้โทร (ทำให้ค่อนข้างยากสำหรับโปรแกรมเมอร์ที่จะให้เหตุผลเกี่ยวกับ), เพราะ internals ของฟังก์ชั่นสามารถ (และทำ) แบบสอบถามสำหรับข้อมูลประเภทเพิ่มเติม (มีข้อผิดพลาดใน Go ไลบรารีมาตรฐานที่ซึ่ง iirc ฟังก์ชันที่ใช้ในการเขียนจะใช้การสะท้อนเพื่อเรียกFlushใช้อินพุตบางตัว

สิ่งปลูกสร้างที่เป็นนามธรรม

นี่เป็นจุดที่ค่อนข้างเจ็บดังนั้นฉันจะพูดเพียงสั้น ๆ แต่การมีนายพล "ที่เหมาะสม" อย่าง Rust ได้อนุญาตให้มีข้อมูลระดับต่ำเช่น Go's mapและ[]สามารถนำไปใช้จริงในไลบรารีมาตรฐานได้อย่างปลอดภัยและปลอดภัย เขียนเป็นสนิม ( HashMapและVecตามลำดับ)

และไม่เพียง แต่เป็นประเภทเหล่านั้นเท่านั้นคุณสามารถสร้างโครงสร้างทั่วไปแบบปลอดภัยได้ที่ด้านบนของพวกเขาเช่นLruCacheเป็นเลเยอร์แคชทั่วไปที่ด้านบนของ hashmap ซึ่งหมายความว่าผู้ใช้สามารถใช้โครงสร้างข้อมูลโดยตรงจากไลบรารีมาตรฐานโดยไม่ต้องจัดเก็บข้อมูลinterface{}และใช้การยืนยันประเภทเมื่อทำการแทรก / แตกไฟล์ นั่นคือถ้าคุณมีLruCache<int, String>คุณรับประกันได้ว่าคีย์จะเป็นints เสมอและค่านั้นจะStringเป็น s เสมอ: ไม่มีวิธีใดที่จะแทรกค่าที่ไม่ถูกต้องโดยไม่ตั้งใจ (หรือพยายามดึงค่าที่ไม่ใช่String)


ของตัวเองคือการสาธิตที่ดีของจุดแข็งของสนิมรวมลักษณะวัตถุที่มียาชื่อสามัญที่จะให้เป็นนามธรรมปลอดภัยและการแสดงออกในสิ่งที่เปราะบางที่ในไปจะมีความจำเป็นต้องเขียนAnyMap map[string]interface{}
Chris Morgan

ขณะที่ผมคาดว่าสนิมมีประสิทธิภาพมากขึ้นและมีทางเลือกมากขึ้นโดยกำเนิด / หรูหรา interface{}แต่ระบบโกอยู่ใกล้พอที่จะทำให้สิ่งที่มากที่สุดพลาดสามารถทำได้ด้วยการแฮ็กขนาดเล็กเช่น ในขณะที่ Rust ดูเหมือนว่าเหนือกว่าในทางเทคนิคฉันก็ยังคิดว่าคำวิจารณ์ของ Go ... นั้นรุนแรงเกินไป พลังของโปรแกรมเมอร์ค่อนข้างจะเท่ากันกับ 99% ของงาน
โลแกน

22
@Logan สำหรับโดเมนระดับต่ำ / ประสิทธิภาพสูง Rust กำลังเล็งหา (เช่นระบบปฏิบัติการ, เว็บเบราว์เซอร์ ... เนื้อหาหลักในการเขียนโปรแกรม "ระบบ" หลัก) ไม่มีตัวเลือกในการส่งแบบคงที่ (และประสิทธิภาพที่ให้ / การเพิ่มประสิทธิภาพ อนุญาต) ไม่สามารถยอมรับได้ เป็นหนึ่งในเหตุผลที่ Go ไม่เหมาะสมกับ Rust สำหรับแอพพลิเคชั่นประเภทนั้น ไม่ว่าในกรณีใดพลังของโปรแกรมเมอร์ไม่ได้เท่ากันคุณสูญเสียความปลอดภัยประเภท (เวลารวบรวม) สำหรับโครงสร้างข้อมูลที่สามารถใช้ซ้ำได้และไม่ได้ติดตั้งกลับคืนสู่การยืนยันประเภทรันไทม์
huon

10
ถูกต้องแล้ว - รัสให้พลังมากกว่ากับคุณ ฉันคิดว่า Rust เป็น C ++ ที่ปลอดภัยและ Go เป็น Python ที่รวดเร็ว (หรือ Java ที่ง่ายกว่าเดิม) สำหรับงานขนาดใหญ่ที่นักพัฒนาซอฟต์แวร์ให้ความสำคัญมากที่สุด (และสิ่งต่าง ๆ เช่นรันไทม์และการรวบรวมขยะไม่มีปัญหา) ให้เลือก Go (เช่นเว็บเซิร์ฟเวอร์ระบบพร้อมกันยูทิลิตีบรรทัดคำสั่งแอปพลิเคชันผู้ใช้ ฯลฯ ) หากคุณต้องการประสิทธิภาพการทำงานขั้นสุดท้าย (และประสิทธิภาพการทำงานของนักพัฒนาหมดไป) ให้เลือกสนิม (เช่นเบราว์เซอร์ระบบปฏิบัติการระบบฝังตัวที่ จำกัด ทรัพยากร)
weberc2
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.