.Net 类型之辩

类型不是类。大多数程序员都知道类是啥,却根本不知道类型是啥。

标准答案-有多少人能分得清值类型和引用类型?》中的答案是完全正确的。而《有多少人能分得清值类型和引用类型?》这篇文章中的这道面试题是完全没问题的,看似简单但要真正弄清需要极深功底。因为大多数程序员都知道类是啥,根本不知道类型是啥。

很多情况下,简单的概念往往是最难搞清楚的。比如说,什么是价格?什么是企业?1991年诺贝尔经济学奖得主科斯两大贡献之一就是回答“什么是企业”(《企业的本质》)。

====

类型是一个古老的词,这个词的真正含义是绝大多数程序员所不了解的。研究类型的人一般是设计程序语言的,写编译器的或者玩函数式语言的。讲类型的,有一本很出名的书,是Benjamin C. Pierce 的《Types and Programming Languages》,简称TAPL,中文翻译是《类型和程序设计语言》。这本书看了几次都只看下去了几页,太艰深了。

按照Benjamin C. Pierce 的观点:

A type system is a syntactic method for enforcing levels of abstraction in programs.

每一种语言都有它自己的类型系统,下面是维基百科中对类型系统的定义:

在计算机科学中,类型系统用于定义如何将编程语言中的数值和表逹式归类为许多不同的类型,如何操作这些类型,这些类型如何互相作用。类型可以确认一个值或者一组值具有特定的意义和目的(虽然某些类型,如抽象类型和函数类型,在程序运行中,可能不表示为值)。类型系統在各种语言之间有非常大的不同,也许,最主要的差异存在于编译时期的语法,以及运行时期的操作实现方式

如果一个语言强制实行类型规则(即通常只允许以不丢失信息为前提的自动类型转换)就称此处理为强类型,反之称为弱类型。

上面俺还看得懂,下面这段话,俺就傻眼了:

类型可分为几个大类:

原始类型
    这是最简单的类型种类,例如:整数和浮点数
整数类型
    全部是数字的类型,例如:整数和自然数
浮点数类型
    以浮点数表示数字的类型
复合类型
    由基本类型组合成的类型,例如:数组或记录单元。抽象数据类型具有复合类型和界面两种属性,这取决于你提及哪一个。
子类型
    派生类型
对象类型
    例如:变量类型
不完全类型(哥傻眼了)
    递归类型
函数类型
    例如:双参函数
全称量化类型(哥不知所云)
    如参数化类型、类型变量
存在量化类型(哥看着天花板)
    如模块
精炼类型(哥看着窗外)
    识别其它类型的子集的类型
依存类型(哥去WC)
    取决于运行时期的数值的类型
所有权类型(哥去找小月月)
    描述或约束面向对象系统结构的类型

我们不是搞程序语言的,不要求看懂上面这段话。直觉上来说:

(1)类型(type)不是类(class),类型和类是八竿子打不着的关系。别觉得类型带个类字,就觉得他们是亲戚,说出interface因为无法直接new就不是类型这种笑话;

(2)类型是一种逻辑概念,它代表一种抽象和约束关系。通过抽象关系来描述程序,通过约束关系来保证程序的健壮性。

====

.Net的类型系统叫公共类型系统(Common Type System,CTS)。公共类型系统是一种简单的类型系统,像上面类型分类中那些复杂的类型在.Net中就很难找到对应物(也许F#能,哥低头看地板,反正那些类型指啥俺都不知道),玩类型的人都跑去玩haskell了。在他们看来,.Net的类型系统 too simple, too naive。

下图是MSDN描述的公共类型系统的类型体系结构:

image

我翻译一下:

image

值类型和引用类型是.Net的最基本的两类类型。下面是MSDN上对值类型和引用类型的解释:

Value types directly contain their data, and instances of value types are either allocated on the stack or allocated inline in a structure. Value types can be built-in (implemented by the runtime), user-defined, or enumerations. For a list of built-in value types, see the .NET Framework Class Library.

Reference types store a reference to the value's memory address, and are allocated on the heap. Reference types can be self-describing types, pointer types, or interface types. The type of a reference type can be determined from values of self-describing types. Self-describing types are further split into arrays and class types. The class types are user-defined classes, boxed value types, and delegates.

从这上面可以看出,值类型和引用类型代表着两类约束:

(1)值类型包含自己的数据,值类型分配在栈上或者内联于其它结构体(structure,非struct!)之中;

(2)引用类型存储的是到堆上数据的一个引用(这句话不完全准确,指针也可以指向栈上的数据)。

这样一来,区分值类型和引用类型就很直接了。

type t = tValue,这里的t如果包含它的数据,则type为值类型,如果t只包含引用,具体取值还要根据内存去查找的话,则就是引用类型。

类型不一定非要有自己的数据。比如接口类型,它所指向的数据就是类类型的数据或者值类型装箱为类类型后的数据。指针类型,它所指向的数据是值类型的数据。

====

下面是争论比较大的。

(1)指针类型。

int* p = xxx。根据上面的判断方式,int*是引用类型。

(2)接口类型

IInterface i = (IInterface)Entity。这里的i也只是一个引用,如果Entity是class,则指向该class在堆上的地址,如果Entity是struct,则指向该struct装箱之后,在堆上的地址。

(3)class和.class之辩

.Net中,万物都是.class,这是实现层面,而关键字class则是类型层面的,它代表“类类型”。

====

有人说这是语言游戏,其实不然。类型代表着约束,你只有清楚这些约束,才能在.Net的规则下得心应手。下面深入谈谈:

(1)托管堆中的值类型

这里需要反驳一个流传甚广的谬论:.Net中值类型在栈上,引用类型在托管堆上。

如:

int[] data = new int[100000000];

上例中数组是引用类型,数组里的1亿个int是值类型,你说这1亿个值类型是在哪里?肯定是在托管堆上,栈里也塞不下啊。所以,我的看法是:

“作为引用类型数据成员的值类型是内联在托管堆上引用类型数据内部的。”

别小看这种说法,下面看两段代码:

// a
int[] data0 = new int[100000000];
for (int i = 0; i < data0.Length; i++)
{
    data0[i] = i;
}

// b
int[] data1 = new int[100000000];
int length = data1.Length;
for (int i = 0; i < length; i++)
{
    data0[i] = i;
}

这两段代码实现的功能都一样。b段代码那句 int length = data1.Length; 看则是多余的,实质上不是。在a中的循环体内,是将在栈中的i和在堆中的data0.Length内联的那个字段做比较,而在b中的循环体中,是将在栈中的i和同在栈中的length做比较。在某些情况下,会产生数量级的性能差异,详情见我之前的一篇博客《也谈谈性能:局部性与性能的实验观察》。

(2)值类型的内存管理

.Net中值类型是很值得探讨的,它是编写高性能.Net程序所必需了解的,也是使用纯.Net代码绕开GC,精细控制内存的唯一方式。

.Net的内存大体上有三块:栈:由编译器自动管理;托管堆:由GC管理;非托管堆:手动管理,手动分配和释放。只有值类型的数据是可以分配在栈上,可以分配在非托管堆上,也可以分配在非托管堆上的,而引用类型的数据(如果有的话),一定是托管堆上的。

值类型和指针类型是完全绕开GC的。这就为我们打开了两扇门:

(a)实时编程。GC是实时编程的最大障碍。大量的使用值类型可以绕开GC,进行实时编程。要知道,对于Java那样的语言,进行实时编程是非常困难的,甚至需要设计专门的实时Java虚拟机,.Net则完全不需要。这一点对有实时要求的控制系统非常重要。

(b)高性能编程。使用值类型和指针可以非常精细的操作内存,编写高效的代码:

· 你可以直接用指针去操作值类型;

· 你可以将值类型内联在class中,委托类类型用GC去管理值类型的生命周期;

· 你可以将值类型分配在非托管堆中,自己管理它的生命周期;

· 你可以将值类型分配在栈中,或者用stackalloc在栈中分配值类型数组。

(3)如果对.Net的类型系统不了解,可能会导致程序开发中的问题。

比如,我以前不知道将值类型转换为接口类型会进行装箱操作,在图像处理库中正准备为每一个像素规定几个接口进行抽象。一张图片有几百万像素几千万像素是很正常的,如果通过接口来操作这些像素会发生大量的装箱操作,导致程序性能急剧下降。

原文地址:https://www.cnblogs.com/xiaotie/p/1898495.html