31.总结
31.1 知识点
学习的主要内容
注意
强调
31.2 核心要点速览
数据结存储概念回顾
顺序存储(Array, List)
- 元素紧密连续
- 查:O(1)
- 增/删(中间):O(n)
链式存储(LinkedList)
- 通过指针链接
- 增/删:O(1)(在已知节点位置)
- 查/改:O(n)(需遍历)
简单数据结构类
容器 | 底层结构 | 存取规则 | 常用操作 | 遍历方式 |
---|---|---|---|---|
ArrayList | 可变长 object[] |
任意索引增删改 | - 增:Add 、AddRange 、Insert - 删: Remove 、RemoveAt 、Clear - 查改:索引器 list[i] get/set、Contains 、IndexOf 、LastIndexOf |
- for (i=0…Count-1) - foreach - 查看容量: Count / Capacity |
Stack | 后进先出(LIFO) | 只能顶端进出 | - Push(item) - Pop() (移除并返回顶)- Peek() (查看顶,不移除)- Contains 、Clear 、Count |
- foreach (从顶到底)- ToArray() + for - while (Pop()) |
Queue | 先进先出(FIFO) | 只能队首进队首出 | - Enqueue(item) - Dequeue() (移除并返回队首)- Peek() (查看队首,不移除)- Contains 、Clear 、Count |
- foreach (从队首到队尾)- ToArray() + for - while (Dequeue()) |
Hashtable | 键/值对散列表 | 按键存取 | - Add(key,value) - Remove(key) 、Clear - 索引器 ht[key] get/set- ContainsKey 、ContainsValue |
- foreach (DictionaryEntry kv in hashtable) - foreach (var k in Keys) + ht[k] - IDictionaryEnumerator |
Tip: 如类型已知,优先使用泛型版本(
List<T>
/Stack<T>
/Queue<T>
/Dictionary<TKey,TValue>
),以避免装箱拆箱并增强类型安全。
泛型
泛型相关分类对比
分类 | 语法示例 | 说明 |
---|---|---|
泛型类 | class MyList<T> |
在声明时使用类型占位符T,使用时指定具体类型。 |
泛型接口 | interface IRepository<T> |
与泛型类类似,可定义泛型属性/方法。 |
泛型方法 | void Swap<T>(ref T a, ref T b) |
方法级别的类型占位,可与泛型类并存;占位符可多个。 |
泛型约束(where
)
约束符 | 形式 | 含义 | 例子 |
---|---|---|---|
struct |
where T : struct |
T 必须是值类型(包括所有结构体) | Test1<T> where T: struct |
class |
where T : class |
T 必须是引用类型 | Test2<T> where T: class |
new() |
where T : new() |
T 必须有公共无参构造函数 | Test3<T> where T: new() |
类名 | where T : BaseType |
T 必须是指定类或其派生类 | Test4<T> where T: Animal |
接口名 | where T : IFly |
T 必须实现指定接口 | Test5<T> where T: IFly |
泛型参数 | where T : U |
T 必须是另一个泛型参数 U 或其派生类 | Test6<T,U> where T: U |
组合示例
class Manager<T, K> where T : class, new() where K : struct { }
何时用泛型
- 代码复用:同一数据结构/算法,可用于多种类型。
- 类型安全:避免向
object
容器中装箱拆箱。 - 性能优化:减少运行时类型转换和垃圾产生。
常用泛型数据结构
容器 | 本质 | 常用操作 | 查 | 遍历方式 |
---|---|---|---|---|
List<T> | 可变长度的泛型数组 | 增:Add 、AddRange 、Insert 删: Remove 、RemoveAt 、Clear 改: list[i] = x |
索引器 list[i] Contains IndexOf /LastIndexOf |
for(int i=0;i<Count;i++) foreach(var x in list) |
Dictionary<K,V> | 泛型的键/值对哈希表 | 增:Add(key,value) 删: Remove(key) 、Clear 改: dict[key] = v |
索引器 dict[key] TryGetValue ContainsKey /ContainsValue |
foreach(var kv in dict) foreach(var k in Keys) |
LinkedList<T> | 泛型的双向链表 | 增:AddFirst 、AddLast 、AddBefore 、AddAfter 删: Remove 、RemoveFirst 、RemoveLast 改: node.Value = x |
First /Last Find(item) Contains |
foreach(var x in ll) for(node=First; node!=null; node=node.Next) |
Stack<T> | 泛型的后进先出栈(LIFO) | 增:Push(item) 取: Pop() 看: Peek() 清: Clear() |
Contains(item) |
foreach(var x in stack) while(stack.Count>0){ var x=stack.Pop(); } |
Queue<T> | 泛型的先进先出队列(FIFO) | 增:Enqueue(item) 取: Dequeue() 看: Peek() 清: Clear() |
Contains(item) |
foreach(var x in queue) while(queue.Count>0){ var x=queue.Dequeue(); } |
委托(Delegate)
概念
- 委托是“方法的容器”——将方法视为可存储、传递的第一类对象。
- 定义了一种签名(返回值类型 + 参数列表),只有与该签名完全匹配的方法才能被存入。
声明语法
// namespace 或 class 中都可声明 [public] delegate 返回类型 委托名(参数类型1 名称1, 参数类型2 名称2, …);
- 如
public delegate void MyAction(int x, string y);
- 与方法声明几乎相同,只是前置
delegate
关键字。
- 如
绑定与调用
MyAction a1 = new MyAction(SomeMethod); MyAction a2 = SomeMethod; // 语法糖 a1(arg1, arg2); // 等价于 a1.Invoke(arg1, arg2);
- 绑定时不带括号;调用时既可使用
Invoke
也可直接像方法一样写a1()
。
- 绑定时不带括号;调用时既可使用
多播功能
可以向一个委托变量中累加多个方法:
a += MethodA; a += MethodB;
调用时会按添加顺序依次执行;用
-=
移除首个匹配的方法。执行前要判空:
if (a != null) a();
作为成员和参数
成员字段:用于类内部或外部的事件/回调存储。
public MyAction OnComplete;
方法参数:允许调用者传入自己的逻辑到方法内部延后执行。
void DoSomething(MyAction callback) { … callback(); }
系统内置委托
- Action:无返回值,支持 0–16 个输入参数。
- **Func<…>**:有返回值,支持 0–16 个输入参数,最后一个泛型参数为返回类型。
使用场景
- 事件处理与回调机制(UI 事件、异步完成通知等)
- 策略模式或动态行为切换
- LINQ、异步编程及并行执行的基础
总结:委托让方法可以像数据一样被存储与传递,是 C# 中实现事件、回调和灵活扩展的关键。
Tip: 委托本质上是一个类,
delegate
关键字只是声明了其Invoke
签名;多播、组合、延迟调用让它成为 C# 事件和 LINQ 中不可或缺的基石。
事件(Event)
概念
- 事件是“安全封装”的委托,只能在声明它的类/结构体/接口内部被触发或清空。
- 用于发布—订阅模式,防止外部随意重置或调用委托。
声明语法
[public] event 委托类型 事件名; // 例如: public event Action OnCompleted;
与委托的异同
特性 委托(Delegate) 事件(Event) 赋值/清空 在任何地方可用 =
只能在类内部可用 +=
/-=
触发/调用 在任何地方可直接 Invoke()
只能在声明它的类内部触发(调用) 作为局部变量 可以 不可(编译器禁止) 主要用途 通用回调、函数指针模拟 发布—订阅、安全的回调管理 使用流程
在类中声明事件:
public event Action DataReady;
外部订阅/退订:
obj.DataReady += Handler; // 注册处理器 obj.DataReady -= Handler; // 注销处理器
内部触发:
if (DataReady != null) DataReady(); // 或 DataReady.Invoke();
总结:事件只是对委托的一层保护,让你只能通过
+=
/-=
注册或注销回调,不会被外部任意赋空或直接调用,从而实现更安全、更规范的发布—订阅机制。
匿名函数(Anonymous Function)
定义
没有名称的方法体,可直接内联声明。
形如:
delegate(参数列表) { // 方法体 };
何时使用
- 作为委托参数:简化回调传递,无需额外命名方法。
- 作为委托返回值:动态构建并返回方法逻辑。
- 典型场景:事件处理器、LINQ 查询、异步回调等。
基本语法变体
// 无参无返回 Action a = delegate() { /*…*/ }; // 有参无返回 Action<int, string> a2 = delegate(int x, string s) { /*…*/ }; // 无参有返回 Func<string> f = delegate() { return "hello"; }; // 直接传递给方法 obj.DoSomething(delegate(int x) { Console.WriteLine(x); });
优点
- 内联:无需额外命名、减少样板代码。
- 临时性:回调逻辑就地定义,更具可读性。
缺点
- 无法单独移除:因为没有名称,
delegate { … }
无法用-=
精确移除单个匿名条目。 - 调试:栈跟踪里看不到方法名,仅显示“anonymous method”。
- 无法单独移除:因为没有名称,
最佳实践
- 小而精的回调逻辑适合匿名函数。
- 复杂或需多次移除的处理器,应使用具名方法并存到变量中,以便管理。
Lambda 表达式
定义:匿名函数的简洁写法,用
=>
分隔参数和函数体。语法:
(参数列表) => { /* 函数体 */ };
与匿名函数等价:
delegate(参数列表) { /* 函数体 */ };
示例:
// 无参无返回 Action a = () => Console.WriteLine("Hello"); // 单参无返回(类型可省略) Action<int> a2 = x => Console.WriteLine(x); // 多参有返回 Func<int, int, int> add = (x, y) => x + y;
使用场景:
- 传递给需要委托/事件的 API(如 LINQ、事件绑定)。
- 简化一次性短逻辑,无需额外命名。
优缺点:
- 优点:更简洁、易读,省去
delegate
关键字和方法名。 - 缺点:与匿名函数一样,无法精确地从多播委托/事件中移除单个 lambda。
- 优点:更简洁、易读,省去
闭包(Closure)
概念:函数(或委托)连同其定义时所处环境(局部变量)一起“打包”,并保持对这些外部变量的访问能力,即使外层作用域已结束。
核心特点:
- 捕获的是变量本身的引用,而非当时的值。
- 外层局部变量的生命周期被延长,直到所有引用它的闭包都不再使用为止。
示例:
Func<int, int> Counter(int increment) { int count = 0; // 被捕获的外部变量 return number => { // 闭包 lambda count += increment; return number + count; }; } var c = Counter(5); Console.WriteLine(c(10)); // 15 (count从0变为5) Console.WriteLine(c(20)); // 25 (count从5变为10)
for
循环与闭包陷阱:for (int i = 0; i < 3; i++) { list.Add(() => Console.WriteLine(i)); } // 调用后,所有 lambda 都打印“3”……
解法:在循环体内引入新的局部变量:
for (int i = 0; i < 3; i++) { int copy = i; list.Add(() => Console.WriteLine(copy)); // 捕获 copy 而非 i }
注意事项:
- 被捕获的局部变量只释放于闭包不再引用它时。
- 过度使用闭包可能隐藏内存泄漏风险。
List.Sort
默认排序
- 方法:
list.Sort()
- 要求元素类型:必须实现
IComparable<T>
(或非泛型IComparable
)。 - 行为:调用元素的
CompareTo
,小于 0 放前面,等于 0 保持, 大于 0 放后面;默认升序。
- 方法:
自定义类型实现接口排序
接口:
IComparable<Item>
实现:
public class Item : IComparable<Item> { public int money; public int CompareTo(Item other) => this.money.CompareTo(other.money); }
调用:
var items = new List<Item>{...}; items.Sort(); // 调用 Item.CompareTo
传委托 Comparison
排序 方法重载:
list.Sort(Comparison<T> comparison)
定义 Comparison 函数:
static int CompareShop(ShopItem a, ShopItem b) => a.id.CompareTo(b.id);
调用:
shopList.Sort(CompareShop);
匿名函数:
shopList.Sort(delegate(ShopItem a, ShopItem b) { return a.id - b.id; });
Lambda 写法:
shopList.Sort((a,b) => a.id - b.id);
自定义排序要点
- 返回值:
<0
→ 前,0
→ 不变,>0
→ 后。 - 升序 vs 降序:调换返回值符号(或参数顺序)可实现降序。
- 性能:
List<T>.Sort
使用快速排序和插入排序的混合实现,平均 O(n log n)。
- 返回值:
协变(Covariance) 和 逆变(Contravariance)
特性 | 关键字 | 修饰位置 | 用途限制 | 里氏替换示例 |
---|---|---|---|---|
协变 | out |
泛型接口 / 泛型委托 | 只能用于返回值(不能作参数) | TestOut1<Father> f = sonProvider; ( Func<Son> 可赋给 Func<Father> ) |
逆变 | in |
泛型接口 / 泛型委托 | 只能用于参数(不能作返回值) | TestIn1<Son> s = fatherConsumer; ( Action<Father> 可赋给 Action<Son> ) |
语法示例
协变委托
public delegate T Producer<out T>(); Producer<Son> sonProducer = () => new Son(); Producer<Father> fatherProducer = sonProducer; // OK (Son → Father)
逆变委托
public delegate void Consumer<in T>(T arg); Consumer<Father> fatherConsumer = f => { /*…*/ }; Consumer<Son> sonConsumer = fatherConsumer; // OK (Father → Son)
接口中使用
协变接口
interface IReadOnly<out T> { T GetItem(int index); // void Add(T item); // ❌ 不允许 }
逆变接口
interface IWriteOnly<in T> { void Add(T item); // T GetItem(); // ❌ 不允许 }
适用范围
- 仅限:泛型 接口 和 泛型委托
- 不适用:泛型类、结构体等(会编译报错)
核心思想:用
out
标记“生产者”→只输出;用in
标记“消费者”→只输入,二者都借助里氏替换原则让“父⇄子”泛型能够安全转换。
多线程
理解进程 vs 线程
- 进程(Process):程序在操作系统中的一次运行实例,拥有独立的内存和资源。
- 线程(Thread):进程内可并发执行的最小调度单位,共享所在进程的资源和内存。
启动新线程的关键步骤
声明循环控制标识(若线程内部需要持续运行/可停止):
static bool isRunning = true;
封装线程逻辑方法(无参无返回值):
static void NewThreadLogic() { while (isRunning) { // … 执行周期性或耗时任务 … } }
创建并启动线程(需
using System.Threading;
):Thread t = new Thread(NewThreadLogic); t.IsBackground = true; // 设置为后台线程 t.Start(); // 启动
结束线程
安全停止:在主线程中
isRunning = false;
,让循环自然退出强制终止(.NET Core 不推荐/可能不支持):
t.Abort(); t = null;
线程控制与调度
Thread.Sleep(ms)
:让当前线程休眠指定毫秒。IsBackground
:将线程标记为后台,主线程结束时后台线程随进程一同退出。
线程间共享数据与加锁
进程内所有线程共享同一内存,并发访问同一资源可能造成冲突。
锁(lock) 用法:
static object lockObj = new object(); … lock (lockObj) { // 仅允许一个线程进入此代码块 }
场景示例:一个线程绘制红色圆,一个线程绘制黄色方块,通过
lock
保证绘制命令的原子性,避免光标定位和颜色设置交叉干扰。
多线程的价值
- 将耗时操作(如网络请求、寻路、文件 I/O、复杂计算)放到子线程,避免阻塞主线程,保持界面或逻辑的流畅响应。
多线程就是在同一进程中开启多条“执行管道”,善用后台线程、循环控制、线程睡眠和锁机制,可让耗时任务与主逻辑并行、安全协作。
编译器与预处理器指令
编译器:将高级语言翻译为机器能识别的中间代码或目标语言(如 DLL 或 EXE)。
预处理器指令:以
#
开头,用于编译前的判断与控制,不以分号结尾。常用指令:
#define
/#undef
:定义或取消符号。#if
/#elif
/#else
/#endif
:条件编译,控制不同代码块是否参与编译。#warning
/#error
:编译时发出警告或终止报错。#region
/#endregion
:代码折叠用,提升可读性。
用途:
- 实现跨平台或版本适配(如判断 Unity 版本)。
- 控制测试/调试代码是否参与编译。
- 提前在编译前排除无效路径,提高开发效率。
反射
程序集和元数据
- 程序集:编译后的代码集合(DLL/EXE),包含元数据、IL代码等。
- 元数据:程序中的结构信息(类、字段、方法等),用于描述程序自身。
什么是反射
- 反射:程序运行时动态查看、调用、修改自身或其他程序集的结构和行为(类型、构造、字段、方法等)。
- 意义:提升程序扩展性、灵活性,可用于插件、脚本系统、序列化、Unity编辑器拓展等。
反射相关核心类与用途
Type
- 获取类型信息:
typeof(ClassName)
、obj.GetType()
、Type.GetType("全名")
- 获取成员:
GetFields()
、GetMethods()
、GetConstructors()
、GetMembers()
- 获取特定成员并操作:
GetField("name")
、GetMethod("name")
、使用SetValue/GetValue/Invoke
- 获取类型信息:
ConstructorInfo / FieldInfo / MethodInfo
- 反射构造函数并
Invoke()
实例化对象 - 获取/设置字段值
- 调用方法
- 反射构造函数并
Activator
- 快速实例化对象:
Activator.CreateInstance(type, 参数...)
- 不需要手动查找构造函数,更简洁
- 快速实例化对象:
Assembly
- 动态加载程序集(DLL):
Load()
/LoadFrom()
/LoadFile()
- 通过程序集获取类型并反射使用其中的类
- 动态加载程序集(DLL):
使用反射的经典场景
- 动态插件加载
- 编辑器工具开发(如自定义 Inspector)
- 自定义序列化/反序列化
- 事件总线、属性注入(依赖注入)
注意事项
- 反射性能略低,不宜频繁使用
- 非公共成员默认无法访问,需指定
BindingFlags
或使用私有访问策略 - 参数类型不匹配、类不存在等问题会导致运行时报错(非编译期)
特性
特性概念
- 本质是类,为程序结构添加元数据。
- 可修饰类、方法、字段等,配合反射读取使用。
自定义特性
- 继承自
Attribute
。 - 命名惯例:
XXXAttribute
(使用时可省略后缀)。 - 构造函数决定传参格式。
特性使用
- 语法:
[特性名(参数)]
- 放在声明上一行,实质是构造函数调用。
反射读取特性
IsDefined()
:判断是否应用某特性。GetCustomAttributes()
:获取全部特性对象并强转使用。
限制使用范围
- 用
[AttributeUsage]
指定可用位置、是否允许重复、是否可继承。
常用系统特性
- Obsolete:标记过时成员,支持警告或错误提示。
- 调用者信息:
CallerFilePath
/CallerLineNumber
/CallerMemberName
- Conditional:配合
#define
控制方法是否编译。 - DllImport:绑定调用 C/C++ DLL 中的方法。
迭代器
迭代器作用
- 提供统一方式遍历集合,不暴露内部结构。
- 可用
foreach
遍历的类型,都实现了迭代器模式。
实现标准迭代器(传统写法)
实现接口:
IEnumerable
(用于遍历) +IEnumerator
(用于控制位置)。核心方法:
GetEnumerator()
:返回自身(IEnumerator);MoveNext()
:移动光标;Current
:返回当前项;Reset()
:重置光标(避免 foreach 多次使用时无效)。
foreach 本质流程
- 调用对象的
GetEnumerator()
。 - 不断调用
MoveNext()
→ 若为 true 则读取Current
。 - 否则结束循环。
yield return 语法糖实现
- 只需继承
IEnumerable
,简化逻辑。 - 系统自动生成
IEnumerator
实现。 - 每次
yield return
会记录当前位置并在下次继续。
泛型迭代器实现(配合 yield return)
- 自定义类继承
IEnumerable
。 yield return
返回泛型元素,无需额外泛型接口。- 可用于任意类型集合的遍历。
迭代器总结
迭代器支持
foreach
的根本。两种方式实现:
- 传统:实现
IEnumerator
+IEnumerable
。 - 简洁:使用
yield return
+IEnumerable
。
- 传统:实现
重点掌握 yield return,写法简单且易维护。
特殊写法
var隐式类型
var
只能用于局部变量,且必须初始化。- 推导类型后不可更改;适合临时变量或简化代码。
对象初始化简写
- 使用
{}
初始化对象的公共字段和属性。 - 可与构造函数同时使用,花括号内赋值优先级更高。
集合初始化简写
- 数组、List、Dictionary 可在声明时使用
{}
初始化元素。 - List 可以包含自定义对象,支持嵌套初始化。
匿名类型
- 使用
var obj = new {}
创建临时、无命名的类型。 - 只支持字段,不支持方法。
- 常用于局部临时数据封装。
可空类型 Nullable
值类型后加
?
可为空(如int?
),底层是Nullable<T>
。常用属性方法:
HasValue
:是否有值;Value
:取值;GetValueOrDefault()
:空时返回默认值;?.
:语法糖,判空后再操作。
空合并操作符 ??
- 表达式:
a ?? b
- 若
a
为 null,返回b
;否则返回a
。 - 可用于引用类型和可空值类型的默认值处理。
字符串内插
- 使用
$"{变量}"
进行字符串拼接,更清晰简洁。
单句逻辑简写
if
/for
/while
等语句中只有一句代码时可省略{}
。支持属性、方法的简写:
get => value;
方法名 => 逻辑;
值和引用知识补充
如何判断值类型与引用类型
- 使用
F12
查看源码定义:struct
是值类型,class
是引用类型。
语句块与变量作用域
- 上层语句块:类、结构体;中层:函数;底层:条件分支/循环。
- 成员变量在上层;临时变量在中、底层。
- 代码逻辑主要写在函数、条件、循环中。
变量生命周期
- 成员变量长期存活;临时变量随语句块结束回收。
- 引用类型:栈上地址被释放,堆中数据变垃圾待 GC。
- 减少循环内重复创建临时变量有助于性能。
使用高层变量记录底层变量
- 用成员字段或静态变量记录函数内的中间值,避免被销毁。
结构体中的值类型与引用类型
- 结构体本身是值类型,存在栈中。
- 内部值类型存具体值;引用类型存地址,指向堆中对象。
类中的值类型与引用类型
- 类本身是引用类型,存在堆中。
- 值类型成员与类共存在堆;引用类型成员是指向另一个堆空间的地址。
数组存储规则
数组是引用类型,栈上存地址。
- 值类型数组:堆中存实际值。
- 引用类型数组:堆中存的是其他堆地址(默认值为 null)。
结构体继承接口(装箱/拆箱)
- 结构体实现接口会发生 装箱(复制到堆)。
- 接口变量互赋共享地址,修改其中一个影响另一个。
- 接口强转为结构体会发生 拆箱(复制回栈)。
- 装箱带来性能开销,结构体应谨慎使用接口。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com