C# 知识点概要
1. 方法重载和重写
| 说明 | |
|---|---|
| 重载 | 同一类中多个同名、形参不同的方法 |
| 重写 | 用 virtual / override 使基类与派生类方法行为不同 |
注:抽象类的抽象方法必须在派生类重写;虚方法可以重写。
2. 面向对象三大特征
封装
将属性与方法结合,限制外部直接访问内部数据;通过公共方法访问,提高安全性。
继承
- 作用:提高代码复用与可维护性,集中管理子类公共属性。
- 特性:传递性、单根性(C# 单继承)。
多态
通过统一接口调用不同类对象,得到不同结果;主要体现为重载与重写。
3. 值类型和引用类型
值类型
| 项 | 说明 |
|---|---|
| 存储 | 直接存实际数据 |
| 类型 | int、float、bool、char、double、struct、enum 等 |
| 位置 | 一般在栈(作为局部变量时) |
引用类型
| 项 | 说明 |
|---|---|
| 存储 | 栈上存引用(地址),堆上存实际数据 |
| 类型 | string、object、class、interface、delegate |
| 位置 | 对象数据在堆 |
注:类的成员变量在堆中;方法内局部变量:值类型与引用各自按规则分配(值在栈,引用变量在栈、对象在堆)。
4. 堆与栈
栈
- 内容:值类型、引用(地址)、参数、局部变量、返回地址等
- 生命周期:随作用域自动创建/销毁
- 内存:地址连续,无碎片问题
堆
- 内容:引用类型的实际数据(如
new出的对象) - 生命周期:由 GC 回收;有引用指向则继续存在
- 内存:地址分散,可能有碎片
5. 装箱与拆箱
| 说明 | |
|---|---|
| 装箱 | 值类型 → 引用类型(如 object) |
| 拆箱 | 引用类型 → 值类型 |
常见触发
- 值类型赋给
object或接口变量 - 以值类型调用
object/接口方法 - 非泛型集合(如
ArrayList)存值类型
避免
- 使用泛型集合
List<T>、泛型方法 - 避免把值类型转为
object - 避免在 Unity 周期函数中频繁装拆箱
- 高频值类型可用
NativeArray<T>
6. private、public 与 protected
| 修饰符 | 访问范围 |
|---|---|
public | 任意类、任意成员 |
private | 仅当前类 |
protected | 当前类 + 派生类 |
7. ArrayList 与 List
| ArrayList | List<T> | |
|---|---|---|
| 类型 | 非泛型,元素为 object | 泛型,类型安全 |
| 开销 | 存取可能装/拆箱 | 无类型转换,更高效 |
8. 接口与抽象类
接口
- 完全抽象(只定义不实现,C# 8+ 可有默认实现例外此处按原文)
- 多继承
- 成员一般为
public - 多类实现同一接口 → 同一调用、不同效果
抽象类
- 可有抽象方法(不实现)+ 具体方法(已实现)
- 单继承
- 成员可为
public/protected/private(private不能修饰抽象方法) - 子类复用具体方法,并必须实现抽象方法
虚方法 virtual | 抽象方法 abstract | |
|---|---|---|
| 实现 | 可有默认实现,子类可选 Override | 无实现,子类必须 Override |
| 所在 | 抽象类或普通类 | 仅抽象类 |
9. Sealed 关键字
- 修饰类:禁止被继承
- 修饰方法:禁止派生类重写(需与
override等配合使用场景见语言规范)
10. unsafe 关键字
- 作用:允许指针操作,直接访问内存
- 用法:可修饰代码块、方法、类、结构体
11. ref 与 out 关键字
ref
- 调用前必须初始化
- 传入现有值,方法内修改会影响实参
| |
out
- 调用前无需初始化
- 用于输出多个结果,方法内必须赋值
| |
注意
ref/out参数不创建新存储位置,操作的是实参别名。- 普通参数传的是引用的副本;
ref传的是变量本身的引用。
12. 结构体与类
结构体 struct
- 值类型;赋值/传参产生副本
- 不能无参构造(C# 10 前常见考点);可定义全参构造
- 适合数据容器
- 不能继承类,可实现接口;结构体赋给接口变量会装箱
| |
类 class
- 引用类型;赋值/传递复制的是引用
- 可有无参构造
- 适合复杂行为、多实例、需要继承的场景
13. 构造函数
- 概念:与类/结构体同名,创建对象时执行
- 不能被继承;非静态构造函数每次
new都会执行
| 类型 | 说明 |
|---|---|
| 默认 | 未写时编译器生成无参构造 |
| 有参 | 初始化字段 |
| 重载 | 多个构造函数,参数列表不同 |
| 静态 | static、无参,初始化静态成员,类首次使用时执行一次 |
| |
14. 泛型
- 概念:类 / 接口 / 方法上的类型占位符,编译期确定具体类型
泛型类
| |
泛型方法
即使类不是泛型,也可定义泛型方法。
泛型接口
| |
泛型约束(常见)
| 约束 | 含义 |
|---|---|
where T : class | T 为引用类型 |
where T : struct | T 为值类型 |
where T : new() | T 有无参公共构造函数 |
where T : IPrint | T 实现某接口(示例见结构体一节) |
15. 泛型容器与非泛型容器
| 泛型 | 非泛型 |
|---|---|
List<T> | ArrayList |
Dictionary<TKey,TValue> | Hashtable |
Queue<T> | Queue |
Stack<T> | Stack |
| 容器 | 特点 |
|---|---|
List<T> | 有序、可重复、动态数组;索引快,中间插删慢 |
HashSet<T> | 不重复;快速判断是否存在 |
Queue | FIFO |
Stack | LIFO |
原文无第 16 节,下接第 17 节。
17. 字典(Dictionary)内部实现原理
1) 概念
基于哈希表的键值对集合。
2) 内部核心结构
| 结构 | 作用 |
|---|---|
| buckets[] | 桶数组,存 entries 的索引;下标为哈希值;初始为质数长度,扩容为下一质数 |
| entries[] | 存条目结构体:键、值、Next(下一索引) |
| 辅助字段 | count、version、freeList、freeCount |
关系:buckets[哈希值] = entries 中的索引(链表头)。
3) 哈希冲突(拉链法)
- 冲突:不同键算出相同桶下标
- 处理:同桶多条目成链表,
Next串联
4) 关键操作
- 扩容:负载因子超阈值 → 桶数量增加(常为质数)→ 重算哈希、重新挂链
- 查找:从桶头遍历链表:比哈希码 → 比键 → 返回值或失败
注:条目=键值对;桶存的是
entries索引;buckets 下标是哈希值;entries 存真实键值对。
18. C# 与 C++ 中的哈希表
1. 底层实现
C# Hashtable
- 拉链法(Separate Chaining),桶为链表
- 使用双散列处理冲突(原文表述)
- 非泛型,
object,有装拆箱 - 已被
Dictionary<TKey,TValue>取代,旧代码仍可见
C++ std::unordered_map
- 开放寻址或拉链法(因编译器/实现而异)
- 模板泛型,编译期定类型
- 内存通常更紧凑
2. 线程安全
| C# Hashtable | C++ unordered_map | |
|---|---|---|
| 默认 | 非线程安全 | 非线程安全 |
| 安全用法 | Hashtable.Synchronized(锁,性能低) | 用户自加 std::mutex 等 |
3. 内存管理
- C#:GC 自动管理
- C++:插入/删除时构造/析构,需关注对象生命周期
4. 性能对比
| 特性 | C# Hashtable | C++ std::unordered_map |
|---|---|---|
| 查找(平均) | O(1) | O(1) |
| 内存开销 | 较高(链表 + 装箱) | 较低(布局更紧) |
| 线程安全 | 内置同步包装(性能低) | 需手动实现 |
19. 元数据与反射
1) 元数据
描述程序集内结构信息的数据;反射 API 本质是读取元数据。
2) 反射
运行时获取、操作类型信息(类、方法、字段、属性等)的机制。
| 类型 | 作用 |
|---|---|
Type | 类型元数据入口 |
Assembly | 程序集,加载并枚举类型 |
MethodInfo | 方法,动态调用 |
PropertyInfo | 属性,动态读/写 |
FieldInfo | 字段,动态读/写 |
| |
用途:分析类型、动态创建对象、调用方法、访问成员、加载未知程序集(插件)等。
20. for、foreach 与 Enumerator
| for | foreach | |
|---|---|---|
| 访问 | 索引 | 枚举器,无索引 |
| 修改集合 | 可按索引改元素 | 不能在遍历中增删元素 |
Enumerator
GetEnumerator()→MoveNext()→Current→ 结束Dispose()- 枚举器是独立对象,不是集合本身
21. C# 编译过程中的文件与概念
Unity 中 C# 大致生命周期
- 编写:C# 源代码(
.cs) - 编译:编译为 IL + 元数据,打包为程序集(
.dll) - 分发/部署:发布程序集
- 执行(IL2CPP):IL2CPP 将 IL + 元数据转为 C++ → 再编译为本地机器码 → 链接为可执行文件 → 设备上运行本地码
flowchart LR
A[.cs 源码] --> B[IL + 元数据 / 程序集]
B --> C[IL2CPP]
C --> D[C++]
D --> E[本地机器码]
22. 小知识点总结
1)A 实例化赋给 B,删除 B,A 是否存在?
存在。B 只是复制了栈上的引用;堆上由 A 创建的对象仍在(若无其它引用则由 GC 回收)。
2)foreach 中删除元素报错怎么办?
foreach 不能在迭代时删元素。应先记录索引或 key,遍历结束后再删除。
3)函数里多次 string += 产生 GC 怎么办?
用 StringBuilder,减少临时字符串对象。
| |
4)频繁创建同一类对象如何省内存?
单例模式或对象池。
5)单例模式违反了什么设计原则?
依赖倒置原则(具体类被硬编码依赖,不利于替换与测试)。
要点速查
| 主题 | 一句话 |
|---|---|
| 重载 vs 重写 | 同名不同参 vs virtual/override |
| 值 / 引用 | 栈+副本 vs 栈引用+堆对象 |
| 装拆箱 | 值↔object,用泛型规避 |
| struct vs class | 值类型小数据 vs 引用类型行为/继承 |
| Dictionary | buckets + entries 拉链法 |
| 反射 | 读元数据,运行时操作类型 |
| Unity | IL → IL2CPP → 本地码 |
补充:类与结构体、foreach
两个容易被“会用”掩盖的问题:值类型和引用类型到底差在哪,foreach 编译后成了什么。
1. 类和结构体的区别以及具体的使用场景
类(Class)
- 类属于引用类型,
- 内存分配在堆上,传递方式时直接去传递引用地址,会产生GC垃圾
- 支持继承和多态
- 数据结构较大时:如果一个数据结构包含大量字段,每次传递都复制整个对象(结构体的行为)会带来巨大的性能开销。此时,传递一个引用(类的行为)会高效得多。
结构体(struct)
- 结构体属于值类型
- 内存分配在栈或者堆上,传递值的副本,不会产生GC垃圾
- 不支持继承但可以实现接口
结构体经典的应用场景
Unity内置范例:Vector2、Vector3、Quaternion、Color、RaycastHit等都是结构体。它们都是小型、不可变的数据集合。
小型、纯数据聚合体:
- 当你的数据结构非常小(官方建议小于16字节),
- 主要用来组合几个简单数据,
- 不会被频繁地作为参数传递
- 它不会被频繁地装箱
堆栈内存本质:
栈:
- 每个线程独立拥有,是一个后进先出(LIFO)的弹夹,只在栈顶操作
- 访问速度极快,分配内存是栈顶指针移动操作,相当于CPU一个指令周期
- 大小固定一般默认只有1到4MB不能分配过大的值类型,否则会引发栈溢出
- 内存自动分配与释放,当一个变量超出作用域时,系统会立即确定性的将其从栈顶弹出并释放你存无GC开销,执行效率高,
堆:
- 进程共享,大小动态增长,
- 访问速度相对较慢,分配时需要遍历堆中空闲的内存块,找到最够大的空间,并维护复杂的内存表,频繁分配会产生内存碎片,进一步降低速度
- 大小比较大,所有需要占用大内存的对象都必须放在堆上。
- 需要手动管理或者Mono Gc管理,释放闲置内存时时会产生GC停顿,内存碎片化严重,需要GC进行压缩整理
值类型可能存在于堆上:
当值类型作为引用类型的字段、被装箱、或在迭代器 / 异步方法中捕获时,会被分配到堆上
局部变量中的类实例(引用类型)→ 实例的实际数据存放在堆,而指向该数据的引用指针(IntPtr)存放在栈。
2. Foreach
一、foreach的底层编译展开
原foreeach代码
| |
编译器翻译成的等价代码
| |
一轮遍历走完就销毁迭代器,不会跨帧保留;
二、和for的区别
- for循环是基于索引直接访问集合,通过维护局部变量,改变下表直接寻址取值,全程不产生额外的堆对象
- foreach循环是基于迭代器模式的会创建迭代器对象,产生额外的迭代器状态机对象产生GC,靠MoveNext()/Current诸葛获取元素;
- for循环只支持有索引结构的数据结构:Array,List等,支持通过下标修改元素,可在循环内安全增删合集
- foreach只要实现IEnumerable就能用,不用关心内部存储结构,不能修改集合长度,item是拷贝,修改item不影响原容器,只能从头到尾顺序遍历,无法自定义步长,无法倒序遍历,必须现反转合集
- foreach 会创建迭代器,带来微小 GC 开销,一帧内连续不停的调用MoveNext一次性遍历完。
本文由 2 篇笔记合并整理。