• 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#类型系统的高级特征。相反,让我们讨论一下最基本的概念:结构和类的差异。

  • 相关阅读:
    ajax技术
    JSDOM获取子节点的一些方法
    防止a标签跳转的几种方法
    关于childNodes和children
    三种预览图片的方法
    异步加载js文件的方法
    跨域访问的解决方案
    关于在JS中设置标签属性
    Eclipse导入web项目发布项目时报Tomcat version 7.0 only supports J2EE 1.2, 1.3, 1.4, and Java EE 5 and 6 Web错误解决方案
    Maven构建项目报No compiler is provided in this environment. Perhaps you are running on a JRE rather than a JDK? 问题的解决方案
  • 原文地址:https://www.cnblogs.com/errornull/p/10019993.html
Copyright © 2020-2023  润新知