1.值类型和引用类型
1)值类型:值类型继承自System.ValueType,所有的值类型都是隐式密封的。值类型包括int、bool、float、char等基元类型以及结构体和枚举。
内存一搬分配在栈上,赋值或者传参的时候会发生复制。
2)引用类型:包括类、string、委托、数组和接口,内存一般分配在堆上,赋值或传参的时候只复制引用(地址)。
2.栈和堆
1)栈:栈是一片连续的内存域,由系统自动分配和维护,大小固定(默认1M,可配置)
内存分配效率高(速度快),栈顶元素使用完毕,立马释放。
2)堆:堆内存是无序的,内存大小动态分配(有上限)。所有的对象都从托管堆分配,CLR维护了一个NextObjPtr指针,指向下一个对象在堆中的分配位置。
当程序需要更多的堆空间时,由GC(垃圾回收机制)帮助我们清理内存。内存分配效率和速度都较低。
3.装箱和拆箱
1)装箱:将值类型转换为引用类型的过程。装箱时,首先在托管堆中分配内存;然后将值类型的字段复制到新分配的堆内存;最后返回对象引用。
频繁装箱可能会触发GC,造成卡顿(GC会将所有线程挂起)
2)拆箱:将引用类型转换为值类型的过程。拆箱时,先获取已装箱对象中各个字段的地址;然后将字段包含的值从堆内存复制到栈上。
装箱和拆箱都是比较耗时的操作,应尽量避免。
4.GC(垃圾回收机制)
1)什么是GC
GC,即Garbage Collection,意为垃圾回收,区别于像原生C++这种需要程序员手动管理内存的机制,垃圾回收机制可以让程序员不再过于关心内存管理问题,
在需要进行垃圾回收时,CLR会收集需要释放掉的对象,进行内存释放。
2)标记压缩法
标记阶段:引用跟踪算法只关心引用类型的变量,包括类的静态和实例字段,或者方法的参数和局部变量。我们称所有引用类型的变量为根。
CLR开始GC时,首先暂停进程中所有的线程,然后CLR会先遍历堆中的所有对象,并全部设置为可回收状态,然后检查所有活动根,查看他们引用了哪些对象,如果一个根包含null,
CLR会忽略这个根并检查下一个根。
任何根如果引用了堆上的对象,CLR都会标记那个对象,并检查这个对象中的根,继续标记它们引用的对象,如果过程中发现对象已标记,则不重新检查,避免循环引用而造成的死循环。
检查完毕后,已标记的对象不能被垃圾回收,因为至少有一个根在引用它。我们说这种对象是可达的,因为可通过仍在引用它的变量访问它,是不能回收的。未标记的对象则是不可达的,可以回收的。
压缩阶段:标记完成后进入压缩阶段,这个阶段是一个碎片整理的过程,通过移动幸存的对象,使它们占用连续的内存空间。移动完成后CLR将每个根减去所引用对象在内存中偏移的字节数,
保证它们还引用原来的对象。
压缩好内存后,托管堆的NextObjPtr指向最后一个幸存对象之后的位置,然后CLR恢复应用程序的所有线程。
3)分代回收
根据经验以及研究证明,对象越新生存期越短;对象越老生存期越长;回收堆的一部分,速度快于回收整个堆。
基于此,GC使用分代回收算法:托管堆在初始化时不包含对象。这时候添加到堆的对象称为第0代对象。当分配一个新对象时,若第0代内存超过预算,就会触发GC进行垃圾回收。不可达的对象被释放,
幸存的对象成为第1代对象。
下一次垃圾回收只检查第0代对象,直到第1代内存也达到预期。这个时候GC会同时检测第0代和第1代对象,第1代中幸存的对象提升到第二代,第0代的提升为第一代。托管堆只支持012三代,如果第二代也达到预期,
GC会执行一次完整的回收,如果内存还是不够,就会抛出OutOfMemoryException异常。
5.委托
定义委托编译器会自动生成一个类派生自System.MulticastDelegate,这个类包含4个方法:一个构造器、Invoke、BeginInvoke、EndInvoke。
调用委托的时候实际上执行的是 Invoke方法。
MulticastDelegate类有三个重要字段:
- _target(System.Object)
当委托对象包装的是一个静态方法,这个字段为null。当委托对象包装一个实例方法,这个字段引用的是回调方法要操作的对象。 - _methodPtr(System.IntPtr)
一个内部的整数值,CLR用它标识要回调的方法。 - _invocationList(System.Object)
该字段通常为null,构造委托链时它引用一个委托数组。若不为null,Invoke的时候会遍历委托数组依次调用。
可以使用GetInvocationList接口获取这个数组。
用委托回调多个方法(委托链)
使用Delegate.Combine 或者用 += 可以将多个委托合并成委托链。每次添加时 _invocationList都会新建一个新的委托数组,数组里存放所有委托,之前引用的数组等待被GC回收。
使用Delegate.Remove 或者 -= 可以从委托链中删除一个委托,通过 _target和_methodPtr找到匹配的委托后,_invocationList同样会引用一个新建数组,新数组不包含移除的那个委托。
若委托链中只剩一个委托,删除成功后返回null。
C#自带的委托
1)Action:无返回值委托,最多支持16个泛型参数。
2)Func:返回一个泛型,最多支持16个泛型参数,最后一个参数必须是返回值类型。
Unity自带的委托
1)UnityAction:无返回值,最多支持4个泛型参数。
2)UnityEvent:Unity对UnityAction的封装。
泛型委托的协变/逆变参数类型
使用 in 关键字表示逆变,参数可以使用类型的派生类。使用 out 关键字表示协变,参数可以使用类型的基类。
6.事件
事件是委托的包装器,内部维护了一个私有的委托链。向事件注册监听的时候其实就是往委托链里面添加一个委托,当事件触发的时候,会遍历委托链依次调用。
如果不需要继续监听的时候要及时注销,因为对象只要向事件登记了他的一个方法就不能被垃圾回收(委托也是一样)。
7.ref 和 out 关键字
CLR所有方法都默认传值。值类型传递的是一个副本,引用类型传递的是地址的拷贝。使用ref和out关键字可以实现按引用传递。
值类型按引用传递避免了复制,节约了性能,但修改形参的同时,实参也会改变。引用类型按引用传参则可以在方法内改变原引用的指向。
ref和out生成的IL代码完全一致,只有一个bit不一样,记录了使用的是哪一个关键字。不同的是ref修饰的参数需要提前初始化,out修饰的参数使用时需要在方法内赋值。
8.协程
1)与进程和线程的关系
进程有自己独立的堆和栈,即不共享堆也不共享栈,进程由操作系统调度。一般一个应用程序对应一个进程。
一个进程可以有多个线程,线程有自己的线程栈。在线程里可以开启协程,避免了无意义的调度,可以提高性能。
2)yield关键字
yield是C#的关键字,其实就是快速定义迭代器的语法糖。
只要是yield出现在其中的方法就会被编译器自动编译成一个迭代器,对于这样的函数可以称之为迭代器函数。迭代器函数的返回值就是自动生成的迭代器类的Current对象。
3)协程原理
在Unity运行时,调用协程就是开启了一个IEnumerator(迭代器),协程开始执行,在执行到yield return之前和其他的正常的程序没有差别,但是当遇到yield return之后会立刻返回,
并将该函数暂时挂起。然后每帧判断yield return后边的条件是否满足,如果满足向下执行。协程常用来执行一些比较耗时的工作。
9.抽象类和接口
1)抽象类和接口都不能实例化。
2)类只能单继承,接口可以多继承。
3)抽象类可以定义实例字段。接口不能定义实例字段。
4)抽象类与派生类的关系一般是 IS ,接口和派生类的关系一般是 HAS
10.结构体和类
1)结构体是隐式密封的,不能被继承;类可以被继承(除密封类)。
2)结构体成员变量申明不能指定初始值,而类可以。
3)结构体是值类型,存在栈中;类是引用类型,存在堆中。
4)结构体不能显示声明无参构造,且声明构造函数时,所有成员变量必须初始化;类随意。
5)结构体不能被静态static修饰(不存在静态结构体);而类可以。
6)什么时候使用结构体?
当类型的实例较小(<=16字节)或类型实例较大但不作为方法实参传递,也不从方法返回时(使用时不需要复制)或者没有成员会修改类型的任何实例字段时(类型不可变)
11.Stringbuilder
1)为什么要使用SB?
因为string类型是不可变的,每次赋值或者修改一个string变量都会在堆上复制一个实例,效率很低,而且容易触发GC。所以引入了Stringbuilder类。
2)SB的实现原理
SB内部采用链表结构,记录了当前节点的上一个节点。同时SB内部维护了一个字符数组,数组的长度等于默认的最大容量(这个最大容量可以在构造的时候指定);
当构造一个SB或者使用Append追加字符串时,如果追加的字符长度加上已有字符的长度没有超过数组容量,则采用指针复制的方式,把Append的字符串,复制到数组的剩余位置。
若超过容量,则会通过计算将一部分字符复制到原有数组中。然后通过一个构造函数将自己作为参数将当前对象的信息复制到上一个节点中去(新建一个SB),并且清空当前的字符数组。
之后,将剩余的字符复制到当前的字符数组中。当调用ToString()时,会递归寻找之前的节点,把字符都取出来返回。
12.async/await异步操作
通过async关键字将方法声明为异步方法,在遇到await关键字前会执行方法内的同步代码,遇到await后跳出去执行方法外的同步代码,异步执行await的代码。
等到await代码执行完毕,继续执行await后面的同步代码。异步方法会返回一个Task实例,可以在一个死循环里通过Task的WhenAny方法监听Task是否执行完毕。