C#进阶

C#进阶
John Doe1.abstract与虚函数
| 关键词 | 它是啥? | 潜台词 | 强制性 |
|---|---|---|---|
| Abstract Class (抽象类) |
未完工的模具 | “我这个类是不完整的,不能直接用来造东西,只能被继承。” | 不能 new,也不能挂在物体上直接用 |
| Abstract Function (抽象函数) |
强制命令 | “不管你怎么做,你必须要有这个功能!具体怎么做你自己定,但我这里不写代码。” | 子类必须重写 (override) |
| Virtual Function (虚函数) |
默认建议 | “我提供了一个通用的做法,你如果觉得好用就直接用;觉得不好用,也可以改。” | 子类可以重写,也可以不重写 |
2.基类的函数封装
在写 Unity UI 基类时,怎么选?
- 如果是供 UIManager 调用的函数(如 Open, Close),用 public。
- 如果是组件引用(如 CanvasGroup, Button)或者子类通用的变量,用 protected。(这是基类里最多的)。
- 如果是基类内部的死逻辑(不想让子类乱改,或者子类根本不需要知道),用 private。
3.简单数据结构类
1.Hashtable
1.本质与声明
2.增删改查:
3.遍历
4.泛型
5.泛型约束
举个例子:
1 | class Test<T> where T:struct |
约束也可以组合使用,where后用逗号隔开就行
如果多个泛型有约束呢,不同泛型约束用空格隔开就行
1 | class Test<T,K> where T:struct where K:class |
6.常用泛型数据结构类
1.Dictionary
1.本质与声明
Hashtable:类型其实是确定的,叫 object
在 C# 早期的 System.Collections.Hashtable 中,键(Key)和值(Value)的类型在底层是被写死(确定)为 object 的。
因为在 C# 中 object 是所有类型的基类,所以你可以把 int、string、bool 甚至自定义对象全塞进去,这利用的是多态(向上转型),而不是泛型。
这会带来两个致命问题:
- 性能损耗(装箱与拆箱): 如果你存入值类型(比如 int),C# 必须将其打包成引用类型(装箱,分配堆内存),取出来时又要强转回值类型(拆箱)。这个过程极其消耗性能。
- 类型不安全: 编译器不知道你存进去的具体是什么,取出来时必须强制转换。如果转错了,运行时直接崩溃。
2.增删改查
和哈希表一样,唯一不同的一小点是通过键查看值时,如果没有该键会报错而不是返回空
3.遍历
注意键值对一起遍历用的类型与哈希表不同(也有通过迭代器的遍历方式,后续讲迭代器再补充)
7.常用委托与事件
第一部分:常用的 5 大委托类型 (Delegate Types)
在现代 C# 开发中,我们极少自己去写 public delegate void MyDelegate(); 这种传统定义了,因为系统已经为你准备好了 5 个“万能委托类型”,直接拿来用即可:
1. Action / Action
—— 最常用的“无返回值”委托
特点:永远没有返回值(void)。
参数:可以没有参数(Action),也可以带 1 到 16 个参数(如 Action<int, string>)。
使用场景:绝大多数的回调函数、普通事件监听。
举例:
1
Action onComplete = () => Debug.Log("完成!"); Action<int, float> onTakeDamage = (hp, time) => Debug.Log($"扣血{hp}");
2. Func / Func<T1, T2… TResult>
—— 最常用的“有返回值”委托
特点:必须有返回值!尖括号里的最后一个类型,就是它的返回值类型。
参数:Func
表示没有输入参数,返回 int;Func<string, bool> 表示输入一个 string,返回 bool。 使用场景:数据计算、LINQ 查询、需要外部告诉你计算结果的回调。
举例:
1
2// 传入一个数字,返回它是否大于
10 Func<int, bool> isGreaterThanTen = (num) => { return num > 10; };
3. Predicate
—— 专职“判断真假”的委托
特点:固定接受一个参数,并且固定返回 bool 类型。
本质:它其实和 Func<T, bool> 一模一样,只是为了语义更清晰。
使用场景:专门用在 List 列表的查找、筛选、删除操作中。
举例:
1
2// 传入一个数字,返回它是否大于10
Func<int, bool> isGreaterThanTen = (num) => { return num > 10; };
4. EventHandler / EventHandler
—— 老派严谨的委托
- 特点:固定带两个参数:(object sender, EventArgs e)。
- 使用场景:开发正规的 C# 桌面软件(WinForm/WPF)、编写需要符合 .NET 标准规范的类库。在游戏开发中除了特定框架,日常手写用得较少。
5. UnityAction / UnityAction
—— Unity 的亲儿子委托
特点:和 Action 完全一致,无返回值。
使用场景:专门用来配合 Unity 自带的 UI 系统(UGUI)。比如给 Button、Slider 动态添加监听器时,人家 API 规定的参数类型就是 UnityAction。
举例:
1
button.onClick.AddListener(MyUnityAction);
第二部分:常用的事件形式 (Event Implementations)
上面说的是“委托类型”,现在我们来看看把它们加上 event 关键字或封装后,在代码里怎么声明事件。常用的有以下三种流派:
1. 极简实用流:event Action (强烈推荐⭐)
做法:直接拿 C# 内置的 Action 委托,前面加上 event 关键字。
优点:代码极其干净,不用写繁琐的 sender 和 EventArgs。
使用场景:游戏开发中的大部分代码逻辑(主角死亡、游戏通关、金币增加等)。
代码模板:
1
2
3
4
5
6public class Player {
// 1. 声明事件
public event Action<int> OnCoinChanged;
public void AddCoin(){
// 2. 触发事件 (?.Invoke 是一种安全的简写,判断不为空才触发)
OnCoinChanged?.Invoke(100); } }
2. 标准规范流:event EventHandler
做法:使用微软官方推荐的事件驱动模型。需要自定义一个类继承自 EventArgs 来传递数据。
使用场景:大型框架设计、或者与其他标准 C# 库进行交互时。
代码模板:
1
2
3public class MyArgs : EventArgs { public string msg; }
public event EventHandler<MyArgs> OnMessage;
// 触发 OnMessage?.Invoke(this, new MyArgs { msg = "Hello" });
3. Unity 检查器流:UnityEvent (重要⭐)
做法:不使用 C# 的 event 关键字,而是直接实例化一个 Unity 提供的类 UnityEvent。
核心优势:**它可以序列化!会显示在 Unity 的 Inspector 面板上!**你可以直接在 Unity 编辑器里拖拽游戏物体来绑定事件(就像给 UGUI Button 绑事件那样)。
注意:UnityEvent 是一个类,不是委托类型,也不是 C# 语法的 event。它内部封装了对 UnityAction 委托的管理。
代码模板:
1
2
3
4
5
6
7using UnityEngine; using UnityEngine.Events; // 必须引入这个命名空间
public class Door : MonoBehaviour {
// 声明一个 UnityEvent (面板上就能看到了!)
public UnityEvent OnDoorOpen;
void Open(){
// 触发事件
OnDoorOpen?.Invoke(); } }
💡 终极一句话总结指南:以后写代码该用哪个?
- 需要返回值 ➡ 用 Func。
- 在 List 里面找东西 ➡ 用 Predicate(或者直接写 Lambda)。
- 给 Unity UI 按钮动态绑方法 ➡ 用 UnityAction。
- 想在 Unity 面板上拖拽连线(给策划和美术用) ➡ 用 UnityEvent。
- 纯粹的代码逻辑交互(自己写代码监听和触发) ➡ 用 event Action(最香、最通用)!
9.lambda表达式
lambda表达式本质是匿名函数,配合委托一起使用,就是为了省事用的,用委托不想让他一个一个函数地加,就可以用lambda表达式直接等于你想让此委托执行的内容,也可以+=一个lambda表达式,弊端就是无法-=(因为是匿名函数)(事件不能直接=一个lambda表达式,因为封装过了)
10.单例模式
这两种单例模式是游戏开发中最经典的两种写法,它们代表了 Unity 开发中的两大流派:“纯 C# 派” 和 “MonoBehaviour 派”。
它们不仅代码长得不一样,底层原理、生命周期和使用场景更是完全不同。
我为你详细拆解这两者的区别,并告诉你该怎么选。
第一种:纯 C# 单例(饿汉模式)
1 | private static JsonMgr instance = new JsonMgr(); |
1. 这是什么?
这是标准的纯 C# 面向对象单例模式。因为它在声明 instance 的时候直接就 new 出来了,不管你用不用它,程序一启动它就在内存里了,所以这叫**“饿汉模式”**。
2. 代码细节解析
new JsonMgr():因为它没有继承MonoBehaviour,所以可以通过new关键字直接在内存中创建实例。private JsonMgr(){}:这是一个极其精妙且必要的设计!私有化构造函数,意味着在类的外部,别的脚本绝对无法写出new JsonMgr()这种代码,从而从根本上保证了单例的唯一性。那么 => 是什么意思?
=> 在这里叫做**“表达式主体定义(Expression-bodied members)”,它是 C# 6.0 引入的一个超级好用的“语法糖”。
在这里,它其实是只读属性(只包含 get 操作)**的简写形式。我们来看一下它的**“前世今生”**,你就秒懂了:
在以前的老 C# 版本中,你必须这样写(非常啰嗦):
1
2
3
4
5
6
7
8public static JsonMgr Instance
{
get
{
return instance; // 当外部访问时,把私有的金库对象交出去
}
// 注意:这里故意不写 set,这就意味着外部只能“读取”,绝对不能“修改(赋值)”!
}
3. 优缺点
- 优点:
- 极其轻量、干净:不依赖 Unity 的场景,不挂载在任何 GameObject 上。
- 跨场景绝对存活:切场景对它没有任何影响,它一直安安静静地躺在内存里。
- 无需配置:不用去 Hierarchy 面板拖拽,代码直接调用即可生效。
- 缺点:
- 不能使用 Unity 的
Awake、Start、Update等生命周期函数。 - 不能开启协程(
StartCoroutine)。 - 不能在 Inspector 面板中暴露参数给策划配置。
- 不能使用 Unity 的
第二种:MonoBehaviour 场景单例
1 | public static UIManager Instance { get; private set; } |
1. 这是什么?
这是 Unity 特有的组件式单例。这个脚本必须继承自 MonoBehaviour,并且必须挂载到场景里的某个 GameObject 身上。
2. 代码细节解析
- 依赖场景:只有当场景加载,并且挂载着这个脚本的 GameObject 被激活时,
Awake函数才会被触发,Instance才会被赋值。 Destroy(this.gameObject):由于 Unity 中可以复制无数个 GameObject,如果你不小心在场景里建了两个 UIManager,或者切场景时带过来了一个新的,这句代码会把多余的那个连同它所在的物体直接销毁,保证场景里只有一个大管家。
3. 优缺点
- 优点:
- 全能:可以使用 Unity 的所有功能(生命周期、协程、物理检测等)。
- 可视化:可以直接在 Unity 面板上给它拖拽各种预制体、配置音频文件、设置参数。
- 缺点:
- 依赖场景:如果场景里没有建这个物体,别的脚本一调用
UIManager.Instance就会报空指针错误。 - 切场景会被销毁:默认情况下,切换 Scene 会把这个单例销毁。必须额外加一句
DontDestroyOnLoad(this.gameObject)才能保住它的命。
- 依赖场景:如果场景里没有建这个物体,别的脚本一调用
💡 我该用哪个?
结论是:两者都极度推荐,但必须“专款专用”!
【第一种:纯 C# 单例】的场景:
只要你的 Manager 不需要在面板上拖拽东西,不需要用 Update 每帧执行逻辑,不需要开协程,统统用第一种!
- ✅
JsonMgr(数据解析):纯计算逻辑,不涉及场景物体,完美适用。 - ✅
DataManager(玩家存档数据):管理金币、等级、背包数据。 - ✅
EventManager(全局事件/消息广播中心)。 - ✅
NetworkManager(网络通信)。
【第二种:MonoBehaviour 单例】的场景:
只要你的 Manager 需要控制场景里的物体、加载预制体、或者需要持续的生命周期刷新,就用第二种!
- ✅
UIManager(UI 管理):需要找 Canvas、需要生成 UI 预制体,必须用 MonoBehaviour。 - ✅
AudioManager(声音管理):身上需要挂载AudioSource组件来播放声音,必须依附于 GameObject。 - ✅
GameManager(游戏主流程/时间控制):需要用到Update和协程。
最佳实战法则:
你的 JsonMgr 写得非常标准且完美,继续保持第一种写法;
你的 UIManager 也是标准的 Unity 做法。这两个代码在同一个游戏项目中并存,才是真正的高手架构!
monobehaviour脚本是否可以用第一种?
绝对不行!这是一个极其危险的“天坑”!
如果你让一个继承了 MonoBehaviour 的脚本(比如你的 UIManager)使用这 3 行代码,Unity 会直接向你疯狂报错,游戏根本跑不起来!
这正是为什么在 Unity 里,单例模式分成了“纯 C# 派”和“MonoBehaviour 派”的最根本原因。
我们来看看为什么带 MonoBehaviour 的脚本绝对不能这么写:
一:MonoBehaviour 绝对不能用 new!
在那 3 行代码里,最核心的是第一句:private static JsonMgr instance = new JsonMgr();
- 在纯 C# 里:用
new凭空创造一个对象是天经地义的。 - 在 Unity 里:只要你的类继承了
MonoBehaviour,它就变成了一个**“组件(挂件)”**。Unity 底层有着铁一般的纪律——组件绝对不能凭空产生,它必须依附在一个实实在在的GameObject(游戏物体)身上!
如果你敢写 new UIManager(),Unity 的控制台会立刻弹出一长串红色的警告:
❌ Warning: MonoBehaviour instances must be instantiated with GameObject.AddComponent
() instead of new!
(警告:MonoBehaviour 实例必须通过 AddComponent 来添加,绝对不能用 new!)
二:MonoBehaviour 不能写构造函数!
在那 3 行代码里,第三句是防克隆的保安:private JsonMgr(){}
- 在纯 C# 里:构造函数是对象诞生的第一步。
- 在 Unity 里:Unity 的底层引擎(C++ 写的)有一套自己的“造物法则”。当 Unity 实例化一个物体时,它会强行绕过你写的构造函数。
- 对于
MonoBehaviour来说,它的“出生证明”不是构造函数,而是Awake()! - 如果你强行给一个
MonoBehaviour写了构造函数(不管是 public 还是 private),不但起不到保护作用,还会严重干扰 Unity 的序列化系统,导致各种莫名其妙的 Bug 甚至编辑器崩溃。
💡 总结
现在你终于知道为什么我要给你列出两种单例写法了吧?
对于不带 MonoBehaviour 的纯 C# 类(自由人):
它们不受 Unity 场景的管辖,所以你必须自己当上帝:自己用new创造它,自己用private 构造函数保护它。这就是那 3 行极简代码的由来。对于带 MonoBehaviour 的类(受 Unity 管控的打工人):
你不能用new,也不能写构造函数。你只能乖乖遵守 Unity 的规矩:- 你必须把它挂载到一个
GameObject上。 - 你必须在 Unity 赐予你的生命周期
Awake()里,通过判断Instance == null来抢占单例的宝座。 - 必须用
Destroy(this.gameObject)来干掉后来的冒充者。
- 你必须把它挂载到一个
这就是为什么:
你的 JsonMgr(纯 C#)用的是最优雅的 3 行代码。
你的 UIManager(MonoBehaviour)必须在 Awake 里写 if (Instance == null)。
这两种写法,就是针对这两种不同身份的“完美解法”!


















