18.Unity进阶C#知识补充总结

18.总结


18.1 知识点

总结主要内容

注意


18.2 核心要点速览

Unity跨平台基本原理

Mono跨平台

  • 原理:C#代码经Mono C#编译器(mcs)编译为IL中间代码,Mono VM转译为操作系统原生代码运行。
  • 优缺点:跨语言和跨平台,但有一定性能和包大小问题。

IL2CPP跨平台

  • 原理:C#代码经Mono C#编译器编译为IL中间代码,Unity用IL2CPP.exe转译为C++代码,各平台C++编译器编译为原生汇编代码,IL2CPP VM运行管理。
  • 优缺点:效率高,包小,但存在类型裁剪和泛型问题。

IL2CPP打包问题及解决方案

类型裁剪问题

  • 问题:可能意外剪裁类型,运行时抛出找不到类型异常。
  • 解决方案:设置PlayerSetting中Managed Stripping Level为Low;使用link.xml告诉引擎哪些类型不能剪裁。

泛型问题

  • 问题:未在打包前显式使用泛型类型,运行时使用会报错。
  • 解决方案:声明类并定义public泛型类变量;编写静态方法并调用泛型方法。

.Net API兼容级别

兼容级别 特点 使用建议
.Net 4.x 具备完整.Net API,含部分无法跨平台API 针对Windows平台且使用.Net Standard 2.0没有的功能时使用
.Net Standard 2.0 .Net标准API集合,内容少,减小可执行文件大小,跨平台支持好 建议使用

各Unity版本与C#版本对应关系

Unity版本 支持的C#版本
Unity 2021.2 C# 9
Unity 2020.3 C# 8
Unity 2019.4 C# 7.3
Unity 2017 C# 6
Unity 5.5 C# 4

命名参数、可选参数和动态类型

命名参数(Named Parameters)

用法 语法 说明 Unity 场景
调用时指定参数名 方法(参数名: 值) 跳过参数顺序,按名赋值 Debug.DrawLine(start: transform.position, end: target.position)
混合位置参数 方法(位置参数, 命名参数: 值) 位置参数需在前,命名参数在后 Raycast(origin, direction: Vector3.forward, layerMask: 1<<8)

优势

  1. 防顺序错误
  2. 自注释代码(复杂参数列表如射线检测的 distance 和 layerMask)

可选参数(Optional Parameters)

用法 语法 说明 Unity 场景
声明默认值 void Method(int a = 0) 参数可省略,调用时不传则用默认值 void Save(string path = "save.dat")
跳过中间默认值 方法(值, 命名参数: 值) 配合命名参数,跳过中间默认值 Test2(i: 1, s: "自定义")(跳过 bool b)

注意

  • 默认值需为编译时常量
  • 公有方法修改默认值会破坏二进制兼容性(扩展功能时用命名参数替代默认值修改)

动态类型(dynamic)

核心 说明 Unity 限制
作用 绕过编译时类型检查,运行时解析成员(类似 object,但延迟绑定) 反射简化(dynamic obj = Activator.CreateInstance(type)
语法 dynamic dyn = GetDynamicObject();dyn.Method(); 与 COM/Python 交互(Unity 极少使用)
优势 1. 减少反射代码量
2. 动态数据处理(如 JSON 反序列化)
临时脚本交互(非 Unity 主流场景)

Unity 注意

  • IL2CPP 不支持(必须用 Mono 脚本后端)
  • 无智能提示,易拼写错误(打包时需切换为 Mono,且避免在核心逻辑中使用)

慎用场景

  1. COM 对象交互
  2. 动态语言桥接(如 IronPython)
  3. 反射简化(优先用 interface 或 object 替代,仅在无法避免时使用)

线程和线程池

线程基础(Thread)

特性 说明 Unity 约束
多线程支持 Unity 允许创建多线程,但子线程禁止访问主线程对象(如 Transform、GameObject) 子线程操作 Unity 对象会报错,需通过 UnityMainThreadDispatcher 切换
线程生命周期 手动创建的线程需在 OnDestroy 中调用 Abort() 终止,否则编辑器关闭才结束 避免内存泄漏和性能浪费

线程池(ThreadPool)

方法 功能 参数 返回值 是否受设置影响 Unity 注意事项
GetAvailableThreads 获取当前可用的工作线程数和 I/O 线程数(不受人为设置影响) out int 工作线程, out int I/O 线程 无(通过 out 参数返回) ❌ 系统动态计算 反映当前系统负载,可用于监控但不可依赖(如动态调整任务量)
SetMaxThreads 设置线程池最大活动的工作线程和 I/O 线程数(超过则任务排队) int 工作线程最大值, int I/O 线程最大值 bool(设置是否成功) ✅ 影响后续 GetMax 结果 Unity 中建议保守设置(如 20, 20),避免过度消耗资源导致主线程卡顿
GetMaxThreads 获取当前线程池最大活动的工作线程和 I/O 线程数(受 SetMax 影响) out int 工作线程, out int I/O 线程 无(通过 out 参数返回) ✅ 反映最后一次 SetMax 用于验证设置是否生效(如打包后环境变化可能导致设置失败)
SetMinThreads 设置线程池最小保留的工作线程和 I/O 线程数(空闲时也保留) int 工作线程最小值, int I/O 线程最小值 bool(设置是否成功) ✅ 影响线程池初始化 Unity 中慎用(保留过多线程可能增加内存开销)
GetMinThreads 获取线程池最小保留的工作线程和 I/O 线程数(受 SetMin 影响) out int 工作线程, out int I/O 线程 无(通过 out 参数返回) ✅ 反映最后一次 SetMin 用于调试线程池初始化策略(如确保最低并发能力)
QueueUserWorkItem 将任务排入线程池,空闲线程自动执行(顺序不可控) Action 任务委托, object 状态参数(可选) bool(入队是否成功) ❌ 仅受线程池负载影响 禁止在任务中访问 Unity 对象(如 GameObject),需通过 UnityMainThreadDispatcher 切换

Task任务类

Task 概述

项目 详情
命名空间 System.Threading.Tasks
本质 基于线程池优点对线程 Thread 的封装,一个 Task 对象可看作一个线程
优势 具备线程池节约性能优点,可带返回值、执行后续操作、更方便取消任务

Task 创建方法

创建方式 无返回值 Task 有返回值 Task 说明
new Task 对象 Task task = new Task(Action action); task.Start(); Task<T> task = new Task<T>(Func<T> function); task.Start(); 手动创建并启动,创建后需调用 Start 方法
Task.Run 静态方法 Task task = Task.Run(Action action); Task<T> task = Task.Run<T>(Func<T> function); 自动启动,更简洁
Task.Factory.StartNew 静态方法 Task task = Task.Factory.StartNew(Action action); Task<T> task = Task.Factory.StartNew<T>(Func<T> function); 自动启动,功能与 Task.Run 类似

获取返回值

方法 语法 注意事项
Task.Result 属性 T result = task.Result; 获取结果时会阻塞线程,若任务未完成,会等待任务完成

同步执行 Task

方法 语法 注意事项
Task.RunSynchronously 方法 Task task = new Task(Action action); task.RunSynchronously(); 需使用 new Task 对象方式,Run 和 StartNew 创建时就启动,无法用于同步执行

任务阻塞方法

方法 语法 说明
Task.Wait 方法 task.Wait(); 等待任务执行完毕,再执行后续代码
Task.WaitAny 静态方法 Task.WaitAny(Task[] tasks); 传入任务中任意一个任务结束就继续执行后续代码
Task.WaitAll 静态方法 Task.WaitAll(Task[] tasks); 任务列表中所有任务执行结束才继续执行后续代码

任务延续方法

方法 语法 说明
Task.WhenAll + Task.ContinueWith Task.WhenAll(Task[] tasks).ContinueWith(Task continuationAction); 传入任务全部完成后执行后续任务
Task.Factory.ContinueWhenAll 方法 Task.Factory.ContinueWhenAll(Task[] tasks, Action<Task[]> continuationAction); 传入任务全部完成后执行后续任务
Task.WhenAny + Task.ContinueWith Task.WhenAny(Task[] tasks).ContinueWith(Task continuationAction); 传入任务中任意一个完成后执行后续任务
Task.Factory.ContinueWhenAny 方法 Task.Factory.ContinueWhenAny(Task[] tasks, Action<Task> continuationAction); 传入任务中任意一个完成后执行后续任务

取消 Task 执行方法

方法 说明
布尔标识控制 如使用 bool isRuning 控制线程内死循环的结束
CancellationTokenSource 类 可实现延迟取消、取消回调等功能,通过 Cancel 方法取消任务,Token.Register 注册取消回调

异步方法

同步与异步基础概念

类型 定义 特点
同步方法 当一个方法被调用时,调用者需要等待该方法执行完毕后返回才能继续执行。 顺序执行,调用者需等待结果,可能造成线程阻塞。
异步方法 当一个方法被调用时立即返回,并获取一个线程执行该方法内部的逻辑,调用者不用等待该方法执行完毕。 调用后立即返回,不阻塞调用者线程,可提高程序运行效率。

异步编程适用场景

  1. 复杂逻辑计算:如复杂的寻路算法,避免阻塞主线程。
  2. 网络下载、网络通讯:网络操作耗时不确定,异步执行让主线程继续处理其他任务。
  3. 资源加载:如加载大型图片、音频、视频等,提高程序响应速度。

async 和 await 关键字

关键字 作用 使用说明
async 用于修饰函数、lambda 表达式、匿名函数。 - 异步方法中建议使用 await 关键字,否则异步方法会以同步方式执行。
- 异步方法名称建议以 Async 结尾。
- 异步方法的返回值只能是 void、Task、Task<>。
- 异步方法中不能声明使用 ref 或 out 关键字修饰的变量。
await 用于在函数中和 async 配对使用,等待某个逻辑结束。 - 遇到 await 关键字时,异步方法将被挂起,将控制权返回给调用者。
- 当 await 修饰内容异步执行结束后,继续通过调用者线程执行后面内容。

示例代码分析

普通异步方法
public async void TestAsync()
{
    print("进入异步方法");
    await Task.Run(() =>
    {
        print("异步方法Task内");
        Thread.Sleep(5000); // 在异步任务中,线程暂停5秒钟(模拟耗时操作)
    });
    print("异步方法后面的逻辑");//这句会在五秒后才打印
}

TestAsync();
print("主线程逻辑执行");

执行流程:调用 TestAsync 方法后,打印“进入异步方法”,遇到 await 时,将 Task.Run 中的任务放入线程池执行,控制权返回给调用者,主线程继续执行“主线程逻辑执行”。5 秒后,Task.Run 中的任务执行完毕,继续执行 TestAsync 方法中 await 后面的代码,打印“异步方法后面的逻辑”。

复杂逻辑计算
public async void CalcPathAsync(GameObject obj, Vector3 endPos)
{
    print("开始处理寻路逻辑");
    int value = 10;
    await Task.Run(() =>
    {
        print("处理复杂逻辑计算");
        Thread.Sleep(1000); // 处理复杂逻辑计算,模拟耗时操作
        value = 50;
        // 不能在多线程里访问 Unity 主线程场景中的对象
        // print(obj.transform.position);
    });
    print("寻路计算完毕 处理逻辑" + value);//这句会在一秒后才打印
    obj.transform.position = Vector3.zero;
}

CalcPathAsync(this.gameObject, Vector3.zero);

执行流程:调用 CalcPathAsync 方法,打印“开始处理寻路逻辑”,遇到 await 时,将 Task.Run 中的任务放入线程池执行,控制权返回给调用者。1 秒后,Task.Run 中的任务执行完毕,继续执行 CalcPathAsync 方法中 await 后面的代码,打印“寻路计算完毕 处理逻辑50”,并将对象的位置设置为 Vector3.zero。

新增语法

C# 6 新增功能和语法

功能和语法 描述 示例
=> 运算符(表达式体成员) 用于简化方法和属性的定义,让代码更简洁 public int Add(int a, int b) => a + b;
public string FullName => $“{FirstName} {LastName}”;
Null 传播器(?.) 在访问对象成员之前检查对象是否为 null,避免 NullReferenceException 异常 string name = person?.Name;
字符串内插($) 在字符串中直接嵌入表达式,无需使用 string.Format 方法 string name = “John”; int age = 30;
string message = $“My name is {name} and I’m {age} years old.”;
静态导入 使用 using static 可以无需指定类型名称即可访问其静态成员和嵌套类型 using static System.Math;
double result = Sqrt(16);
异常筛选器 在 catch 语句后使用 when 关键字来筛选异常,根据不同的异常条件进行不同的处理 try { /* 可能抛出异常的代码 */ } catch (Exception ex) when (ex.Message.Contains(“特定错误信息”)) { /* 处理特定异常 */ } catch (Exception ex) { /* 处理其他异常 */ }
nameof 运算符 可以将变量、类型、成员等的名称转换为字符串 int number = 10;
string variableName = nameof(number);

C# 7 新增功能和语法

功能和语法 描述 示例
字面值改进 可以在数值之间插入 _ 作为分隔符,方便查看数值 int largeNumber = 1_000_000;
out 参数相关和弃元知识点 out 变量可直接在函数参数中声明;使用 _ 作为占位符忽略不需要的 out 参数 if (int.TryParse(“123”, out int result)) { /* 使用 result */ }
int.TryParse(“123”, out _);
ref 返回值 函数可以返回引用类型,调用者可通过返回的引用修改原数据 public ref int Find(int[] array, int value) { /* 查找逻辑 */ return ref array[index]; }
本地函数 在函数内部声明临时函数,只能在声明该函数的函数内部使用,可使用父函数变量 public int TestLocal(int i) { bool b = false; i += 10; Calc(); print(b); return i; void Calc() { i += 10; b = true; } }
抛出表达式 可以在表达式中抛出异常,而不仅仅是在语句中 string name = null; string result = name?? throw new ArgumentNullException(nameof(name));
元组 用于将多个值组合成一个单一的对象,无需定义新的类型 (string name, int age) person = (“John”, 30);
Console.WriteLine($“{person.name} is {person.age} years old.”);
模式匹配 允许根据数据的类型和结构进行条件判断和处理 if (o is 1){ print(“o是1”); }
object obj = 123; if (obj is int number) { Console.WriteLine($“The number is {number}”); }
if (o is var v){print(o); }

C# 8 的新增功能和语法

功能和语法 描述 示例
静态本地函数 在本地函数前加 static,使其不能访问上层方法变量,避免逻辑混乱 public int CalcInfo(int i) { static void Calc(ref int i) { ... } ... }
Using 声明 using() 语法简写,函数执行完自动调用对象 Dispose 方法,对象需继承 System.IDisposable 接口 using StreamWriter sw = new StreamWriter("path");
Null 合并赋值 ??= 左侧为空时将右侧值赋给左侧变量 string s = null; s ??= "val";
解构函数 Deconstruct 可在自定义类中声明,用元组写法获取类对象变量,类中可有多个但参数数量不同 class Person { public void Deconstruct(out string n, out bool s) { ... } }

模式匹配增强功能

模式类型 描述 示例
switch 表达式 对有返回值的 switch 语句的缩写,用 => 代替 case:,用 _ 代替 default public Vector2 GetPos(PosType type) => type switch { PosType.Top_Left => new Vector2(0, 0), ... _ => new Vector2(0, 0) };
属性模式 在常量模式基础上判断对象属性,语法为 变量 is {属性:值, 属性:值} DiscountInfo info = new DiscountInfo("5折", true);
if (info is { discount: "6折", isDiscount: true }) print("信息相同");
元组模式 更简单地完成多个变量的判断,直接用元组进行判断 int ii = 10; bool bb = true;
if((ii, bb) is (11, true)) { print("元组的值相同"); }
位置模式 自定义类实现解构函数 Deconstruct 后,可用对应类对象与元组进行 is 判断 if (info is ("5折", true)) { print("位置模式 满足条件"); }

C# 9 新增功能和语法

功能和语法 描述 示例
Records (记录类型) 不可变引用类型,编译器自动生成常见方法,可通过 with 表达式创建新实例 public record Person(string FirstName, string LastName);
var p1 = new Person("John", "Doe");
var p2 = p1 with { LastName = "Smith" };
改进的模式匹配 支持记录类型的模式匹配,新增逻辑模式和常量模式 if (shape is Circle { Radius: > 0 } circle) { /*... */ }
switch (num) { case 0: /*... */ break; case int n when n > 0: /*... */ break; }
改进的模式组合 引入 and、or 和 not 运算符,简化复杂模式匹配 if (shape is Circle { Radius: > 0 } circle and not null) { /*... */ }
允许部分方法实现 部分方法声明和实现可分离,接口中定义部分方法,实现类可选择实现 public interface ILogger { partial void LogHeader(); void Log(string message); }
public class FileLogger : ILogger { public partial void LogHeader() { /*... */ } public void Log(string message) { /*... */ } }

C# 自带异常类

  • IndexOutOfRangeException:当一个数组的下标超出范围时运行时引发。
  • NullReferenceException:当一个空对象被引用时运行时引发。
  • ArgumentException:方法的参数是非法的。
  • ArgumentNullException:一个空参数传递给方法,该方法不能接受该参数。
  • ArgumentOutOfRangeException:参数值超出范围。
  • SystemException:其他用户可处理的异常的基本类。
  • OutOfMemoryException:内存空间不够。
  • StackOverflowException:堆栈溢出。
  • ArithmeticException:出现算术上溢或者下溢。
  • ArrayTypeMismatchException:试图在数组中存储错误类型的对象。
  • BadImageFormatException:图形的格式错误。
  • DivideByZeroException:除零异常。
  • DllNotFoundException:找不到引用的DLL。
  • FormatException:参数格式错误。
  • InvalidCastException:使用无效的类。
  • InvalidOperationException:方法的调用时间错误。
  • MethodAccessException:试图访问受保护的方法。
  • MissingMemberException:访问一个无效版本的DLL。
  • NotFiniteNumberException:对象不是一个有效的数字。
  • NotSupportedException:调用的方法在类中没有实现。

DateTime日期时间和TimeSpan时间跨度

DateTime

方面 描述 示例
概述 位于 System 命名空间,处理日期和时间的结构体,默认值和最小值是 0001 年 1 月 1 日 00:00:00,最大值是 9999 年 12 月 31 日 23:59:59 -
初始化 主要参数有年、月、日、时、分、秒、毫秒、ticks;次要参数有 DateTimeKind、Calendar DateTime dateTime = new DateTime(2022, 12, 1, 13, 30, 45, 500);
获取时间 DateTime.Now 返回当前本地日期和时间;DateTime.Today 返回今日日期;DateTime.UtcNow 返回当前 UTC 日期和时间 DateTime now = DateTime.Now;
DateTime today = DateTime.Today;
DateTime utcNow = DateTime.UtcNow;
计算时间 可通过 AddDays 等方法进行时间计算 DateTime newTime = now.AddDays(-1);
字符串输出 多种输出格式,也支持自定义 now.ToString("yyyy - MM - dd - ddd / HH - mm - ss");
字符串转 DateTime 字符串格式需为 “年/月/日 时:分:秒”,用 TryParse 转换 DateTime.TryParse("1988/5/4 18:00:08", out DateTime date);
存储时间 建议存储时间戳,通过 DateTimeOffset 获取 long timestamp = ((DateTimeOffset)DateTime.Now).ToUnixTimeSeconds();

TimeSpan

方面 描述 示例
概述 位于 System 命名空间,时间跨度结构体,可由两个 DateTime 对象相减得到 TimeSpan ts = DateTime.Now - new DateTime(1970, 1, 1);
初始化 可指定天、时、分、秒等初始化 TimeSpan ts2 = new TimeSpan(1, 0, 0, 0);
相互计算 可进行加减运算 TimeSpan ts4 = ts2 + new TimeSpan(0, 1, 1, 1);
常量 有 TicksPerSecond 等常量用于计算 ts4.Ticks / TimeSpan.TicksPerSecond;

正则表达式

方法 描述 代码示例
public bool IsMatch( string input ) 指示 Regex 构造函数中指定的正则表达式是否在指定的输入字符串中找到匹配项 var regex = new Regex(@"\d"); bool isMatch = regex.IsMatch("abc123");
public bool IsMatch( string input, int startat ) 指示 Regex 构造函数中指定的正则表达式是否在指定的输入字符串中找到匹配项,从字符串中指定的开始位置开始 var regex = new Regex(@"\d"); bool isMatch = regex.IsMatch("abc123", 3);
public static bool IsMatch( string input, string pattern ) 指示指定的正则表达式是否在指定的输入字符串中找到匹配项 bool isMatch = Regex.IsMatch("abc123", @"\d");
public MatchCollection Matches( string input ) 在指定的输入字符串中搜索正则表达式的所有匹配项 var regex = new Regex(@"\d"); var matches = regex.Matches("abc123");
public string Replace( string input, string replacement ) 在指定的输入字符串中,把所有匹配正则表达式模式的所有匹配的字符串替换为指定的替换字符串 var regex = new Regex(@"\d"); string result = regex.Replace("abc123", "X");
public string[] Split( string input ) 把输入字符串分割为子字符串数组,根据在 Regex 构造函数中指定的正则表达式模式定义的位置进行分割 var regex = new Regex(@"\d"); string[] parts = regex.Split("abc123");


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏