C# & Unity 高级知识点汇总文档
学习范围:数据类型、内存原理、集合容器、委托事件、GC、结构体坑点、C#与C++对比
目录
- 索引器 Indexer
- IEquatable 相等判断接口
- Func<> / Action / 委托基础
- Func 异步委托调用原理与坑点
- 一次性事件订阅:回调内取消订阅方案
- SortedDictionary 与优先队列对比
- .NET基础类型字节大小 + 内存对齐 Padding
- GC垃圾回收完整原理、触发时机、卡顿影响、Unity优化方向
- Struct结构体内部包含引用类型时的内存行为与坑
- C#核心特性与C++功能对照表、底层差异
1.索引器 Indexer
定义
索引器是一种不带名字、使用this关键字声明、可带参数的特殊属性,让自定义类/结构体支持 对象[下标] 的方括号访问语法。
基础语法
public string this[int index]
{
get { return _data[index]; }
set { _data[index] = value; }
}
底层本质
编译器自动生成一个名为 Item 的隐藏属性;obj[0] 等价调用 Item 属性。
关键特性
- 索引器参数不限于int,可以使用string、元组等类型,实现字典效果
- 支持多参数,可模拟二维数组访问
- 不能声明为static,只能实例成员
- 只读索引器可使用Lambda简写:
public string this[int i]=>_data[i];
Unity实例
transform[0] 获取第一个子物体,Transform内部就实现了索引器。
和普通属性、方法区分
| 类型 | 调用语法 | 是否携带参数 |
|---|---|---|
| 普通属性 | obj.Name |
❌不可带参数 |
| 索引器 | obj[0] |
✅支持任意参数 |
| 普通方法 | obj.Get(0) |
✅支持任意参数 |
2.IEquatable
作用
提供高性能、类型安全的相等判断接口,用来替代 object.Equals(object obj)。
接口签名:
public interface IEquatable<T>
{
bool Equals(T other);
}
痛点:Object.Equals的缺陷
//值类型传入会装箱,产生GC开销
public virtual bool Equals(object obj);
实现收益
- 值类型:参数为T,无装箱,集合
Contains/IndexOf/Distinct性能大幅提升 - 编译期类型校验,传错类型直接编译报错
完整最佳实践(必须配套重写)
实现 IEquatable<T> 规范上必须同时重写两个方法:
override bool Equals(object obj)override int GetHashCode()
public struct Point : IEquatable<Point>
{
public int X;
public int Y;
public bool Equals(Point other)
{
return X == other.X && Y == other.Y;
}
public override bool Equals(object obj)
{
return obj is Point p && Equals(p);
}
public override int GetHashCode()
{
return HashCode.Combine(X,Y);
}
}
补充要点
IEquatable<T>.Equals()和==运算符互不绑定;想要==生效需要手动重载运算符- 引用类型class实现IEquatable无装箱收益,收益仅为类型安全
3.委托、Func<>、Action<>
委托 Delegate
委托就是存放函数的容器,一种可以指向方法的数据类型。
自定义委托声明语法:delegate 返回值类型 委托名(参数列表);
Func<>
微软预定义好的泛型委托,必须带有返回值;最后一个泛型参数为返回类型。
Func<int,int,int>:两个int入参,int返回值。最多支持16个入参。
Action<>
预定义泛型委托,无返回值(void)。
三者关系
- 委托:大类,类型(盒子)
- Func/Action:系统内置现成的委托盒子
- Lambda
()=>{}:填充委托盒子的方法代码
什么时候自定义委托,什么时候用Func/Action
✅优先Func/Action:简单回调、LINQ、临时lambda ✅自定义委托:事件回调,需要强语义、提高可读性
4.Func 异步委托
底层本质
Func<Task> 就是普通无参委托,返回值类型为 Task。
delegate Task MyAsyncFunc();
两种完全等价调用方式
func.Invoke();
func();
()只是Invoke的语法糖;Invoke本身不会自动等待异步任务完成!
高频大坑
//只启动任务,不等待,即发即弃;异常会丢失
func.Invoke();
//正确写法,等待任务执行完毕
await func.Invoke();
⚠️不要和Unity
MonoBehaviour.Invoke()延迟函数混淆;委托的Invoke立刻执行。
四种调用对比
| 写法 | 是否等待异步任务完成 |
|---|---|
func.Invoke() |
❌否 |
func() |
❌否 |
await func.Invoke() |
✅是 |
await func() |
✅是 |
额外提醒:Invoke不会新开子线程;lambda内部没有Task.Run时,代码运行在当前调用线程。
5.一次性事件订阅(收到消息自动取消订阅)
基础可行方案
在回调内部执行 -= 解绑,实现单次触发。
Action callback = null;
callback = () =>
{
OnMsg -= callback;
Debug.Log("一次性消息");
};
OnMsg += callback;
致命坑1:Lambda匿名委托无法解绑
//错误写法!每次 ()=>{} 创建全新委托实例,移除失败
OnMsg += ()=>{ OnMsg -= ()=>{}; };
原因:
+=和-=的委托不是同一个对象;一次性订阅必须缓存委托变量。
致命坑2‑Invoke快照机制
事件Invoke触发时,会生成当前委托列表的快照。回调内解绑,不会中断当前一轮已经运行的回调;解绑只会对下一次事件触发生效。
异步一次性订阅模板(Func)
Func<Task> wrapper = null;
wrapper = async () =>
{
OnAsyncEvent -= wrapper; //第一行立刻解绑,防止重入
await DoWork();
};
OnAsyncEvent += wrapper;
多线程风险
事件从子线程触发时,+=/-=存在线程竞争,可能解绑后回调仍然执行;尽量主线程调度事件。
6.SortedDictionary 与优先队列
核心结论
SortedDictionary<TKey,TValue> 不能直接当做优先队列,不推荐。
.NET6+官方优先队列:PriorityQueue<TElement,TPriority>(底层最小堆)
SortedDictionary底层:红黑树
缺点:
- Key必须唯一,无法直接存放多个相同优先级的任务;补救方案:复合键
(优先级,自增序号) - 需要维护全集有序,多余开销;优先队列仅需保证堆顶最小
性能对比
- SortedDictionary Add/Remove:O(log n),红黑树常数开销大
- PriorityQueue Enqueue/Dequeue:O(log n),堆内存开销更小
Unity版本适配
- Unity2023.2+ .NET7运行时 → 原生PriorityQueue
- 低版本Unity无API:手写最小堆优先队列
7.基础类型字节占用 & 内存对齐
C#托管基础类型大小(32/64位CLR一致)
| 类型 | 字节数 |
|---|---|
| bool、byte、sbyte | 1 |
| short、ushort、char | 2 |
| int、uint、float | 4 |
| long、ulong、double | 8 |
| decimal | 16 |
引用类型变量(GameObject、class)64位程序指针占8字节,指针仅保存内存地址,不含对象本体大小。
内存对齐原理
CPU不会单字节读取内存;按块读取;变量起始地址必须是自身大小的整数倍。 编译器自动在字段之间插入Padding(空白填充字节)。
结构体对齐案例
struct S
{
byte a; //1字节
int b; //4字节
}
内存布局:0:a;1‑3:Padding;4‑7:int b →总大小:8字节,不是5字节。
结构体优化技巧
字段按从大到小顺序摆放:long→double→int→short→byte;减少Padding浪费内存。
布局控制特性
[StructLayout(LayoutKind.Explicit,Pack=1)] Pack=1代表1字节对齐,取消填充;C#结构体和C++原生dll交互时必须保证对齐一致,否则数据错乱。
Class引用类型内存开销
托管堆上对象 = 对象头+方法表指针(64位合计16字节开销)+字段;字段同样遵守内存对齐。
8.GC垃圾回收完整原理
GC是什么
托管堆自动内存管理器;自动回收无任何引用指向的class对象。
值类型struct、int、Vector3不在托管堆,不受GC管理。
GC核心原理:可达性标记
- 根Root:存活引用起点(局部变量、static、场景激活物体)
- Mark标记阶段:从所有根遍历,可达对象标记存活
- Sweep清除阶段:回收没有标记的垃圾对象,堆产生内存碎片
- Compact压缩阶段:移动存活对象消除碎片;压缩阶段 Stop‑The‑World,主线程暂停卡顿
分代回收(.NET GC核心优化)
托管堆分为0代、1代、2代
| 代 | 生命周期 | 回收频率 | 卡顿代价 |
|---|---|---|---|
| Gen0 | 新建短期对象 | 极高 | 很小,基本无感 |
| Gen1 | 0代存活下来的对象 | 中等 | 低 |
| Gen2 | 长期存活对象 | 极低 | Full GC,高卡顿风险 |
晋升规则:0代回收存活对象→晋升1代;1代回收存活→晋升2代。
LOH大对象堆
≥85000字节的对象进入大对象堆;默认不会被压缩,极易产生内存碎片。
GC触发时机
- 自动触发:new对象,对应代托管堆内存不足(按需触发,非定时)
- 操作系统内存压力
- 场景切换(Unity)
- 手动强制:
GC.Collect(),业务代码禁止随便调用
GC带来的负面影响
- Stop‑The‑World主线程冻结,游戏掉帧卡顿(Gen2 Full GC最严重)
- 托管堆内存碎片
- 对象切断引用后不会立刻释放,等待下一次GC才回收
- 带析构函数对象二次回收,极易晋升Gen2
- GC泄漏:并非C++野指针泄漏;长期保留废弃对象引用,对象永远无法回收(事件忘记解绑、static引用场景物体)
Unity GC优化方向
- 对象池复用,减少频繁new class
- Update避免每帧堆分配,杜绝装箱
- 字符串缓存,减少字符串频繁创建
- 加载界面等空闲时机主动调用GC.Collect
- Profiler查看GC Alloc分配
9.Struct结构体包含引用类型字段
内存规则
struct本身存栈/结构体缓冲区;引用类型字段只存储8字节指针,class本体仍然存放在托管堆。
最大坑点:struct赋值是浅拷贝
struct赋值时拷贝所有字段;引用字段仅仅拷贝指针;两个结构体指向堆中同一个对象。
MyStruct a;
a.Obj = new GameObject();
MyStruct b = a;
b.Obj.name = "test"; //a.Obj.name也同步被修改!
GC回收判定
GC只关心堆对象有没有可达引用,不管引用存放位置(栈上struct字段也属于有效引用)。
所有指向堆对象的引用全部消失,堆对象才被判定为垃圾。
内存泄漏风险
static结构体缓存场景物体引用;场景销毁后结构体仍然保存引用 → 对象无法被GC回收,内存泄漏。解决:用完手动赋值 Obj=null,切断引用。
Job System红线
IJob结构体不能直接存储UnityEngine.Object引用,引擎对象非线程安全,子线程访问崩溃。
10.C# 与 C++功能对比
底层本质区别
| 维度 | C++ | C# |
|---|---|---|
| 编译运行模式 | 原生AOT编译,机器码;无虚拟机 | IL中间代码+CLR虚拟机,JIT运行编译 |
| 内存管理 | 手动new/delete;智能指针shared_ptr;无内置GC | 托管堆自动GC回收 |
| 内存安全 | 默认不安全,裸指针、野指针风险 | 托管内存安全,unsafe才可操作指针 |
| 继承 | 支持多类继承 | 单类继承,多接口实现 |
| 类型划分 | 无值/引用类型区分 | struct值类型、class引用类型 |
C#高级功能与C++对照表
| C#功能 | C++原生支持 | C++替代方案 & 差异 |
|---|---|---|
| GC垃圾回收 | ❌无 | std::shared_ptr 引用计数;无法解决循环引用;第三方Boehm‑GC不属于标准 |
| Delegate/Func/Action | ❌无委托关键字 | std::function;仅单回调,无原生多播 |
| event事件 | ❌无 | vector<std::function> 手动封装回调列表;无语言层面保护 |
| 泛型 | ✅模板 template | C#泛型运行时JIT实例化,支持反射;C++模板编译期实例化,运行时无模板信息 |
| 反射Reflection | ❌无完整反射 | RTTI(typeid)能力极弱;引擎手动宏生成反射(UE UCLASS) |
| Attribute特性 | ❌无运行时元数据特性 | [[nodiscard]]编译期注解;运行时信息丢失;UE宏生成元数据 |
| 属性 {get;set;} | ❌无语法糖 | Get/Set普通成员函数 |
| 索引器 this[] | ✅ | operator[]运算符重载;功能等价 |
| async/await Task | ❌无高层异步 | C++20协程 co_await;底层原语,没有Task高层调度封装 |
如果你需要,我可以把这份文档导出为 .md 文件文本,或者生成一份配套的Unity实战练习题。


0 条回复
还没有留言,来说点什么吧。