关于这个问题的最高的答案是关于Liskov替代原理的努力,竭力区分子类型和子类。这也说明有些语言将两者混为一谈,而另一些则没有。
对于我最熟悉的面向对象语言(Python,C ++),“类型”和“类”是同义词。对于C ++,在子类型和子类之间进行区分意味着什么?举例来说,这Foo
是的子类,但不是子类型FooBase
。如果foo
是的实例Foo
,则此行:
FooBase* fbPoint = &foo;
不再有效?
关于这个问题的最高的答案是关于Liskov替代原理的努力,竭力区分子类型和子类。这也说明有些语言将两者混为一谈,而另一些则没有。
对于我最熟悉的面向对象语言(Python,C ++),“类型”和“类”是同义词。对于C ++,在子类型和子类之间进行区分意味着什么?举例来说,这Foo
是的子类,但不是子类型FooBase
。如果foo
是的实例Foo
,则此行:
FooBase* fbPoint = &foo;
不再有效?
Answers:
子类型是类型多态性的一种形式,其中一个子类型是与另一数据类型(超类型)通过可替代性的一些概念数据类型,这意味着程序元素,典型地子例程或函数,写入到父类型的元素进行操作也可以对子类型的元素进行操作。
如果S
是的子类型T
,则通常会写上子类型关系S <: T
,以表示S
可以在T
期望使用类型项的上下文中安全地使用任何类型项。子类型化的精确语义至关重要地取决于给定编程语言中“在上下文中安全使用”的含义的特殊性。
子类化不应与子类型混淆。通常,子类型化建立is-a关系,而子类化仅重用实现并建立语法关系,而不一定是语义关系(继承不能确保行为子类型化)。
为了区分这些概念,子类型化也称为接口继承,而子类化则称为实现继承或代码继承。
public
继承会引入一个子类型,而private
继承会引入一个子类。
一个类型,在我们这里所说的背景下,实际上是一组行为的保证。一个合同,如果你愿意。或者,从协议 Small Small借用术语。
甲类是方法束。这是一组行为实现。
子类型化是完善协议的一种方法。子类化是差分代码重用的一种方法,即仅通过描述行为差异来重用代码。
如果您使用过Java或C♯,那么您可能会想到所有类型都应该是interface
类型的建议。实际上,如果您阅读了William Cook的《理解数据抽象,复习》,那么您可能知道,要使用这些语言进行OO,必须仅使用interface
s作为类型。(还有一个有趣的事实:Java interface
直接来自于Objective-C的协议,而后者又直接来自Smalltalk。)
现在,如果我们遵循编码建议其逻辑结论和想象的Java版本,其中仅 interface
s为类型,和类和原语都没有,那么一个interface
从另一个继承将创建一个子类型关系,而一个class
来自另一个意志继承仅用于通过进行差分代码重用super
。
据我所知,没有主流的静态类型语言严格区分继承代码(实现继承/子类化)和继承合同(子类型化)。在Java和C♯中,接口继承是纯子类型化(或者至少是直到Java 8中以及在可能的C until 8中引入默认方法为止),但是类继承也是子类型化和实现继承。我记得读过一篇关于实验性的静态类型的面向对象的LISP方言的文章,该方言严格区分了mixin(包含行为),structs(包含状态),interfaces(描述行为)和类(由一个或多个mixin组成零个或多个结构并符合一个或多个接口)。只能实例化类,并且只能将接口用作类型。
在动态类型的OO语言(例如Python,Ruby,ECMAScript或Smalltalk)中,我们通常将对象的类型视为对象所遵循的协议集。请注意复数形式:一个对象可以具有多种类型,而我不仅在谈论一个事实,即每个类型String
的对象也是一个type的对象Object
。(顺便说一句:请注意我是如何使用类名来谈论类型的?我有多么愚蠢!)一个对象可以实现多种协议。例如,在Ruby中,Arrays
可以附加,可以建立索引,可以对其进行迭代以及可以进行比较。他们实现了四种不同的协议!
现在,Ruby没有类型。但是Ruby 社区有类型!但是,它们仅存在于程序员的头脑中。并在文档中。例如,任何each
通过调用一个元素一个元素来响应方法的对象都被视为可枚举的对象。还有一个叫混入Enumerable
这取决于该协议。所以,如果你的对象具有正确的类型(只存在于程序员的头),则允许在(继承)的混合Enumerable
混入,并顺利拿到各种很酷的方法免费的,比如map
,reduce
,filter
等上。
同样,如果一个物体响应<=>
,那么它被认为是实现可比的协议,它可以在混合Comparable
混入,并得到这样的东西<
,<=
,>
,<=
,==
,between?
,和clamp
是免费的。但是,它本身也可以实现所有这些方法,而不是完全继承Comparable
,因此仍被认为是可比较的。
一个很好的例子是StringIO
库,该库实质上是使用字符串伪造 I / O流。它实现了与IO
该类相同的所有方法,但是两者之间没有继承关系。但是,StringIO
可以在可以使用的任何地方IO
使用a。这在单元测试中非常有用,在单元测试中,您可以替换文件或stdin
以替换文件,StringIO
而无需对程序进行任何进一步的更改。因为它们StringIO
遵循与相同的协议IO
,所以即使它们是不同的类,它们也属于同一类型,并且不共享任何关系(除了它们Object
在某个点上的琐碎扩展之外)。
首先区分类型和类,然后深入研究子类型化和子类化之间的区别也许是很有用的。
对于此答案的其余部分,我将假设讨论中的类型是静态类型(因为子类型通常在静态上下文中出现)。
我将开发一个玩具伪代码来帮助说明类型和类之间的区别,因为大多数语言至少将它们部分地融合在一起(出于充分的理由,我将简要介绍一下)。
让我们从一个类型开始。类型是代码中表达式的标签。可以通过外部程序(类型检查器)来确定此标签的值以及它与所有其他标签的值是否一致(对于某些类型的系统特定的一致定义),而无需运行您的程序。这就是使这些标签与众不同并应有自己名字的原因。
在我们的玩具语言中,我们可能允许像这样创建标签。
declare type Int
declare type String
然后,我们可以将各种值标记为这种类型。
0 is of type Int
1 is of type Int
-1 is of type Int
...
"" is of type String
"a" is of type String
"b" is of type String
...
有了这些语句,我们的类型检查器现在可以拒绝诸如
0 is of type String
如果我们的类型系统的要求之一是每个表达式都有一个唯一的类型。
现在让我们暂且不谈这是多么笨拙,以及如何分配无限数量的表达式类型会遇到问题。我们可以稍后再返回。
另一方面,类是组合在一起的方法和字段的集合(可能带有访问修饰符,例如private或public)。
class StringClass:
defMethod concatenate(otherString): ...
defField size: ...
此类的实例可以创建或使用这些方法和字段的预先定义。
我们可以选择将一个类与一个类型相关联,以使一个类的每个实例都自动用该类型标记。
associate StringClass with String
但是并不是每个类型都需要有一个关联的类。
# Hmm... Doesn't look like there's a class for Int
也可以想象,在我们的玩具语言中,并非每个类都有一个类型,特别是如果不是所有的表达式都具有类型。想象一下,如果某些表达式具有类型而有些则没有,那么类型系统一致性规则看起来会有些棘手(但并非不可能)。
而且,在我们的玩具语言中,这些关联不必是唯一的。我们可以将两个具有相同类型的类相关联。
associate MyCustomStringClass with String
现在请记住,我们的类型检查器不需要跟踪表达式的值(在大多数情况下,这样做不会或不可能这样做)。它所知道的就是您告诉过的标签。提醒一下,0 is of type String
由于我们的人为创建的类型规则,表达式必须具有唯一的类型,而我们已经将表达式标记为0
其他名称,因此类型检查器只能拒绝该语句。它对的价值没有任何特殊的了解0
。
那么子类型化呢?好的子类型是类型检查中一条通用规则的名称,它可以放宽您可能拥有的其他规则。也就是说,如果A is subtype of B
您的类型检查器到处都需要的标签B
,它也会接受A
。
例如,我们可能会对数字进行以下操作,而不是对以前的操作进行以下操作。
declare type NaturalNum
declare type Int
NaturalNum is subtype of Int
0 is of type NaturalNum
1 is of type NaturalNum
-1 is of type Int
...
子类化是声明一个新类的简写,该类使您可以重用以前声明的方法和字段。
class ExtendedStringClass is subclass of StringClass:
# We get concatenate and size for free!
def addQuestionMark: ...
我们不必副实例ExtendedStringClass
有String
像我们那样用StringClass
,因为毕竟这是一个全新的类,我们只是没有写的一样多。从ExtendedStringClass
类型检查器String
的角度来看,这将允许我们提供与类型不兼容的类型。
同样,我们本来可以决定上一堂全新的课NewClass
并完成
associate NewClass with String
现在StringClass
,NewClass
从类型检查器的角度来看,可以用的每个实例替换。
因此,从理论上讲,子类型化和子类化是完全不同的东西。但据我所知,没有语言具有类型和类实际上是通过这种方式执行操作的。让我们开始精简一下语言,并解释一些决策背后的理由。
首先,即使从理论上说,完全不同的类可以被赋予相同的类型,或者一个类可以被赋予与不是任何类的实例的值相同的类型,但这严重地妨碍了类型检查器的实用性。类型检查器实际上失去了检查您在表达式中调用的方法或字段是否确实存在于该值上的能力,这可能是您想要与a一起玩的麻烦时要进行的检查类型检查器。毕竟,谁知道该String
标签下的实际值是多少?它可能是根本没有的东西,例如根本没有concatenate
方法!
好的,让我们规定每个类自动生成一个与该类同名的新类型,并自动生成associate
具有该类型的s实例。这让我们摆脱associate
,以及之间的不同的名称StringClass
和String
。
出于相同的原因,我们可能希望自动在两个类的类型之间建立子类型关系,其中一个是另一个的子类。毕竟,子类被保证具有父类具有的所有方法和字段,但事实并非如此。因此,尽管子类可以在任何时候需要父类的类型时通过,但是如果需要子类的类型,则应拒绝父类的类型。
如果将此与所有用户定义的值都必须是类的实例的规定结合在一起,则可以具有is subclass of
双重职责并摆脱is subtype of
。
这使我们了解到大多数流行的静态类型的OO语言所共有的特征。有一组“原始”类型(例如int
,float
等),它们与任何类都不相关并且不是用户定义的。然后,您将拥有所有用户定义的类,这些类将自动具有相同名称的类型,并通过子类型识别子类。
我最后要说明的是围绕声明值和类型分开的笨拙之处。大多数语言将两者的创建混为一谈,因此类型声明也是一种用于生成全新值的声明,这些值会自动用该类型标记。例如,类声明通常同时创建类型和实例化该类型的值的方式。这消除了一些笨拙,并且在构造函数存在的情况下,还使您可以一次敲击一个类型来创建无限多个标签。