C#知识点概要

C# 语言核心知识点系统性总结,涵盖基础语法、面向对象、LINQ、异步编程等

一、基础语法

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;    // 所有键值对

工作原理:

  1. 通过键的 GetHashCode() 计算哈希值
  2. 取模定位到桶:buckets[hash % size]
  3. 遍历桶对应的链表查找键
  4. 插入/查找时间复杂度平均 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+,性能敏感
不嘻嘻
使用 Hugo 构建
主题 Stack 由 Jimmy 设计