Nó có an toàn cho các cấu trúc để triển khai các giao diện không?


93

Tôi dường như nhớ đã đọc điều gì đó về việc các cấu trúc triển khai giao diện trong CLR thông qua C # có hại như thế nào, nhưng dường như tôi không thể tìm thấy gì về nó. Nó có tồi không? Làm như vậy có hậu quả không mong muốn không?

public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }

Câu trả lời:


45

Có một số điều đang xảy ra trong câu hỏi này ...

Một cấu trúc có thể triển khai một giao diện, nhưng có những lo ngại về truyền, khả năng thay đổi và hiệu suất. Xem bài đăng này để biết thêm chi tiết: https://docs.microsoft.com/en-us/archive/blogs/abhinaba/c-structs-and-interface

Nói chung, cấu trúc nên được sử dụng cho các đối tượng có ngữ nghĩa kiểu giá trị. Bằng cách triển khai một giao diện trên một cấu trúc, bạn có thể gặp phải những lo lắng về quyền anh khi cấu trúc được truyền qua lại giữa cấu trúc và giao diện. Do kết quả của quyền anh, các hoạt động thay đổi trạng thái bên trong của cấu trúc có thể không hoạt động đúng.


3
"Do kết quả của quyền anh, các thao tác thay đổi trạng thái bên trong của cấu trúc có thể không hoạt động đúng." Đưa ra một ví dụ và nhận được câu trả lời.

2
@Will: Không chắc bạn đang đề cập đến điều gì trong nhận xét của mình. Bài đăng trên blog mà tôi đã tham khảo có một ví dụ cho thấy nơi mà việc gọi một phương thức giao diện trên cấu trúc không thực sự thay đổi giá trị nội bộ.
Scott Dorman

12
@ScottDorman: Trong một số trường hợp, việc có các cấu trúc triển khai giao diện có thể giúp tránh quyền anh. Các ví dụ chính là IComparable<T>IEquatable<T>. Việc lưu trữ một cấu trúc Footrong một biến kiểu IComparable<Foo>sẽ yêu cầu quyền chọn, nhưng nếu một kiểu chung Tbị hạn chế thì IComparable<T>người ta có thể so sánh nó với kiểu khác Tmà không cần phải đóng hộp một trong hai kiểu và không cần biết bất cứ điều gì Tkhác ngoài việc nó thực hiện ràng buộc. Hành vi có lợi như vậy chỉ có thể thực hiện được nhờ khả năng triển khai các giao diện của cấu trúc. Điều đó đã được nói ...
supercat

3
... nó có thể là tốt nếu có một phương tiện tuyên bố rằng một giao diện cụ thể chỉ nên được coi là áp dụng cho các cấu trúc không được đóng hộp, vì có một số ngữ cảnh mà đối tượng lớp hoặc cấu trúc được đóng hộp sẽ không thể có được mong muốn hành vi cư xử.
supercat

2
"struct nên được sử dụng cho các đối tượng có ngữ nghĩa kiểu giá trị. ... các hoạt động thay đổi trạng thái bên trong của struct có thể không hoạt động đúng." Không phải vấn đề thực sự ở đó là ngữ nghĩa kiểu giá trị và khả năng biến đổi không kết hợp tốt với nhau sao?
jpmc 26

184

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);
    }
}
  1. 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.
  2. 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>( Tchú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)
    ;//no-op

Đ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 IDisposablethì 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.


1
Trang web egheadcafe.com đã chuyển đi, nhưng không làm tốt việc giữ lại nội dung của nó. Tôi đã thử, nhưng không thể tìm thấy tài liệu gốc của eggheadcafe.com/software/aspnet/31702392/… , thiếu kiến ​​thức về OP. (PS +1 cho một bản tóm tắt xuất sắc).
Abel

2
Đây là một câu trả lời tuyệt vời, nhưng tôi nghĩ bạn có thể cải thiện nó bằng cách chuyển "Tóm tắt" lên đầu dưới dạng "TL; DR". Việc đưa ra phần kết luận trước giúp người đọc biết bạn đang đi đâu với mọi thứ.
Hans

Cần có cảnh báo trình biên dịch khi truyền một structđến một interface.
Jalal,

8

Trong một số trường hợp, cấu trúc có thể tốt để triển khai một giao diện (nếu nó không bao giờ hữu ích, thì chắc chắn rằng những người tạo ra .net sẽ cung cấp cho nó). Nếu một struct triển khai giao diện chỉ đọc IEquatable<T>, chẳng hạn như việc lưu trữ cấu trúc trong một vị trí lưu trữ (biến, tham số, phần tử mảng, v.v.) của kiểu IEquatable<T>sẽ yêu cầu nó được đóng hộp (mỗi kiểu struct thực sự xác định hai loại thứ: một bộ nhớ loại vị trí hoạt động như một loại giá trị và một loại đối tượng đống hoạt động như một loại lớp; loại đầu tiên hoàn toàn có thể chuyển đổi thành loại thứ hai - "quyền anh" - và loại thứ hai có thể được chuyển đổi thành loại đầu tiên thông qua truyền rõ ràng-- "mở hộp"). Tuy nhiên, có thể khai thác việc triển khai giao diện của một cấu trúc mà không có quyền sử dụng, sử dụng những gì được gọi là generics ràng buộc.

Ví dụ, nếu một phương thức có một phương thức CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>, một phương thức như vậy có thể gọi thing1.Compare(thing2)mà không cần phải chọn hộp thing1hoặc thing2. Nếu thing1xảy ra, ví dụ: an Int32, thời gian chạy sẽ biết rằng khi nó tạo mã cho CompareTwoThings<Int32>(Int32 thing1, Int32 thing2). Vì nó sẽ biết loại chính xác của cả thứ lưu trữ phương thức và thứ đang được truyền dưới dạng tham số, nên nó sẽ không phải chọn một trong hai.

Vấn đề lớn nhất với các cấu trúc triển khai giao diện là một cấu trúc được lưu trữ trong một vị trí của kiểu giao diện Object, hoặc ValueType(trái ngược với một vị trí của kiểu riêng của nó) sẽ hoạt động như một đối tượng lớp. Đối với các giao diện chỉ đọc, đây không phải là một vấn đề, nhưng đối với một giao diện đột biến như IEnumerator<T>nó có thể mang lại một số ngữ nghĩa lạ.

Ví dụ, hãy xem xét đoạn mã sau:

List<String> myList = [list containing a bunch of strings]
var enumerator1 = myList.GetEnumerator();  // Struct of type List<String>.IEnumerator
enumerator1.MoveNext(); // 1
var enumerator2 = enumerator1;
enumerator2.MoveNext(); // 2
IEnumerator<string> enumerator3 = enumerator2;
enumerator3.MoveNext(); // 3
IEnumerator<string> enumerator4 = enumerator3;
enumerator4.MoveNext(); // 4

Câu lệnh được đánh dấu # 1 sẽ là số nguyên tố enumerator1để đọc phần tử đầu tiên. Trạng thái của điều tra viên đó sẽ được sao chép sang enumerator2. Câu lệnh số 2 được đánh dấu sẽ chuyển bản sao đó để đọc phần tử thứ hai, nhưng sẽ không ảnh hưởng enumerator1. Trạng thái của điều tra viên thứ hai sau đó sẽ được sao chép sang enumerator3, trạng thái này sẽ được nâng cao bằng câu lệnh số 3 được đánh dấu. Sau đó, bởi vì enumerator3enumerator4đều là các kiểu tham chiếu, một REFERENCE tới enumerator3sau đó sẽ được sao chép sang enumerator4, do đó, câu lệnh được đánh dấu sẽ tăng hiệu quả cả hai enumerator3enumerator4.

Một số người cố gắng giả vờ rằng loại giá trị và loại tham chiếu là cả hai loại Object, nhưng điều đó không thực sự đúng. Các loại giá trị thực có thể chuyển đổi thành Object, nhưng không phải là trường hợp của nó. Một trường hợp List<String>.Enumeratorđược lưu trữ trong một vị trí của kiểu đó là kiểu giá trị và hoạt động như kiểu giá trị; sao chép nó vào một vị trí của kiểu IEnumerator<String>sẽ chuyển đổi nó thành một kiểu tham chiếu và nó sẽ hoạt động như một kiểu tham chiếu . Loại thứ hai là một loại Object, nhưng loại trước thì không.

BTW, một vài lưu ý nữa: (1) Nói chung, các loại lớp có thể thay đổi phải có các Equalsphương thức của chúng kiểm tra sự bình đẳng tham chiếu, nhưng không có cách nào phù hợp để một cấu trúc đóng hộp làm như vậy; (2) mặc dù tên của nó, ValueTypelà một kiểu lớp, không phải là một kiểu giá trị; tất cả các kiểu bắt nguồn từ System.Enumđều là kiểu giá trị, cũng như tất cả các kiểu bắt nguồn từ ValueTypengoại trừ System.Enum, nhưng cả hai ValueTypeSystem.Enumđều là kiểu lớp.


3

Các cấu trúc được thực hiện dưới dạng các kiểu giá trị và các lớp là kiểu tham chiếu. Nếu bạn có một biến kiểu Foo và bạn lưu trữ một phiên bản của Fubar trong đó, nó sẽ "đóng hộp" nó thành một kiểu tham chiếu, do đó đánh bại lợi thế của việc sử dụng cấu trúc ngay từ đầu.

Lý do duy nhất tôi thấy để sử dụng một cấu trúc thay vì một lớp là vì nó sẽ là một kiểu giá trị chứ không phải là một kiểu tham chiếu, nhưng cấu trúc không thể kế thừa từ một lớp. Nếu bạn có struct kế thừa một giao diện và bạn chuyển xung quanh các giao diện, bạn sẽ mất bản chất kiểu giá trị đó của struct. Cũng có thể chỉ cần biến nó thành một lớp nếu bạn cần giao diện.


Nó có hoạt động như thế này đối với các nguyên thủy triển khai các giao diện không?
áo dài

3

(Cũng không có gì chính để thêm nhưng chưa có năng lực chỉnh sửa vì vậy hãy tiếp tục ..)
Perfectly Safe. Không có gì bất hợp pháp với việc triển khai giao diện trên cấu trúc. Tuy nhiên, bạn nên đặt câu hỏi tại sao bạn muốn làm điều đó.

Tuy nhiên, có được một tham chiếu giao diện đến một cấu trúc sẽ HỘP nó. Vì vậy, hình phạt hiệu suất và như vậy.

Kịch bản hợp lệ duy nhất mà tôi có thể nghĩ đến ngay bây giờ được minh họa trong bài đăng của tôi ở đây . Khi bạn muốn sửa đổi trạng thái của một cấu trúc được lưu trữ trong một bộ sưu tập, bạn phải thực hiện việc đó thông qua một giao diện bổ sung được hiển thị trên cấu trúc.


Nếu một người chuyển Int32một phương thức chấp nhận một kiểu chung T:IComparable<Int32>(có thể là một tham số kiểu chung của phương thức hoặc lớp của phương thức), phương thức đó sẽ có thể sử dụng Comparephương thức trên đối tượng được truyền vào mà không cần chọn nó.
supercat


0

Không có hậu quả nào đối với một cấu trúc triển khai một giao diện. Ví dụ, cấu trúc hệ thống tích hợp sẵn thực hiện các giao diện như IComparableIFormattable.


0

Có rất ít lý do để một kiểu giá trị triển khai một giao diện. Vì bạn không thể phân lớp một kiểu giá trị, bạn luôn có thể coi nó là kiểu cụ thể của nó.

Tất nhiên, trừ khi bạn có nhiều cấu trúc, tất cả đều triển khai cùng một giao diện, khi đó nó có thể hữu ích một chút, nhưng tại thời điểm đó, tôi khuyên bạn nên sử dụng một lớp và làm đúng.

Tất nhiên, bằng cách triển khai một giao diện, bạn đang sử dụng cấu trúc, vì vậy nó bây giờ nằm ​​trên đống, và bạn sẽ không thể chuyển nó theo giá trị nữa ... Điều này thực sự củng cố ý kiến ​​của tôi rằng bạn chỉ nên sử dụng một lớp trong tình huống này.


Làm thế nào để bạn vượt qua IComp so sánh xung quanh thay vì triển khai cụ thể?
FlySwat

Bạn không cần phải chuyển IComparablexung quanh để đóng hộp giá trị. Bằng cách đơn giản gọi một phương thức mong đợi IComparablevới một kiểu giá trị thực thi nó, bạn sẽ hoàn toàn đóng hộp kiểu giá trị.
Andrew Hare

1
@AndrewHare: Các generic bị ràng buộc cho phép các phương thức trên IComparable<T>được gọi trên các cấu trúc thuộc loại Tkhông có quyền anh.
supercat

-10

Các cấu trúc cũng giống như các lớp sống trong ngăn xếp. Tôi không thấy lý do gì khiến họ phải "không an toàn".


Ngoại trừ họ thiếu tính kế thừa.
FlySwat

7
Tôi phải không đồng ý với mọi phần của câu trả lời này; chúng không nhất thiết phải tồn tại trên ngăn xếp, và ngữ nghĩa sao chép rất khác nhau đối với các lớp.
Marc Gravell

1
Chúng là bất biến, việc sử dụng quá nhiều struct sẽ khiến trí nhớ của bạn trở nên buồn bã :(
Teoman shipahi

1
@Teomanshipahi Việc sử dụng quá nhiều các cá thể lớp sẽ khiến người thu gom rác của bạn nổi điên.
IllidanS4 hỗ trợ Monica

4
Đối với một người có 20k + rep, câu trả lời này chỉ là không thể chấp nhận được.
Krythic
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.