一、基础语法
1.1 变量与数据类型
C# 是强类型语言,所有变量都有明确的类型。
值类型:直接存储数据,分配在栈上。
1
2
3
4
5
6
| int age = 25; // 整型
float price = 19.99f; // 单精度浮点(需加 f 后缀)
double pi = 3.14159; // 双精度浮点
bool isActive = true; // 布尔值
char grade = 'A'; // 单个字符
decimal salary = 5000.00m; // 高精度十进制(财务计算用,需加 m 后缀)
|
引用类型:存储数据的引用(地址),数据在堆上。
1
2
3
| string name = "张三"; // 字符串(不可变)
int[] numbers = { 1, 2, 3 }; // 数组
object obj = "任何类型"; // 所有类型的基类
|
类型转换:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 隐式转换(小范围 → 大范围)
int x = 10;
long y = x;
// 显式转换(大范围 → 小范围,可能丢失精度)
double d = 3.14;
int i = (int)d; // i = 3
// Convert 类(字符串 → 数值)
string text = "123";
int num = Convert.ToInt32(text);
// TryParse(安全转换,不会抛异常)
if (int.TryParse("456", out int result))
Console.WriteLine(result);
|
1.2 运算符
C# 提供丰富的运算符用于数据处理。需要特别注意整数除法的截断行为和 null 条件运算符。
算术运算符:+ - * / %。整数相除会截断小数部分,取余 % 获取除法余数。
比较运算符:== != < > <= >=,返回 bool 值。
逻辑运算符:&& || !。&& 存在短路特性,左侧为 false 时右侧不执行。
null 处理运算符:
??(null 合并):左侧不为 null 则取左侧,否则取右侧?.(null 条件访问):左侧为 null 时不执行后续操作,返回 null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| int a = 10, b = 3;
// 算术运算
int sum = a + b; // 13
int div = a / b; // 3,整数除法结果截断
int mod = a % b; // 1,取余
// 比较运算
bool eq = a == b; // false
bool ne = a != b; // true
// 逻辑运算
bool and = true && false; // false,&& 左侧 false 则右侧不执行
bool or = true || false; // true
bool not = !true; // false
// null 处理运算符
string name = null;
string display = name ?? "默认值"; // name 为 null,取"默认值"
int? length = name?.Length; // name 为 null,length 也为 null
// 三元运算符
string result = a > b ? "更大" : "更小"; // 条件 ? 真值 : 假值
|
1.3 流程控制
C# 提供四种控制结构。if/else 用于范围判断,switch 用于多分支匹配,for/foreach/while 用于循环。
if/else:条件为 true 时执行对应分支,支持 else if 链式判断。
switch 语句:传统 switch 需要 break 跳出,C# 7+ 支持模式匹配可直接匹配类型。
switch 表达式:C# 8+ 新语法,用 => 取代 case:,更简洁,必须覆盖所有情况。
循环三兄弟:
for:知道次数时使用,需初始化、条件、迭代三部分foreach:遍历集合元素,无需关心索引,不能修改集合结构while:条件满足时重复执行,适合不确定次数的场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| // if/else
int score = 85;
if (score >= 90)
Console.WriteLine("优秀");
else if (score >= 60)
Console.WriteLine("及格");
else
Console.WriteLine("不及格");
// switch 语句(支持模式匹配)
string status = "admin";
switch (status)
{
case "admin":
Console.WriteLine("管理员");
break;
case "user":
Console.WriteLine("普通用户");
break;
default:
Console.WriteLine("未知");
break;
}
// switch 表达式(C# 8+ 新语法,更简洁)
string level = status switch
{
"admin" => "管理员",
"user" => "普通用户",
_ => "未知" // _ 表示默认情况
};
// for 循环(已知次数时使用)
for (int i = 0; i < 5; i++)
Console.WriteLine(i);
// foreach 循环(遍历集合,只读)
int[] arr = { 1, 2, 3 };
foreach (var item in arr)
Console.WriteLine(item);
// while 循环(不确定次数时使用)
int n = 0;
while (n < 3)
{
Console.WriteLine(n);
n++;
}
|
1.4 字符串操作
C# 中 string 是引用类型,但具有不可变性——每次修改都会创建新对象。大量拼接时要用 StringBuilder。
字符串不可变:str += "x" 不是修改原字符串,而是创建新对象并让变量指向它。频繁操作会导致大量临时对象。
字符串插值:$"Hello {name}" 比 + 拼接更可读,推荐用于少量拼合。
StringBuilder:当循环内大量拼接时,用 StringBuilder 避免创建中间对象,显著提升性能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| string s1 = "Hello";
string s2 = "World";
// 拼接字符串
string result = s1 + " " + s2; // Hello World,产生新字符串
string result2 = $"{s1} {s2}"; // 字符串插值,推荐写法
// StringBuilder(大量拼接时使用,避免 GC 压力)
var sb = new StringBuilder();
for (int i = 0; i < 100; i++)
sb.Append(i).Append(", "); // 不会创建临时字符串
string big = sb.ToString();
// 常用方法
string text = " Hello World ";
text.Length; // 获取长度
text.Trim(); // 去除首尾空格
text.ToLower(); // 转小写
text.ToUpper(); // 转大写
text.Contains("World"); // 判断是否包含子串
text.StartsWith("Hello"); // 判断是否以指定字符开头
text.Replace("World", "C#"); // 替换子串
text.Split(' '); // 按字符分割为数组
text.Substring(2, 5); // 取子串,从索引 2 开始取 5 个字符
|
二、面向对象(OOP)
2.1 类与对象
类是 C# 面向对象的基本单位,包含字段、属性、方法、构造函数四种核心成员。
字段 vs 属性:字段是存储数据的变量,属性是对字段的封装(get/set),外部应通过属性访问而不是直接操作字段。自动属性 { get; set; } 由编译器自动生成背后字段。
构造函数:与类同名,创建对象时调用,用于初始化成员。如果不写,编译器提供无参默认构造。
静态成员:属于类本身而非实例,通过类名.成员名直接调用,所有实例共享一份。
只读属性:只提供 get 不提供 set,或如例中 { get; private set; } 外部只读。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| // 定义 Person 类
public class Person
{
// 字段(私有,外部不可访问)
private string name;
// 属性(推荐用属性代替公开字段)
public string Name { get; set; } // 自动属性,编译器生成背后字段
public int Age { get; private set; } // 外部只能读,内部可设置
// 表达式体属性(只读)
public bool IsAdult => Age >= 18;
// 构造函数(创建对象时初始化)
public Person(string name, int age)
{
Name = name; // 通过属性赋值
Age = age;
}
// 实例方法
public void Introduce()
{
Console.WriteLine($"我是{Name},{Age}岁");
}
// 静态成员(属于类,不属于对象)
public static string Species = "人类";
}
// 使用类
var p = new Person("张三", 25); // new 创建实例,调用构造函数
p.Introduce(); // 调用实例方法
Console.WriteLine(Person.Species); // 通过类名访问静态成员
|
2.2 封装
封装是 OOP 三大特性之一,将数据和行为绑定,对外隐藏内部细节,通过访问修饰符控制外部访问权限。
封装的目的:保护数据不被外部随意修改,通过方法提供受控的操作入口。
属性访问器:
get 读取时执行,可包含逻辑(如计算字段)set 写入时执行,参数 value 表示传入的值- 可单独限制
get/set 的访问级别(如 private set)
带验证的属性:在 set 中添加检查逻辑,确保数据合法性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| public class BankAccount
{
// 私有字段(外部无法直接访问 balance)
private decimal balance;
// 属性:提供受控的访问入口
public decimal Balance
{
get { return balance; } // 读取时直接返回字段
private set { balance = value; } // 写入权限限制为私有,外部不能改
}
// 带验证的属性
private string accountNumber;
public string AccountNumber
{
get => accountNumber; // 表达式体 get
set
{
if (value.Length != 10)
throw new ArgumentException("账号必须10位");
accountNumber = value;
}
}
// 通过公开方法暴露可受控的操作
public void Deposit(decimal amount)
{
if (amount <= 0) return; // 业务规则校验
balance += amount;
}
}
|
2.3 继承
继承让子类复用父类的成员,形成层次关系。C# 支持单继承(一个类只能有一个父类),但支持多层传递。
关键字:
virtual:父类中标记可被子类重写的方法override:子类重写父类的 virtual 方法base:在子类中调用父类构造函数或方法sealed:阻止进一步继承(类前加 sealed 则不可被继承)
构造链:创建子类对象时,先调用父类构造函数(通过 base),再执行子类构造函数。如果不写 base,隐式调用父类的无参构造。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| // 基类
public class Animal
{
public string Name { get; set; }
public Animal(string name) // 父类构造函数
{
Name = name;
}
// virtual 标记:子类可重写此方法
public virtual void Speak()
{
Console.WriteLine("..."); // 默认实现
}
}
// Dog 继承 Animal
public class Dog : Animal
{
// base(name) 调用父类构造函数,传入 name
public Dog(string name) : base(name) { }
// override 重写父类的 Speak 方法
public override void Speak()
{
Console.WriteLine("汪汪!");
}
}
// sealed 类禁止被继承
public sealed class Cat : Animal
{
public Cat(string name) : base(name) { }
public override void Speak()
{
Console.WriteLine("喵~");
}
}
// class Tiger : Cat { } // 编译错误,Cat 是 sealed
|
2.4 多态
多态指同一操作在不同对象上产生不同行为。C# 中分为两种:
方法重载(编译时多态):同类中同名方法但参数不同,编译器根据参数类型和数量决定调用哪个版本。与返回值无关。
方法重写(运行时多态):通过 virtual/override 实现,运行时根据对象实际类型调用对应方法,而非引用类型。
抽象类 vs 接口:
- 抽象类:用
abstract 标记,可包含实现和字段,用于"是什么"(is-a 关系) - 接口:纯抽象契约,只能声明方法,可多实现,用于"能做什么"(can-do 关系)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| // 方法重载:同名方法,参数不同
public class Calculator
{
public int Add(int a, int b) => a + b; // 两个 int
public double Add(double a, double b) => a + b; // 两个 double
public int Add(int a, int b, int c) => a + b + c; // 三个 int
}
// 抽象类:包含未实现的抽象方法
public abstract class Shape
{
public abstract double GetArea(); // 抽象方法,子类必须 override
}
// 接口:纯方法声明
public interface IDrawable
{
void Draw(); // 接口方法隐式 public abstract
}
// 一个类可同时继承抽象类并实现多个接口
public class Circle : Shape, IDrawable
{
public double Radius { get; set; }
public override double GetArea() => Math.PI * Radius * Radius;
public void Draw() => Console.WriteLine("画圆形");
}
|
2.5 泛型
泛型允许在定义类/方法时将类型作为参数,避免为每种类型重复编写代码,同时保持类型安全。
为什么要用泛型:没有泛型时集合只能存 object,导致装箱拆箱和运行时类型错误。泛型在编译时确定类型,无需转换。
泛型类:用 <T> 声明类型参数,类内部所有使用 T 的地方都会被实际类型替换。
泛型方法:可单独给方法指定类型参数,不依赖所在类是否泛型。
泛型约束 where:限制类型参数必须具备的能力(如实现某接口、有无参构造)。
协变 out 与逆变 in:解决泛型接口的类型兼容问题。out 表示返回值类型(只出),in 表示参数类型(只进)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| // 泛型类:T 是类型参数
public class Stack<T>
{
private T[] items = new T[10];
private int index = 0;
public void Push(T item) => items[index++] = item;
public T Pop() => items[--index];
}
// 使用:Stack<int> intStack = new Stack<int>(); // T 被替换为 int
// 泛型方法
public T Max<T>(T a, T b) where T : IComparable // T 必须实现 IComparable
{
return a.CompareTo(b) > 0 ? a : b;
}
// 泛型约束
public class Repository<T> where T : class, new() // T 必须是引用类型且有无参构造
{
public T Create() => new T(); // 因为 new() 约束,可安全调用 new T()
}
// 协变 out:T 只能出现在返回值(只读)
public interface IProducer<out T> { T Produce(); }
// IProducer<Dog> 可视为 IProducer<Animal>(Dog 是 Animal 的子类)
// 逆变 in:T 只能出现在参数(只写)
public interface IConsumer<in T> { void Consume(T item); }
// IConsumer<Animal> 可视为 IConsumer<Dog>(取 Animal 的消费者也能消费 Dog)
|
三、核心特性
3.1 委托与事件
委托是方法的类型安全引用,可以将方法当作变量传递。C# 提供预定义泛型委托 Action(无返回值)和 Func(有返回值),大部分场景无需自定义委托。
多播委托:同一个委托变量可通过 += 绑定多个方法,调用时会按注册顺序依次执行。
事件:基于委托的发布-订阅模式。和委托的关键区别是——事件只能在声明类内部 Invoke,外部只能 += / -=,更安全。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| // 自定义委托(定义方法的"签名")
public delegate void LogHandler(string message);
// 内置委托:无需自定义,直接用 Action/Func
Action<string> log = msg => Console.WriteLine(msg); // Action 无返回值
Func<int, int, int> add = (a, b) => a + b; // Func<in, in, out> 有返回值
// 多播委托:一个委托绑定多个方法
LogHandler handler = Console.WriteLine; // 第一个方法
handler += msg => File.WriteAllText("log.txt", msg); // 再追加一个
handler("执行完毕"); // 两个方法依次执行
// 事件:类内部触发,外部只能订阅/取消
public class Button
{
public event EventHandler Clicked; // 声明事件
public void Click()
{
Console.WriteLine("按钮被点击");
Clicked?.Invoke(this, EventArgs.Empty); // 只能在类内部触发
}
}
// 使用事件
var btn = new Button();
btn.Clicked += (sender, args) => Console.WriteLine("事件处理"); // 订阅
btn.Click(); // 触发事件
btn.Clicked -= (sender, args) => Console.WriteLine("事件处理"); // 取消订阅
// btn.Clicked?.Invoke(null, null); // 编译错误!事件不能在外部触发
|
3.2 Lambda 与 LINQ
Lambda 表达式:匿名函数的简洁写法。格式为 (参数) => 表达式,常用于 LINQ 查询和委托参数。
LINQ(Language Integrated Query):C# 内置的集合查询能力,两种写法:
- 方法语法:通过链式调用扩展方法(
Where、Select、OrderBy 等),更常用 - 查询语法:类似 SQL 的
from...where...select,编译器会转换为方法语法
延迟执行:LINQ 查询不会立即执行,只有在遍历结果时(如 foreach、.ToList())才真正运算,多次遍历会多次执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| // Lambda 表达式
Func<int, int> square = x => x * x; // 单参数,省略括号
Func<int, int, bool> equals = (x, y) => x == y; // 多参数,需要括号
// LINQ 方法语法
int[] numbers = { 5, 2, 8, 1, 9, 3 };
var evenNumbers = numbers.Where(n => n % 2 == 0); // 筛选偶数 → [2, 8]
var sorted = numbers.OrderBy(n => n); // 升序排序
var top3 = numbers.OrderByDescending(n => n).Take(3); // 前3大 → [9, 8, 5]
var sum = numbers.Sum(); // 求和 → 28
var hasBig = numbers.Any(n => n > 7); // 是否有 >7 的元素 → true
// LINQ 查询语法(类 SQL,编译器转为方法语法)
var query = from n in numbers
where n > 3
orderby n descending
select n;
// 等价于:numbers.Where(n => n > 3).OrderByDescending(n => n)
// 实际应用:对象集合复杂查询
var students = new List<Student>
{
new Student { Name = "张三", Score = 85 },
new Student { Name = "李四", Score = 92 },
new Student { Name = "王五", Score = 67 }
};
var passed = students
.Where(s => s.Score >= 60) // 筛选及格学生
.OrderByDescending(s => s.Score) // 按分数降序
.Select(s => $"{s.Name}: {s.Score}"); // 投影为字符串
// 结果:["李四: 92", "张三: 85", "王五: 67"]
|
3.3 异常处理
异常是程序运行时的错误,C# 通过 try-catch-finally 机制处理。原则是捕获你能处理的异常,不要吞掉不该吞的错误。
try-catch-finally 流程:
try:包裹可能出错的代码catch:捕获并处理特定类型的异常,可按异常类型分层捕获(子类在前,父类在后)finally:无论是否抛出异常都执行,通常用于释放资源
using 语句:语法糖,等价于 try-finally + Dispose()。用于确保 IDisposable 对象(文件流、数据库连接等)被及时释放。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| try
{
int x = 10;
int y = 0;
int result = x / y; // 抛出 DivideByZeroException
}
catch (DivideByZeroException ex) // 先捕获具体异常
{
Console.WriteLine($"除零异常:{ex.Message}");
}
catch (Exception ex) // 再捕获所有其他异常
{
Console.WriteLine($"通用异常:{ex.Message}");
}
finally // 不管有没有异常都执行
{
Console.WriteLine("清理资源:关闭文件、断开连接等");
}
// using 语句:自动释放非托管资源
using (var reader = new StreamReader("file.txt"))
{
string content = reader.ReadToEnd();
} // 离开 using 块自动调用 reader.Dispose()
// using 声明(C# 8+):更简洁的写法
using var writer = new StreamWriter("log.txt");
writer.WriteLine("日志内容");
// 离开作用域时自动释放,无需缩进嵌套
|
3.4 异步编程
异步编程让程序在等待 I/O 操作时不阻塞线程,提升响应性和吞吐量。核心关键字 async 和 await。
async/await 原理:await 碰到未完成的操作时,立即返回调用方,待操作完成后从断点继续执行。期间线程不会被占用,可处理其他请求。
async 方法命名约定:方法名应以 Async 结尾(如 GetDataAsync)。
Task.WhenAll / WhenAny:
WhenAll:等待所有任务完成,并行执行多个异步操作WhenAny:任意一个完成就继续,可用于超时控制或最快响应
异步流 IAsyncEnumerable:C# 8+ 引入,用于异步地"逐个"产生数据,配合 await foreach 消费。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| // async/await 核心用法
public async Task<string> FetchDataAsync()
{
using var client = new HttpClient();
string result = await client.GetStringAsync("https://api.example.com");
// await 期间线程不会被阻塞,可处理其他请求
return result;
}
// 并行执行多个任务(比串行快一倍)
public async Task ProcessAllAsync()
{
var task1 = FetchDataAsync(); // 任务已开始执行
var task2 = FetchDataAsync(); // 第二个任务也开始了
// 等待所有任务完成
await Task.WhenAll(task1, task2);
// 或等待任意一个先完成(如竞态请求)
var first = await Task.WhenAny(task1, task2);
Console.WriteLine(first.Result);
}
// 异步 Lambda
Button clickHandler = async (sender, args) =>
{
await Task.Delay(1000); // 异步等待1秒,不阻塞 UI 线程
Console.WriteLine("一秒后执行");
};
// 异步流:逐个异步产生数据项
public async IAsyncEnumerable<int> GenerateNumbersAsync()
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(100); // 模拟异步延迟
yield return i; // 逐个返回
}
}
// 消费异步流
await foreach (var num in GenerateNumbersAsync())
Console.WriteLine(num); // 每100ms输出一个数字
|
四、进阶主题
4.1 反射与特性
反射:在运行时动态获取类型信息并操作对象的能力。通过 System.Reflection 命名空间中的类实现。常用于:ORM 框架、依赖注入容器、序列化工具、编辑器扩展。
Type:反射的核心类,代表一个类型。可通过 typeof() 或 .GetType() 获取。
Activator:动态创建类型的实例,不需要在编译时知道具体类型。
特性(Attribute):给代码元素(类、方法、属性等)附加元数据。可通过反射在运行时读取。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| using System.Reflection;
// 获取 Type 对象的两种方式
Type type = typeof(Person); // 编译时已知类型
// Type type = obj.GetType(); // 运行时获取对象类型
// 遍历类型的公开属性
foreach (var prop in type.GetProperties())
Console.WriteLine($"{prop.Name}: {prop.PropertyType}");
// 动态创建实例(无需编译时知道类型名)
object obj = Activator.CreateInstance(type);
// 动态调用方法
MethodInfo method = type.GetMethod("Introduce");
method.Invoke(obj, null); // 在 obj 上执行 Introduce()
// 自定义特性
[AttributeUsage(AttributeTargets.Class)] // 指定此特性可应用的目标
public class DisplayNameAttribute : Attribute
{
public string Name { get; }
public DisplayNameAttribute(string name) => Name = name;
}
// 使用自定义特性
[DisplayName("人员类")]
public class Employee { }
// 读取特性(运行时)
var attr = typeof(Employee).GetCustomAttribute<DisplayNameAttribute>();
Console.WriteLine(attr?.Name); // 输出 "人员类"
|
4.2 集合与数据结构
C# 集合分为泛型集合(System.Collections.Generic)和非泛型集合(System.Collections),优先使用泛型。
List:动态数组,支持索引访问,内部用数组实现,容量不足时自动2 倍扩容(将旧数据复制到新数组)。
Dictionary<K,V>:哈希表实现,键值对存储,查找 O(1)。内部通过 拉链法 解决哈希冲突,负载因子超 0.72 时自动扩容(2 倍质数,重哈希)。
HashSet:无序唯一集合,适合去重和集合运算(交集、并集、差集)。
Queue:先进先出,尾部入队、头部出队。适用于任务队列、BFS 算法。
Stack:后进先出,顶部压入、顶部弹出。适用于撤销操作、括号匹配、DFS 算法。
线程安全集合(System.Collections.Concurrent):ConcurrentBag、ConcurrentDictionary、ConcurrentQueue 等,支持多线程安全操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| // List:动态数组,索引访问
List<string> names = new List<string> { "张三", "李四" };
names.Add("王五"); // 添加
names.Remove("李四"); // 移除
names[0] = "赵六"; // 索引访问
// Dictionary:键值对,快速查找
Dictionary<string, int> scores = new()
{
["张三"] = 85,
["李四"] = 92
};
scores.TryGetValue("张三", out int score); // 安全取值,不存在返回 false
// HashSet:唯一元素集合
HashSet<int> set = new() { 1, 2, 3, 3, 3 }; // 重复元素自动忽略 → { 1, 2, 3 }
set.Add(4);
bool exists = set.Contains(2); // true,O(1) 查找
// Queue:先进先出
Queue<string> queue = new();
queue.Enqueue("第一个");
queue.Enqueue("第二个");
string first = queue.Dequeue(); // "第一个"(先入先出)
// Stack:后进先出
Stack<string> stack = new();
stack.Push("底部");
stack.Push("顶部");
string top = stack.Pop(); // "顶部"(后入先出)
// 线程安全集合
ConcurrentBag<int> bag = new();
Parallel.For(0, 100, i => bag.Add(i)); // 多线程安全添加
|
4.3 内存管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| // 值类型(struct)
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
// 值类型赋值是复制:Point p2 = p1; // 独立副本
// 引用类型(class)
// 引用类型赋值是共享引用:Person p2 = p1; // 指向同一对象
// 弱引用(允许 GC 回收)
WeakReference weakRef = new WeakReference(new Person("临时", 1));
// IDisposable 模式
public class ResourceHolder : IDisposable
{
private bool disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
// 释放托管资源
// 释放非托管资源
disposed = true;
}
}
}
|
4.4 文件与 IO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // 读写文本文件
string content = File.ReadAllText("input.txt"); // 全部读取
string[] lines = File.ReadAllLines("input.txt"); // 按行读取
File.WriteAllText("output.txt", "新内容"); // 覆盖写入
File.AppendAllText("log.txt", "追加内容"); // 追加写入
// Stream 方式(适合大文件)
using (var reader = new StreamReader("bigfile.txt"))
{
string line;
while ((line = reader.ReadLine()) != null)
Console.WriteLine(line);
}
// JSON 序列化(System.Text.Json)
var person = new Person { Name = "张三", Age = 25 };
string json = JsonSerializer.Serialize(person);
var restored = JsonSerializer.Deserialize<Person>(json);
// 传统 JSON 序列化(Newtonsoft.Json / Json.NET)
// string json = JsonConvert.SerializeObject(person);
// var restored = JsonConvert.DeserializeObject<Person>(json);
|
五、.NET 生态概要
5.1 常用框架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| // ASP.NET Core Web API 最小示例
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/hello", () => "Hello World");
app.MapPost("/user", (User user) => Results.Created($"/user/{user.Id}", user));
app.Run();
// Entity Framework Core(ORM)
// 定义实体
public class Blog
{
public int Id { get; set; }
public string Title { get; set; }
public List<Post> Posts { get; set; }
}
// 定义上下文
public class AppDbContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseSqlServer("连接字符串");
}
// 使用 EF Core
using var db = new AppDbContext();
var blogs = db.Blogs.Where(b => b.Title.Contains("C#")).ToList();
|
5.2 依赖注入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // 注册服务(ASP.NET Core 启动时)
builder.Services.AddSingleton<ILogger, FileLogger>(); // 单例(全局唯一)
builder.Services.AddScoped<IService, MyService>(); // 作用域(每次请求)
builder.Services.AddTransient<IHelper, MyHelper>(); // 临时(每次注入都创建)
// 构造函数注入
public class UserController
{
private readonly IUserService userService;
public UserController(IUserService userService)
{
this.userService = userService;
}
}
// 配置读取
public class MyConfig
{
public string ConnectionString { get; set; }
}
// 读取配置
var config = builder.Configuration
.GetSection("Database")
.Get<MyConfig>();
|
六、常见陷阱与最佳实践
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| // 1. foreach 中不要修改集合
var list = new List<int> { 1, 2, 3 };
// foreach (var i in list) list.Remove(i); // 抛异常!
for (int i = list.Count - 1; i >= 0; i--)
list.RemoveAt(i); // 正确方式
// 2. 字符串拼接用 StringBuilder
// 错误:大量循环中用 += 拼接
string s = "";
for (int i = 0; i < 10000; i++)
s += i.ToString(); // 性能极差!
// 正确:用 StringBuilder
var sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
sb.Append(i);
string result = sb.ToString();
// 3. 异步方法命名约定
// 方法名以 Async 结尾
public async Task<int> CalculateAsync() { ... }
// 4. 避免 async void(事件处理除外)
// 错误:public async void DoSomething() { ... }
// 正确:public async Task DoSomething() { ... }
// 5. null 检查
string name = GetName();
// 旧写法
if (name != null) Console.WriteLine(name.Length);
// 新写法(C# 6+)
Console.WriteLine(name?.Length);
// 或者用模式匹配(C# 9+)
if (name is not null) Console.WriteLine(name.Length);
|
七、推荐学习路线
| 阶段 | 内容 | 目标 |
|---|
| 基础 | 变量、控制流、方法、类 | 能写简单控制台程序 |
| 进阶 | 继承、接口、泛型、委托、LINQ | 理解面向对象和函数式编程 |
| 高级 | 异步、反射、DI、EF Core | 能开发 Web 应用 |
| 实战 | ASP.NET Core + EF Core | 独立完成全栈项目 |
本文是对 C# 各知识点的系统性梳理,适合有一定编程基础的同学快速查阅和回顾。代码示例均基于 .NET 6+ 和 C# 10+ 语法。
八、高频面试题(八股文)
8.1 重载和重写的区别
| 重载(Overload) | 重写(Override) |
|---|
| 发生位置 | 同一个类中 | 基类和派生类之间 |
| 方法名 | 相同 | 相同 |
| 参数列表 | 必须不同(个数/类型/顺序) | 必须相同 |
| 关键字 | 无要求 | virtual / override |
| 返回值 | 可以不同 | 必须相同 |
1
2
3
4
5
6
7
8
9
10
| // 重载:同类中同名不同参
public class Calc
{
public int Add(int a, int b) => a + b;
public double Add(double a, double b) => a + b;
}
// 重写:父子类同名同参
public class Animal { public virtual void Speak() => Console.WriteLine("..."); }
public class Dog : Animal { public override void Speak() => Console.WriteLine("汪汪"); }
|
8.2 OOP 三大特点
封装:将数据和操作数据的方法绑定在一起,隐藏内部实现,通过 private/public/protected 控制访问权限。
继承:子类继承父类的属性和方法,提高代码复用性。
- 传递性:C → B → A,C 拥有 A 的特性
- 单根性:C# 中一个类只能继承一个父类
多态:同一接口在不同对象上有不同表现。两种形式:
- 编译时多态 → 方法重载
- 运行时多态 → 方法重写(通过基类引用指向派生类对象实现动态绑定)
8.3 值类型和引用类型的区别
| 值类型 | 引用类型 |
|---|
| 存储位置 | 栈上 | 栈存引用,堆存数据 |
| 赋值行为 | 复制数据 | 复制引用(指向同一对象) |
| 基类 | System.ValueType | System.Object |
| 包含 | int, bool, char, float, struct, enum | string, class, interface, delegate, array |
| 访问速度 | 快 | 慢 |
| GC 回收 | 不参与(出作用域即释放) | 参与(由 GC 回收) |
8.4 访问修饰符
| 修饰符 | 作用域 |
|---|
public | 任何类都可以访问 |
private | 仅当前类内部 |
protected | 当前类 + 派生类 |
internal | 同一程序集内 |
protected internal | 同一程序集 + 派生类 |
private protected | 同一程序集中的派生类 |
8..5 ref 和 out 的区别
| ref | out |
|---|
| 变量初始化 | 调用前必须赋值 | 调用前可不赋值 |
| 函数内赋值 | 可读可写 | 必须赋值(返回前必须写) |
| 语义 | 又进又出(传引用) | 只出不进(输出参数) |
1
2
3
4
5
6
7
| void ModifyRef(ref int x) { x += 10; }
void SetOut(out int x) { x = 100; } // 必须赋值
int a = 5;
ModifyRef(ref a); // a = 15
SetOut(out int b); // b = 100,无需提前初始化
|
8.6 ArrayList 和 List<T> 的区别
| ArrayList | List<T> |
|---|
| 类型 | 非泛型(存储 object) | 泛型(强类型) |
| 类型安全 | 不安全(可混存不同类型) | 安全(编译时检查) |
| 性能 | 低(装箱拆箱开销) | 高(无需转换) |
| 使用建议 | 避免使用(兼容旧代码) | 推荐使用 |
8.7 GC 原理与优化
GC 产生原因:自动回收堆上不再被引用的对象,防止内存泄漏。
如何减少 GC 压力:
- 减少
new 产生对象的次数(对象池、复用) - 使用
StringBuilder 代替字符串 += - 避免在频繁调用的方法中创建临时对象
- 使用结构体代替小对象(值类型不参与 GC)
- 使用
WeakReference 持有可重建对象
8.8 接口和抽象类的区别
| 接口(interface) | 抽象类(abstract class) |
|---|
| 实现 | 完全抽象(无实现) | 可包含部分实现 |
| 多继承 | 支持(一个类可实现多个接口) | 不支持(单继承) |
| 成员修饰符 | 默认 public,不能用其他修饰符 | 可⽤ public/protected/private |
| 字段 | 不能包含字段 | 可以包含字段 |
| 构造函数 | 不能有 | 可以有 |
| 使用场景 | 定义契约/能力(能做什么) | 定义基类共性(是什么) |
8.9 sealed 关键字
1
2
3
| sealed class FinalClass { } // 不可被继承
class Base { public virtual void Foo() { } }
class Derived : Base { sealed override void Foo() { } } // 派生类不可再重写
|
8.10 反射的实现原理
反射:在运行时动态获取程序集、类型、成员信息并调用的机制。
核心类(System.Reflection):
| 类 | 作用 |
|---|
Assembly | 描述程序集 |
Type | 描述类/接口/结构体 |
ConstructorInfo | 描述构造函数 |
MethodInfo | 描述方法 |
FieldInfo | 描述字段 |
PropertyInfo | 描述属性 |
1
2
3
4
| Type type = typeof(Person);
object obj = Activator.CreateInstance(type);
MethodInfo method = type.GetMethod("SayHello");
method.Invoke(obj, null);
|
8.11 静态构造函数
1
2
3
4
5
6
7
| class MyClass
{
static MyClass() // 静态构造函数,类首次被加载时执行一次
{
Console.WriteLine("静态初始化");
}
}
|
- 无访问修饰符
- 不带参数
- 在类被加载到内存时自动调用,且只执行一次
- 用于初始化静态成员
8.12 委托和事件的区别
委托:方法的类型安全引用,可将方法作为参数传递。
事件:基于委托的发布-订阅机制。
1
2
3
4
5
6
7
| // 委托:可任意 Invoke
public Action Notify;
Notify?.Invoke(); // 外部可以直接调用
// 事件:只能类内部 Invoke,外部只能 += / -=
public event Action Notified;
Notified?.Invoke(); // 仅限声明类内部调用
|
关键区别:
- 事件只能在声明它的类内部触发,外部只能注册/注销
- 委托可以在外部任意调用
- 事件是委托的封装,更安全
8.13 结构体和类的区别
| 结构体(struct) | 类(class) |
|---|
| 类型 | 值类型 | 引用类型 |
| 存储 | 栈 | 堆 |
| 赋值 | 复制全部数据 | 复制引用 |
| 继承 | 不能继承(但可实现接口) | 支持继承 |
| 构造函数 | 必须带参(无参构造 C# 10+ 才支持) | 任意 |
| 默认值 | 不可为 null(除非 Nullable) | 可为 null |
| 适用场景 | 小数据对象(Point、Color) | 复杂业务对象 |
8.14 装箱和拆箱
装箱:值类型 → 引用类型(object / 接口)
拆箱:引用类型 → 值类型
1
2
3
4
5
6
| int i = 123;
object o = i; // 装箱:i 复制到堆上
int j = (int)o; // 拆箱:堆上数据复制回栈
// 性能开销:内存分配 + 复制
// 避免:使用泛型集合代替 ArrayList
|
8.15 for、foreach、迭代器遍历的区别
1
2
3
4
5
6
7
8
9
10
| // for:通过索引访问,可修改集合
for (int i = 0; i < list.Count; i++) { list[i] = ...; }
// foreach:通过迭代器只读遍历,不能修改集合结构
foreach (var item in list) { Console.WriteLine(item); }
// 迭代器原理
// 1. 集合实现 IEnumerable 接口,提供 GetEnumerator()
// 2. GetEnumerator() 返回 IEnumerator
// 3. 每次循环调用 MoveNext(),返回 true 则通过 Current 获取元素
|
foreach 本质:
1
2
3
4
5
6
| var enumerator = list.GetEnumerator();
while (enumerator.MoveNext())
{
var item = enumerator.Current;
// 循环体
}
|
8.16 Dictionary 内部实现原理
数据结构:哈希表(拉链法解决冲突)
1
2
3
4
5
6
7
8
9
| // 内部结构
private struct Entry {
public int hashCode; // 哈希码(-1 表示空)
public int next; // 链表下一节点索引
public TKey key;
public TValue value;
}
private int[] buckets; // 桶数组,存链表头索引
private Entry[] entries; // 所有键值对
|
工作原理:
- 通过键的
GetHashCode() 计算哈希值 - 取模定位到桶:
buckets[hash % size] - 遍历桶对应的链表查找键
- 插入/查找时间复杂度平均 O(1)
扩容机制:
- 负载因子超过阈值(默认 0.72)时自动扩容
- 新容量为原容量 2 倍(取质数)
- 所有元素重新哈希分配到新桶
8.17 泛型的作用
- 类型安全:编译时检查类型,避免运行时转型错误
- 性能提升:避免值类型的装箱拆箱
- 代码复用:一套代码适配多种类型
1
2
3
4
5
6
7
8
9
| // 非泛型:需装箱拆箱
ArrayList list = new ArrayList();
list.Add(123); // 装箱
int val = (int)list[0]; // 拆箱 + 类型转换
// 泛型:无需转换
List<int> list = new List<int>();
list.Add(123);
int val = list[0];
|
8.18 泛型容器 vs 非泛型容器
| 泛型容器 | 非泛型容器 | 区别 |
|---|
List<T> | ArrayList | 泛型安全无装箱 |
Dictionary<K,V> | Hashtable | 泛型安全无装箱 |
Queue<T> | Queue | 泛型安全 |
Stack<T> | Stack | 泛型安全 |
HashSet<T> | 无直接替代 | 唯一集合 |
8.19 unsafe 关键字
用于编写不安全代码(非托管代码),允许使用指针操作。
1
2
3
4
5
| unsafe
{
int* p = &x;
*p = 100;
}
|
适用场景:
- 与 C/C++ 库互操作
- 高性能计算(图像处理、加密算法)
- Unity 中的指针优化
8.20 堆和栈的区别
| 栈 | 堆 |
|---|
| 存储内容 | 局部变量、方法参数、返回地址 | 对象实例、静态成员 |
| 分配速度 | 快(移动指针) | 慢(查找空闲块) |
| 释放方式 | 方法结束自动释放 | GC 自动回收 |
| 大小 | 小(默认 ~1MB) | 大(可用虚拟内存) |
| 碎片 | 无碎片 | 可能产生内存碎片 |
8.21 MVC 架构
MVC(Model-View-Controller):将业务逻辑、数据、界面分离的设计模式。
1
2
3
4
5
| 用户交互 → Controller(控制逻辑)
↓
Model(数据处理)→ 数据库
↓
View(界面显示)→ 响应用户
|
- Model:数据和业务逻辑
- View:界面展示(UI)
- Controller:接收用户输入,协调 Model 和 View
8.22 引用类型的 ref 传递行为
1
2
3
4
5
6
| void ModifyRef(ref Person p)
{
p.Name = "新名字"; // 修改原对象属性
p = new Person("新对象"); // 重新赋值后,形参指向新对象
}
// 方法结束后,实参还是指向原始对象,但原始对象的属性已被修改
|
总结:ref 传递引用类型时,方法内修改对象属性会影响原对象;但重新 new 并赋值不会改变原对象的引用地址。
九、Unity 相关 C# 知识点
9.1 MonoBehaviour 生命周期
1
2
3
4
5
6
7
8
9
10
11
| public class Player : MonoBehaviour
{
void Awake() { } // 对象加载时调用(仅一次),早于 Start
void OnEnable() { } // 每次启用时调用
void Start() { } // 第一次 Update 前调用(仅一次)
void Update() { } // 每帧调用
void FixedUpdate() { } // 固定时间间隔调用(物理更新)
void LateUpdate() { } // 所有 Update 完成后调用
void OnDisable() { } // 每次禁用时调用
void OnDestroy() { } // 销毁时调用
}
|
执行顺序:Awake → OnEnable → Start → FixedUpdate → Update → LateUpdate → OnDisable → OnDestroy
Awake vs Start:
- Awake:用于自身初始化,不依赖其他对象
- Start:用于依赖其他对象的初始化(如获取其他组件)
9.2 常用协同程序(Coroutine)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| public class CoroutineExample : MonoBehaviour
{
void Start()
{
StartCoroutine(DelayExecute());
}
IEnumerator DelayExecute()
{
Debug.Log("开始");
yield return new WaitForSeconds(2f); // 等待2秒
Debug.Log("2秒后执行");
yield return new WaitForEndOfFrame(); // 等待帧结束
yield return new WaitForFixedUpdate(); // 等待物理帧更新
yield return null; // 等待下一帧
}
// 停止协程
void Stop()
{
StopCoroutine("DelayExecute");
StopAllCoroutines();
}
}
|
协程本质:不是多线程,而是利用 yield return 将方法拆分为多个片段,在主线程上分时执行。
9.3 对象池(Object Pool)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| public class ObjectPool : MonoBehaviour
{
[SerializeField] private GameObject prefab;
private Queue<GameObject> pool = new Queue<GameObject>();
public GameObject Get()
{
if (pool.Count > 0)
{
GameObject obj = pool.Dequeue();
obj.SetActive(true);
return obj;
}
return Instantiate(prefab);
}
public void Return(GameObject obj)
{
obj.SetActive(false);
pool.Enqueue(obj); // 回收到池中
}
}
|
为什么用对象池:
- 频繁
Instantiate/Destroy 会产生大量 GC 垃圾 - 对象池复用对象,减少 GC 压力
- 典型场景:子弹、敌人、特效
9.4 Unity 中的单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| // 普通单例
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject); // 保证全局唯一
return;
}
Instance = this;
DontDestroyOnLoad(gameObject); // 场景切换不销毁
}
}
// 泛型单例基类
public class Singleton<T> : MonoBehaviour where T : Component
{
public static T Instance { get; private set; }
protected virtual void Awake()
{
if (Instance == null)
{
Instance = this as T;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
}
|
9.5 Unity 中字符串拼接的性能陷阱
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // ❌ 错误:Update 中频繁拼接字符串产生大量 GC
void Update()
{
string text = "HP: " + health + "/" + maxHealth;
// 每次 Update 产生 3 个临时字符串对象
}
// ✅ 优化:使用 StringBuilder 或预先分配
private StringBuilder sb = new StringBuilder(64);
void Update()
{
sb.Clear();
sb.Append("HP: ").Append(health).Append("/").Append(maxHealth);
text.text = sb.ToString();
}
// ✅ 或使用 string.Format(固定格式时)
text.text = $"HP: {health}/{maxHealth}";
|
Unity 特别提醒:Update 每帧调用,字符串操作不当会导致严重的 GC 问题。
9.6 Unity 中 foreach 的 GC 问题
1
2
3
4
5
| // ❌ Unity 旧版本中 foreach 会产生 GC Alloc
foreach (var item in list) { }
// ✅ 改用 for 或 GetEnumerator 手动管理
for (int i = 0; i < list.Count; i++) { }
|
原因:Unity 旧版中 foreach 会生成 IEnumerator 临时对象,新版(2020+)已优化。
最佳实践:
- 高频调用的 Update/FixedUpdate 中用
for 代替 foreach - 非高频位置(按钮点击、协程)可用
foreach,可读性更好
9.7 SerializeField 与序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
| public class Player : MonoBehaviour
{
[SerializeField] private int health; // 私有字段在 Inspector 中显示
[SerializeField] private GameObject prefab;
[Range(0, 100)] public float speed; // 滑动条限制范围
[Tooltip("移动速度")] public float moveSpeed; // 鼠标悬停提示
[Header("攻击设置")]
public int attackPower;
public float attackRange;
[HideInInspector] public int debugValue; // 公开字段不在 Inspector 显示
}
|
9.8 GetComponent 性能优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| public class Player : MonoBehaviour
{
// ❌ 每次调用都查找
void Update()
{
GetComponent<Rigidbody>().AddForce(Vector3.up);
}
// ✅ 缓存引用
private Rigidbody rb;
void Awake()
{
rb = GetComponent<Rigidbody>();
}
void Update()
{
rb.AddForce(Vector3.up);
}
}
|
尽量使用 TryGetComponent(C# 7+ / Unity 2019+):
1
2
| if (TryGetComponent(out Rigidbody rb))
rb.AddForce(Vector3.up);
|
性能对比:
| 方法 | 性能 |
|---|
GetComponent<T>() | 较慢(抛出异常/隐式搜索) |
TryGetComponent<T>() | 较快(无异常开销) |
| 缓存引用 | 最快(推荐) |
9.9 Unity 中的事件系统
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| // C# 事件(解耦)
public class HealthSystem : MonoBehaviour
{
public event Action<float> OnHealthChanged;
public event Action OnPlayerDied;
private float health = 100;
public void TakeDamage(float damage)
{
health -= damage;
OnHealthChanged?.Invoke(health);
if (health <= 0) OnPlayerDied?.Invoke();
}
}
// 订阅事件
public class UIManager : MonoBehaviour
{
[SerializeField] private HealthSystem player;
void Start()
{
player.OnHealthChanged += UpdateHealthBar;
player.OnPlayerDied += ShowGameOver;
}
void UpdateHealthBar(float health) { /* 更新血条 */ }
void ShowGameOver() { /* 显示游戏结束 */ }
void OnDestroy()
{
// 记得取消订阅,防止内存泄漏
player.OnHealthChanged -= UpdateHealthBar;
player.OnPlayerDied -= ShowGameOver;
}
}
|
UnityEvent(可在 Inspector 中绑定):
1
2
3
4
5
6
7
8
9
| public class Button : MonoBehaviour
{
public UnityEvent onClick; // 可在编辑器拖拽绑定
void OnMouseDown()
{
onClick?.Invoke();
}
}
|
9.10 Resources 和 Addressables 加载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // Resources 加载(简单但性能差)
GameObject prefab = Resources.Load<GameObject>("Prefabs/Player");
Instantiate(prefab);
// Resources 注意事项:
// - 所有 Resources 文件夹中的资源都会被打包,增加包体
// - 加载是同步的,大资源会卡帧
// - 已过时,推荐用 Addressables
// Addressables 加载(异步、按需加载)
AsyncOperationHandle<GameObject> handle =
Addressables.LoadAssetAsync<GameObject>("Player");
handle.Completed += (op) =>
{
Instantiate(op.Result);
};
|
9.11 Unity 中的物理与碰撞
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| public class PhysicsExample : MonoBehaviour
{
private Rigidbody rb;
void Awake() => rb = GetComponent<Rigidbody>();
// 触发检测(不产生物理力)
void OnTriggerEnter(Collider other) { }
void OnTriggerStay(Collider other) { }
void OnTriggerExit(Collider other) { }
// 碰撞检测(产生物理力)
void OnCollisionEnter(Collision collision) { }
void OnCollisionStay(Collision collision) { }
void OnCollisionExit(Collision collision) { }
// 射线检测
void RaycastExample()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, 100f))
{
Debug.Log($"击中: {hit.collider.name}");
}
}
}
|
Layer 和 LayerMask:
1
2
3
4
5
6
7
8
9
10
| public LayerMask targetLayer;
void Shoot()
{
int mask = 1 << LayerMask.NameToLayer("Enemy");
if (Physics.Raycast(transform.position, transform.forward, out RaycastHit hit, 100f, mask))
{
// 只检测 Enemy 层的物体
}
}
|
9.12 Unity 中常用的设计模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| // 单例模式(全局管理者,参考 9.4)
// 对象池模式(减少频繁创建销毁,参考 9.3)
// 观察者模式(事件解耦,参考 9.9)
// 状态模式(角色状态切换)
public interface IState
{
void Enter();
void Update();
void Exit();
}
public class PlayerStateMachine
{
private Dictionary<System.Type, IState> states = new();
private IState currentState;
public void AddState(System.Type type, IState state) => states[type] = state;
public void ChangeState(System.Type newStateType)
{
currentState?.Exit();
currentState = states[newStateType];
currentState.Enter();
}
public void Update() => currentState?.Update();
}
// 工厂模式(解耦对象创建)
public class EnemyFactory
{
public enum EnemyType { Goblin, Slime, Dragon }
public Enemy Create(EnemyType type) => type switch
{
EnemyType.Goblin => new Goblin(),
EnemyType.Slime => new Slime(),
EnemyType.Dragon => new Dragon(),
_ => throw new ArgumentException()
};
}
|
9.13 Unity 编辑器扩展基础
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // 自定义 Inspector
[CustomEditor(typeof(Player))]
public class PlayerEditor : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
Player player = (Player)target;
if (GUILayout.Button("重置血量"))
{
player.ResetHealth();
}
}
}
// 自定义菜单项
public static class EditorTools
{
[MenuItem("Tools/清除所有 PlayerPrefs")]
public static void ClearPlayerPrefs()
{
PlayerPrefs.DeleteAll();
Debug.Log("PlayerPrefs 已清除");
}
}
|
9.14 Unity 中的协程与异步对比
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // 协程(老旧但兼容性好)
IEnumerator LoadDataCoroutine()
{
var request = UnityWebRequest.Get("https://api.example.com");
yield return request.SendWebRequest();
Debug.Log(request.downloadHandler.text);
}
// async/await(C# 原生,推荐,需 Unity 2018+)
async Task LoadDataAsync()
{
var request = UnityWebRequest.Get("https://api.example.com");
await request.SendWebRequest();
Debug.Log(request.downloadHandler.text);
}
// Unity 2022+ 支持 Awaitable(零 GC)
async Awaitable LoadDataAwaitable()
{
await Awaitable.WaitForSecondsAsync(1f);
Debug.Log("一秒后执行,无 GC 开销");
}
|
| 方式 | GC 开销 | 适用场景 |
|---|
| Coroutine | 有 | 旧项目、简单延迟 |
| async/await | 有(Task 分配) | 网络请求、复杂异步 |
| Awaitable | 无 | Unity 2022+,性能敏感 |