Vì không có ai khác cung cấp câu trả lời này một cách rõ ràng nên tôi sẽ thêm phần sau:
Việc triển khai một giao diện trên một cấu trúc không có hậu quả tiêu cực nào.
Bất kỳ biến nào của kiểu giao diện được sử dụng để chứa một cấu trúc sẽ dẫn đến một giá trị đóng hộp của cấu trúc đó được sử dụng. Nếu cấu trúc là bất biến (một điều tốt) thì điều này tồi tệ nhất là một vấn đề hiệu suất trừ khi bạn:
- sử dụng đối tượng kết quả cho mục đích khóa (một ý tưởng vô cùng tồi tệ theo bất kỳ cách nào)
- sử dụng ngữ nghĩa bình đẳng tham chiếu và mong đợi nó hoạt động cho hai giá trị đóng hộp từ cùng một cấu trúc.
Cả hai điều này sẽ khó xảy ra, thay vào đó bạn có thể đang làm một trong những điều sau:
Generics
Có lẽ nhiều lý do hợp lý cho việc cấu trúc triển khai giao diện là để chúng có thể được sử dụng trong ngữ cảnh chung với các ràng buộc . Khi được sử dụng trong kiểu này, biến như vậy:
class Foo<T> : IEquatable<Foo<T>> where T : IEquatable<T>
{
private readonly T a;
public bool Equals(Foo<T> other)
{
return this.a.Equals(other.a);
}
}
- Cho phép sử dụng cấu trúc làm tham số kiểu
- miễn là không có ràng buộc khác thích
new()
hoặc class
được sử dụng.
- Cho phép tránh quyền anh trên các cấu trúc được sử dụng theo cách này.
Sau đó this.a KHÔNG phải là một tham chiếu giao diện do đó nó không gây ra một hộp bất cứ thứ gì được đặt vào nó. Hơn nữa khi trình biên dịch c # biên dịch các lớp chung và cần chèn các lệnh gọi của các phương thức cá thể được xác định trên các cá thể của tham số Kiểu T, nó có thể sử dụng opcode bị ràng buộc :
Nếu thisType là một kiểu giá trị và thisType triển khai phương thức thì ptr được truyền không sửa đổi dưới dạng con trỏ 'this' tới một lệnh gọi phương thức, để thực hiện phương thức bởi thisType.
Điều này tránh quyền anh và vì kiểu giá trị đang triển khai giao diện là phải thực hiện phương thức, do đó sẽ không có quyền anh nào xảy ra. Trong ví dụ trên, lệnh Equals()
gọi được thực hiện mà không có hộp nào trên này. A 1 .
API ma sát thấp
Hầu hết các cấu trúc phải có ngữ nghĩa giống như nguyên thủy trong đó các giá trị giống hệt nhau theo từng bit được coi là bằng 2 . Thời gian chạy sẽ cung cấp hành vi như vậy một cách ngầm định Equals()
nhưng điều này có thể chậm. Ngoài ra, sự bình đẳng ngầm định này không được tiết lộ khi triển khai IEquatable<T>
và do đó ngăn cản việc dễ dàng sử dụng các cấu trúc làm khóa cho Từ điển trừ khi chúng tự triển khai rõ ràng. Do đó, nhiều loại cấu trúc công khai thường khai báo rằng chúng thực thi IEquatable<T>
( T
chúng tự ở đâu) để làm cho việc này dễ dàng hơn và hoạt động tốt hơn cũng như phù hợp với hành vi của nhiều loại giá trị hiện có trong CLR BCL.
Tất cả các nguyên thủy trong BCL đều triển khai tối thiểu:
IComparable
IConvertible
IComparable<T>
IEquatable<T>
(Và do đó IEquatable
)
Nhiều người cũng triển khai IFormattable
, hơn nữa nhiều kiểu giá trị do Hệ thống xác định như DateTime, TimeSpan và Guid cũng triển khai nhiều hoặc tất cả những kiểu này. Nếu bạn đang triển khai một kiểu 'hữu ích rộng rãi' tương tự như cấu trúc số phức hoặc một số giá trị văn bản có chiều rộng cố định thì việc triển khai nhiều giao diện chung này (chính xác) sẽ làm cho cấu trúc của bạn hữu ích và khả dụng hơn.
Loại trừ
Rõ ràng nếu giao diện ngụ ý mạnh mẽ đến khả năng thay đổi (chẳng hạn như ICollection
) thì việc triển khai nó là một ý tưởng tồi vì điều đó có nghĩa là bạn đã tạo cấu trúc có thể thay đổi (dẫn đến các loại lỗi được mô tả ở nơi các sửa đổi xảy ra trên giá trị được đóng hộp chứ không phải là bản gốc ) hoặc bạn gây nhầm lẫn cho người dùng bằng cách bỏ qua hàm ý của các phương pháp như Add()
hoặc ném các ngoại lệ.
Nhiều giao diện KHÔNG ngụ ý khả năng thay đổi (chẳng hạn như IFormattable
) và được dùng như một cách thành ngữ để thể hiện một số chức năng nhất định theo một kiểu nhất quán. Thường thì người dùng cấu trúc sẽ không quan tâm đến bất kỳ chi phí quyền anh nào cho hành vi như vậy.
Tóm lược
Khi được thực hiện một cách hợp lý, trên các loại giá trị bất biến, việc triển khai các giao diện hữu ích là một ý tưởng hay
Ghi chú:
1: Lưu ý rằng trình biên dịch có thể sử dụng điều này khi gọi các phương thức ảo trên các biến được biết là thuộc một kiểu cấu trúc cụ thể nhưng trong đó nó được yêu cầu để gọi một phương thức ảo. Ví dụ:
List<int> l = new List<int>();
foreach(var x in l)
;
Điều tra viên được Danh sách trả về là một cấu trúc, một sự tối ưu hóa để tránh phân bổ khi liệt kê danh sách (Với một số hệ quả thú vị ). Tuy nhiên, ngữ nghĩa của foreach chỉ rõ rằng nếu điều tra viên triển khai IDisposable
thì Dispose()
sẽ được gọi sau khi hoàn tất quá trình lặp. Rõ ràng điều này xảy ra thông qua một lệnh gọi đóng hộp sẽ loại bỏ bất kỳ lợi ích nào của việc điều tra viên là một cấu trúc (thực tế là nó sẽ tồi tệ hơn). Tệ hơn nữa, nếu lệnh vứt bỏ sửa đổi trạng thái của điều tra viên theo một cách nào đó thì điều này sẽ xảy ra trên phiên bản đóng hộp và nhiều lỗi tinh vi có thể được đưa ra trong các trường hợp phức tạp. Do đó IL phát ra trong tình huống này là:
IL_0001: newobj System.Collections.Generic.List..ctor
IL_0006: stloc.0
IL_0007: nop
IL_0008: ldloc.0
IL_0009: callvirt System.Collections.Generic.List.GetEnumerator
IL_000E: stloc.2
IL_000F: br.s IL_0019
IL_0011: ldloca.s 02
IL_0013: gọi System.Collections.Generic.List.get_Current
IL_0018: stloc.1
IL_0019: ldloca.s 02
IL_001B: gọi System.Collections.Generic.List.MoveNext
IL_0020: stloc.3
IL_0021: ldloc.3
IL_0022: brtrue.s IL_0011
IL_0024: rời khỏi.s IL_0035
IL_0026: ldloca.s 02
IL_0028: bị hạn chế. System.Collections.Generic.List.Enumerator
IL_002E: callvirt System.IDisposable.Dispose
IL_0033: nop
IL_0034: cuối cùng
Do đó, việc triển khai IDisposable không gây ra bất kỳ vấn đề hiệu suất nào và khía cạnh có thể thay đổi (đáng tiếc) của điều tra viên được giữ nguyên nếu phương thức Dispose thực sự có tác dụng gì!
2: double và float là các ngoại lệ đối với quy tắc này khi các giá trị NaN không được coi là bằng nhau.