为了让叙述简化,先定义几个用到的术语:
函数:可以给出输出的那种抽象体,变量可以认为是无参数函数。对于成员函数或者更习惯的叫做方法的那种函数,我认为它就是隐含了对象参数的函数。
类型系统是现在OO语言的核心和基石。类型系统是保证正确性的基础,现在的编程语言大多强调静态安全性,其实就是编译时类型正确性。动态类型系统对应着运行期类型检查,保证运行时的类型正确性。经常所说的安全性其实就是类型正确性。
类型转换是对类型系统的公然藐视。它不是安全的。可能会引入很多问题。有时候,我们可能会觉得需要类型转换,但是那是错觉,我们可以定义一个函数,接受某种类型的对象,返回另一种类型的对象。所以类型转换是不必要的。有人会说,你把类型转换看成某种形式奇怪的函数调用得了。我承认,这个想法很好,但是有两个缺陷,一、类型转换比函数更强横霸道,能干很多函数不能干的事。二、类型转换有时候会自动发生。
数组,一个常见的语言构造,大多数语言都支持它。它一般被定义为同一类型的一堆对象,可以通过index来认领这一堆对象中的任意一个。很多语言都把这一堆对象起一个名字,同时通过[index]运算认领。但是,有很多语言都不支持数组作为一个一类构造。原因是他们认为:数组也不过是一个对象而已,它包含多个其他对象,这种包含多个其他对象的对象一般叫做容器。还有另一种观点是,数组不外是函数而已。它完成一个从index到对象的映射。不管是哪种观点,都比原始的数组的观念强大,更容易加上一些辅助的特性如边界检查什么的。当然,这似乎暗示引入[]是无价值的。()就行了。嘻嘻,BASIC啊。
单单静态类型检查是不够的。最常见的一个例子是list,如果我们要求必须是静态检查,那么有可能要求我们要么放宽list容器容纳的对象类型(也就是通用化、泛化(这是根特化和继承相对的那个概念,不要联系到泛型)),要么强大的限制了list的能力。举例吧:一个list,在它的偶数位置放置鹦鹉,奇数位置放置大象,如果是完全的静态类型检查,那么我们可能会说:我们有一个放置动物的list,但是这容易造成我们放了一只蚂蚁进去!而且类型系统不能够阻止这个行为。实际上造成了不正确性。
协变(Covariance)、抗变(Contravariance)和不变(Nonvariance)是OO理论中最容易引起争论的话题。不过它们是如此重要和基本,非说不可了。它们跟继承多态什么的密切相关。同时,它们也会涉及到如何搞定上面提到的list的问题。
所谓协变,就是随同主类型一起变化。故此称为“协”。还是举例说明比较清楚:
class Animal
{
public:
marry(Animal another);
};
class Elephant :
public Animal
{
public:
marry(Elephant another);
};
class Fox : public Animal
{
public:
marry(Fox another);
};
其中,marry的参数another就是协变的。大家可能觉得,咦,这不是挺好么?其实,对于参数的协变还有很多别的说法。就一般情况而言,大多数人认为,参数应该是抗变的,那么,什么是抗变呢?抗变,又叫反变,意思是说,子类型的函数参数应该是父类型的函数参数的父类型,这儿稍微有点绕,仔细看清楚了,呵呵,等你理解了这句话,你会觉得这不符合直觉,对吗。可是,事实上,这句话是对的,在一般意义上是对的。我举个例子:
你是一个打印服务供货商,你向客户承诺,可以接受TXT和PS格式的文件,返回一堆打印纸张,拥有TXT或者PS文件描述的内容。
后来,你想升级换代,你又增加了一种文件格式PDF,你没有错,你可以赢得新的客户,但是如果你说我要取消TXT文件格式,那么,你的客户可能会抱怨你的。也就是说,你只能放宽你接受的参数类型(泛化、父类什么的),但是变窄(特化、子类什么的)会导致原来能够正常工作的东西突然失效。
现在你或许觉得参数抗变是合理的,应该的。可是别急,想想前面的例子,难道说,动物和动物结婚没错,大象和应该和动物或者更上层的类生物结婚么?是啊,这是一个问题,你或许会觉得这可能是个特例,但实际上这是普遍存在的,回想一下我们的成员函数(方法),就会发现,该函数有一个隐含的this参数一直就是协变的。另外,函数的返回值类型也是协变的,这个几乎没有争议。就如同上面那个例子,你给你的客户是一堆打印了内容的纸,如果你改成它的子类,一堆打印了内容的好纸,你的客户不会反对的。对于变量,我说过它也不过是没有参数的函数的返回值,那么这儿似乎暗示它们应该是协变的。
还有更复杂的问题。比如我们上面那个动物类型体系,我们有一个叫做吃的函数,动物吃食物,大象吃草,狐狸吃肉。这个该算做什么?协变吧。这么说,我们没有办法决定究竟该怎么弄了?究竟怎么回事?继续看:)
现在介绍一个概念,分派(Dispatch)。分派其实就是函数调用。不过稍微有点复杂的是:它根据它的参数类型来选择特定的函数实施这个调用。
Motor* m = new ...();
m->run();
这个run就是一个函数。究竟调用那一个run,依赖于m这个参数(m其实就是那个隐含的this参数)的实际类型,这就是分派。很明显,我们这个是OO
里面多态的基础,另外还有一个术语叫做双分派,其实就是根据两个参数来决定调用那个函数,当然,推而广之就有多分派这个说法了。
某位大牛(记不得叫什么了,抱歉啊)经过研究最终发话了:一个函数中,那些决定分派的参数应该是协变的,而其他的参数应该是反变的。看到这儿,手抚额头,恍然大悟啊。原来如此,就应该如此,非如此不可啊。呵呵。
上面啰里啰唆说了一大通,发现没有提到动态类型相关的东西,现在说说吧。C++里面关于动态类型的构造叫做RTTI——运行时类型信息。关于这个的有两个操作,一个叫做dynamic_cast<T*>(pO),一个就是臭名昭著的typeid了。第一个操作是在运行时得到实际的类型信息,当然,有可能失败的。比如:一个动物,其实际类型是蚂蚁,但是我在运行时想通过dynamic_cast变换成大象,这个肯定不可能。但是我们可以进行这种尝试,如果失败,我们也能得到某种信息不是吗。对于typeid,我就不说别的了,它的增强版就是typeof,其实也就是所有支持反射的语言的基本机制,这个东西一般被认为是不合规矩的:),但它确实在某种程度上增加了我们的表达能力。
分享到:
相关推荐
7. **数组协变**:在C#中,数组的引用类型可以被赋值给更广泛的类型,但仅限于对象数组。例如,`object[] objects = new string[3];`是允许的,因为字符串是对象的子类型。 8. **数组初始值设定项**:可以在声明...
在.NET中,协变是指当泛型类型参数从更具体类型向更抽象类型转换时发生的概念,而逆变则是指从更抽象类型向更具体类型转换。这与面向对象编程中的继承关系是相反的。 ### 协变和逆变在数组中的应用 在.NET 4.0之前...
C#中的协变和逆变是面向对象编程中的重要概念,它们涉及到类型系统中的安全性以及多态性。在C#中,协变(Covariance)和逆变(Contravariance)主要应用于委托(Delegate)和泛型(Generic)。这两个特性使得类型...
这些特性使得程序员可以更加方便地处理类型转换,特别是在涉及到多态性时。 协变的概念意味着允许使用比原始指定类型派生程度更大的类型。在C#中,这主要体现在泛型接口和委托的返回类型上。例如,如果你有一个返回...
- Swift和Kotlin都支持泛型,但Kotlin的泛型更具表现力,支持类型投影和协变/逆变。 5. **集合**: - Swift的数组和字典在Kotlin中分别对应List和Map,Kotlin提供了更丰富的操作和扩展函数。 使用SwiftKotlin...
泛型的引入是为了解决对象容器类(如集合)中可能出现的类型转换异常,提高了代码的可读性和安全性。 在Java中,泛型并不像C++中的模板那样在运行时存在,而是通过一种称为类型擦除的技术实现的。这意味着在编译后...
协变(covariance)是指类型转换的方向与继承关系相同。以数组为例,如果你有一个`String[]`数组,由于`String`是`Object`的子类,所以你可以将`String[]`安全地转换为`Object[]`。这种类型转换的能力就是协变。在...
泛型使得程序员能够在编程时指定类型参数,从而避免运行时的类型转换错误。 - **实现原理**:泛型的实现机制被称为“类型擦除”。这意味着在编译阶段,编译器会根据泛型参数进行类型检查,确保代码的安全性,之后会...
这种类型关系是协变的。 3. 通配符:通配符使用问号'?'作为类型参数的占位符,表示未知类型。它通常与边界配合使用,如'? extends T'表示某种未知类型,但是它是T或其子类的类型;'? super T'则表示它是T或其父类的...
这意味着你可以将数组视为IList类型的实例,从而在处理数组和实现IList接口的其他集合类型时使用相同的代码。例如,你可以编写一个泛型方法,接受IList作为参数,然后通过这个接口遍历数组或列表,就像在上面的代码...
Java泛型是Java语言的一项重要改进,它增强了类型安全性,简化了代码编写,减少了强制类型转换的使用。然而,理解和掌握泛型的擦除、协变限制、数组使用和构造延迟等概念对于避免常见陷阱至关重要。通过深入研究这些...
6.5.2 匿名函数转换为表达式树类型的计算 124 6.5.3 实现示例 124 6.6 方法组转换 126 7. 表达式 129 7.1 表达式的分类 129 7.1.1 表达式的值 130 7.2 静态和动态绑定 130 7.2.1 绑定时间 131 7.2.2 动态绑定 131 ...
在此之前,开发人员在处理集合类时,如`List`或`Set`,往往需要进行显式类型转换,这不仅增加了代码的复杂度,还容易引发运行时错误。泛型的出现,旨在解决这些问题,提供了一种类型安全的方式来编写参数化类型的...
数组是协变的,这意味着`Integer[]`是`Number[]`的子类型。然而,泛型不具有这种协变性,`List<Integer>`不是`List<Number>`的子类型。这种差异可能导致一些开发者在从泛型转到数组或反之亦然时感到困惑。 总结: ...
6.5.2 匿名函数转换为表达式树类型的计算 124 6.5.3 实现示例 124 6.6 方法组转换 126 7. 表达式 129 7.1 表达式的分类 129 7.1.1 表达式的值 130 7.2 静态和动态绑定 130 7.2.1 绑定时间 131 7.2.2 动态绑定 131 ...
6.5.2 匿名函数转换为表达式树类型的计算 124 6.5.3 实现示例 124 6.6 方法组转换 126 7. 表达式 129 7.1 表达式的分类 129 7.1.1 表达式的值 130 7.2 静态和动态绑定 130 7.2.1 绑定时间 131 7.2.2 动态绑定 131 ...
6.5.2 匿名函数转换为表达式树类型的计算 124 6.5.3 实现示例 124 6.6 方法组转换 126 7. 表达式 129 7.1 表达式的分类 129 7.1.1 表达式的值 130 7.2 静态和动态绑定 130 7.2.1 绑定时间 131 7.2.2 动态绑定 131 ...
- **数组是协变的**:String数组是Object数组的子类型,这意味着你可以将一个String数组赋值给Object数组变量。但这是不安全的,因为可能会导致运行时错误。 - **列表(List)不是协变的**:对于List接口,如`List...
* 类型转换 * Widening and Narrowing Conversions * 隐式转换和显式转换 * 字符串和其他类型之间的转换 数据类型疑难解答 * 已声明的元素 * 委托 * 早期绑定和后期绑定 * 错误类型 * 事件 * 接口 * 演练:创建和...
8. 对于数组,由于历史原因,Java的泛型不支持通配符数组,这意味着你不能创建一个`Number[]`类型的数组并将其赋值给`List<Number>`,因为数组是固定类型的,而泛型列表是协变的。因此,通常需要将数组转换为`List`...