何时使用泛型而不是继承更为合适?

20

使用泛型和继承的情况及其相关优点分别是什么,如何最好地组合它们?

谢谢大家的回答。

我将尽力陈述这个问题的动机:

我有一个如下所示的类:

class InformationReturn<T> where T : Info
{
    InformationReturn(Employee employee, List<T>) { ... }
}

现在假设我有一个仓库,它接受一个InformationReturn参数,并且必须根据Info对象T的类型将不同的字段存储在数据库中。是创建针对T类型的不同仓库更好,还是创建一个使用反射来确定类型的仓库,或者是使用泛型的继承能力的更好方法?

注意:其他客户端代码也必须根据T的类型进行不同的操作。

8个回答

32

在你希望对多种类型应用相同功能(Add、Remove、Count)并且它会以相同的方式实现时,应该使用泛型。继承是当你需要相同功能(GetResponse),但想要以不同的方式实现时使用。


7
我认为这实际上是这个问题的最佳答案!在 Stack Exchange 上搜索了约20分钟后... - Pingi

13

泛型和继承是两个不同的概念。继承是面向对象编程(OOP)的一个概念,而泛型是CLR的一个特性,允许您在类型暴露它们时在编译时指定类型参数。

继承和泛型实际上非常适合结合使用。

继承:

继承允许我创建一个类型:

class Pen { }

然后稍后创建另一种扩展Pen的类型:

class FountainPen : Pen { }

这很有帮助,因为我可以重复使用基类的所有状态和行为,并在 FountainPen 中公开任何新的行为或状态。继承允许我快速创建现有类型的更具体版本。

泛型:

泛型是 CLR 的一个功能,它可以让我创建这样一种类型:

class Foo<T> 
{
    public T Bar { get; set; }
}

现在当我使用Foo<T>时,我可以通过提供一种泛型类型参数来指定T的类型:
Foo<int> foo = new Foo<int>();

现在,既然我已经指定刚创建的对象的T类型为int,则声明为T类型的Foo.Bar也将是int类型。


7
何时应该使用它们? - C. Ross
1
有点龟毛:你提供了一个通用类型的参数,即T是类型参数,int是类型实参。我会让你自己决定是否值得进行编辑 :) - Jon Skeet
已修复!感谢指出,苛求细节从未是不受欢迎的 - 我自己也经常这样做 :) - Andrew Hare
感谢您的快速回复。我已经修改了问题,以更清楚地表明我的提问动机。 - Laz

11

使用泛型来指定算法或类型的行为,可以用一些“未知类型”来表达,同时保持 API 在该未知类型方面是强类型化的。未知类型称为 类型参数,在代码中如下所示:

public class List<T>
{
    public void Add(T item)
}

(等等) - 这里的T是类型参数。泛型方法类似:

public void Foo<T>(T item)

调用代码指定它想要处理的类型参数,例如:

List<string> list = new List<string>();
list.Add("hi");

使用继承来定制类型的行为。

我真的想不到有很多地方它们可以互相替代...


5
虽然我没有给你的回答点踩,但是你的回答有点无用。提问者显然对 .NET 是新手,你极为简短的字典定义可能很难消除他/她的困惑。 - BFree
谢谢您的回复,您说“用某些‘未知类型’表示”时,能否澄清一下您的意思?另外,我已经修改了问题,以使问题所在的上下文更加清晰。 - Laz

11
它们是完全不同的思想。泛型允许您以一般的方式声明常见的“特定”功能(冒得出格)。一个List<int>与一个List<string>没有任何不同,除了其中保存的数据类型。
虽然继承也可以实现相同的功能,但我可以创建一个List类,然后再创建一个IntList和一个StringList类来继承它。我可以轻松地使这两个类完全不同,或者让其中一个提供另一个类中没有的功能。
编辑
在查看问题的编辑后,答案有点是“这取决于。”你可以为双方提出论据 - 实际上,LINQ to SQL和Entity Framework都是反射检查与泛型强类型实体类和其他存储库类的结合体。您肯定可以采用您考虑的方法,只是要知道,通常用反射或其他方式解决的问题在使用“其他方式”解决时可能会更快。它取决于您在性能、可维护性、可读性和可靠性之间需要做出多少权衡。

2
这个答案帮助我指出了当行为没有差异和需要继承时应该使用泛型,谢谢。我已经修改了问题,以更清楚地表达我的动机。 - Laz

7
当你想要创建一个可以适用于许多未知类的“模板”时,请使用泛型。例如,持有???的集合是泛型的好选择。继承是为了当子类“是”该概念的扩展时使用的基本概念。
我的一般规则:
- 如果您在代码中开始将属性定义为Object并且需要频繁进行类型转换,则可能是时候使用泛型了。 - 如果您想针对“更高级”的概念以及派生类进行编程,则使用继承。 - 如果您的类将包装或处理具体类,则使用泛型。 - 如果您依赖于未知的“尚未定义”的类的特定内容,则最好使用泛型。 - 如果您的派生类“是一个”,那么通常是继承。 - 如果您的超类“使用某些x”,则使用泛型。

4

继承更多地涉及“是一个”(青蛙是一种动物),而泛型则是关于创建作用于类型数据的容器(T的列表,T的处理器等)。它们并不是互斥的

如果您愿意,可以这样做:

public class Base<T>
{

}

public class Derived : Base<Foo>
{

}

事实上,你可以拥有泛型接口: 公共接口IBase<T> -> 公共抽象类Base<T> : IBase<T> -> 公共类Derived : Base<Foo> - Michael Meadows

1

虽然有些晚了,但我对这个问题有更多的价值可添加。

泛型(Generics)用于在提供不同类型定义给客户端同时维护相同实现时使用。考虑以下示例:

const rabbitPopulation = new Population<Rabbit>();
const mousePopulation = new Population<Mouse>();

rabbitPopulation.add(rabbit) // ✅ will pass
mousePopulation.add(rabbit) // ❌ will not compile

无论是 rabbitPopulation 还是 mousePopulation,它们都有相同的方法实现,只是类型签名不同。当您以这种方式对代码进行建模时,您隐含地表示所有“种群”应该行为和行动类似。

同时,继承 用于扩展或完全重写实现。

class RabbitPopulation extends Population {
  increase() { /* increase rabbit population in a specific way */}
}
const rabbitPopulation = new RabbitPopulation();

class MousePopulation extends Population {
  increase() { /* increase mouse population in a specific way */}
}
const mousePopulation = new MousePopulation();

正如您所看到的,现在rabbitPopulationmousePopulation都可以拥有自己实现类似方法的能力。当我们想要捕捉两个对象之间的语义概念(即它们都是人口),同时允许它们在行为上有所不同(每个对象具有不同的方法)时,这非常有用。
您可以使用两者结合起来以非常丰富的方式表达您的建模,使基类成为通用类。请考虑以下内容:
class MousePopulation extends Population<Mouse> {/**/}

这样,您可以在所有“人口”的语义概念中提供类似的基本功能实现,同时以丰富类型的方式为人口的子类型(例如MousePopulation)提供特定的实现。但是大多数情况下,当您这样做时,您几乎总是希望扩展Population<T> ,并且最好将Population类设置为abstract类,以获得更多好处。

abstract class Population<T> {}
class MousePopulation extends Population<Mouse> {}

我喜欢这种设置,因为它允许将常见的语义思想捕获在基类中,同时允许子类扩展特定的功能。


0

在运行时,既不能改变继承,也不能改变参数化类型

继承允许您为操作提供默认实现,并允许子类对其进行覆盖。

参数化类型允许您更改类可以使用的类型。

但是,哪种方法最好取决于您的设计和实现约束

《设计模式 可复用面向对象软件的基础》


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接