it-swarm-vi.tech

Có an toàn cho các cấu trúc để thực hiện giao diện?

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

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

Có một vài điều đang diễn ra trong câu hỏi này ...

Có thể cho một cấu trúc để thực hiện một giao diện, nhưng có những lo ngại liên quan đến việc truyền, khả năng biến đổi và hiệu suất. Xem bài đăng này để biết thêm chi tiết: http://bloss.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

Nói chung, các cấu trúc nên được sử dụng cho các đối tượng có ngữ nghĩa loại 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 các mối quan tâm 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.

45
Scott Dorman

Vì không ai khác cung cấp rõ ràng câu trả lời này, tôi sẽ thêm vào như sau:

Việc triển khai giao diện trên cấu trúc không có hậu quả tiêu cực nào.

Bất kỳ biến của loại giao diện được sử dụng để giữ cấu trúc sẽ dẫn đến giá trị được đó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ì đây là vấn đề tồi tệ nhất trừ khi bạn là:

  • sử dụng đối tượng kết quả cho mục đích khóa (một ý tưởng cực kỳ tồi tệ theo bất kỳ cách nào)
  • sử dụng ngữ nghĩa đẳng thức tham chiếu và hy vọng nó hoạt động cho hai giá trị được đóng hộp từ cùng một cấu trúc.

Cả hai điều này đều khó xảy ra, thay vào đó bạn có khả năng sẽ thực hiện một trong những điều sau đây:

Generics

Có lẽ nhiều lý do hợp lý cho các cấu trúc triển khai giao diện là để chúng có thể được sử dụng trong bối cảnh chung với ràng buộc. Khi được sử dụng trong thời trang 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 như một tham số loại [.__.]
    • miễn là không có ràng buộc nào khác như 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 của 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 yêu cầu của các phương thức cá thể được xác định trong các thể hiện của tham số Type T, nó có thể sử dụng ràng buộc opcode:

Nếu thisType là một loại giá trị và thisType thực hiện phương thức này thì ptr được truyền không được sửa đổi thành con trỏ 'this' cho một lệnh phương thức gọi, để thực hiện phương thức bởi thisType.

Điều này tránh quyền anh và vì loại 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, việc gọi Equals() được thực hiện mà không có hộp nào trên this.a1.

API ma sát thấp

Hầu hết các cấu trúc nên có ngữ nghĩa giống như nguyên thủy trong đó các giá trị giống hệt nhau được coi là bằng nhau2. Thời gian chạy sẽ cung cấp hành vi như vậy trong Equals() ẩn nhưng điều này có thể chậm. Ngoài ra, đẳng thức ngầm này là không được hiển thị dưới dạng triển khai IEquatable<T> Và do đó ngăn chặn các cấu trúc được sử dụng dễ dàng làm khóa cho Từ điển trừ khi chúng tự thực hiện một cách rõ ràng. Do đó, thông thường nhiều loại cấu trúc công khai tuyên bố rằng chúng triển khai IEquatable<T> (Trong đó T là chính chúng) để 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 giá trị hiện có các loại trong CLR BCL.

Tất cả các nguyên thủy trong BCL thực hiện ở mức 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 loại giá trị do Hệ thống xác định như DateTime, TimeSpan và Guid cũng thực hiện nhiều hoặc tất cả các loại này. Nếu bạn đang triển khai một loại '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 phổ biến này (chính xác) sẽ giúp cấu trúc của bạn hữu dụng hơn và có thể sử dụng được.

Loại trừ

Rõ ràng nếu giao diện ngụ ý mạnh mẽ khả năng biến đổi (chẳng hạn như ICollection) thì việc triển khai nó là một ý tưởng tồi vì nó có nghĩa là bạn sẽ biến cấu trúc thành biến đổi (dẫn đến các loại các lỗi đã được mô tả trong đó các sửa đổi xảy ra trên giá trị được đóng hộp thay vì ban đầu) hoặc bạn gây nhầm lẫn cho người dùng bằng cách bỏ qua các hàm ý của các phương thức như Add() hoặc ném ngoại lệ.

Nhiều giao diện KHÔNG ngụ ý khả năng biến đổi (chẳng hạn như IFormattable) và đóng vai trò là cách thức thành ngữ để thể hiện chức năng nhất định theo cách nhất quán. Thông thường người sử dụng cấu trúc sẽ không quan tâm đến bất kỳ quyền anh nào cho hành vi đó.

Tóm lược

Khi được thực hiện hợp lý, trên các loại giá trị bất biến, việc thực hiện các giao diện hữu ích là một ý tưởng tốt


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 là đã biết là một kiểu cấu trúc cụ thể nhưng trong đó bắt buộc phải gọi một phương thức ảo. Ví dụ:

List<int> l = new List<int>();
foreach(var x in l)
    ;//no-op

Trình liệt kê được Danh sách trả về là một cấu trúc, tối ưu hóa để tránh phân bổ khi liệt kê danh sách (Với một số điều thú vị hậu quả ). Tuy nhiên, ngữ nghĩa của foreach xác định rằng nếu điều tra viên thực hiện IDisposable thì Dispose() sẽ được gọi sau khi hoàn thành việc lặp. Rõ ràng việc điều này xảy ra thông qua một cuộc gọi đóng hộp sẽ loại bỏ bất kỳ lợi ích nào của điều tra viên là một cấu trúc (thực tế nó sẽ tồi tệ hơn). Tồi tệ hơn, nếu cuộc gọi xử lý thay đổ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 trong trường hợp được đó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 loại 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_C hiện [.__.] 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.s IL_0035 [.__.] IL_0026: ld .s 02 [.__.] IL_0028: bị ràng buộc. System.Collections.Generic.List.Enumerator [.__.] IL_002E: callvirt System.IDisftime.Dispose [.__.] IL_0033: nop [.__.] IL_0034: endfinally [.__.]

Do đó, việc triển khai IDis Dùng không gây ra bất kỳ vấn đề nào về hiệu suất 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 Vứt bỏ thực sự làm bất cứ điều gì!

2: double và float là các ngoại lệ cho quy tắc này trong đó các giá trị NaN không được coi là bằng nhau.

161
ShuggyCoUk

Trong một số trường hợp, một cấu trúc có thể tốt khi thực hiện một giao diện (nếu nó không bao giờ hữu ích, thì chắc chắn những người tạo ra .net sẽ cung cấp cho nó). Nếu một cấu trúc thực hiện giao diện chỉ đọc như IEquatable<T>, 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 loại IEquatable<T> Sẽ yêu cầu nó phải được đóng hộp ( mỗi loại cấu trúc thực sự xác định hai loại: một loại vị trí lưu trữ hoạt động như một loại giá trị và một loại đối tượng heap hoạt động như một loại lớp, loại thứ nhất hoàn toàn có thể chuyển đổi thành loại thứ hai - "quyền anh" - và thứ hai có thể được chuyển đổi thành thứ nhất thông qua cast rõ ràng - "unboxing"). Tuy nhiên, có thể khai thác cấu trúc của một giao diện mà không cần quyền anh, tuy nhiên, bằng cách sử dụng cái được gọi là tổng quát bị hạn chế.

Ví dụ: nếu một người có phương thức CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>, thì phương thức đó có thể gọi thing1.Compare(thing2) mà không cần phải đóng hộp thing1 Hoặc thing2. Nếu thing1 Xảy ra, ví dụ: 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ứ được truyền dưới dạng tham số, nên nó sẽ không phải đóng hộp một trong hai.

Vấn đề lớn nhất với các cấu trúc thực hiện giao diện là một cấu trúc được lưu trữ trong một vị trí của loại giao diện, Object hoặc ValueType (trái ngược với vị trí của loại chính nó) sẽ hoạt động như một đối tượng lớp. Đối với các giao diện chỉ đọc, điều này thường 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ạ.

Hãy xem xét, ví dụ, mã sau đây:

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 số 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 vào enumerator2. Câu lệnh được đánh dấu # 2 sẽ chuyển bản sao đó để đọc phần tử thứ hai, nhưng sẽ không ảnh hưởng đến enumerator1. Trạng thái của điều tra viên thứ hai sau đó sẽ được sao chép thành enumerator3, Sẽ được nâng cao bởi câu lệnh được đánh dấu # 3. Sau đó, vì enumerator3enumerator4 Đều là cả hai loại tham chiếu, nên một tham chiếu đến enumerator3 được sao chép vào enumerator4, do đó, câu lệnh được đánh dấu sẽ tiến lên một cách hiệu quả cả haienumerator3enumerator4.

Một số người cố gắng giả vờ rằng các loại giá trị và loại tham chiếu đều là 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 thể hiện của List<String>.Enumerator Được lưu trữ ở vị trí của loại đó là loại giá trị và hoạt động như một loại giá trị; sao chép nó vào vị trí của loại IEnumerator<String> sẽ chuyển đổi nó thành loại tham chiếu và nó sẽ hoạt động như một loại tham chiế. Cái sau là một loại Object, nhưng cá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 nên có phương thức kiểm tra tham chiếu Equals của chúng, nhưng không có cách nào hợp lý để cấu trúc hình hộp làm như vậy; (2) mặc dù tên của nó, ValueType là loại lớp, không phải loại giá trị; tất cả các loại có nguồn gốc từ System.Enum là các loại giá trị, cũng như tất cả các loại có nguồn gốc từ ValueType ngoại trừ System.Enum, nhưng cả ValueTypeSystem.Enum Là các loại lớp.

8
supercat

(Cũng không có gì quan trọng để thêm nhưng chưa có khả năng chỉnh sửa nên ở đây đi ..)
[.__.] Hoàn toàn an toàn. Không có gì bất hợp pháp với việc thực hiện các giao diện trên các 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 tham chiếu giao diện đến cấu trúc sẽ HỘP . Vì vậy, hiệu suất phạt và như vậy.

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

3
Gishu

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

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 loại giá trị và không phải là một loại 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ó cấu trúc kế thừa một giao diện và bạn chuyển qua các giao diện, bạn sẽ mất bản chất loại giá trị của cấu trúc đó. Cũng có thể chỉ làm cho nó một lớp nếu bạn cần giao diện.

3
dotnetengineer

Tôi nghĩ vấn đề là nó gây ra quyền anh vì cấu trúc là loại giá trị nên có một hình phạt hiệu suất nhẹ.

Liên kết này cho thấy có thể có các vấn đề khác với nó ...

http://bloss.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

1
Simon Keep

Không có hậu quả cho một cấu trúc thực hiện một giao diện. Ví dụ, các cấu trúc hệ thống tích hợp thực hiện các giao diện như IComparableIFormattable.

0
Joseph Daigle

Có rất ít lý do cho một loại giá trị để thực hiện một giao diện. Vì bạn không thể phân lớp một loại giá trị, bạn luôn có thể gọi nó là loại cụ thể.

Tất nhiên, trừ khi bạn có nhiều cấu trúc tất cả thực hiện cùng một giao diện, 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à thực hiện đúng.

Tất nhiên, bằng cách thực hiện một giao diện, bạn đang cấu trúc cấu trúc, vì vậy nó hiện đang nằm trên đống, và bạn sẽ không thể vượt qua nó bằng 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.

0
FlySwat