Rust Traits khác với Giao diện Go như thế nào?


64

Tôi tương đối quen thuộc với Go, đã viết một số chương trình nhỏ trong đó. Rust, tất nhiên, tôi ít quen thuộc hơn nhưng để mắt đến.

Gần đây đã đọc http://yager.io/programming/go.html , tôi nghĩ rằng cá nhân tôi đã kiểm tra hai cách xử lý Generics vì bài viết dường như chỉ trích không công bằng Go khi, trong thực tế, không có nhiều Giao diện không thể hoàn thành một cách thanh lịch. Tôi liên tục nghe thấy sự cường điệu về những đặc điểm của Rust mạnh mẽ như thế nào và không có gì ngoài những lời chỉ trích từ mọi người về Go. Có một số kinh nghiệm trong Go, tôi tự hỏi nó đúng như thế nào và sự khác biệt cuối cùng là gì. Những gì tôi tìm thấy là Đặc điểm và Giao diện khá giống nhau! Cuối cùng, tôi không chắc là mình có thiếu thứ gì không, vì vậy đây là một bản tóm tắt giáo dục nhanh về những điểm tương đồng của chúng để bạn có thể cho tôi biết những gì tôi đã bỏ lỡ!

Bây giờ, hãy xem Giao diện Go từ tài liệu của họ :

Các giao diện trong Go cung cấp một cách để xác định hành vi của một đối tượng: nếu một cái gì đó có thể làm điều này, thì nó có thể được sử dụng ở đây.

Cho đến nay, giao diện phổ biến nhất là Stringertrả về một chuỗi đại diện cho đối tượng.

type Stringer interface {
    String() string
}

Vì vậy, bất kỳ đối tượng đã String()xác định trên nó là một Stringerđối tượng. Điều này có thể được sử dụng trong các chữ ký loại sao cho func (s Stringer) print()gần như tất cả các đối tượng và in chúng.

Chúng tôi cũng có interface{}bất kỳ đối tượng. Sau đó chúng ta phải xác định loại tại thời gian chạy thông qua sự phản chiếu.


Bây giờ, chúng ta hãy xem Rust Traits từ tài liệu của họ :

Đơn giản nhất, một đặc điểm là một tập hợp các chữ ký phương thức bằng 0 hoặc nhiều hơn. Ví dụ: chúng ta có thể khai báo đặc điểm Printable cho những thứ có thể được in ra bàn điều khiển, với một chữ ký phương thức duy nhất:

trait Printable {
    fn print(&self);
}

Điều này ngay lập tức trông khá giống với Giao diện Go của chúng tôi. Sự khác biệt duy nhất tôi thấy là chúng tôi xác định 'Triển khai' các Đặc điểm thay vì chỉ xác định các phương thức. Vì vậy chúng tôi làm

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

thay vì

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

Câu hỏi thưởng: Điều gì xảy ra trong Rust nếu bạn xác định chức năng thực hiện một đặc điểm nhưng bạn không sử dụng impl? Nó không hoạt động?

Không giống như Giao diện của Go, hệ thống loại của Rust có các tham số loại cho phép bạn thực hiện các khái quát chung và những thứ như interface{}trong khi trình biên dịch và thời gian chạy thực sự biết loại. Ví dụ,

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

hoạt động trên bất kỳ loại nào và trình biên dịch biết rằng loại phần tử Sequence tại thời gian biên dịch thay vì sử dụng sự phản chiếu.


Bây giờ, câu hỏi thực tế: tôi có thiếu bất kỳ sự khác biệt nào ở đây không? Họ có thực sự đó tương tự? Không có một số khác biệt cơ bản hơn mà tôi đang thiếu ở đây? (Trong cách sử dụng. Chi tiết triển khai rất thú vị, nhưng cuối cùng không quan trọng nếu chúng hoạt động giống nhau.)

Bên cạnh sự khác biệt về cú pháp, sự khác biệt thực tế tôi thấy là:

  1. Go có phương thức gửi tự động so với Rust yêu cầu (?) implĐể thực hiện Đặc điểm
    • Thanh lịch so với rõ ràng
  2. Rust có các tham số loại cho phép tạo ra các tổng quát thích hợp mà không bị phản xạ.
    • Đi thực sự không có phản ứng ở đây. Đây là điều duy nhất mạnh hơn đáng kể và cuối cùng nó chỉ là sự thay thế cho phương pháp sao chép và dán với các chữ ký loại khác nhau.

Đây có phải là sự khác biệt không tầm thường? Nếu vậy, nó sẽ xuất hiện hệ thống Giao diện / Loại của Go, trên thực tế, không yếu như nhận thức.

Câu trả lời:


59

Điều gì xảy ra trong Rust nếu bạn xác định một hàm thực hiện một đặc điểm nhưng bạn không sử dụng hàm impl? Nó không hoạt động?

Bạn cần thực hiện rõ ràng các đặc điểm; tình cờ có một phương thức với tên / chữ ký phù hợp là vô nghĩa đối với Rust.

Gửi cuộc gọi chung

Đây có phải là sự khác biệt không tầm thường? Nếu vậy, nó sẽ xuất hiện hệ thống Giao diện / Loại của Go, trên thực tế, không yếu như nhận thức.

Không cung cấp công văn tĩnh có thể là một cú đánh hiệu suất đáng kể cho một số trường hợp nhất định (ví dụ như trường hợp Iteratortôi đề cập dưới đây). Tôi nghĩ rằng đây là những gì bạn có nghĩa là

Đi thực sự không có phản ứng ở đây. Đây là điều duy nhất mạnh hơn đáng kể và cuối cùng nó chỉ là sự thay thế cho phương pháp sao chép và dán với các chữ ký loại khác nhau.

nhưng tôi sẽ đề cập chi tiết hơn, bởi vì nó đáng để hiểu sự khác biệt sâu sắc.

Trong Rust

Cách tiếp cận của Rust cho phép người dùng lựa chọn giữa công văn tĩnh và công văn động . Ví dụ, nếu bạn có

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());
}

sau đó hai call_barcuộc gọi ở trên sẽ biên dịch thành các cuộc gọi tương ứng

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

trong đó các .bar()cuộc gọi phương thức đó là các cuộc gọi hàm tĩnh, tức là đến một địa chỉ hàm cố định trong bộ nhớ. Điều này cho phép tối ưu hóa như nội tuyến, bởi vì trình biên dịch biết chính xác chức năng nào đang được gọi. (Đây là những gì C ++ cũng làm, đôi khi được gọi là "đơn phân".)

Đi

Go chỉ cho phép gửi động cho các hàm "chung", nghĩa là, địa chỉ phương thức được tải từ giá trị và sau đó được gọi từ đó, vì vậy hàm chính xác chỉ được biết khi chạy. Sử dụng ví dụ trên

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"))
}

Bây giờ, hai call_bars đó sẽ luôn được gọi ở trên call_bar, với địa chỉ được bartải từ vtable của giao diện .

Cấp thấp

Để viết lại các từ trên, trong ký hiệu C. Phiên bản của Rust tạo ra

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

Đối với Go, đó là một cái gì đó giống như:

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

(Điều này không chính xác --- phải có thêm thông tin trong vtable --- nhưng cuộc gọi phương thức là con trỏ hàm động là điều có liên quan ở đây.)

Rust cung cấp sự lựa chọn

Sẽ trở lại

Cách tiếp cận của Rust cho phép người dùng lựa chọn giữa công văn tĩnh và công văn động.

Cho đến nay, tôi chỉ chứng minh Rust có các tướng được gửi tĩnh, nhưng Rust có thể chọn tham gia vào các động lực như Go (với cách thực hiện tương tự), thông qua các đối tượng đặc điểm. Ký hiệu như &Foo, đó là một tài liệu tham khảo mượn cho một loại không xác định thực hiện các Foođặc điểm. Các giá trị này có biểu diễn vtable giống / rất giống với đối tượng giao diện Go. (Một đối tượng đặc điểm là một ví dụ về "loại tồn tại" .)

Có những trường hợp trong đó công văn động thực sự hữu ích (và đôi khi có hiệu suất cao hơn, ví dụ, bằng cách giảm sự phình to / sao chép mã), nhưng công văn tĩnh cho phép trình biên dịch thực hiện các cuộc gọi và áp dụng tất cả các tối ưu hóa của chúng, nghĩa là nó thường nhanh hơn. Điều này đặc biệt quan trọng đối với những thứ như giao thức lặp của Rust , trong đó các cuộc gọi phương thức đặc tính gửi tĩnh cho phép các trình lặp đó nhanh như tương đương C, trong khi vẫn có vẻ cao và biểu cảm .

Tl; dr: Cách tiếp cận của Rust cung cấp cả công văn tĩnh và động trong tổng quát, theo ý của lập trình viên; Đi chỉ cho phép công văn năng động.

Đa hình tham số

Hơn nữa, việc nhấn mạnh các đặc điểm và phản xạ khử cho phép đa hình tham số mạnh hơn của Rust : lập trình viên biết chính xác chức năng có thể làm gì với các đối số của nó, bởi vì nó phải khai báo các đặc điểm mà các kiểu chung thực hiện trong chữ ký hàm.

Cách tiếp cận của Go rất linh hoạt, nhưng có ít sự đảm bảo hơn cho người gọi (khiến lập trình viên khó hiểu hơn một chút), bởi vì phần bên trong của hàm có thể (và thực hiện) truy vấn thông tin loại bổ sung (có lỗi trong Go thư viện tiêu chuẩn trong đó, iirc, một hàm lấy một nhà văn sẽ sử dụng sự phản chiếu để gọi Flushmột số đầu vào, nhưng không phải là các hàm khác).

Xây dựng trừu tượng

Đây là một vấn đề nhức nhối, vì vậy tôi sẽ chỉ nói ngắn gọn, nhưng có những khái quát "đúng đắn" như Rust đã cho phép các loại dữ liệu cấp thấp như Go map[]thực sự được triển khai trực tiếp trong thư viện tiêu chuẩn theo cách an toàn mạnh mẽ và được viết bằng Rust ( HashMapVectương ứng).

Và không chỉ các loại đó, bạn có thể xây dựng các cấu trúc chung an toàn kiểu trên đầu chúng, ví dụ: LruCachemột lớp bộ đệm chung ở trên cùng của một hashmap. Điều này có nghĩa là mọi người chỉ có thể sử dụng các cấu trúc dữ liệu trực tiếp từ thư viện chuẩn mà không phải lưu trữ dữ liệu dưới dạng interface{}và sử dụng các xác nhận loại khi chèn / trích xuất. Đó là, nếu bạn có một LruCache<int, String>, bạn đảm bảo rằng các khóa luôn intlà s và các giá trị luôn luôn Stringlà s: không có cách nào để vô tình chèn sai giá trị (hoặc cố gắng trích xuất một giá trị không String).


Bản thân tôi AnyMaplà một minh chứng tốt cho những điểm mạnh của Rust, kết hợp các đối tượng đặc điểm với khái quát để cung cấp một sự trừu tượng hóa an toàn và biểu cảm của điều mong manh mà trong Go sẽ cần phải được viết map[string]interface{}.
Chris Morgan

Như tôi dự đoán, Rust mạnh hơn và cung cấp nhiều sự lựa chọn hơn / thanh lịch, nhưng hệ thống của Go đủ gần để hầu hết mọi thứ mà nó bỏ lỡ có thể được thực hiện với các bản hack nhỏ như interface{}. Mặc dù Rust có vẻ vượt trội về mặt kỹ thuật, tôi vẫn nghĩ rằng những lời chỉ trích về Go ... đã hơi quá khắc nghiệt. Sức mạnh của lập trình viên là khá nhiều ngang bằng với 99% nhiệm vụ.
Logan

22
@Logan, đối với các miền cấp thấp / hiệu suất cao, Rust đang nhắm đến (ví dụ: hệ điều hành, trình duyệt web ... công cụ lập trình "hệ thống" cốt lõi), không có tùy chọn gửi tĩnh (và hiệu suất mà nó mang lại / tối ưu hóa nó cho phép) là không thể chấp nhận. Đó là một trong những lý do khiến Go không phù hợp như Rust cho các loại ứng dụng đó. Trong mọi trường hợp, sức mạnh của lập trình viên không thực sự ngang bằng, bạn mất (thời gian biên dịch) an toàn cho mọi cấu trúc dữ liệu có thể sử dụng lại và không tích hợp, rơi vào các xác nhận loại thời gian chạy.
huon

10
Điều đó hoàn toàn chính xác - Rust cung cấp cho bạn nhiều sức mạnh hơn. Tôi nghĩ rằng Rust là một C ++ an toàn và Go là một Python nhanh (hoặc một Java đơn giản hóa rất nhiều). Đối với phần lớn các tác vụ trong đó năng suất của nhà phát triển là vấn đề quan trọng nhất (và những thứ như thời gian chạy và bộ sưu tập rác không có vấn đề), hãy chọn Go (ví dụ: máy chủ web, hệ thống đồng thời, tiện ích dòng lệnh, ứng dụng người dùng, v.v.). Nếu bạn cần từng chút hiệu suất cuối cùng (và năng suất của nhà phát triển bị nguyền rủa), hãy chọn Rust (ví dụ: trình duyệt, hệ điều hành, hệ thống nhúng bị hạn chế tài nguyên).
weberc2 3/03/2015
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.