机制
委托
委托类似于函数指针:
1 | //定义委托类型 / 定义函数指针类型 |
在我们定义委托类型时,本质上编译器会生成一个继承自标准库的类,如下所示。这个类实际上会形成一个委托的闭包,其会包含调用所需的所有信息,如调用实例、待调用方法的位置。
1 | class MyDelegateType :System.MulticastDelegate |
对于单播委托,其会在_methodPtr
中直接存储方法地址。然而正如上文所示,一个委托可以注册多个方法。实际上在注册多个方法后,C#会生成一个独立的委托实例,用来指向多个单播委托:

注意,每次注册方法+=
,都会new一个新的多播实例,并且原有的多播实例会被废弃,等待垃圾回收。如上图中,如果再注册一个函数Func3
,多播委托A会被丢弃,然后会新new一个多播委托B,并指向三个单播委托实例。
另外在多播委托中,委托链的执行是顺序的,因此如果有一个抛出异常则会终止后续调用。
装箱拆箱
装箱实际上就是把值类型转换为Object引用类型,并且将原数据复制过去,最终返回该引用类型的地址。拆箱则是从装箱的Object类型中复制到栈上,重新构造一个栈上的值类型。
标准类型
string
不可变的引用类型。注意字符串对象初始化之后就不会再修改,所有的修改操作都会new一个新的字符串对象,例如str1+str2
。因此如果要构造变化的字符串,应该使用System.Text.StringBuilder
。这是一个类似于List<char>
的简单类,内部字段为char[]
,其所有修改都是基于同一个对象的引用上。
- 为什么要设计为不可变?操作频繁,线程安全,并且如下可以留用复用。
- 对于字面值字符串,编译器会不重复地写入到模块元数据中,以便复用,即字符串池。但是注意,其他动态创建的字符串不会引用字符串池,即ReferenceEqual为False。
- 注意C#内char类型为2字节的Unicode码,而string的字节长度取决于你用的解码方式,例如默认UTF-8编码英文每字符1字节,中文每字符2字节。而UTF-16/Unicode编码内中英文都是每字符2字节。
- System.Object默认实现一个
ToString()
,返回当前对象的真实类型名称。 - string的GetHashCode基于字符串值计算。
List
底层为动态2倍扩容的数组。注意与纯数组Array不同的是,List<Struct>
返回值也为结构体,不可引用和修改,而Array可以。
- 由于扩容会频繁new数组,因此尽量在初始化时给定好一个合适的大小,减少扩容。注意初始化给定的只是capacity,List的Count依然为0,直接访问会发生访问越界。
- 增删操作Insert和Remove底层都会使用Array.Copy进行数组移动,没有什么特别优化,因此每次增删复杂度为O(N)。
- ToArray会new一个新的数组返回,而不是直接获取内部数组,因此会造成额外的内存分配。
- Find为线性查找,Sort为快速排序。
衍生: LinkedList 是双向链表。 SortedList<TKey,TValue>通过拆分key和value两个数组来维持有序结构。 Queue 数组 + 队头队尾指针维护而成的队列。
另外由于List是引用类型,因此在Add(List)的地方需要注意这里Add的是引用,会随着List在外部的改变而改变。
Dictionary
底层为int[]? _buckets
和Entry[] _entries
双数组的Hash结构,是旧的Hashtable的泛型版本。
GetHashCode()
的默认实现:引用实例用内存地址当做Key计算Hash值,而值实例使用字段值来当做key。- Hash值冲突时使用了拉链法解决。
- 动态扩容与List类似,但是每次容量x2后会进一步选择一个贴近的素数。
- Remove只会重置目标条目,不会像List一样有数据移动。
- 使用值作为Key比实例更快,因为不需要去计算内存地址。
衍生: HashSet Value和Key合体的Dictionary。 SortedSet 红黑树存储的有序HashSet。 SortedDictionary 红黑树存储的有序字典。
PriorityQueue
底层为数组表示的D叉最小堆
(TElement, TPriority)[] _nodes;
- 插入时从底层往上冒泡插入 (和大学实现一样 hhh。
- 删除时弹出堆顶,并由上往下逐层更新。
- 仅.NET 6以上可用,像Unity内就用不了。