泛型与通用接口?


20

我不记得上次写泛型类的时间。每次经过思考后我都认为不需要时,我认为不需要它。

这个问题的第二个答案让我要求澄清(因为我还不能发表评论,所以我提出了一个新问题)。

因此,让我们以给定的代码为例,说明需要泛型的情况:

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

它具有类型约束: IBusinessObject

我通常的想法是:该类只能使用IBusinessObject,使用该类的也是如此Repository。存储库存储这些对象IBusinessObject,最有可能的客户端Repository将希望通过IBusinessObject接口获取和使用对象。那为什么不只是为了

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

那个例子不好,因为它只是另一种类型的集合,而通用集合是经典的。在这种情况下,类型约束看起来也很奇怪。

实际上,该示例class Repository<T> where T : class, IBusinessbBjectclass BusinessObjectRepository我非常相似。泛型是要修复的东西。

重点是:泛型除了集合之外,是否对其他任何东西都有用,并且类型约束不会使泛型成为专用对象,就像使用类约束而不是在类内部使用泛型类型参数一样?

Answers:


24

让我们首先讨论纯参数多态性,然后再讨论有界多态性。

参数多态

参数多态性是什么意思?好吧,这意味着类型或类型构造函数由类型进行参数化。由于类型是作为参数传递的,因此您无法事先知道它可能是什么。您不能基于此做出任何假设。现在,如果您不知道它可能是什么,那么有什么用?你能做什么呢?

好吧,例如,您可以存储和检索它。您已经提到过这种情况:集合。为了将一个项目存储在列表或数组中,我对该项目一无所知。列表或数组可以完全忽略该类型。

但是Maybe类型呢?如果您不熟悉它,那Maybe是一个可能有值但可能没有值的类型。您将在哪里使用它?好吧,例如,当从字典中取出一个项目时:一个项目可能不在字典中的事实并不是一种例外情况,因此,如果该项目不存在,您就不应该抛出异常。相反,您返回的子类型的实例,该实例Maybe<T>恰好具有两个子类型:NoneSome<T>int.Parse是真正应该返回a Maybe<int>而不是抛出异常或整个int.TryParse(out bla)舞步的东西的另一个候选者。

现在,您可能会认为这Maybe有点像列表,只能包含零个或一个元素。因此有点收藏。

那又如何Task<T>呢?它是一种有望在将来某个时刻返回值的类型,但现在不一定具有值。

还是呢Func<T, …>?如果不能抽象类型,您将如何表示从一种类型到另一种类型的功能概念?

或更笼统地说:考虑到抽象和重用是软件工程的两个基本操作,为什么希望能够对类型进行抽象呢?

有界多态性

因此,现在让我们谈谈有界多态性。有界多态性基本上是参数多态性和子类型多态性相遇的地方:您可以类型绑定(或约束)为某个指定类型的子类型,而不是完全不理会其类型参数的类型构造函数。

让我们回到集合。拿一个哈希表。上面我们说过,列表不需要了解其元素。哈希表确实可以做到:它需要知道可以对它们进行哈希处理。(注意:在C#中,所有对象都是可哈希的,就像可以比较所有对象是否相等一样。不过,并非所有语言都适用,有时甚至在C#中也被视为设计错误。)

因此,您想将散列表中的键类型的类型参数约束为IHashable

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

想象一下,如果您有:

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

你会用什么做value你的存在吗?您不能对它做任何事情,只能知道它是一个对象。而且,如果您对其进行迭代,那么得到的就是一对您知道的东西IHashable(对它没有多大帮助,因为它只有一个属性Hash),而您知道的东西对object(这对您甚至没有多大帮助)。

或基于您的示例的内容:

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

该项目需要可序列化,因为它将存储在磁盘上。但是如果您有这个呢?

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

与一般情况下,如果你把BankAccount在,你会得到一个BankAccount回来了,方法和属性一样OwnerAccountNumberBalanceDepositWithdraw,等你的东西可以工作。现在,另一种情况?您输入,BankAccount但又返回Serializable,其中只有一个属性:AsString。你打算怎么办?

您还可以使用有限多态来完成一些巧妙的技巧:

F界多态性

F界量化基本上是类型变量再次出现在约束中的位置。在某些情况下这可能很有用。例如,您如何编写ICloneable接口?您如何编写方法,其中返回类型是实现类的类型?在具有MyType功能的语言中,这很容易:

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

在具有有限多态性的语言中,您可以改为执行以下操作:

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

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

请注意,这不如MyType版本安全​​,因为没有什么可以阻止某人简单地将“错误”类传递给类型构造函数的:

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

抽象类型成员

事实证明,如果您具有抽象类型成员和子类型,则实际上可以完全不用参数多态性,而仍然可以做所有相同的事情。Scala是在这个方向迈进,成为第一大语言,开始了仿制药,然后试图删除它们,而这正是从如Java和C#的其他方式。

基本上,在Scala中,就像可以将字段,属性和方法作为成员一样,也可以具有类型。就像字段,属性和方法可以被抽象为稍后在子类中实现一样,类型成员也可以被抽象为抽象。让我们回到一个简单的集合,List如果在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

我了解,对类型的抽象很有用。我只是看不到它在“现实生活”中的用途。Func <>和Task <>和Action <>是库类型。谢谢,我interface IFoo<T> where T : IFoo<T>也记得。那显然是现实生活中的应用。这个例子很棒。但是由于某种原因,我感到不满意。我宁愿在合适的时候动脑子,什么时候不合适。答案对这个过程有一定的帮助,但是我仍然觉得自己在这方面不适应。这很奇怪,因为语言级别的问题已经困扰我这么久了。
jungle_mole

很好的例子。我感觉就像回到了教室。+1
Chef_Code

1
@Chef_Code:我希望这是一种恭维:-P
约尔格W¯¯米塔格

是的。后来,我想了想在发表评论后如何看待它。因此,要确认诚意...是的,别无他意。
Chef_Code

14

重点是:泛型除了集合之外,是否对其他任何东西都有用,并且类型约束不会使泛型成为专用对象,就像使用类约束而不是在类内部使用泛型类型参数一样?

号你在想太多Repository,它几乎相同的。但这不是泛型的用途。它们在那里供用户使用

这里的关键不是存储库本身是更通用的。这使用户更specialized-即,Repository<BusinessObject1>Repository<BusinessObject2>不同类型的,而且,如果我需要Repository<BusinessObject1>,我知道我会得到BusinessObject1从退了出来Get

您不能通过简单的继承提供这种强类型。提议的存储库类无济于事,不会阻止人们将不同类型的业务对象的存储库弄混,也不保证人们可以得到正确类型的业务对象。


谢谢,这很有意义。但是,使用这种高度赞扬的语言功能的全部意义是否就像帮助关闭IntelliSense的用户一样简单?(我有点夸张,但我确定你明白了这一点)
jungle_mole 2015年

@zloidooraque:而且IntelliSense也不知道存储库中存储了哪种对象。但是,是的,如果您愿意使用强制类型转换,则可以在没有泛型的情况下执行任何操作。
2015年

重点是@gexicide:如果使用通用接口,我看不到需要使用强制转换的地方。我从来没有说过“使用Object”。我也理解为什么在编写集合时使用泛型(DRY原理)。也许,我最初的问题应该是一些有关使用收藏范围之外的仿制药..
jungle_mole

@zloidooraque:与环境无关。Intellisense无法告诉您a IBusinessObject是a BusinessObject1还是a BusinessObject2。它无法根据未知的派生类型解析重载。它不能拒绝传递错误类型的代码。有上百万种更强的键入方式,Intellisense绝对不能做。更好的工具支持是一个不错的好处,但实际上与核心原因无关。
DeadMG

@DeadMG,这就是我的观点:intellisense无法做到:使用泛型,所以可以吗?有关系吗?当通过接口获取对象时,为什么要向下转换?如果这样做的话,这是不好的设计,不是吗?为什么以及什么是“解决超载”?如果将权限方法的调用委托给系统(即多态性),则用户不能决定调用方法是否基于派生类型。这又使我想到一个问题:泛型在容器之外有用吗?我不是在和你吵架,我真的需要明白这一点。
jungle_mole 2015年

12

“此存储库中最有可能的客户端将希望通过IBusinessObject接口获取和使用对象”。

不,他们不会。

让我们考虑一下IBusinessObject具有以下定义:

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

它只是定义Id,因为它是所有业务对象之间唯一的共享功能。而且,您有两个实际的业务对象:人和地址(由于人没有街道,而地址没有名称,因此您不能将它们都限制为具有两者功能的通用接口。这将是一个糟糕的设计,违反了接口Segragation原则,“我”在SOLID

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; }
}

现在,使用存储库的通用版本会发生什么:

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

当您在通用存储库上调用Get方法时,将强类型化返回的对象,从而允许您访问所有类成员。

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;

另一方面,当您使用非通用存储库时:

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

您将只能从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.

因此,由于以下几行,先前的代码将无法编译:

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

当然,您可以将IBussinesObject强制转换为实际的类,但是您将失去泛型所允许的所有编译时魔术(导致InvalidCastExceptions失败),不必要地遭受强制转换开销……即使您没有关心编译时检查性能(您不应该),之后的转换绝对不会给您比起最初使用泛型带来的任何好处。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.