Hai khái niệm rất rất giống nhau. Trong các ngôn ngữ OOP bình thường, chúng tôi đính kèm một vtable (hoặc cho các giao diện: có thể) cho từng đối tượng:
| this
v
+---+---+---+
| V | a | b | the object with fields a, b
+---+---+---+
|
v
+---+---+---+
| o | p | q | the vtable with method slots o(), p(), q()
+---+---+---+
Điều này cho phép chúng ta gọi các phương thức tương tự như this->vtable.p(this)
.
Trong Haskell, bảng phương thức giống như một đối số ẩn ẩn:
method :: Class a => a -> a -> Int
sẽ trông giống như chức năng C ++
template<typename A>
int method(Class<A>*, A*, A*)
trong đó Class<A>
là một ví dụ của typeclass Class
cho loại A
. Một phương thức sẽ được gọi như
typeclass_instance->p(value_ptr);
Ví dụ là tách biệt với các giá trị. Các giá trị vẫn giữ loại thực tế của chúng. Mặc dù kiểu chữ cho phép một số đa hình, nhưng đây không phải là đa hình. Điều đó làm cho không thể lập danh sách các giá trị thỏa mãn a Class
. Ví dụ, giả sử chúng ta có instance Class Int ...
và instance Class String ...
, chúng ta không thể tạo một loại danh sách không đồng nhất như thế [Class]
có giá trị như thế nào [42, "foo"]
. (Điều này có thể xảy ra khi bạn sử dụng tiện ích mở rộng các loại hiện có của kiểu Cameron, giúp chuyển đổi hiệu quả sang cách tiếp cận Go).
Trong Go, một giá trị không triển khai một bộ giao diện cố định. Do đó, nó không thể có một con trỏ vtable. Thay vào đó, các con trỏ tới các loại giao diện được triển khai dưới dạng các con trỏ béo bao gồm một con trỏ tới dữ liệu, một con trỏ khác tới nó:
`this` fat pointer
+---+---+
| | |
+---+---+
____/ \_________
v v
+---+---+---+ +---+---+
| o | p | q | | a | b | the data with
+---+---+---+ +---+---+ fields a, b
itable with method
slots o(), p(), q()
this.itable->p(this.data_ptr)
Nó có thể được kết hợp với dữ liệu thành một con trỏ béo khi bạn chuyển từ một giá trị bình thường sang một loại giao diện. Khi bạn có một loại giao diện, loại dữ liệu thực tế đã trở nên không liên quan. Thực tế, bạn không thể truy cập trực tiếp vào các trường mà không thông qua các phương thức hoặc hạ thấp giao diện (có thể thất bại).
Cách tiếp cận của Go để gửi giao diện đi kèm với chi phí: mỗi con trỏ đa hình lớn gấp đôi một con trỏ bình thường. Ngoài ra, truyền từ giao diện này sang giao diện khác liên quan đến việc sao chép các con trỏ phương thức sang một vtable mới. Nhưng một khi chúng ta đã xây dựng được nó, điều này cho phép chúng ta gửi các cuộc gọi phương thức đến nhiều giao diện, một thứ mà các ngôn ngữ OOP truyền thống phải chịu. Ở đây, m là số phương thức trong giao diện đích và b là số lớp cơ sở:
- C ++ không cắt đối tượng hoặc cần đuổi theo con trỏ kế thừa ảo khi truyền, nhưng sau đó có quyền truy cập vtable đơn giản. O (1) hoặc O (b) chi phí phát sóng, nhưng gửi phương thức O (1).
- Java Hotspot VM không phải làm bất cứ điều gì khi upcasting, nhưng khi tra cứu phương thức giao diện thực hiện tìm kiếm tuyến tính thông qua tất cả các khả năng được thực hiện bởi lớp đó. O (1) upcasting, nhưng phương thức O (b) gửi.
- Python không phải làm bất cứ điều gì khi upcasting, nhưng sử dụng tìm kiếm tuyến tính thông qua danh sách lớp cơ sở được tuyến tính hóa C3. O (1) upcasting, nhưng phương thức O (b²) gửi? Tôi không chắc độ phức tạp thuật toán của C3 là gì.
- .NET CLR sử dụng một cách tiếp cận tương tự như Hotspot nhưng thêm một mức độ gián tiếp khác trong nỗ lực tối ưu hóa cho việc sử dụng bộ nhớ. O (1) upcasting, nhưng phương thức O (b) gửi.
Độ phức tạp điển hình cho việc gửi phương thức tốt hơn nhiều vì việc tra cứu phương thức thường có thể được lưu trong bộ nhớ cache, nhưng trường hợp phức tạp nhất là khá kinh khủng.
Để so sánh, Go có phát sóng phương thức O (1) hoặc O (m) và phương thức O (1). Haskell không có upcasting (ràng buộc một kiểu với một lớp loại là hiệu ứng thời gian biên dịch) và gửi phương thức O (1).
[42, "foo"]
. Đó là một ví dụ sinh động.