委托
知识点一、委托是什么
委托是函数的容器。可以理解为表达函数的变量类型,用来存储,传递函数方法,不同的函数必须对应和各自“格式一致的委托
知识点二、基本用法
关键字:delegate 语法:访问修饰符 delegate 返回值 委托名(参数列表) 大部分写在namespace中
知识点三、自定义委托类
访问修饰符默认不写时pubic,在别的命名空间中式可以使用的
委托相当于一个自定义的规则,用来存储对应的格式的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20//在同一个语句块中是不能重名的 //表示用来装载或者传递,返回值为int,没有参数的函数 delegate void Myfun(); delegate int Myfun2(); delegate void Myfun(); delegate int Myfun2(); static void Main() { Debug.Log("委托"); Myfun f = new Myfun(Fun); //通过复合运算符来直接实现多播 Myfun f += Fun; f(); Myfun f2 = Fun; f2(); f.Invoke(); static void Fun() { Debug.Log("Fun"); } }
知识点四、使用定义好的委托
委托作为函数的参数可以实现先处理一些别的逻辑,当这些逻辑处理完了,再传入的函数
1 2 3 4 5 6 7 8 9 10class Test { public MyFun fun; public MyFun2 fun2; public void TestFun(MyFun fun, MyFun2 fun2) { fun(); fun2(); } }
知识点五、系统定义好的委托
action为无返回值类型,最大支持16位数的传入类型
func为有返回值类型,参数前面的为传入的参数,最后一个为返回值的类型,
知识点六、事件与委托的区别
事件不能在类外部直接赋值,但是可以加减其中的函数
事件不能在类外部直接调用,但是通过在类中封装好调用的函数
事件只能作为成员变量来存在于类中,不能作为临时变量
知识点七、匿名函数的配合使用脱离委托和事件是不会使用匿名函数的
匿名函数的基本语法:
匿名函数可以作为函数的参数也可以作为返回值,同时,匿名函数也不可以直接从事件中-=,因为没有注册的具体名字,无法单独移除,只能直接清除整个事件,直接把整个事件赋值为null,C#:Lambda 和匿名方法功能几乎一样,Lambda 更强(支持异步、表达式树)
在使用匿名函数或者lambda表达式时会产生闭包
闭包的含义:内层的函数可以引用包含在他外层的函数的变量,即使是外层函数的执行已经终止也不会被回收
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24//测试闭包产生 //闭包提供的变量的值并不是变量创建的时候的值,而是父函数范围内的最终值。 //匿名函数体中如果使用了外部函数的变量,那么就会把这个变量拷贝到堆上,所以在外部函数执行完毕后还能使用 class Test { public event Action action; public void Test { public int Value; //在构造函数被执行完毕时,其中的零食变量Value的生命周期被改变了,只有当这个值被取消引用的时候才会被正常回收 action =()=> { Console.WriteLine(value); } for(int i=0;i<10;i++) { //在这里订阅每一个i都是该循环的最终值即10, action+= { Console.WriteLine(i); } } } }匿名函数体中如果使用了外部函数的变量,那么就会把这个变量拷贝到堆上,所以在外部函数执行完毕后还能使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20//1.无参无返回 Action a = delegate () { Console.WriteLine("匿名函数逻辑"); }; //可以先声明后调用 a(); //2.有参,和声明函数的方法一致 Action<int, string> b = delegate (int a, string b) { Console.WriteLine(a); Console.WriteLine(b); }; b(100, "123"); //3.有返回值,不需要在乎匿名函数的返回值,返回什么就是什么返回值 Func<string> c = delegate () { return "123123"; };
Array List
知识点一、ArrayList的本质
ArrayListshiyige C#为我们封装好的类本质上时一个Object[]的数组
知识点二、申明
需要引用using System.Collections
1ArrayList array = new ArrayList();
知识点三、增删嘎查
增
- 单个增加
1array.Add(1);
- 增加范围
1 2ArrayList array2=new ArrayList(); array.AddRange(array2);
- 插入一个元素
1array.Insert(插入的位置,"插入的值");
删
- 移除指定元素,从头开始找,找到就删
1array.Remove(1);//传递参数是要移除的元素
- 移除指定位置的元素
1array.RemoveAt(0);//传递参数是要移除元素的位置
- 清空所有的元素
1array.Clear();
查
- 查找元素是否存在
1array.Contains("需要查找的属性")
- 正向查找元素位置,返回值是位置找不到的返回值是-1
1 2int index = array.IndexOf("需要查找的属性"); Console.WriteLine(index);
- 反向查找元素位置,返回值是位置找不到的返回值是-1
1 2int index = array.LastIndexOf("需要查找的属性"); Console.WriteLine(index);
改
直接通过索引器进行更改
知识点四、遍历
长度(Count)
容量(Capacity)arrayList中每一次在Add扩充时都会自动的增加相对应的容量。防止每次增删产生的垃圾
知识点五、装箱拆箱
万物之父来存储数据就存在装箱拆箱,在王其中进行值类型存储时就是在装箱,将值类型对象转换使用时,就是在拆箱
栈
知识点一、Stack的本质
栈是一种C#封装好的一种数据结构,本质是object[]数组只是封装了特殊的存储规则·
栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。
先存入的数据后获取,后存入的数据先获取(遵循先入后出的原则)
知识点二、Stack的申明
需要引用using System.Collections
知识点三、增取查改
增(Push)
压栈(只能一个一个放)
| |
取(Pop)
栈中不存在删除的概念,只有弹栈,直接弹出顶部的
| |
查(Peek,Contains)
无法查看指定位置的元素,只能查看栈顶的内容(没有提供相应的索引 器)(Peek)
查看元素是否存在于栈中(Contains)
改(Clear)
无法直接更改某一个指定元素,所以只能清空所有元素进行压栈和弹栈。
知识点四、遍历
这里提出一个问题他既然都可以找出来他的数量了为什么不能通过索引访问用for来遍历
栈是后进先出的受限线性结构,标准定义只允许在栈顶进行 Push/Pop/Peek 操作,C# 的 Stack
类也通过语法封装强制了这一规则,不提供索引器,目的是保证栈的使用规范。所以不能用for 循环遍历
可以遍历长度(Count)
可以用foreach来遍历(遍历出来的顺序是从栈顶到栈底)
也可以强制转换为object数组在进行遍历,顺序也是从栈顶到栈底,(stack.ToArray())
到这里就可以用for遍历了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25#region知识点四遍历 //1.长度 Console.WriteLine(stack.Count); //2.用foreach遍历 //而且遍历出来的顺序也是从栈顶到栈底 foreach(object item in stack) { Console.WriteLine(item); } //3.还有一种遍历方式 // 将栈转换为object数组 // 遍历出来的顺序 也是从栈顶到栈底 object[] array = stack.ToArray(); for (int i = 0; i < array.Length; i++) { Console.WriteLine(array[i]); } //4.循环弹栈循环弹栈用了之后直接就把这个数取出来了配合while使用 while( stack.Count > 0 ) { object o = stack.Pop(); Console.WriteLine(o); } #endregion
知识点五、装箱拆箱
万物之父来存储数据就存在装箱拆箱,在王其中进行值类型存储时就是在装箱,将值类型对象转换使用时,就是在拆箱
队列
知识点一、Queue的本质
Queue是一个C#为我们封装好的类,本质是object[]数组,只是封装了特殊的存储规则
队列是一种先进先出的数据结构 (先存入的先获取,后存入的数据后获取
知识点二、Queue的申明
需要引用using System.Collections
1Queue queue = new Queue();
知识点三、增取查改
增(Enqueue)
1queue.Enqueue(1);
取(Dequeue)
1queue.Dequeue();队列中不存在删除的概念,只有取的概念,取出最先加入的对象
查(Peek,Contains)
1 2 3 4 5 6 7 8 9 10//查看元素是否存在队列中 #region 查 //1.查看队列头部元素但不会移除 v = queue.Peek(); Console.WriteLine(v); //2.查看元素是否存在于队列中 if( queue.Contains((1.4f) ) { Console.WriteLine("队列中存在1.4f"); }
改(Clear)
队列中无法改变其中的元素,只能进入队列,只有清空之后再添加
知识点四、遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22#region知识点四 遍历 //1.长度 Console.WriteLine(queue.Count); //2.用foreach遍历 foreach (object item in queue) { Console.WriteLine(item); } //3.还有一种遍历方式 //将队列转换为object数组 object[] array = queue.ToArray(); for (int i = 0; i < array.Length; i++) { Console.WriteLine(array[i]); } //4.循环出列 while( queue.Count > 0 ) { object o = queue.Dequeue(); Console.WriteLine(o); } #endregion
Hash table
知识点一、Hashtable的本质
Hash Table又称散列表,是基于键和哈希代码组织起来的键/值对,主要作用是提高数据查询的效率
使用键来访问集合中的元素
知识点二、申明
需要引用using System.Collections
1Hashtable hashtable= new Hashtable
知识点三、增删查改
增(Add)
不能出现相同键
1hashtable.Add(键,值)
删(Remove,Clear)
- 只能通过键去删除
1hashtable.Remove(键);
删除不存在的键 没反应
直接清空
查(直接通过键来查找值,Contains(键),ContainsKey(值))
- 通过键来查看值,找不到会返回空
1 2 3Console.WriteLine(hashtable[1]); Console.WriteLine(hashtable[4]);//null Console.WriteLine(hashtable["123123"]);
- 查找是否存在(根据键检测 )
1 2 3 4 5 6 7 8//根据键检测 if( hashtable.Contains(2) ) { Console.WriteLine("存在键为2的键值对"); if( hashtable.ContainsKey(2) ) { Console.WriteLine("存在键为2的键值对");
- 查找是否存在(根据值检测 )
1 2 3 4 5//根据值检测 if(hashtable.ContainsValue(12)) { Console.WriteLine("存在值为12的键值对"); }
改
只能修改键对应的值的内容,无法修改键
1hashtable[1] = 100.5f;
知识点四、遍历
得到键值对 对数 (Count)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26//得到键值对对数 Console.WriteLine(hashtable.Count); //1.遍历所有键 foreach (object item in hashtable.Keys) { Console.WriteLine("键:"+item); Console.WriteLine("值:"+hashtable[item]); } //2.遍历所有值 foreach(object item in hashtable.Values) { Console.WriteLine("值:"+item); } //3.键值对一起遍历 foreach(DictionaryEntry item in hashtable) { Console.WriteLine("键:"+item.keys); Console.WriteLine("值:"+item.Values); } //4.迭代器遍历法,具有参考迭代器具体实现过程 IDictionaryEnumerator myEnumerator = hashtable.GetEnumerator(); bool flag = myEnumerator.MoveNext(); while (flag) { Console.WriteLine("键:"+myEnumerator.Key+"值:"myEnumerator.Value); }
迭代器
迭代器(iterator)有时又称为光标(cursor)是程序设计的软件设计模式 迭代器模式提供一个方法顺序访问一个聚合对象中的各个元素而不暴露其内部的标识
从表现效果上来看是可以在容器对象上遍历访问的接口设计人员无需关系容器对象的内存分配的实现细节 可以用foreach便利的类 都是实现了迭代器的
主要有两种方式:
- 传统方式:通过继承两个接口,实现四个方法
- 用语法糖:yield return 去返回内容,只继承一个接口
知识点一、标准迭代器的实现方法
- 关键接口:IEnumerator,IEnumerable 命名空间:using System.Collections; 可以通过同时继承IEnumerable 和IEnumerator实现其中的方法
- 如果想要在自己类中实现迭代器:需要一个名字为GetEnumerator返回值是IEnumerator的方法
- 继承接口只是为了便于书写
- foraech的本质:
- 获取in后面对象的IEnumerator,调用GetEnumerator方法来获取
- 执行IEnumerator对象中的MoveNext方法
- 只要得到了MoveNext返回值是true就回去得到Current,然后赋值到item
- 在完成了一轮之后,光标节点需要重置调用Reset函数一般写在获取GetEnumerator函数中用于第一次重置光标位置
| |
知识点二、用yield return 语法糖为泛型类实现迭代器
语法糖也称为糖衣语法主要作用是将复杂逻辑简单化,可以增加程序的可读性,从而减少程序代码出错的机会
| |
知识点三、泛型迭代器(yield return语法糖)
| |
特殊语法
知识点一、var隐式类型
var是一种特殊的变量类型,可以用来表示任意类型的变量
- var不能作为类的成员,只能用于临时变量申明一般写在语法快中
- var必须初始化
知识点二、设置对象初始值
申明对象时可以通过直接写大括号的形式来初始化公共成员变量和属性
| |
知识点三、设置集合初始值
申明集合对象时也可以通过大括号直接初始化内部属性
| |
知识点四、匿名类型
var变量可以声明为自定义的匿名类型
| |
知识点五、可空类型
在值类型后面添加?来变成可空类型,同时应用类型和委托类型也适用减少了可空判断的部分
如:
o?.ToString
action?.Invoke
| |
知识点六、空合并操作符
空合并操作符??
书写格式:左边值??右边值
如果左边值为null就返回有编制否则返回左边值只要是可以为null的类型都能用
| |
知识点七、内插字符串
关键符号:$
用$来构建字符串,让字符串可以拼接字符串
| |
知识点八、单据逻辑简略写法
- 当循环和判断只有一句代码的话可以省略大括号
- 函数和属性中直接用get=>,直接返回指向的值,不需要写return
补充:集合与 GC
面试问到集合,真正想听的不是 API,而是“底层是什么、什么时候分配、什么时候回收”。这篇从数据结构讲到 GC,最后对比 Lua 的回收策略。
1. C#常见数据结构
| 数据结构 | 类别 | 存储方式 | 有序性 | 允许重复 | 适用场景 |
|---|---|---|---|---|---|
**Array** (数组) | 基础 | 连续内存 | 是 | 是 | 存储数量固定、频繁随机访问的同类数据。 |
**List<T>** | 泛型集合 | 连续内存 | 是 | 是 | 最常用的动态列表,需要频繁按索引访问和末尾增删。 |
**LinkedList<T>** | 泛型集合 | 不连续节点 | 是 | 是 | 需要频繁在任意位置插入或删除,且很少按索引访问。 |
**Dictionary<TKey, TValue>** | 泛型集合 | 哈希表 | 否 | 键唯一 | 极速查找,通过唯一键快速取值,如用户ID查用户信息。 |
**HashSet<T>** | 泛型集合 | 哈希表 | 否 | 元素唯一 | 去重和快速判断存在性,如过滤重复关键词。 |
**SortedList/SortedDictionary** | 泛型集合 | 有序树/表 | 是(按键排序) | 键唯一 | 需要始终按键排序存储,并能快速按范围查找。 |
**Stack<T>** | 泛型集合 | 连续内存 | 是 | 是 | 后进先出(LIFO) 场景,如函数调用、撤销操作。 |
**Queue<T>** | 泛型集合 | 连续内存 | 是 | 是 | 先进先出(FIFO) 场景,如消息队列、任务排队。 |
**ArrayList** | 非泛型 | 连续内存 | 是 | 是 | 旧代码兼容,存储object 类型,有装箱开销,新代码避免使用。 |
数组和List
List本质上是动态数组,具有总容量和存储的实际元素个数,连续存储,自动扩容,扩容机制是当前容量*2,整个过程中旧数组会被丢弃等待GC回收,
优点:可以用索引查找,cpu缓存友好。适用于按索引随机访问的,需要频繁按索引访问和末尾增删,
需要排序的
LinkedList(双向链表)
内部是一个双向链表,每一个元素都被包装成一个节点,只能直接访问链表的首尾,没有索引器只能从头或尾开始遍历,内存分布比较分散
适用于频繁的在头部插入或者删除元素,需要频繁在任意位置插入或删除(知道中间值的索引的情况下),且很少按索引访问。
Dictionary<TKey, TValue>
本质上是哈希表,不支持线程安全
- 计算哈希值:当你插入一个键值对时,
Dictionary会调用key.GetHashCode()获取一个整数哈希值。 - 映射到桶(Bucket):哈希值会通过一个算法映射到内部数组的某个索引位置(称为“桶”)。
- 存储 Entry:键值对会存储在这个桶里。
- 查找:查找时,再次计算 Key 的哈希值,直接定位到对应的桶,然后在该桶内比较 Key 是否相等(通过
Equals方法),找到后返回 Value。
哈希冲突:多个key的Hash值计算的值指向了同一个桶中造成了哈希冲突
解决办法:拉链法,用链表把同一个地址的元素串起来,一般采用头插法
核心思想:一个桶中可能调进来多个元素那就再桶里面挂一条“链”通常是链表,吧所有哈希到同一个同的元素串起来
工作原理:
插入时:
- 计算 Key 的哈希值,定位到对应的桶(Bucket)。
- 如果桶是空的,直接把新元素放进去。
- 如果桶里已经有元素了(发生冲突),就把新元素添加到这个桶的链表末尾(或头部)。
查找时:
- 计算 Key 的哈希值,定位到桶。
- 遍历这个桶里的链表,用
Equals方法逐个比较 Key,找到匹配的就返回 Value。
删除时:
- 定位到桶。
- 遍历链表,找到匹配的 Key,从链表中移除该节点。
解决哈希冲突的其他方法对比
开放寻址法
线性探测:
- 冲突了就往后挪+i,直到找到空位置,有点CPU缓存极其友好,连续地址,
- 问题:整个区域聚集的元素堆积,查找效率急剧下降
二次探测:
- 冲突了就往后挪+i*2二次方,跳跃式地寻找空位,
- 能有效缓解线性探测的“主聚集”问题,但会产生相对较轻的二次聚集
双重哈希:
- 当发生冲突时,使用第二个哈希函数来计算探测步长:step = hash2(key),然后依次探测 index + step,index + 2_step,index + 3_step
- 即使初始哈希位置相同,只要第二个哈希函数设计得好,它们的探测步长就不同,从而完全避免聚集现象
HashSet
HashSet是专门存储唯一元素的集合,本质是只有一个Key、没有Value的字典
Dictionary主要是“通过Key快速查找Value”HashSet主要是“判断某个元素是否存在”,重复的值不会添加,会返回false
本质上是哈希表
数据结构格式内部结构简化
| |
先调用GetHashCode计算出来桶的位置,定位到桶,如果为空则直接放入,
HashTable
非泛型会产生装箱拆箱,
支持多线程读取通过一个方法,多个线程同时调用单个方法时,内部数据结构不会损坏
Hash函数的妙用
核心思想是:不在按照顺序找,而是根据哈希公式算出桶数组的容器,让索引值当作标签来减小查找的范围,使得查找的时间复杂度无限接近On(1)
2. C#的GC和Lua的GC机制
C#的GC回收机制
C#的GC垃圾回收主要是为为大规模、高性能应用设计的
- 核心算法是:分代回收,基于一个统计规律:大多数对象生命周期都很短,为此,将托管堆分成三代
- 一个对象的代数,等于它已经成功存活的垃圾回收次数
- 第 0 代 (Gen 0):存放新对象,回收最频繁。
- 第 1 代 (Gen 1):存放存活过一次GC的对象。
- 第 2 代 (Gen 2):存放长期存活的对象,回收最少。
这种设计让GC能集中精力高效清理“短命”对象,大幅提升整体内存管理效率
- 回收过程
一次完整的GC通常包含三个阶段:- 标记 (Mark):从根对象(如全局变量、局部变量)出发,标记所有可达对象。
- 清除 (Sweep):回收未被标记的对象内存。
- 压缩 (Compact):整理堆内存,消除碎片,为后续分配提供连续空间。
在GC过程中导致的卡顿是不是因为整理堆内存时,要把其他内存拷贝到新的位置上导致的
Lua的GC回收机制
lua的GC机制是一个轻量级的工具,Lua 的 GC 设计目标是在保证自动内存管理的同时,将对程序执行的干扰降到最低。
- 核心算法是:三色增量标记-清除,为了避免一次性GC造成的长时间停顿,它将GC工作拆分为多个小的步骤,分散在程序中运行
- 三色标记法过程:在增量过程中,通过三种颜色来追踪对象状态:
- 白色 (White):未被标记,若扫描结束仍为白色则被回收。(需要被回收的)
- 灰色 (Gray):已标记,但其引用的对象尚未被扫描。(没有查完,的待办事项)
- 黑色 (Black):已标记,且其所有引用对象也已被扫描(已经确保有被引用不需要被回收)
Lua 5.4 引入了分代GC(和C#类似):
将对象分为年轻对象(刚创建的)和老年对象(活过好几次GC的)。
GC会只盯着年轻对象猛查,因为它们大概率会很快死掉(比如子弹、伤害数字)。对于老年对象(比如全局单例),GC减少扫描数量,极大减少了扫描工作量。
减少装箱GC的办法:
- 优先使用泛型,List<>替代ArrayList,用Func
代替非泛型委托 - 用StringBulider处理字符串的加减
Gc的触发条件,Mono垃圾回收优化手段:
- 运行过程中内存不足的情况
- 手动调用:调用GC.Collect()(不推荐频繁调用),GC回收过程中会产生卡顿
- 应用程序退出
- 每帧结束后检查,达到阈值触发GC
- 系统物理内存不足:强制触发GC
(1)减少对象创建(最核心)
- 对象池:复用频繁创建销毁的对象(如子弹、敌人、UI 元素)
- 值类型替代引用类型:对于小数据,使用 struct 替代 class,避免堆分配
- 复用数组和集合:提前分配足够大的数组,重复使用,避免频繁创建
- 避免字符串拼接:用
StringBuilder或字符串插值替代+ - 避免装箱拆箱:
(2)减少 GC 扫描开销
- 及时释放引用:将不再使用的对象引用设为
null,让 GC 尽早回收 - 禁用未使用的组件和 GameObject:避免不必要的对象存活
- 减少静态变量引用:静态变量是 GC 根对象,引用的对象永远不会被回收
(3)控制 GC 触发时机
- 手动控制 GC:在加载界面、关卡切换等不影响用户体验的时机手动调用
GC.Collect() - 设置 GC 阈值:通过
GC.TryStartNoGCRegion()和GC.EndNoGCRegion()在关键帧禁止 GC
需要注意闭包问题
闭包:事件+Lambda表达式+匿名函数时,会把外部变量的引用释放不了,需要取消订阅才可以释放
本文由 2 篇笔记合并整理。