C# in Depth-类型系统的特征

2.2 类型系统的特征

类型系统被分为强/弱、安全/不安全、静态/动态以及其他一些让人更不好懂的说法。

由于不同的人经常用不同的术语来指代差别不是太大的两种东西,所以很容易产生沟通障碍。

本节只适用于安全代码,如果只考虑安全代码,那么类型系统的各种特征会变得更容易描述和理解。


2.2.1 C#在类型系统世界中的位置

C# 1的类型系统是静态的、显式的和安全的

强类型存在多种不同的定义。

在某些强类型定义中,要求禁止任何形式的转换,不管是显式还是隐式转换,这明显会使C#失去资格。但在另一些定义中,却相当接近甚至等同于静态类型,这会使C#获得资格。

大多数文章和书籍都将C#描述成强类型的语言,但最终的意思实际都是指它是一种静态类型的语言。现在,让我们依次阐述上述结论中的每一个术语。


1. 静态类型和动态类型

C#是静态类型

每个变量都有一个特定的类型,而且该类型在编译时是已知的。只有该类型已知的操作才是允许的,这一点由编译器强制生效。

来看下面这个强制生效的例子:

Object o = "hello";
Console.WriteLine(o.Length);

o 的值是一个字符串,同时 string 类型有一个 Length 属性。但是编译器只把 o 看做 object 类型。

如果想访问 Length 属性,必须让编译器知道 o 的值实际是一个字符串:

Object o = "hello";
Console.WriteLine((string)o.Length);

接下来编译器会查找 System.String 的 Length 属性。以此来验证调用是否正确,生成适当的IL,并计算出整个表达式的类型。

表达式的编译时类型仍然为静态类型,因此我们可以说,“ o的静态类型为 System.Object ”。

假如C#是动态类型

动态类型的实质是变量中含有值,但那些值并不限于特定的类型,所以编译器不能执行相同形式的检查,而是试图采取一种合适的方式来理解引用值的给定表达式。

假定C# 1是动态类型的,就可以做下面的事情:

o = "hello";
Console.WriteLine(o.Length);
o = new string[] {"hi", "there"};
Console.WriteLine(o.Length);

通过在执行时动态检查类型,最终会调用两个完全无关的 Length 属性—— String.Length和Array.Length 。

动态类型具有不同的级别

有的语言允许在你希望的任何地方指定类型,除了在赋值时指定。指定的类型可能仍被当作动态类型处理,但在其他地方仍然使用没有指定类型的变量。

尽管多次声明这是C# 1,但直到C# 3时它还是一门完全静态的语言。C# 4引入了动态类型,然而大多数C# 4应用程序中的大部分代码仍然是静态类型的。


2. 显式类型和隐式类型

显式类型和隐式类型的区别只有在静态类型的语言中才有意义。

对于显式类型来说,每个变量的类型都必须在声明中显式指明。隐式类型则允许编译器根据变量的用途来推断变量的类型,语言可以推断出变量的类型是用于初始赋值的那个表达式的类型。

假设一个语言,它用关键字 var 告诉编译器进行类型推断。表2-1左边一列的代码在C# 1中是不允许的,但是右边一列是等价的有效代码。

显示/隐式类型只能是静态类型

无论隐式类型还是显式类型,变量的类型在编译时都是已知的。即使在代码中没有显式地声明。在动态类型的情况下,变量根本没有一个类型可供声明或推断。


3. 类型安全与类型不安全

有的语言允许做一些非常“不正当”的事情。但在合适的时候,其功能可能会很强大。

但是所谓“合适的时候”实际很少能够遇到。如使用不当,反而极有可能“搬起石头砸自己的脚”。滥用类型系统就属于这种情况。

使用一些非正当的方法,可以使语言将一种类型的值当作另一种完全不同的类型的值,同时不必进行任何转换。

C#编译器和CLR都会检查类型安全

在完全无关的结构(struct)之间进行强制类型转换,很容易造成严重的后果。C#中虽然可以进行大量转换,但不能说一种类型的数据是全然不同的另一种类型的数据。

可以试着添加一个强制类型转换,为编译器提供这种额外的(和不正确的)信息。但是假如编译器发现这种转换实际是不可能的,就会触发一个编译时错误。如果理论上允许,但在执行时发现不正确,CLR也会抛出一个异常。


2.2.2 C# 1 的类型系统何时不够用

在两种常见的情况下,你可能想向方法的调用者揭示更多的信息,或者想强迫调用者对它们在参数值中提供的内容进行限制。

第一种情况涉及集合,第二种情况涉及继承和覆盖方法或实现接口。我们将依次进行讨论。


1. 集合,强和弱

.NET 1.1内建了以下3种集合类型:

①数组——强类型——内建到语言和运行时中。

②System.Collections 命名空间中的弱类型集合。

③System.Collections.Specialized 命名空间中的强类型集合。

数组是强类型的,所以在编译时不可能将 string[] 的一个元素设置成一个 FileStream 。

协变

如果引用类型的数组支持协变,只要元素的类型之间允许这样的转换,就能隐式将一种数组类型转换成另一种类型。执行时会进行检查,以确保类型有误的引用不会被存储下来,如代码清单2-3所示。

//应用协变式转换
string[] strings = new string[5];
object[] objects=strings;
//试图存储一个按钮引用
objects[0] = new Button();

运行代码清单2-3,会抛出一个 ArrayTypeMismatchException 异常 。这是由于从string[] 转换成 object[] 会返回原始引用,无论 strings 还是 objects 都引用同一个数组。

数组本身“知道”它是一个字符串数组,所以会拒绝存储对非字符串的引用。数组协变有时会派上用场,但代价是一些类型安全性在执行时才能实现,而不能在编译时实现。

弱类型的集合

写一个 ArrayList 方法时,没有办法保证在编译时调用者会传入一个字符串列表。

虽然只要将列表的每个元素都强制转换为string ,运行时的类型安全性就会帮你强制使这个限制生效。但是,这样不会获得编译时的类型安全性。

同样,如果返回一个 ArrayList ,可以让他它只包含字符串。但是调用者必须相信你说的是实话,而且在访问列表元素时必须插入强制类型转换。

强类型的集合

如 StringCollection ,这些集合提供了一个强类型的API。所以,如果接受一个 StringCollection 作为参数或返回值,可以肯定它只包含 string 。另外,在取回集合的元素时,不需要进行强制类型转换。

这听起来似乎很理想,但有两个问题。

①它实现了 IList ,所以仍然可以为它添加非字符串(的对象),虽然运行时会失败。

②它只能处理字符串。还有一些专门的集合,但它们包括的范围不是很大。

例如,CollectionBase 类型,可以用它构建你自己的强类型集合,但那意味着要为每种元素类型都创建一个新集合,所以同样不理想。

接着看看覆盖方法和实现接口时发生的问题,它们和协变的概念有关。


2. 缺乏协变的返回类型

ICloneable 是框架中最简单的接口之一。它只有一个 Clone 方法,该方法返回调用方法的那个对象的一个副本。先看看 Clone 方法的签名:

object Clone()

这意味着它需要返回同类型的一个对象,或至少兼容类型的一个对象,具体含义要取决于类型。

返回类型的协变性

用一个覆盖方法的签名更准确地描述该方法实际的返回值,应该讲得通。比如在 Person类中,像下面这样实现 ICloneable 接口:

public Person Clone()

这应该破坏不了任何东西,代码期待的旧的对象仍然能够正常工作。这个特性称为返回类型的协变性。

但接口实现和方法覆盖不支持这一特性,正常的解决方法是使用显式接口实现来获得预期的效果。

public Person Clone()
{
    [Implementation goes here]
}
object IColoneable.Clone()//显式实现接口
{
    return Clone();//调用非接口方法
}

这样一来,任何代码为一个表达式调用 Clone() 时,如果编译器知道这个表达式的类型是Person ,就会调用上面的方法,如果表达式的类型只是 ICloneable ,就会调用下面的方法。这虽然可行,但真的太别扭了。

参数的逆变性

假定一个接口方法或一个虚方法,其签名是 void Process(string x) ,那么在实现或者覆盖这个方法时,使用一个放宽了限制的签名应该是合乎逻辑的,如 void Process(object x) 。

这称为参数类型的逆变性。和返回类型的协变性一样,参数类型的逆变性也是不支持的。

对于接口,解决方案是一样的,同样都是进行显式接口实现。对于虚方法,解决方案则是进行普通的方法重载。虽然不是什么大问题,却着实烦人。

当然,C# 1的开发者被这些问题折磨了很长时间,Java开发者的情况类似,而且他们被折磨了更长时间。

虽然编译时的类型安全性总的来说是非常出色的一个特性,但许多bug实际上是由于在集合中放置了错误类型的元素造成的。

C# 2在这方面也不是完美的,但它确实有了相当大的改进。C# 4的改进更大,但还是不包括返回类型协变和参数逆变。


2.2.3 类型系统特征总结

本节描述了不同类型系统的一些差异,并具体描述了C# 1的特征:

①C# 1是静态类型的——编译器知道你能使用哪些成员;

②C# 1是显式的——必须告诉编译器变量具有什么类型;

③C# 1是安全的——除非存在真实的转换关系,否则不能将一种类型当做另一种类型;

④静态类型仍然不允许一个集合成为强类型的“字符串列表”或者“整数列表”,除非针对不同的元素使用大量的重复代码;

⑤方法覆盖和接口实现不允许协变性/逆变性。

下一节暂停讨论C#类型系统的高级特征。相反,让我们讨论一下最基本的概念:结构和类的差异。

posted @ 2018-11-26 19:06  田错  阅读(228)  评论(0编辑  收藏  举报