值类型、引用类型和泛型之因果
多语言
咱们先不说主题,先说说CLR支持多语言。 .net有个非常强大的特点,那就是跨语言,支持很多语言,比如C#、J#等。先来个图看一看
C# J# VB 等等等
👇 👇 👇 👇
C#编译器 J#编译器 VB编译器 编译器
👇 👇 👇 👇
---------------------------------
| 中间语言 |
---------------------------------
👇
编译(运行)
👇
机器语言
看到这个图,每个语言都有自己的编译器,通过第一次编译,编译成中间文件(dll或是exe文件)。在程序运行的时候,再次编译把中间文件编译成机器语言。
但是,CLR支持这么多语言不会出为题么?换句话说是为什么CLR会支持这么多语言?
就像一个四川人和一个河南人对话,互相的听不懂,但是,他们两个人都说普通话就能交流了。在CLR中,每种语言就相当于各个地方的人们,相互交流困难,但是有了普通话这个规范(CTS(公共语言类型)和CLS(公共语言规范)),就可以说话了。CTS相当于我们普通话的音节,而CLS相当于普通话的约定的语法。这样子呢,就组成了.net大家庭。在CTS中,就是值类型和引用类型
进程 线程 内存空间
本来想单独整理一下进程和线程的,后来想和堆栈一起简单的说更好
在一个应用程序中,会分配一个进程和最大4G的内存空间,这个内存空间会分为4个区,全局数据区、代码区、线程堆栈区、托管堆区。
(线程是程序运行的最小单位,线程之间是隔离的,在Window下,每个程序都会有主进程,分配的内容最大为4G)
整个项目都是在这个进程运行的。在这个进程中,可能有很多线程,每一个线程都会在栈下分配一个1M的托管堆栈的空间。
全局数据区:存放着全局变量、静态数据、常量
代码区:存放所有的程序代码
线程堆栈区:存放为运行而分配的局部变量、参数、返回数据、返回地址
托管堆区:自由存储区
--------------------------------------------------------------------------------
| ---------------------------------------- -------------------- |
| | ---- --- --- ---- | | | |
| | |线| |线| |线| |线| | | | |
| | |程| |程| |程| |程| | | | |
| | |1 | |2| |3| | 3| | | 托 管 堆 | |
| | ---- --- --- ---- | | | |
| | (线程数量取决于你的代码) | | | |
| | 线 程 堆 栈 | | | |
| ---------------------------------------- -------------------- |
| |
| |
| ------------------------------- ----------------------------------- |
| | | | | |
| | 全 局 数 据 区 | | 代 码 区 | |
| | | | | |
| | | | | |
| ------------------------------- ----------------------------------- |
--------------------------------------------------------------------------------
值类型与引用类型
C#中,一般情况下值类型存在它申明的地方,引用类型存在托管堆上。值类型转换引用类型叫装箱,引用类型转换值类型叫拆箱
值类型
对于值类型的实例,CLR在运行时有两种分配方式:
(1) 如果该值类型的实例作为类型中的方法(Method)中的局部变量,则该实例被创建在线程栈上;
(2) 如果该值类型的实例作为类型的成员,则该实例作为引用类型(引用类型在GC堆或者LOH上创建)的实例的一部分,被创建在GC堆上
引用类型
对于引用类型的实例,CLR在运行时也有两种分配方式:
(1) 如果该引用类型的实例的Size<85000Byte,则该实例被创建在GC(Garbage Collection)堆上(当CLR在分配和回收对象时,GC可能会对GC堆进行压缩);
(2) 如果该引用类型的实例的Size>=85000byte,则该实例被创建在LOH(Large Object Heap)上(LOH不会被压缩)。
说到了这里,铺垫也Ok了,下面有一个题: 在不考虑多线程的条件下,定义了个结构StructObj,里面有int类型的变量x;又定义了一个类ClassObj,里面也有int类型的变量x。
ClassObj r1 = new ClassObj();//在堆上分配 StructObj v1 = new StructObj();//在栈上分配 r1.x = 5;//根据地址找到引用类型,进行修改 v1.x = 5;//在栈上修改 ClassObj r2 = r1;//只复制引用 StructObj v2 = v1;//在栈上分配空间并复制成员 v2.x = 6; r2.x = 6; Console.WriteLine("v2.x=" + v2.x); Console.WriteLine("r2.x=" + r2.x); Console.WriteLine("v1.x=" + v1.x); Console.WriteLine("r1.x=" + r1.x); Console.WriteLine(r1.Equals(r2).ToString()); Console.WriteLine(v1.Equals(v2).ToString());
现在的打印结果是什么? 答案是:
v2.x=6
r2.x=6
v1.x=5
r1.x=6
True
False
来个图理解下
线程堆栈 托管堆
r1-------|
v1.x=5 |------R=6(r2重新赋值前这个值为5)
r2-------|
v2.x=6
画的比较抽象,r1是引用类型,r1的值存在声明他的地方,在栈上存的是托管堆的地址,赋值r1.x=5,v1.x=5 再次实例化了,StructObj被存储在了他声明的地方,又在栈上开辟了一个空间存储x=6,r2实例化只是在复制了引用,再次赋值的时候会覆盖原先的5,那么r1.x和r2.x值就会相等,说的明白点就是r1和r2就是一个东西。
那么r1就会和r2完全相等(值,地址等等) 比较值类型只会比较他们的值是否相等,所以为False
要是还是看不懂,这还能怪我咯(有本事你来打我)
突然想到一个有意思的问题:为什么要分值类型个引用类型?
早在C#出现之前就已经存在了值类型和引用类型。着么说呢,也应该是一种无奈把。在Window下的应用程序,一个应用程序下假设有四个线程,每一个线程都会分配一个1M的空间,那么4个线程就是4M的空间。先来了一个1.2M的包存进去了,又来了一个1.2的包存进了,现在来了个3M的包发现不足了,这 是他不会开辟新的空间,可能释放了一个1.2包还存不进去。这是就有内存碎片产生了。如果把所以的数据都当值类型用,线程堆栈区很快就会满了而且丢出很多内存碎片拖慢服务器速度。那么我将大的数据存在托管堆上,在线程堆栈上保存一个我在托管堆存放数据的地址,而值类型是一些很小的数据,并且是线程运行的时候才能使用的,所以放在了线程堆栈上。而引用类型里面可能有很多东西,数据比较大,就存在了托管堆上
现在再来谈装箱和拆箱
装修是将值类型转换成object,再将包装后的对象存储在堆上的一个过程。而拆箱是从object到值类型的转换,先检查对象,确保他是给定值类型的一个装箱值后。将值复制到新的值类型中,释放当前对象 由此看来,不管是装箱还是拆箱,都需要CPU大量的计算
数组、ArrayList、泛型
数组被大部分语言支持,其优点在于是连续存储的,所以他的索引速度是非常的快,而且赋值与修改元素也很简单,访问速度较快。缺点是数组是定长的,在创建的时候,就需要知道其大小。而且插入数据和删除很费劲。 相对于数组来说,ArrayList解决了数据的问题,可以很灵活的插入数据和删除。由于ArrayList对数据类型并不严格要求,在添加的时候使用object类型来容纳添加的对象,这里会有装箱拆箱的操作,照成了性能的损失。
为了解决这种问题,微软推出了泛型集合List,如下
List<int> list=new List<int>();
可以看到,在定义泛型集合的时候已经指定了类型。
因为泛型指定了类型,所以在存取的时候值限制于限定的类型内。这样就避免看装箱拆箱所消耗的性能问题,又同时提高了安全性
然后再说说泛型,在这里需要特别说明的是,泛型不能直接的说是值类型还是引用类型,而是指定类型.
在工作总,常常会遇到泛型的代码,我在操作的时候不需要在乎你是什么类型,你给我传过来是什么类型我就处理什么类型,或是你需要什么类型我给你什么类型。比如:
public class Www<T> where T: ClassObjP { public Www() { } }
在这里,T表示任意类型,但是,T只是个符号,不是关键字,写A、B、Z等等等等都可以。ClassObjP表示是约定的类,约定制定类的范围(这类以及这个类的儿子),遵循历史替换原则的类(原谅自己表达能力)
好了,总结的够多了。现在最后在拓展一下
协变 逆变
上面刚刚在约束里过了一下,也提了下历史替换原则,而在工作中,我们又很小的可能会遇到一些因为没有遵循里氏替换原则的代码而报错,这里就可能用得到协变逆变
从使用上来说:协变是子类转父类,只能用在输出参数out;逆变是父类转子类,只能用在输入参数
在用法上呢,可以使用协变,可以使用逆变,也可以协变和逆变辅助使用