Generics vs giao diện chung?


20

Tôi không nhớ khi tôi viết lớp chung lần trước. Mỗi lần tôi nghĩ tôi cần nó sau một vài suy nghĩ tôi lại đưa ra kết luận là không.

Câu trả lời thứ hai cho câu hỏi này khiến tôi phải yêu cầu làm rõ (vì tôi chưa thể bình luận, tôi đã đưa ra một câu hỏi mới).

Vì vậy, hãy lấy mã đã cho làm ví dụ về trường hợp người ta cần thuốc generic:

public class Repository<T> where T : class, IBusinessOBject
{
  T Get(int id)
  void Save(T obj);
  void Delete(T obj);
}

Nó có các ràng buộc kiểu: IBusinessObject

Cách suy nghĩ thông thường của tôi là: lớp bị hạn chế sử dụng IBusinessObject, các lớp sử dụng cái này Repositorycũng vậy. Kho lưu trữ những IBusinessObjects này , rất có thể các khách hàng của điều này Repositorysẽ muốn nhận và sử dụng các đối tượng thông qua IBusinessObjectgiao diện. Vậy tại sao không chỉ để

public class Repository
{
  IBusinessOBject Get(int id)
  void Save(IBusinessOBject obj);
  void Delete(IBusinessOBject obj);
}

Tho, ví dụ không tốt, vì nó chỉ là một loại bộ sưu tập khác và bộ sưu tập chung là kinh điển. Trong trường hợp này, ràng buộc kiểu trông kỳ lạ quá.

Trong thực tế, ví dụ này class Repository<T> where T : class, IBusinessbBjecttrông khá giống class BusinessObjectRepositoryvới tôi. Đó là điều chung chung được thực hiện để sửa chữa.

Toàn bộ vấn đề là: các thuốc generic có tốt cho mọi thứ ngoại trừ các bộ sưu tập và các ràng buộc kiểu không biến thành chung chung như chuyên ngành, vì việc sử dụng ràng buộc loại này thay vì tham số loại chung trong lớp không?

Câu trả lời:


24

Trước tiên chúng ta hãy nói về đa hình tham số thuần túy và đi vào đa hình giới hạn sau.

Đa hình tham số

Đa hình tham số có nghĩa là gì? Vâng, điều đó có nghĩa là một kiểu, hay đúng hơn là hàm tạo được tham số hóa bởi một kiểu. Vì loại được truyền vào dưới dạng tham số, bạn không thể biết trước nó có thể là gì. Bạn không thể đưa ra bất kỳ giả định nào dựa trên nó. Bây giờ, nếu bạn không biết nó có thể là gì, thì công dụng của nó là gì? bạn có thể làm gì với nó?

Vâng, bạn có thể lưu trữ và lấy nó, ví dụ. Đó là trường hợp bạn đã đề cập: bộ sưu tập. Để lưu trữ một mục trong danh sách hoặc một mảng, tôi không cần biết gì về mục đó. Danh sách hoặc mảng có thể hoàn toàn không biết về loại.

Nhưng còn Maybeloại nào? Nếu bạn không quen thuộc với nó, Maybelà một loại có thể có giá trị và có thể không. Bạn sẽ sử dụng nó ở đâu? Chà, ví dụ, khi lấy một mục ra khỏi từ điển: thực tế là một mục có thể không có trong từ điển không phải là một tình huống đặc biệt, vì vậy bạn thực sự không nên ném ngoại lệ nếu mục đó không có. Thay vào đó, bạn trả về một thể hiện của một kiểu con Maybe<T>, trong đó có chính xác hai kiểu con: NoneSome<T>. int.Parselà một ứng cử viên khác của một cái gì đó thực sự nên trả lại Maybe<int>thay vì ném một ngoại lệ hoặc toàn bộ int.TryParse(out bla)điệu nhảy.

Bây giờ, bạn có thể lập luận rằng đó Maybelà một loại giống như một danh sách chỉ có thể có 0 hoặc một phần tử. Và do đó, sắp xếp một bộ sưu tập.

Vậy thì Task<T>sao? Đây là một loại hứa hẹn sẽ trả lại một giá trị tại một thời điểm nào đó trong tương lai, nhưng không nhất thiết phải có một giá trị ngay bây giờ.

Hay những gì về Func<T, …>? Làm thế nào bạn đại diện cho khái niệm của một chức năng từ loại này sang loại khác nếu bạn không thể trừu tượng hơn các loại?

Hoặc, nói chung hơn: coi việc trừu tượng hóa và tái sử dụng là hai hoạt động cơ bản của công nghệ phần mềm, tại sao bạn không muốn có thể trừu tượng hóa các loại?

Đa hình giới hạn

Vì vậy, bây giờ hãy nói về đa hình giới hạn. Đa hình giới hạn về cơ bản là ở đó đa hình tham số và đa hình phụ gặp nhau: thay vì một hàm tạo kiểu hoàn toàn không biết về tham số kiểu của nó, bạn có thể liên kết (hoặc ràng buộc) loại thành một kiểu con của một số loại được chỉ định.

Hãy quay trở lại bộ sưu tập. Hãy băm. Chúng tôi đã nói ở trên rằng một danh sách không cần biết gì về các yếu tố của nó. Chà, một hashtable nào: nó cần biết rằng nó có thể băm chúng. (Lưu ý: trong C #, tất cả các đối tượng đều có thể băm, giống như tất cả các đối tượng có thể được so sánh bằng nhau. Tuy nhiên, điều đó không đúng với tất cả các ngôn ngữ và đôi khi được coi là lỗi thiết kế ngay cả trong C #.)

Vì vậy, bạn muốn giới hạn tham số loại của mình cho loại khóa trong hashtable là một thể hiện của IHashable:

class HashTable<K, V> where K : IHashable
{
  Maybe<V> Get(K key);
  bool Add(K key, V value);
}

Hãy tưởng tượng nếu thay vào đó bạn có điều này:

class HashTable
{
    object Get(IHashable key);
    bool Add(IHashable key, object value);
}

Bạn sẽ làm gì với một valuebạn ra khỏi đó? Bạn không thể làm bất cứ điều gì với nó, bạn chỉ biết đó là một đối tượng. Và nếu bạn lặp đi lặp lại, tất cả những gì bạn nhận được là một cặp thứ bạn biết là một thứ IHashable(nó không giúp bạn nhiều vì nó chỉ có một tài sản Hash) và thứ bạn biết là object(giúp bạn thậm chí ít hơn).

Hoặc một cái gì đó dựa trên ví dụ của bạn:

class Repository<T> where T : ISerializable
{
    T Get(int id);
    void Save(T obj);
    void Delete(T obj);
}

Mục này cần được tuần tự hóa vì nó sẽ được lưu trữ trên đĩa. Nhưng nếu bạn có cái này thay thế:

class Repository
{
    ISerializable Get(int id);
    void Save(ISerializable obj);
    void Delete(ISerializable obj);
}

Với trường hợp chung chung, nếu bạn đặt một BankAccounttrong, bạn sẽ có được một BankAccounttrở lại, với các phương pháp và các thuộc tính thích Owner, AccountNumber, Balance, Deposit, Withdraw,, vv Một cái gì đó bạn có thể làm việc với. Bây giờ, trường hợp khác? Bạn đặt vào BankAccountnhưng bạn nhận lại a Serializable, chỉ có một thuộc tính : AsString. Bạn sẽ làm gì với điều đó?

Ngoài ra còn có một số thủ thuật gọn gàng bạn có thể làm với đa hình bị ràng buộc:

Đa hình giới hạn F

Định lượng giới hạn F về cơ bản là ở đó biến loại xuất hiện lại trong ràng buộc. Điều này có thể hữu ích trong một số trường hợp. Ví dụ, làm thế nào để bạn viết một ICloneablegiao diện? Làm thế nào để bạn viết một phương thức trong đó kiểu trả về là kiểu của lớp triển khai? Trong một ngôn ngữ có tính năng MyType , thật dễ dàng:

interface ICloneable
{
    public this Clone(); // syntax I invented for a MyType feature
}

Trong một ngôn ngữ có tính đa hình giới hạn, bạn có thể làm một cái gì đó như thế này:

interface ICloneable<T> where T : ICloneable<T>
{
    public T Clone();
}

class Foo : ICloneable<Foo>
{
    public Foo Clone()
    {
        // …
    }
}

Lưu ý rằng điều này không an toàn như phiên bản MyType, vì không có gì ngăn ai đó chỉ đơn giản chuyển lớp "sai" sang hàm tạo kiểu:

class EvilBar : ICloneable<SomethingTotallyUnrelatedToBar>
{
    public SomethingTotallyUnrelatedToBar Clone()
    {
        // …
    }
}

Thành viên loại trừu tượng

Hóa ra, nếu bạn có các thành viên loại trừu tượng và phân nhóm, bạn thực sự có thể có được hoàn toàn mà không cần đa hình tham số và vẫn làm tất cả những điều tương tự. Scala đang đi theo hướng này, là ngôn ngữ chính đầu tiên bắt đầu với khái quát và sau đó cố gắng loại bỏ chúng, đó chính xác là cách khác xung quanh từ ví dụ Java và C #.

Về cơ bản, trong Scala, giống như bạn có thể có các trường và thuộc tính và phương thức làm thành viên, bạn cũng có thể có các loại. Và giống như các trường và các thuộc tính và phương thức có thể được để lại trừu tượng để được triển khai trong một lớp con sau này, các thành viên kiểu cũng có thể được để lại trừu tượng. Chúng ta hãy quay trở lại các bộ sưu tập, một cách đơn giản List, sẽ trông giống như thế này, nếu nó được hỗ trợ trong C #:

class List
{
    T; // syntax I invented for an abstract type member
    T Get(int index) { /* … */ }
    void Add(T obj) { /* … */ }
}

class IntList : List
{
    T = int;
}
// this is equivalent to saying `List<int>` with generics

tôi hiểu rằng sự trừu tượng về các loại là hữu ích. tôi chỉ không thấy công dụng của nó trong "đời thực". Func <> và Nhiệm vụ <> và Hành động <> là các loại thư viện. và cảm ơn tôi interface IFoo<T> where T : IFoo<T>cũng nhớ về . Đó rõ ràng là ứng dụng thực tế. ví dụ rất tuyệt nhưng vì một số lý do tôi không cảm thấy hài lòng tôi muốn có được tâm trí của tôi khi nó phù hợp và khi nó không. câu trả lời ở đây có một số đóng góp cho quá trình này, nhưng tôi vẫn cảm thấy bản thân không hài lòng với tất cả những điều này. Thật lạ vì các vấn đề ở cấp độ ngôn ngữ không làm phiền tôi quá lâu.
Jungle_mole

Ví dụ tuyệt vời. Tôi cảm thấy như mình đã trở lại phòng học. +1
Chef_Code

1
@Chef_Code: Tôi hy vọng đó là một lời khen :-P
Jörg W Mittag

Vâng, nó là. Sau đó tôi đã nghĩ về cách nó có thể được cảm nhận sau khi tôi đã nhận xét. Vì vậy, để khẳng định sự chân thành ... Vâng, đó là một lời khen không có gì khác.
Chef_Code

14

Toàn bộ vấn đề là: các thuốc generic có tốt cho mọi thứ ngoại trừ các bộ sưu tập và các ràng buộc kiểu không biến thành chung chung như chuyên ngành, vì việc sử dụng ràng buộc loại này thay vì tham số loại chung trong lớp không?

Số Bạn đang suy nghĩ quá nhiều về Repository, nơi mà nó khá nhiều giống nhau. Nhưng đó không phải là những gì thuốc generic có. Họ ở đó cho người dùng .

Điểm mấu chốt ở đây không phải là bản thân Kho lưu trữ chung chung hơn. Đó là người dùng chuyên biệt hơn - nghĩa là, Repository<BusinessObject1>Repository<BusinessObject2>các loại khác nhau, và hơn nữa, nếu tôi lấy Repository<BusinessObject1>, tôi biết rằng tôi sẽ lấy BusinessObject1lại được Get.

Bạn không thể cung cấp kiểu gõ mạnh mẽ này từ thừa kế đơn giản. Lớp Kho lưu trữ được đề xuất của bạn không làm gì để ngăn chặn mọi người nhầm lẫn các kho lưu trữ cho các loại đối tượng kinh doanh khác nhau hoặc đảm bảo loại đối tượng kinh doanh chính xác xuất hiện trở lại.


Cảm ơn, điều đó có ý nghĩa. Nhưng liệu toàn bộ việc sử dụng tính năng ngôn ngữ được đánh giá cao này có đơn giản như giúp người dùng đã tắt IntelliSense không? (Tôi hơi phóng đại một chút, nhưng tôi chắc chắn bạn sẽ hiểu được điểm này)
jung_mole

@zloidooraque: Ngoài ra IntelliSense không biết loại Đối tượng nào được lưu trữ trong kho lưu trữ. Nhưng vâng, bạn có thể làm bất cứ điều gì mà không cần thuốc generic nếu bạn sẵn sàng sử dụng phôi thay thế.
gex sát thương

@gexinf đó là điểm: Tôi không thấy nơi tôi cần sử dụng phôi nếu tôi sử dụng giao diện chung. Tôi chưa bao giờ nói "sử dụng Object". Ngoài ra tôi hiểu tại sao sử dụng thuốc generic khi viết bộ sưu tập (nguyên tắc DRY). Có lẽ, câu hỏi ban đầu của tôi đáng lẽ phải là một cái gì đó về việc sử dụng thuốc generic ngoài bối cảnh bộ sưu tập ..
jung_mole

@zloidooraque: Nó không liên quan gì đến môi trường. Intellisense không thể cho bạn biết nếu an IBusinessObjectlà a BusinessObject1hay a BusinessObject2. Nó không thể giải quyết tình trạng quá tải dựa trên loại dẫn xuất mà nó không biết. Nó không thể từ chối mã đi sai loại. Có một triệu bit gõ mạnh hơn mà Intellisense hoàn toàn không thể làm được gì. Hỗ trợ công cụ tốt hơn là một lợi ích tốt đẹp nhưng thực sự không có gì để làm với các lý do cốt lõi.
DeadMG

@DeadMG và đó là quan điểm của tôi: intellisense không thể làm điều đó: sử dụng chung chung, vì vậy nó có thể? có vấn đề gì không Khi bạn nhận được đối tượng bởi giao diện của nó, tại sao lại downcast nó? Nếu bạn làm nó, đó là thiết kế xấu, phải không? và tại sao và "giải quyết quá tải" là gì? Người dùng không được quyết định, cho dù phương thức gọi hay không dựa trên loại dẫn xuất nếu anh ta ủy quyền cho phương thức gọi đúng cho hệ thống (đó là đa hình). và điều này một lần nữa dẫn tôi đến một câu hỏi: thuốc generic có hữu ích bên ngoài container không? Tôi không tranh luận với bạn, tôi thực sự cần phải hiểu điều đó.
Jungle_mole

12

"Nhiều khả năng khách hàng của Kho lưu trữ này sẽ muốn nhận và sử dụng các đối tượng thông qua giao diện IBusinessObject".

Không, họ sẽ không.

Hãy xem xét rằng IBusinessObject có định nghĩa sau:

public interface IBusinessObject
{
  public int Id { get; }
}

Nó chỉ định nghĩa Id vì đây là chức năng được chia sẻ duy nhất giữa tất cả các đối tượng kinh doanh. Và bạn có hai đối tượng kinh doanh thực tế: Người và Địa chỉ (vì Người không có đường và Địa chỉ không có tên, bạn không thể đặt cả hai giao diện chung với giao diện của cả hai. Đó là một thiết kế khủng khiếp, vi phạm Nguyên tắc phân chia giao diện , "tôi" trong RẮN )

public class Person : IBusinessObject
{
  public int Id { get; private set; }
  public string Name { get; private set; }
}

public class Address : IBusinessObject
{
  public int Id { get; private set; }
  public string City { get; private set; }
  public string StreetName { get; private set; }
  public int Number { get; private set; }
}

Bây giờ, điều gì xảy ra khi bạn sử dụng phiên bản chung của kho lưu trữ:

public class Repository<T> where T : class, IBusinessObject
{
  T Get(int id)
  void Save(T obj);
  void Delete(T obj);
}

Khi bạn gọi phương thức Get trên kho lưu trữ chung, đối tượng được trả về sẽ được gõ mạnh, cho phép bạn truy cập tất cả các thành viên của lớp.

Person p = new Repository<Person>().Get(1);
int id = p.Id;
string name = p.Name;

Address a = new Repository<Address>().Get(1);
int id = a.Id;
string cityName = a.City;
int houseNumber = a.Number;

Mặt khác, khi bạn sử dụng Kho lưu trữ không chung chung:

public class Repository
{
  IBusinessOBject Get(int id)
  void Save(IBusinessOBject obj);
  void Delete(IBusinessOBject obj);
}

Bạn sẽ chỉ có thể truy cập các thành viên từ giao diện IBusinessObject:

IBussinesObject p = new Repository().Get(1);
int id = p.Id; //OK
string name = p.Name; //Oooops, you dont have "Name" defined on the IBussinesObject interface.

Vì vậy, mã trước đó sẽ không được biên dịch vì các dòng sau:

string name = p.Name;
string cityName = a.City;
int houseNumber = a.Number;

Chắc chắn, bạn có thể truyền IBussinesObject cho lớp thực tế, nhưng bạn sẽ mất tất cả phép thuật thời gian biên dịch mà thuốc generic cho phép (dẫn đến UnlimitedCastExceptions xuống đường), sẽ bị bỏ qua một cách không cần thiết ... Và ngay cả khi bạn không quan tâm đến việc biên dịch thời gian kiểm tra cả hiệu suất (bạn nên), việc chọn sau chắc chắn sẽ không mang lại cho bạn bất kỳ lợi ích nào khi sử dụng thuốc generic ở vị trí đầu tiên.

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.