前言
深入浅出讲解C#面试中的核心知识点,包括值类型与引用类型、泛型、委托事件、异步编程、LINQ等重要概念
值类型与引用类型
值类型:直接存储数据本身,如int、bool、struct、enum等,存储在栈(Stack)上(或作为引用类型的字段时存储在堆上)。
引用类型:储存数据的引用(内存地址),实际数据在堆(Heap)上,如string、class、array、delegate等。
区别:
- 赋值行为:值类型赋值时赋值数据本身;引用类型赋值时赋值时复制引用(指向同一堆内存)。
- 内存管理:值类型超出作用域自动释放;引用类型由GC(垃圾回收器)管理生命周期。
代码举例:
1
2
3
4
5
6
7
8
9
10
11
12// 值类型示例
int a = 10;
int b = a; // 复制值,b独立于a
b = 20;
Console.WriteLine(a); // 输出10(a不受影响)
// 引用类型示例
class Person { public int Age; }
Person p1 = new Person { Age = 20 };
Person p2 = p1; // 复制引用,p1和p2指向同一对象
p2.Age = 30;
Console.WriteLine(p1.Age); // 输出30(p1受影响)方法参数传递,值类型和引用类型的传递方式区别
- 值类型:方法内修改不会影响外部变量。
- 引用类型:默认按引用类型传递的引用地址,方法内部修改对象内容会影响外部(指向同一对象)
装箱(Boxing)和拆箱(Unboxing)
装箱:将值类型转换为引用类型(object或接口)的过程
- 过程:在堆上分配内存 → 复制值类型数据到堆 → 返回堆对象的引用
拆箱:将引用类型(装箱后的对象)转换回值类型的过程
- 过程:检查对象类型是否匹配 → 从堆中提取值 → 复制到栈上
性能影响
- 装箱和拆箱都涉及内存分配和复制操作,开销较大
- 频繁装箱会增加GC压力(堆上产生大量临时对象)
- 应尽量避免:使用泛型集合(如
List<T>)代替非泛型集合(如ArrayList)
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 装箱示例
int num = 123;
object obj = num; // 装箱:int → object,在堆上创建对象
// 拆箱示例
int value = (int)obj; // 拆箱:object → int,需显式类型转换
// 性能对比:避免装箱
// 不推荐:非泛型集合会导致装箱
ArrayList list1 = new ArrayList();
list1.Add(1); // 装箱
int x = (int)list1[0]; // 拆箱
// 推荐:泛型集合避免装箱
List<int> list2 = new List<int>();
list2.Add(1); // 无装箱
int y = list2[0]; // 无拆箱常见装箱场景
- 值类型赋值给object或接口变量
- 值类型作为参数传递给接受object的方法(如
Console.WriteLine(123)) - 值类型存入非泛型集合(ArrayList、Hashtable等)
接口和抽象类
接口:仅定义方法、属性、事件或索引器的签名(无实现)。
Interface定义,成员默认public。抽象类:可包含抽象成员和具体成员的类,不能实例化。
abstract定义,成员可指定访问修饰符。接口和抽象类区别
维度 接口 抽象类 继承方式 支持多继承(一个类可实现多个接口) 单继承(一个类只能继承一个抽象类) 成员实现 所有成员无实现(纯契约) 可混合抽象成员(无实现)和具体成员 访问修饰符 成员默认public,不可修改 成员可指定public/protected等 构造函数 无构造函数 有构造函数(供派生类调用) 字段 不能包含字段(仅属性/方法等) 可以包含字段 版本兼容 新增成员会导致所有实现类必须修改 可新增具体成员,不影响派生类 什么时候用接口?什么时候用抽象类?
- 优先用接口场景:
- 需要继承多能力时
- 定义跨多个不相关类的通用功能(如IDisposable接口)
- 纯契约设计,不提供默认实现(如服务接口)
- 优先用抽象类场景:
- 多个相关类共享部分实现逻辑(提取公共代码)
- 需要定义字段或构造函数时
- 希望控制继承层次(单继承体系),且可能后续扩展具体方法
- 优先用接口场景:
接口和抽象类在依赖注入中的应用场景
接口是依赖注入的核心,用于定义服务契约,实现依赖倒置(高层模块依赖抽象)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 接口定义服务
public interface IUserService { void AddUser(); }
// 具体实现
public class UserService : IUserService { ... }
// 注入时依赖接口,而非具体类
public class UserController
{
private readonly IUserService _userService;
public UserController(IUserService userService) // 依赖注入
{
_userService = userService;
}
}抽象类较少直接用于依赖注入,但若多个服务共享基础逻辑,可作为基类
泛型
什么是泛型?为什么使用泛型?
- 定义:泛型是.NET 2.0 引入的特性,允许在定义类、接口、方法、委托时使用未指定的 “类型参数”,在使用时再指定具体类型(如
List<T>中的 T 就是类型参数) - 作用
- 类型安全:避免运行时类型转换错误(如非泛型 ArrayList 存储 object,需强制转换,可能抛 InvalidCastException)
- 性能优化:减少装箱 / 拆箱操作(值类型存入非泛型集合时会装箱,取出时拆箱,泛型直接操作具体类型)。
- 代码复用:用一套逻辑支持多种类型(如
List<int>、List<string>共享List<T>的实现)。
- 定义:泛型是.NET 2.0 引入的特性,允许在定义类、接口、方法、委托时使用未指定的 “类型参数”,在使用时再指定具体类型(如
泛型类型参数的约束(Constraints)有哪些?如何使用?
约束用于限制类型参数的范围,确保其满足特定条件(如必须是引用类型、必须有默认构造函数等)。常见约束:- where T : class:T 必须是引用类型(类、接口、委托等)。
- where T : struct:T 必须是值类型(int、struct 等,不包括
Nullable<T>)。 - where T : new():T 必须有公共无参构造函数(需放在其他约束最后)。
- where T : 基类名:T 必须是指定基类或其派生类。
- where T : 接口名:T 必须实现指定接口。
- where T : U:T 必须是 U 的派生类型(泛型参数间的约束)。
示例:
1
2
3
4
5
6
7public class MyClass<T> where T : MyBase, new()
{
public T CreateInstance()
{
return new T(); // 因有 new() 约束,可实例化
}
}泛型方法与泛型类的区别?
- 泛型类:在类级别声明类型参数,类的所有成员(方法、属性等)可使用该参数(如
List<T>)。 - 泛型方法:在方法级别声明类型参数,仅该方法可用,可独立于类的泛型参数(即使类非泛型,也可定义泛型方法)。
- 泛型类:在类级别声明类型参数,类的所有成员(方法、属性等)可使用该参数(如
泛型类型在运行时的表现(泛型类型的实例化)?
- 值类型:对不同值类型的泛型实例(如
List<int>、List<double>),CLR 会生成不同的原生代码(因值类型大小不同)。 - 引用类型:对不同引用类型的泛型实例(如
List<string>、List<object>),CLR共享一套原生代码(因引用类型在内存中都是指针,大小相同)。
- 值类型:对不同值类型的泛型实例(如
委托与事件
委托(Delegate):委托是一种类型安全的函数指针,可以引用一个或多个具有相同签名的方法
- 定义:使用
delegate关键字定义,指定返回类型和参数列表 - 特点:
- 类型安全:编译时检查方法签名是否匹配
- 支持多播:一个委托可以引用多个方法(通过
+=添加,-=移除) - 可以作为参数传递或作为返回值
- 常见内置委托:
Action<T>:无返回值的委托(可带0-16个参数)Func<T, TResult>:有返回值的委托(可带0-16个参数)Predicate<T>:返回bool的委托(用于条件判断)
- 定义:使用
事件(Event):事件是对委托的封装,用于实现发布-订阅模式
- 定义:使用
event关键字修饰委托 - 特点:
- 只能在类内部触发(通过
Invoke或?.Invoke()) - 外部只能订阅(
+=)或取消订阅(-=),不能直接赋值或触发 - 提供了更好的封装性,防止外部误触发事件
- 只能在类内部触发(通过
- 定义:使用
委托与事件的区别
- 委托是类型,事件是成员(事件是委托实例的包装器)
- 委托可被外部赋值(
=)和调用,事件只能被订阅/取消订阅 - 事件通常用于类之间的松耦合通信(如UI事件、观察者模式)
代码示例:
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// 委托示例
public delegate void LogHandler(string message);
public class Logger
{
public LogHandler OnLog; // 委托字段
public void Log(string msg)
{
OnLog?.Invoke(msg); // 外部可直接赋值和调用(不安全)
}
}
// 事件示例
public class Button
{
public event EventHandler Clicked; // 事件(基于EventHandler委托)
public void Click()
{
Clicked?.Invoke(this, EventArgs.Empty); // 只能在类内部触发
}
}
// 使用示例
var button = new Button();
button.Clicked += (sender, e) => Console.WriteLine("按钮被点击"); // 订阅事件
// button.Clicked = null; // 编译错误!事件不能直接赋值
button.Click(); // 输出:按钮被点击
// 多播委托示例
Action<string> notify = msg => Console.WriteLine($"通知1: {msg}");
notify += msg => Console.WriteLine($"通知2: {msg}");
notify("测试"); // 输出两行:通知1和通知2委托的应用场景
- 回调函数:如 LINQ 的
Where(x => x > 10)、异步编程的回调 - 事件处理:UI 框架中的按钮点击、数据变更通知等
- 策略模式:动态选择算法(如排序策略)
- 链式调用:如中间件管道(ASP.NET Core 的
Use方法)
- 回调函数:如 LINQ 的
匿名方法与 Lambda 表达式
匿名方法(Anonymous Method):C# 2.0 引入的特性,用于内联定义委托的实现(无需单独定义方法)
- 语法:
delegate (参数列表) { 方法体 } - 特点:
- 可访问外部作用域的变量(形成闭包)
- 可省略参数列表(如果委托不需要参数)
- 语法:
Lambda 表达式(Lambda Expression):C# 3.0 引入的更简洁的匿名方法语法
- 语法:
- 表达式 Lambda:
(参数) => 表达式(单行返回值) - 语句 Lambda:
(参数) => { 语句块 }(多行代码块)
- 表达式 Lambda:
- 特点:
- 更简洁:省略
delegate关键字和返回类型 - 支持表达式树:可转换为
Expression<Func<T>>类型(用于 LINQ to SQL 等场景) - 广泛用于 LINQ、事件订阅、异步编程等
- 更简洁:省略
- 语法:
匿名方法 vs Lambda 表达式
维度 匿名方法 Lambda 表达式 语法简洁性 冗长(需 delegate关键字)简洁(仅需 =>符号)表达式树 不支持 支持(可转换为 Expression<T>)参数推断 需显式声明参数类型 可推断参数类型(如 x => x > 5)适用场景 已被 Lambda 取代(兼容旧代码) 现代 C# 的首选方式 代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 匿名方法示例
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var even1 = numbers.FindAll(delegate(int x) { return x % 2 == 0; });
// Lambda 表达式示例(等价于上面的匿名方法)
var even2 = numbers.FindAll(x => x % 2 == 0); // 表达式 Lambda
// 语句 Lambda(多行)
var even3 = numbers.FindAll(x =>
{
Console.WriteLine($"检查 {x}");
return x % 2 == 0;
});
// 闭包示例:Lambda 捕获外部变量
int threshold = 3;
var greaterThanThreshold = numbers.Where(x => x > threshold).ToList();
// threshold 被"捕获"到 Lambda 中,即使在方法外部修改 threshold,Lambda 内部的值也会同步
// 表达式树示例(用于 LINQ to SQL)
Expression<Func<int, bool>> expr = x => x > 10;
// 可分析 expr 的结构:expr.Body、expr.Parameters 等(用于动态查询生成)闭包(Closure)的注意事项
Lambda 和匿名方法会捕获外部变量的引用(而非值)
如果在循环中使用 Lambda,需注意变量捕获的时机:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 错误示例:循环变量被捕获
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.WriteLine(i)); // 捕获的是 i 的引用
}
actions.ForEach(a => a()); // 输出:3 3 3(循环结束后 i = 3)
// 正确示例:使用局部变量
var actions2 = new List<Action>();
for (int i = 0; i < 3; i++)
{
int temp = i; // 每次循环创建新的局部变量
actions2.Add(() => Console.WriteLine(temp));
}
actions2.ForEach(a => a()); // 输出:0 1 2
Lambda 表达式的应用场景
- LINQ 查询:
var result = list.Where(x => x > 10).Select(x => x * 2); - 事件订阅:
button.Click += (s, e) => Console.WriteLine("点击"); - 异步编程:
await Task.Run(() => DoWork()); - 函数式编程:高阶函数(接受或返回函数的函数)
- LINQ 查询: