C# & Unity 高级知识点汇总文档


学习范围:数据类型、内存原理、集合容器、委托事件、GC、结构体坑点、C#与C++对比

目录

  1. 索引器 Indexer
  2. IEquatable 相等判断接口
  3. Func<> / Action / 委托基础
  4. Func 异步委托调用原理与坑点
  5. 一次性事件订阅:回调内取消订阅方案
  6. SortedDictionary 与优先队列对比
  7. .NET基础类型字节大小 + 内存对齐 Padding
  8. GC垃圾回收完整原理、触发时机、卡顿影响、Unity优化方向
  9. Struct结构体内部包含引用类型时的内存行为与坑
  10. C#核心特性与C++功能对照表、底层差异

1.索引器 Indexer

定义

索引器是一种不带名字、使用this关键字声明、可带参数的特殊属性,让自定义类/结构体支持 对象[下标] 的方括号访问语法。

基础语法

public string this[int index]
{
    get { return _data[index]; }
    set { _data[index] = value; }
}

底层本质

编译器自动生成一个名为 Item 的隐藏属性;obj[0] 等价调用 Item 属性。

关键特性

  1. 索引器参数不限于int,可以使用string、元组等类型,实现字典效果
  2. 支持多参数,可模拟二维数组访问
  3. 不能声明为static,只能实例成员
  4. 只读索引器可使用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);

实现收益

  1. 值类型:参数为T,无装箱,集合 Contains/IndexOf/Distinct 性能大幅提升
  2. 编译期类型校验,传错类型直接编译报错

完整最佳实践(必须配套重写)

实现 IEquatable<T> 规范上必须同时重写两个方法:

  1. override bool Equals(object obj)
  2. 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);
    }
}

补充要点

  1. IEquatable<T>.Equals() 和 == 运算符互不绑定;想要 == 生效需要手动重载运算符
  2. 引用类型class实现IEquatable无装箱收益,收益仅为类型安全

3.委托、Func<>、Action<>

委托 Delegate

委托就是存放函数的容器,一种可以指向方法的数据类型。 自定义委托声明语法:delegate 返回值类型 委托名(参数列表);

Func<>

微软预定义好的泛型委托,必须带有返回值;最后一个泛型参数为返回类型。 Func<int,int,int>:两个int入参,int返回值。最多支持16个入参。

Action<>

预定义泛型委托,无返回值(void)。

三者关系

  1. 委托:大类,类型(盒子)
  2. Func/Action:系统内置现成的委托盒子
  3. 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底层:红黑树

缺点:

  1. Key必须唯一,无法直接存放多个相同优先级的任务;补救方案:复合键 (优先级,自增序号)
  2. 需要维护全集有序,多余开销;优先队列仅需保证堆顶最小

性能对比

  • 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核心原理:可达性标记

  1. 根Root:存活引用起点(局部变量、static、场景激活物体)
  2. Mark标记阶段:从所有根遍历,可达对象标记存活
  3. Sweep清除阶段:回收没有标记的垃圾对象,堆产生内存碎片
  4. Compact压缩阶段:移动存活对象消除碎片;压缩阶段 Stop‑The‑World,主线程暂停卡顿

分代回收(.NET GC核心优化)

托管堆分为0代、1代、2代

代 生命周期 回收频率 卡顿代价
Gen0 新建短期对象 极高 很小,基本无感
Gen1 0代存活下来的对象 中等 低
Gen2 长期存活对象 极低 Full GC,高卡顿风险

晋升规则:0代回收存活对象→晋升1代;1代回收存活→晋升2代。

LOH大对象堆

≥85000字节的对象进入大对象堆;默认不会被压缩,极易产生内存碎片。

GC触发时机

  1. 自动触发:new对象,对应代托管堆内存不足(按需触发,非定时)
  2. 操作系统内存压力
  3. 场景切换(Unity)
  4. 手动强制:GC.Collect(),业务代码禁止随便调用

GC带来的负面影响

  1. Stop‑The‑World主线程冻结,游戏掉帧卡顿(Gen2 Full GC最严重)
  2. 托管堆内存碎片
  3. 对象切断引用后不会立刻释放,等待下一次GC才回收
  4. 带析构函数对象二次回收,极易晋升Gen2
  5. GC泄漏:并非C++野指针泄漏;长期保留废弃对象引用,对象永远无法回收(事件忘记解绑、static引用场景物体)

Unity GC优化方向

  1. 对象池复用,减少频繁new class
  2. Update避免每帧堆分配,杜绝装箱
  3. 字符串缓存,减少字符串频繁创建
  4. 加载界面等空闲时机主动调用GC.Collect
  5. 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实战练习题。


25 8 月, 2026Garfield God学习笔记阅读 1 次

0 条回复

还没有留言,来说点什么吧。

留言