Trong các triển khai C # và Java, các đối tượng thường có một con trỏ tới lớp của nó. Điều này là có thể bởi vì chúng là ngôn ngữ kế thừa đơn. Cấu trúc lớp sau đó chứa vtable cho hệ thống phân cấp kế thừa đơn. Nhưng các phương thức giao diện gọi cũng có tất cả các vấn đề của nhiều kế thừa. Điều này thường được giải quyết bằng cách đặt các vtables bổ sung cho tất cả các giao diện được triển khai vào cấu trúc lớp. Điều này tiết kiệm không gian so với các triển khai kế thừa ảo điển hình trong C ++, nhưng làm cho việc gửi phương thức giao diện trở nên phức tạp hơn - có thể được bù một phần bằng bộ đệm.
Ví dụ, trong JVM OpenJDK, mỗi lớp chứa một mảng vtables cho tất cả các giao diện được triển khai (một giao diện vtable được gọi là một itable ). Khi một phương thức giao diện được gọi, mảng này được tìm kiếm tuyến tính cho khả năng của giao diện đó, sau đó phương thức có thể được gửi qua đó. Bộ nhớ đệm được sử dụng để mỗi trang web cuộc gọi ghi nhớ kết quả của việc gửi phương thức, do đó tìm kiếm này chỉ phải được lặp lại khi loại đối tượng cụ thể thay đổi. Mã giả cho công văn phương thức:
// Dispatch SomeInterface.method
Method const* resolve_method(
Object const* instance, Klass const* interface, uint itable_slot) {
Klass const* klass = instance->klass;
for (Itable const* itable : klass->itables()) {
if (itable->klass() == interface)
return itable[itable_slot];
}
throw ...; // class does not implement required interface
}
(So sánh mã thực trong trình thông dịch OpenJDK HotSpot hoặc trình biên dịch x86 .)
C # (hay chính xác hơn là CLR) sử dụng một phương pháp liên quan. Tuy nhiên, ở đây, nó không chứa con trỏ tới các phương thức, mà là các bản đồ vị trí: chúng trỏ đến các mục trong vtable chính của lớp. Cũng như Java, việc tìm kiếm chính xác nó chỉ là trường hợp xấu nhất và dự kiến bộ nhớ đệm tại trang web cuộc gọi có thể tránh tìm kiếm này gần như luôn luôn. CLR sử dụng một kỹ thuật gọi là Virtual Stub Dispatch để vá mã máy do JIT biên dịch với các chiến lược bộ đệm khác nhau. Mã giả:
Method const* resolve_method(
Object const* instance, Klass const* interface, uint interface_slot) {
Klass const* klass = instance->klass;
// Walk all base classes to find slot map
for (Klass const* base = klass; base != nullptr; base = base->base()) {
// I think the CLR actually uses hash tables instead of a linear search
for (SlotMap const* slot_map : base->slot_maps()) {
if (slot_map->klass() == interface) {
uint vtable_slot = slot_map[interface_slot];
return klass->vtable[vtable_slot];
}
}
}
throw ...; // class does not implement required interface
}
Sự khác biệt chính đối với mã giả OpenJDK là trong OpenJDK, mỗi lớp có một mảng tất cả các giao diện được triển khai trực tiếp hoặc gián tiếp, trong khi CLR chỉ giữ một mảng các bản đồ vị trí cho các giao diện được triển khai trực tiếp trong lớp đó. Do đó, chúng ta cần đưa hệ thống phân cấp thừa kế lên trên cho đến khi tìm thấy bản đồ vị trí. Đối với hệ thống phân cấp thừa kế sâu, điều này dẫn đến tiết kiệm không gian. Những điều này đặc biệt có liên quan trong CLR do cách thức triển khai thuốc generic: đối với chuyên môn hóa chung, cấu trúc lớp được sao chép và các phương thức trong vtable chính có thể được thay thế bằng các chuyên ngành. Các bản đồ vị trí tiếp tục trỏ đến các mục vtable chính xác và do đó có thể được chia sẻ giữa tất cả các chuyên ngành chung của một lớp.
Như một lưu ý kết thúc, có nhiều khả năng hơn để thực hiện giao diện. Thay vì đặt con trỏ vtable / itable trong đối tượng hoặc trong cấu trúc lớp, chúng ta có thể sử dụng các con trỏ chất béo cho đối tượng, về cơ bản là một (Object*, VTable*)
cặp. Hạn chế là điều này làm tăng gấp đôi kích thước của con trỏ và việc phát sóng (từ loại cụ thể sang loại giao diện) không miễn phí. Nhưng nó linh hoạt hơn, ít bị gián đoạn hơn và cũng có nghĩa là các giao diện có thể được thực hiện bên ngoài từ một lớp. Các cách tiếp cận liên quan được sử dụng bởi giao diện Go, đặc điểm Rust và kiểu chữ Haskell.
Tài liệu tham khảo và đọc thêm:
- Wikipedia: Bộ nhớ đệm nội tuyến . Thảo luận về các phương pháp lưu trữ có thể được sử dụng để tránh tra cứu phương pháp đắt tiền. Thông thường không cần thiết cho công văn dựa trên vtable, nhưng rất mong muốn cho các cơ chế công văn đắt tiền hơn như các chiến lược công văn giao diện ở trên.
- OpenJDK Wiki (2013): Cuộc gọi giao diện . Thảo luận về nó.
- Pobar, Neward (2009): SSCLI 2.0 Nội bộ. Chương 5 của cuốn sách thảo luận về các bản đồ vị trí rất chi tiết. Không bao giờ được xuất bản nhưng được cung cấp bởi các tác giả trên blog của họ . Các liên kết PDF từ đó đã di chuyển. Cuốn sách này có lẽ không còn phản ánh tình trạng hiện tại của CLR.
- CoreCLR (2006): Công cụ khai thác ảo . Trong: Cuốn sách của thời gian chạy. Thảo luận về bản đồ vị trí và bộ nhớ đệm để tránh tra cứu đắt tiền.
- Kennedy, Syme (2001): Thiết kế và triển khai Generics cho .NET Runtime Ngôn ngữ chung . ( Liên kết PDF ). Thảo luận về các phương pháp khác nhau để thực hiện thuốc generic. Generics tương tác với phương thức gửi bởi vì các phương thức có thể là chuyên biệt nên vtables có thể phải được viết lại.