全面深入的C#核心知识体系与编程实践精要------从语法基础到高级特性系统学习指南
- C#基本数据类型
- 方法与参数
- 什么是对象?
- 什么是面向对象?
- C#的对象有什么特征?
-
- 三大特征实现
-
- [字段 vs 属性](#字段 vs 属性)
- [`const` vs `readonly`](#
constvsreadonly) - [实现只写属性(Write-Only Property)](#实现只写属性(Write-Only Property))
- C#内存管理核心机制
- 什么是复合?什么是继承?
- C#的继承特点
- 对象类型转换
- UML类关系详解(耦合度由低到高)
- [方法重写(Method Overriding)](#方法重写(Method Overriding))
- 多态(Polymorphism)
- 什么是接口?
- 什么是单元测试?
-
- 定义
- [如何对 C# 代码进行单元测试](# 代码进行单元测试)
- 单元测试的好处?
- 测试驱动开发(TDD)
- [NuGet 是什么?](#NuGet 是什么?)
- 什么是依赖注入(DI)?
-
- 定义
- 核心思想
- [为什么需要 DI?](#为什么需要 DI?)
- 实现方式
- 与控制反转(IoC)的关系
- 什么是测试驱动开发(TDD)?
- 单元测试在TDD中的角色
-
- [TDD中的单元测试 vs 传统单元测试](#TDD中的单元测试 vs 传统单元测试)
- TDD的核心流程详解
- TDD的优缺点
- 结构(Struct)与类(Class)有什么区别?
- 结构(Struct)数据保存在栈内存还是堆内存?
- [C# 中如何处理 `null`?](# 中如何处理
null?) - 反射的原理是什么?
- 元数据(Metadata)是什么?
- [C# 中如何处理异常?](# 中如何处理异常?)
- 集合相关问题
- [Foreach 相关问题](#Foreach 相关问题)
-
- Q1:集合迭代的原理和工作流程是什么?
-
- [1. 集合迭代的核心原理](#1. 集合迭代的核心原理)
- [2. `foreach` 的执行流程](#2.
foreach的执行流程)
- [Q2:foreach 是如何实现的?](#Q2:foreach 是如何实现的?)
- [Q3:IEnumerator 与 IEnumerable 是什么?](#Q3:IEnumerator 与 IEnumerable 是什么?)
-
- [enum 关键字:用于声明一个枚举类型。](#enum 关键字:用于声明一个枚举类型。)
- (一)IEnumerable
- (二)IEnumerator
- (三)两者的区别
- [Q4:如何使用 `yield return` 简化迭代器?](#Q4:如何使用
yield return简化迭代器?) - [Q5:如何使用 Benchmark 做性能基准测试?](#Q5:如何使用 Benchmark 做性能基准测试?)
- [LINQ 相关问题](#LINQ 相关问题)
- 数据处理相关问题
- 进程与线程相关问题
- 异步编程相关问题
- 垃圾回收(GC)相关问题
-
- Q1:什么是垃圾(Garbage)?
- Q2:垃圾回收(GC)是什么?
- [Q3:GC 如何工作?(分代回收机制)](#Q3:GC 如何工作?(分代回收机制))
- Q4:垃圾回收有什么好处?
- Q5:析构函数(Destructor)与终结器(Finalizer)
- Q6:为什么不能依赖终结器回收资源?
- 非托管资源处理
-
- [Q1:托管资源 vs 非托管资源](#Q1:托管资源 vs 非托管资源)
- [Q2:Disposable 模式的作用](#Q2:Disposable 模式的作用)
- [Q3:如何实现 Disposable 模式?](#Q3:如何实现 Disposable 模式?)
- Q4:使用模式(`using`语句)
- Q5:常见非托管资源处理示例
C#基本数据类型
| 类别 | 类型 | 描述 | 示例 |
|---|---|---|---|
| 整数类型 | byte |
8位无符号整数 (0~255) | byte a = 100; |
int |
32位有符号整数(默认) | int b = -5000; |
|
long |
64位有符号整数(大范围数值) | long c = 1_000_000L; |
|
| 浮点类型 | float |
32位单精度浮点数 | float d = 3.14f; |
double |
64位双精度浮点数(默认) | double e = 2.718; |
|
| 字符与字符串 | char |
单个Unicode字符(16位) | char f = 'A'; |
string |
字符序列(不可变) | string g = "Hello"; |
|
| 布尔类型 | bool |
true 或 false |
bool h = true; |
方法与参数
方法(函数)定义
-
作用:封装可重用的代码块,通过名称调用。
-
语法:
csharp访问修饰符 返回类型 方法名(参数列表) { // 逻辑代码 return 结果; // 若返回类型非void }csharppublic int Add(int a, int b) { return a + b; }
参数传递方式
-
值传参(默认):
- 传递副本,方法内修改不影响原始变量。
csharpvoid Modify(int x) { x = 10; } int num = 5; Modify(num); // num仍为5 -
引用传参(
ref):- 传递内存地址,方法内修改影响原始变量。
csharpvoid Modify(ref int x) { x = 10; } int num = 5; Modify(ref num); // num变为10 -
输出传参(
out):- 用于返回多个值,必须在方法内赋值。
csharpvoid Calculate(int a, int b, out int sum, out int product) { sum = a + b; product = a * b; } int s, p; Calculate(3, 4, out s, out p); // s=7, p=12
构造函数
构造函数(Constructor)是类中用于初始化对象的特殊方法。它在创建对象时自动调用,用于设置初始状态、分配资源或执行必要的初始化操作。
核心特性
- 命名:与类名相同
- 无返回值 :无需声明返回类型(包括
void) - 自动调用 :使用
new创建对象时触发 - 可重载:支持多个不同参数的构造函数
构造函数类型
1. 默认构造函数(无参)
- 编译器自动生成(如果未显式定义)
- 初始化字段为默认值(数值为0,引用类型为
null)
csharp
public class Car
{
public string Model;
// 默认构造函数(可省略)
public Car()
{
Model = "Unknown";
}
}
2. 参数化构造函数
- 通过参数初始化对象
csharp
public class Car
{
public string Model;
public Car(string model)
{
Model = model; // 初始化字段
}
}
// 使用
Car myCar = new Car("Tesla Model 3");
3. 构造函数链(this 关键字)
- 复用其他构造函数的逻辑
csharp
public class Car
{
public string Model;
public int Year;
public Car() : this("Unknown", 2023) // 调用下方构造函数
{
}
public Car(string model, int year)
{
Model = model;
Year = year;
}
}
4. 静态构造函数
- 初始化 静态成员
- 在类首次访问时自动执行(仅一次)
- 不能有参数或访问修饰符
csharp
public class Logger
{
public static string LogPath;
static Logger() // 静态构造函数
{
LogPath = "/logs/app.log";
}
}
5. 私有构造函数
- 禁止外部实例化
- 常用于单例模式或工具类
csharp
public class Singleton
{
private static Singleton _instance;
private Singleton() {} // 私有构造函数
public static Singleton Instance
{
get => _instance ??= new Singleton();
}
}
继承中的构造函数
- 派生类必须调用基类构造函数(通过
base) - 默认调用基类无参构造(若基类无无参构造,必须显式调用)
csharp
public class Vehicle
{
public int Wheels;
public Vehicle(int wheels) => Wheels = wheels;
}
public class Bicycle : Vehicle
{
public string Type;
// 调用基类构造函数
public Bicycle(string type) : base(2)
{
Type = type;
}
}
重要注意事项
-
结构体构造函数:
- 必须显式初始化所有字段
- 不能定义无参构造函数(C# 10 起允许,但有条件限制)
csharppublic struct Point { public int X, Y; public Point(int x, int y) { X = x; Y = y; } } -
只读字段:只能在构造函数中初始化
csharppublic class Immutable { public readonly int Id; public Immutable(int id) => Id = id; } -
执行顺序:
- 字段初始化器 → 基类构造函数 → 当前类构造函数
csharppublic class Demo { private int _a = 1; // 先执行 public Demo() => _a = 2; // 后执行 }
最佳实践
-
简洁性:避免在构造函数中编写复杂逻辑
-
异常处理:构造函数内抛异常会导致对象创建失败
-
依赖注入:通过构造函数传递依赖(提高可测试性)
csharppublic class UserService { private readonly IUserRepository _repository; public UserService(IUserRepository repository) { _repository = repository; // 依赖注入 } }
总结
| 类型 | 场景 | 示例 |
|---|---|---|
| 默认构造函数 | 简单初始化 | public MyClass() {} |
| 参数化构造函数 | 灵活初始化对象 | public MyClass(int id) |
| 构造函数链 | 复用初始化逻辑 | : this("default") |
| 静态构造函数 | 初始化静态资源 | static MyClass() {} |
| 私有构造函数 | 单例模式/工具类 | private MyClass() {} |
什么是对象?
核心定义
在C#中,对象(Object) 是面向对象编程的基本单位,它是类(Class)的实例,代表现实世界中的实体。每个对象包含:
- 属性(Properties):描述对象的特征(如颜色、尺寸)
- 方法(Methods):定义对象的行为(如移动、计算)
类与实例的关系
- 类是对象的蓝图/模板(如"汽车"设计图)
- 对象是类的具体实现(如"一辆红色宝马轿车")
csharp
// 定义类
class Car {
public string Color; // 属性
public void Drive() { // 方法
Console.WriteLine("Driving...");
}
}
// 创建对象(实例化)
Car myCar = new Car();
myCar.Color = "Red";
内存分配
- 使用
new初始化对象时,对象存储在堆(Heap)内存中 - 对象引用(变量如
myCar)存储在栈(Stack)内存中(指向堆中的实际对象)
什么是面向对象?
核心概念
面向对象编程(OOP) 是一种程序设计范式,核心思想是将程序分解为相互协作的对象。三大支柱:
- 封装(Encapsulation):隐藏内部实现细节
- 继承(Inheritance):子类复用父类特性
- 多态(Polymorphism):同一接口不同实现
高内聚低耦合
- 高内聚:一个模块(如类)内部各元素(属性、方法)联系紧密,专注完成单一功能(如 "用户类" 只处理用户数据和操作)。
- 低耦合:模块之间依赖程度低,修改一个模块不易影响其他模块(如 "用户类" 与 "订单类" 通过接口交互,而非直接依赖)。
方法重载 vs 重写
| 特性 | 重载(Overload) | 重写(Override) |
|---|---|---|
| 作用范围 | 同一类内 | 父子类之间 |
| 要求 | 方法名相同,参数不同 | 方法签名相同,父类方法标记virtual |
| 示例 | Calculate(int a) vs Calculate(double a) |
子类使用override覆盖父类方法 |
访问修饰符
| 修饰符 | 访问范围 |
|---|---|
public |
无限制访问 |
private |
仅本类内可访问(默认) |
protected |
本类及子类可访问 |
internal |
同一程序集内可访问 |
C#的对象有什么特征?
三大特征实现
- 继承(Inheritance):子类可以继承父类的属性和方法,从而实现代码的复用。例如:
csharp
class Animal
{
public void Eat()
{
Console.WriteLine("Animal is eating.");
}
}
class Dog : Animal
{
// 继承了 Animal 类的 Eat 方法
}
- 多态(Polymorphism) :多态允许不同的对象对同一消息做出不同的响应。通过方法重载和方法重写实现。例如上述的方法重写示例,不同的对象(
Animal和Dog)调用Eat方法会有不同的行为。 - 封装(Encapsulation):将数据和操作数据的方法封装在一个类中,通过访问修饰符控制对数据的访问。例如:
csharp
class Person
{
private string name; // 私有字段,外部无法直接访问
public string Name
{
get { return name; }
set { name = value; }
}
}
字段 vs 属性
| 特性 | 字段(Field) | 属性(Property) |
|---|---|---|
| 访问控制 | 直接数据存储 | 通过get/set方法控制访问 |
| 数据验证 | 无法实现 | 可在setter中添加验证逻辑 |
| 示例 | public int Speed; |
public int Speed { get; private set; } |
const vs readonly
| 特性 | const (常量) |
readonly (只读) |
|---|---|---|
| 赋值时机 | 编译时(声明时必须赋值) | 运行时(构造函数中赋值) |
| 内存占用 | 不占用实例内存 | 占用实例内存 |
| 修改权限 | 完全不可修改 | 构造函数内可修改 |
实现只写属性(Write-Only Property)
仅提供set访问器,不提供get访问器(需搭配私有字段使用):
csharp
private string _secret;
public string Secret { set => _secret = value; } // 只写属性
C#内存管理核心机制
内存生命周期
所有程序语言的内存生命周期都遵循三个阶段:
- 分配内存:创建变量/对象时分配(如new关键字创建对象)。
- 使用内存:读写数据操作(如修改对象属性、调用方法)。
- 释放内存:不再使用时回收(C# 中由垃圾回收机制 GC 自动处理)。
内存分区
| 分区 | 存储内容 | 管理方式 | 示例 |
|---|---|---|---|
| 栈区 | 值类型数据、引用类型的地址指针 | 编译器自动分配释放 | int n = 1;(4字节空间存数字1) |
| 堆区(托管堆) | 引用类型对象本身 | .NET垃圾回收器(GC)管理 | new Car()(实际Car对象数据) |
| 静态/常量区 | 静态类、静态成员、常量 | 程序结束时释放 | static int counter; |
| 代码区 | 函数二进制代码 | 程序结束时释放 | 方法体的机器指令 |
值类型 vs 引用类型
值类型
引用类型
声明变量
类型
直接存储在栈区
栈存指针 + 堆存实际对象
栈区特性
- 大小固定(编译时确定)
- 仅存储确定大小的数据(如int=4字节)
- 自动释放(超出作用域立即回收)
- 示例:
int a = 10;直接在栈分配4字节
堆区特性
- 动态分配大小(运行时确定)
- 存储对象实际数据
- 由GC自动回收(非即时)
- 示例:
Car c = new Car();:- 栈:存储指针(4/8字节)
- 堆:存储Car对象所有字段数据
垃圾回收机制(GC)
- 托管语言特性:C#无需手动管理内存(对比C的malloc/free)
- GC工作流程 :
- 标记不再使用的对象(无引用指向)
- 压缩堆空间(消除内存碎片)
- 更新对象引用地址
- 触发时机 :
- 堆内存不足时
- 调用
GC.Collect()(不推荐手动调用)
内存管理最佳实践
-
值类型优先:小型数据用struct(栈分配更快)
-
避免大对象:>85KB对象直接进大对象堆(LOH),回收效率低
-
谨慎使用静态成员:
csharpstatic List<Data> cache = new List<Data>(); // 常驻内存直到程序结束 -
及时解除引用:
csharpvar data = new byte[1000000]; // 使用后... data = null; // 提示GC可回收
指针说明
-
本质:内存地址的数值表示
-
C#中的限制:
- 普通代码无法直接操作指针
- 需使用
unsafe上下文(特定场景如高性能计算)
csharpunsafe { int* ptr = &n; // 获取变量地址 }
什么是复合?什么是继承?
复合(Composition)
-
定义:一个类包含其他类的实例作为成员变量("has-a"关系)
-
特点:
- 通过组合多个对象实现功能
- 对象生命周期由容器类管理
- 更灵活,支持运行时动态修改
-
示例:
csharpclass Engine { /* 引擎实现 */ } class Car { private Engine _engine; // 复合关系 public Car() { _engine = new Engine(); } }
继承(Inheritance)
-
定义:子类基于父类创建,获得父类特性("is-a"关系)
-
特点:
- 代码复用:子类自动获得父类方法和属性
- 支持多态:父类引用可指向子类对象
-
示例:
csharpclass Vehicle { /* 交通工具基类 */ } class Car : Vehicle { /* 汽车子类 */ } // 继承关系
继承和复合是面向对象编程中扩展类的两种方式,主要区别如下:
- 继承:假设,B类继承自A类,B类是A类的派生类,子类B具有A类的某些特性。那么可以说,A类和B类是同一种东西,也就能使用is - a来表示两者的关系。继承分为接口继承和实现继承,主要目标是代码重用。例如,车具有引擎和车轮,奔驰车也具有引擎和车轮,同时奔驰车具备基础车不具备的Boss音响,即便如此,它仍然是车,因为它具备基础车所有的属性,只是在基础车上增加了Boss音响。
- 复合:假设,B类需要引用A类的某些属性和功能,B类只需要A类的属性,并不需要暴露/知道A类的具体细节,当需要获取A类的属性或数据时,仅需要暴露A类的API提供访问就可以。我们认为,A类和B类不是同一种东西,B类只是需要A类的部分信息,也就能使用has - a来表示两者的关系。B类的任何一个实例对象都可以调用现有方法来返回A类的结果,这种方式叫转发。例如,车库里有各种各样的灯和不同的车,车库并不关心车库内有什么品牌的车,车本质上并不是车库的一部分,而是只需要实现停车的功能或者需要灯具实现照亮车库的功能,那么我们只需要在车库中包含车辆、灯具的实例就可以实现。
高内聚低耦合
高内聚、低耦合的含义:在软件开发中,耦合是指两个或多个模块之间的依赖程度,内聚是指模块内部各个部分之间的关联程度。高内聚、低耦合是指模块内的功能联系紧密,而模块间的依赖性较弱。一个高内聚的模块,其内部的功能应该是紧密相关的,所有组成部分都为实现一个特定的功能而存在。当一个模块的改变会影响到另一个模块时,说明这两个模块是耦合的。耦合程度越高,模块之间的依赖性越强,意味着更改一个模块时可能会带来连锁反应,影响到其他模块的功能和行为。
-
高内聚 :类内部功能紧密相关(如
Car只处理驾驶逻辑) -
低耦合:类间依赖最小化(通过接口而非具体实现交互)
-
松耦合实现:
- 依赖接口而非具体类
- 使用依赖注入
- 遵循"面向接口编程"原则
-
复合优于继承:
场景 继承 复合 灵活性 编译时固定关系 运行时动态替换组件 层次结构 易导致过深继承链 扁平结构,易维护 破坏封装 子类可能破坏父类实现 内部实现完全封装 典型应用 ElectricCar : CarCar.HasEngine() -
UML(统一建模语言) :
统一建模语言(UML)是一种通用的可视化建模语言,可以用来描述、可视化、构造和文档化软件密集型系统的各种工件。UML独立于过程,适用于各种软件开发方法、软件生命周期的各个阶段、各种应用领域以及各种开发工具。其目标是为建模者提供可用的、富有表达力的、可视化的建模语言,以开发和交换有意义的模型;提供可扩展性和特殊化机制以延伸核心概念等。UML不是一种程序设计语言,其描述的模型可以和各种编程语言相联系。
- 可视化类关系的标准图表
- 继承:空心三角箭头(▷──)
- 复合:实心菱形箭头(◆──)
C#的继承特点
核心特性
-
单继承:类只能继承一个父类(但可实现多个接口)
csharpclass ElectricCar : Car, IRechargeable { ... } -
构造方法继承:
-
子类必须调用父类构造函数(隐式或显式)
-
使用
base关键字显式调用:csharppublic ElectricCar() : base("Tesla") { ... }
-
访问修饰符
| 修饰符 | protected |
internal |
|---|---|---|
| 访问范围 | 本类及子类(跨程序集) | 同一程序集内所有类 |
| 典型应用 | 父类定义被子类重写的方法 | 组件内部共享的辅助类 |
| 组合修饰符 | protected internal:子类或同程序集 |
对象类型转换
向上转型(Upcasting)
-
定义:子类→父类的转换(隐式安全)
-
特点:
- 丢失子类特有成员
- 保留父类接口
-
示例:
csharpCar myCar = new ElectricCar(); // 向上转型 myCar.Drive(); // 只能调用Car类方法
向下转型(Downcasting)
-
定义:父类→子类的转换(显式,可能失败)
-
安全方式:
csharpif (myCar is ElectricCar ev) { ev.Charge(); // 访问子类特有方法 } // 或 ElectricCar ev = myCar as ElectricCar; if (ev != null) { ... }
装箱与拆箱
| 操作 | 定义 | 示例 | 性能影响 |
|---|---|---|---|
| 装箱 | 值类型→引用类型(object) | object obj = 42; |
内存分配+GC压力 |
| 拆箱 | 引用类型→值类型(显式转换) | int num = (int)obj; |
类型检查开销 |
| 避免 | 使用泛型集合 | List<int> 而非 ArrayList |
消除装箱/拆箱开销 |
向上转型的作用
- 实现多态:通过父类引用调用子类方法(如 "动物" 数组存储 "狗""猫" 实例,统一调用
Sound())。
向下转型的风险
- 若父类引用实际指向的对象非目标子类类型,强转会报错(如
Animal→Cat时实际是Dog对象)。
装箱拆箱的性能影响
- 频繁装箱拆箱会产生额外内存开销(值类型在堆中创建临时对象),应尽量避免在循环中使用。
UML类关系详解(耦合度由低到高)
依赖(Dependency)
依赖关系可以简单理解为一个类A使用到了另一个类B,而这种使用关系具有偶然性、临时性且非常弱,但B类的变化会影响到A类。例如,某人要过河,需要借用一条船,此时人与船之间的关系就是依赖。动物要有生命力,需要氧气、水以及食物等,这表明动物依赖于氧气和水。
-
定义:临时性使用关系("use-a")
-
耦合度:★☆☆☆☆(最弱)
-
UML表示 :
虚线箭头──→ -
特点:
- 一个类短暂使用另一个类,不持有其引用
- 被依赖类的变化可能影响依赖类
-
代码实现:
csharppublic class Computer { public static void Start() { Console.WriteLine("电脑启动"); } } public class Student { // 1. 方法参数构成依赖 public void Program(Computer computer) { ... } // 2. 局部变量构成依赖 public void PlayGame() { Computer computer = new Computer(); // 临时使用 } // 3. 静态方法调用构成依赖 public void Study() { Computer.Start(); // 无实例引用 } }
关联(Association)
关联关系体现的是两个类、或者类与接口之间语义级别的一种强依赖关系,这种关系比依赖更强,不存在依赖关系的偶然性,关系一般是长期性的,而且双方的关系通常是平等的。关联可以是单向关联、双向关联、自身关联、多维关联等。例如,学生与老师是关联的,学生可以不用电脑,但是学生不能没有老师;客户和订单,每个订单对应特定的客户,每个客户对应一些特定的订单;公司和员工,每个公司对应一些特定的员工,每个员工对应一特定的公司。
-
定义:结构性使用关系("has-a")
-
耦合度:★★☆☆☆
-
UML表示 :
实线箭头──→ -
特点:
- 一个类长期持有另一个类的引用
- 被关联类是关联类的必要组成部分
-
代码实现:
csharppublic class Teacher { ... } public class Student { // 持有Teacher的长期引用(成员变量) private Teacher _teacher; // 关联关系 public Student(Teacher teacher) { _teacher = teacher; // 初始化时建立关联 } public void AttendClass() { _teacher.Teach(); // 持续使用 } } -
与依赖的关键区别:
特征 依赖(Dependency) 关联(Association) 持续时间 临时(方法内) 长期(成员变量) 代码表现 参数/局部变量/静态调用 类成员字段 关系强度 弱(电脑可随时更换) 强(老师相对固定)
其他类关系
聚合(Aggregation)
聚合是关联关系的一种特例,它体现的是整体与部分的拥有关系(has a),整体与部分之间是可分离的,它们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享。例如,班级与学生之间存在聚合关系,学校和教师,工厂和工人,电脑和它的显示器、键盘、主板以及内存等。
-
定义:整体-部分关系,部分可独立存在
-
耦合度:★★★☆☆
-
UML表示 :
空心菱形 + 实线◇──→ -
示例:
csharppublic class Tire { ... } public class Car { private Tire[] _tires; // 聚合关系 public Car(Tire[] tires) { _tires = tires; // 轮胎可独立于汽车存在 } }
组合(Composition)
组合也是关联关系的一种特例,它体现的是整体与部分的包含关系(contains a),这种关系比聚合更强,也称为强聚合。在组合关系中,整体与部分是不可分的,整体的生命周期结束意味着部分的生命周期也结束。例如,人和心脏,电脑和CPU(如果CPU没了,电脑就失去了基本的运算能力等同于电脑报废),公司和部门(没有公司就不存在部门)。
-
定义:强整体-部分关系,部分不能独立
-
耦合度:★★★★☆
-
UML表示 :
实心菱形 + 实线◆──→ -
示例:
csharppublic class Heart { ... } public class Person { private Heart _heart; // 组合关系 public Person() { _heart = new Heart(); // 心脏随人创建/销毁 } }
泛化(Generalization)
泛化是学术名称,通俗来讲,泛化指的是类与类之间的继承关系和类与接口之间的实现关系。它指定了子类如何特化父类的所有特征和行为,是一种耦合度最高的关系,体现了"is a"的关系。例如,哺乳动物具有恒温、胎生、哺乳等生理特征,猫和牛都是哺乳动物,也都具有这些特征,但除此之外,猫会捉老鼠,牛会耕地。
-
定义:继承关系("is-a")
-
耦合度:★★★★★(最强)
-
UML表示 :
空心三角箭头▷─── -
示例:
csharppublic class Vehicle { ... } public class Car : Vehicle { ... } // 泛化关系
耦合度总结
最弱
最强
依赖
关联
聚合
组合
泛化
高耦合
设计原则
- 优先弱耦合:尽量使用依赖而非关联
- 慎用继承:泛化导致高耦合,优先选择组合
- 关系选择准则 :
- 临时使用 → 依赖
- 长期持有但可替换 → 关联/聚合
- 生命周期绑定 → 组合
- 逻辑"is-a"关系 → 泛化
方法重写(Method Overriding)
方法重写(Method Overriding)是面向对象编程中的一个重要概念,它允许子类重新定义父类中已有的方法,从而实现特定于子类的行为。这一机制使得子类可以根据自身的需求,对从父类继承来的方法进行定制化修改。
核心概念
- 定义:子类重新实现父类中已定义的虚方法(virtual method)
- 目的:修改或扩展父类方法的行为,实现多态性
- 必要条件 :
- 父类方法必须使用
virtual或abstract修饰 - 子类方法必须使用
override关键字 - 方法签名(名称、参数、返回类型)必须完全一致
- 父类方法必须使用
方法重写 vs 方法重载
| 特性 | 方法重写(Override) | 方法重载(Overload) |
|---|---|---|
| 作用范围 | 父子类之间 | 同一类内 |
| 方法签名 | 必须相同 | 必须不同(参数类型/数量) |
| 关键字 | virtual + override |
无 |
| 绑定方式 | 运行时动态绑定(多态) | 编译时静态绑定 |
| 目的 | 修改实现逻辑 | 提供相同功能的不同参数版本 |
虚方法的作用
- 允许父类定义方法框架
- 子类可选择性重写具体实现
- 实现"一个接口,多种实现"的多态特性
代码示例
csharp
class Animal {
public virtual void Speak() { // 虚方法
Console.WriteLine("Animal sound");
}
}
class Dog : Animal {
public override void Speak() { // 重写方法
Console.WriteLine("Woof!");
}
}
// 使用
Animal myDog = new Dog();
myDog.Speak(); // 输出 "Woof!"(调用子类重写方法)
多态(Polymorphism)
多态是指同一个方法调用在不同对象上表现出不同行为的现象 ,本质是 "接口统一,实现各异 "。它通过 "父类引用指向子类对象" 实现,使程序能够以统一的方式处理不同类型的对象。
核心概念
- 定义:同一操作作用于不同类的对象时,产生不同的执行结果
- 核心形式 :
- 编译时多态:方法重载
- 运行时多态:方法重写(虚方法机制)
多态可以分为编译时多态(静态多态)和运行时多态(动态多态):
- 编译时多态(静态多态) :也称为重载多态(Overloading Polymorphism),是在编译时期确定的多态性。在静态多态中,编译器根据函数的参数类型、个数或者返回类型来选择合适的函数实现。这种多态性是通过函数重载来实现的,同一个函数名可以有多个不同的定义,根据传递给函数的参数来确定使用哪个定义。例如,在一个计算器类中,可以定义多个
add方法,分别处理不同类型和数量的参数。
java
class Calculator {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
public double add(double a, double b, double c) {
return a + b + c;
}
}
- 运行时多态(动态多态):也称为覆盖多态(Overriding Polymorphism),是在运行时确定的多态性。在动态多态中,通过继承和虚函数的机制实现。基类可以定义一个虚函数,子类可以重写该虚函数并提供自己的实现。当通过基类指针或引用调用该虚函数时,实际上会根据对象的类型来调用对应的子类实现。这种多态性允许在运行时动态决定使用哪个函数实现,提供了更大的灵活性。例如,在一个动物类层次结构中,通过父类引用调用子类重写的方法,会根据实际引用的对象类型来决定调用哪个子类的方法。
java
class Animal {
public void makeSound() {
System.out.println("动物发出声音");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("狗发出汪汪的声音");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("猫发出喵喵的声音");
}
}
public class Main {
public static void main(String[] args) {
Animal myDog = new Dog();
Animal myCat = new Cat();
myDog.makeSound(); // 输出: 狗发出汪汪的声音
myCat.makeSound(); // 输出: 猫发出喵喵的声音
}
}
多态示例
csharp
class Shape {
public virtual void Draw() => Console.WriteLine("Drawing shape");
}
class Circle : Shape {
public override void Draw() => Console.WriteLine("Drawing circle");
}
class Square : Shape {
public override void Draw() => Console.WriteLine("Drawing square");
}
// 多态应用
void RenderShapes(List<Shape> shapes) {
foreach (var s in shapes) {
s.Draw(); // 同一调用,不同结果
}
}
// 使用
var shapes = new List<Shape> { new Circle(), new Square() };
RenderShapes(shapes);
/* 输出:
Drawing circle
Drawing square */
多态的作用
- 代码扩展性:新增子类无需修改父类代码
- 接口统一:通过父类引用操作不同子类对象
- 解耦合:调用方无需知道具体子类类型
- 可维护性:业务逻辑集中处理,减少条件分支
实现机制
- 虚方法表(vtable) :
- 每个类维护虚方法地址表
- 子类重写时更新表项
- 动态绑定 :
- 运行时根据对象实际类型调用方法
- 示例:
Animal a = new Dog(); a.Speak()调用Dog.Speak()
什么是接口?
定义
接口(Interface)是一种完全抽象的契约,它定义了一组方法、属性或事件的签名("是什么"),但不包含具体实现。实现接口的类必须遵守该契约,提供所有成员的具体实现("怎么做")。
为什么需要接口?
- 解耦与多态 :允许不同类通过相同接口互换使用(如
IPayment接口被CreditCard和PayPal类实现)。 - 强制规范 :确保实现类具备特定能力(如
IComparable要求实现排序逻辑)。 - 支持组合:通过接口而非继承构建灵活的系统(避免多重继承限制)。
has-a vs is-a 关系
| 关系类型 | 含义 | 示例 |
|---|---|---|
is-a(继承) |
类与父类的层级关系 | Dog is a Animal |
has-a(组合) |
类包含其他对象作为成员 | Car has a Engine(通过接口 IEngine 组合) |
接口实现对象复合
通过让类实现多个接口(如 ICamera + IGPS),将不同功能组合到单一对象中,避免继承链膨胀。
python
public interface IEngine { void Start(); }
public class Car {
private readonly IEngine _engine; // 依赖接口
public Car(IEngine engine) { _engine = engine; } // 构造函数注入具体实现
public void StartCar() { _engine.Start(); }
}
什么是单元测试?
定义
单元测试是对软件中最小可测试单元(如函数、类方法)的隔离验证,通常使用测试框架(如JUnit、NUnit)自动化执行。
如何对 C# 代码进行单元测试
-
选择测试框架 :常用框架包括 NUnit 、xUnit 、MSTest。
-
创建测试项目:在 Visual Studio 中添加 "单元测试项目",引用待测试的项目。
-
编写测试类 :使用框架特性(如
[TestFixture]/[TestClass])标记测试类,用[Test]/[TestMethod]标记测试方法。 -
编写测试用例 :使用断言(如
Assert.AreEqual)验证方法输出是否符合预期。
示例(xUnit):
csharp
public class CalculatorTests {
[Fact] // 测试方法
public void Add_ValidNumbers_ReturnsCorrectSum() {
var calculator = new Calculator();
var result = calculator.Add(2, 3);
Assert.Equal(5, result); // 断言验证
}
}
单元测试的好处?
- 早期发现逻辑错误。
- 支持重构(测试确保修改不破坏现有功能)。
- 文档作用(测试用例描述代码行为)。
测试驱动开发(TDD)
- 流程:先写失败测试 → 写最小实现通过测试 → 重构优化。
- 优点 :
- 强制清晰的需求理解。
- 高测试覆盖率。
- 促进模块化设计(易于测试的代码通常耦合度低)。
NuGet 是什么?
NuGet 是.NET 平台的包管理工具,用于:
- 搜索、安装、更新第三方库(如日志组件
NLog、ORM 框架Entity Framework)。 - 管理项目依赖项,简化手动引用 dll 的流程。
- 通过
nuget.org官方仓库或私有仓库共享自定义组件。
使用方式 :通过 Visual Studio 的 NuGet 包管理器或命令行dotnet add package <包名>安装包。
什么是依赖注入(DI)?
定义
依赖注入是一种实现控制反转(IoC)的设计模式,将对象的依赖项(如服务、数据库连接)从外部"注入"而非在内部创建。
核心思想
"给予调用方它所需要的事物" ------ 由框架/容器管理依赖对象的创建与传递。
为什么需要 DI?
- 解耦:类不直接依赖具体实现,而是依赖抽象(如接口)。
- 可测试性:通过注入模拟对象(Mock)隔离测试单元。
- 可维护性:更换依赖无需修改业务代码(如切换数据库驱动)。
实现方式
| 注入类型 | 示例 |
|---|---|
| 构造函数注入 | public UserService(IUserRepository repo) { ... } |
| 属性注入 | public ILogger Logger { set; get; } |
| 方法注入 | public void Process(ILogger logger) { ... } |
python
通过构造函数参数注入依赖项,适用于必需的依赖。
public class UserService {
private readonly ILogger _logger;
public UserService(ILogger logger) { // 构造函数注入
_logger = logger;
}
}
通过公共属性注入依赖项,适用于可选依赖。
public class UserService {
public ILogger Logger { get; set; } // 属性注入
}
与控制反转(IoC)的关系
- IoC 是原则:将控制权从程序转移至容器(如管理对象生命周期)。
- DI 是具体实现:容器通过构造函数/属性等注入依赖项。
什么是测试驱动开发(TDD)?
定义
TDD(Test-Driven Development)是一种先编写测试代码,再实现功能 的开发方法。其核心流程遵循 "红→绿→重构" 循环:
- 红:针对需求编写一个失败的单元测试(测试未通过)。
- 绿 :编写最小功能代码使测试通过(不求完美,只求通过)。
- 重构:优化代码结构(消除冗余、提升可读性),确保测试始终通过。
与传统开发的区别
| 维度 | 传统开发 | TDD |
|---|---|---|
| 开发顺序 | 先写业务代码 → 后补测试 | 先写测试 → 再写业务代码 |
| 目标导向 | 实现功能 | 通过测试驱动功能实现 |
| 设计影响 | 代码可能耦合度高 | 倒逼模块化、低耦合设计(易测试的代码) |
| 重构时机 | 后期集中优化 | 每轮循环后立即重构(持续优化) |
TDD解决的问题
- 减少Bug:测试覆盖所有功能边界,提前暴露逻辑错误。
- 提升设计质量:迫使开发者思考接口与解耦(测试需隔离依赖)。
- 文档即测试:测试用例是活的文档,明确代码行为预期。
单元测试在TDD中的角色
- 测试范围:针对最小单元(如一个函数、一个类方法)。
- 测试内容 :
- 正常流程(如输入
2+3,输出5)。 - 异常流程(如输入
null,抛出预期异常)。 - 边界条件(如数组空值、数值溢出)。
- 正常流程(如输入
- 测试工具 :
- 框架:JUnit(Java)、NUnit(.NET)、Pytest(Python)。
- 依赖隔离:使用Mock对象(如Mockito)模拟数据库、网络等外部依赖。
TDD中的单元测试 vs 传统单元测试
- TDD :测试是设计的起点,定义功能契约(接口该做什么)。
- 传统 :测试是验证工具,用于检查已完成的代码。
TDD的核心流程详解
以"计算字符串数字的和"为例:
-
需求 :输入
"1,2",输出3;输入空字符串,输出0。 -
循环1:
-
测试(红):
java@Test public void emptyString_ReturnsZero() { int result = Calculator.add(""); Assert.assertEquals(0, result); // 预期失败(无add方法) } -
实现(绿):
javapublic class Calculator { public static int add(String numbers) { return 0; // 最小实现通过测试 } }
-
-
循环2:
-
测试(红):
java@Test public void singleNumber_ReturnsNumber() { Assert.assertEquals(1, Calculator.add("1")); // 失败(当前返回0) } -
实现(绿):
javapublic static int add(String numbers) { if (numbers.isEmpty()) return 0; return Integer.parseInt(numbers); // 仅处理单数字 }
-
-
循环3:
-
测试(红):
java@Test public void twoNumbers_ReturnsSum() { Assert.assertEquals(3, Calculator.add("1,2")); // 失败(当前仅解析一个数字) } -
实现(绿):
javapublic static int add(String numbers) { if (numbers.isEmpty()) return 0; String[] nums = numbers.split(","); int sum = 0; for (String num : nums) { sum += Integer.parseInt(num); } return sum; } -
重构:优化循环逻辑,提取拆分方法。
-
TDD的优缺点
| 优点 | 挑战 |
|---|---|
| ✅ 代码质量高(低Bug率、高覆盖率) | ⚠️ 学习曲线陡峭(需改变开发习惯) |
| ✅ 设计灵活(依赖接口而非具体实现) | ⚠️ 初期速度慢(需写大量测试) |
| ✅ 重构安全(测试保护已有功能) | ⚠️ 不适用UI/复杂集成测试 |
结构(Struct)与类(Class)有什么区别?
| 对比维度 | 结构(Struct) | 类(Class) |
|---|---|---|
| 类型 | 值类型(Value Type) | 引用类型(Reference Type) |
| 内存分配 | 存储在栈(Stack)或内联于堆对象中 | 存储在堆(Heap)中,栈中仅存储引用地址 |
| 继承性 | 不能继承其他类型,也不能被继承(密封类型) | 支持继承和多态(基类默认为object) |
| 默认构造函数 | 隐式存在,不可自定义无参构造函数 | 可自定义无参构造函数,若未定义则编译器自动生成 |
| 实例化方式 | 可直接赋值创建(无需new),也可用new |
必须使用new关键字实例化 |
| 相等性比较 | 按值比较(==比较字段内容) |
按引用比较(==默认比较内存地址) |
| 装箱 / 拆箱 | 支持装箱(值类型→引用类型)和拆箱 | 无需装箱(本身为引用类型) |
| 适用场景 | 轻量级数据结构(如坐标点Point、向量Vector) |
复杂业务逻辑、需要继承或多态的场景 |
结构(Struct)数据保存在栈内存还是堆内存?
- 栈内存 :当结构是局部变量 或方法参数时,分配在栈上。
- 堆内存 :当结构作为引用类型的成员 (如类的字段)或装箱时,会嵌入堆中。
📌 关键:结构本身是值类型,其存储位置取决于上下文(栈或堆),但语义上按值传递。
python
// 栈存储(局部变量)
void Method() {
Point p = new Point(1, 2); // p存储在栈中
}
// 堆存储(作为类的字段)
class MyClass {
public Point p = new Point(1, 2); // p内联于MyClass的堆对象中
}
C# 中如何处理 null?
- 引用类型 :默认可赋值为
null(表示无对象引用)。
python
string name = null; // 合法
- 值类型 :
- 普通值类型(如
int)不可为null。 - 通过
Nullable<T>(简写T?)支持可空值类型(如int? num = null;)。
- 普通值类型(如
python
int? age = null; // 等价于 Nullable<int>,允许为null
- 空检查操作符 :
?.(安全导航):obj?.Method()(若obj为null则返回null)。??(空合并):value ?? defaultValue(若左侧为null用默认值)。
python
string result = name ?? "默认值"; // 若name为null,返回"默认值"
int length = name?.Length ?? 0; // 若name为null,length为0
- 模式匹配 :
if (obj is null) { ... }
反射的原理是什么?
- 核心机制 :通过运行时访问程序集的元数据(Metadata),动态获取类型信息(类、方法、属性等)。
- 实现流程 :
- 加载程序集(
Assembly.Load())。 - 获取类型(
Type.GetType("ClassName"))。 - 动态创建实例(
Activator.CreateInstance())。 - 调用方法或访问属性(
MethodInfo.Invoke())。
- 加载程序集(
- 用途:依赖注入、序列化、IDE智能提示等。
- 性能代价:反射操作较慢,需谨慎使用。
python
Type type = typeof(MyClass);
object instance = Activator.CreateInstance(type); // 动态创建实例
MethodInfo method = type.GetMethod("MyMethod");
method.Invoke(instance, null); // 动态调用方法
元数据(Metadata)是什么?
- 定义 :嵌入在.NET程序集中的结构化数据,描述代码结构(如类型定义、方法签名、属性、依赖关系等)。
- 包含内容 :
- 类型名称、基类、实现的接口。
- 成员信息(方法、字段、属性)。
- 程序集版本、引用信息。
- 作用 :
- 支持反射、代码编译、运行时类型检查。
- 实现跨语言互操作(如C#调用VB.NET组件)。
C# 中如何处理异常?
python
try {
// 可能抛出异常的代码
} catch (ExceptionType1 ex) { // 捕获特定类型异常
// 处理逻辑
} catch (ExceptionType2 ex) { // 按继承关系,子类异常需先捕获
// 处理逻辑
} finally { // 可选,无论是否发生异常都会执行
// 资源清理(如关闭文件、释放连接)
}
-
关键字机制:
csharptry { // 可能抛出异常的代码 File.ReadAllText("path.txt"); } catch (FileNotFoundException ex) // 捕获特定异常 { Console.WriteLine($"文件未找到: {ex.Message}"); } catch (Exception ex) // 捕获所有异常 { Console.WriteLine($"错误: {ex.Message}"); } finally { // 无论是否异常都会执行(如释放资源) } -
异常类型 :所有异常均继承自
System.Exception,常见类型包括:NullReferenceException(空引用异常)ArgumentNullException(参数为空异常)DivideByZeroException(除零异常)
-
抛出异常 :使用
throw关键字手动抛出异常:csharpif (value == null) { throw new ArgumentNullException(nameof(value)); } -
异常传播:若当前作用域未处理异常,会向上层调用栈传播,直至被捕获或导致程序终止。
-
最佳实践:
- 捕获具体异常类型,避免滥用
catch (Exception)。 - 在
finally块中释放资源(如使用IDisposable接口的using语句)。 - 自定义异常类型以区分业务逻辑错误。
- 捕获具体异常类型,避免滥用
集合相关问题
在C#中,集合(Collection)类是专门用于数据存储和检索的类。这些类提供了对栈(stack)、队列(queue)、列表(list)和哈希表(hash table)的支持,大多数集合类实现了相同的接口。集合类服务于不同的目的,如为元素动态分配内存,基于索引访问列表项等等,这些类创建Object类的对象的集合,在C#中,Object类是所有数据类型的基类。
-
核心接口 :
ICollection是 C# 集合的基础接口,定义了集合的通用操作(如Count、Add、Remove、Clear等)。 -
分类 :根据特性不同,集合可分为多种类型,例如:
- 有序集合(如数组、列表):元素有固定顺序,可通过索引访问。
- 动态集合 (如
List<T>):大小可动态调整,无需预先指定容量。 - 泛型集合 (如
List<T>、Dictionary<TKey, TValue>):支持类型安全,避免装箱拆箱操作。
Q1:C#中集合有什么作用?
- 核心作用 :高效管理一组相关对象,提供统一的增删改查操作。
- 关键能力 :
- 动态调整大小(如
List)。 - 支持排序、搜索、过滤等操作(如
LINQ)。 - 实现数据结构的标准化(如列表、字典、队列)。
- 动态调整大小(如
Q2:数组(Array)与列表(List)有什么区别?
| 特性 | 数组(Array) | 列表(List<T>) |
|---|---|---|
| 长度 | 固定长度(创建后不可变) | 动态扩容(自动调整大小) |
| 内存 | 连续内存分配 | 基于数组封装,动态分配内存 |
| 类型安全 | 支持泛型(如int[]) |
强类型泛型(List<int>) |
| 功能方法 | 基础操作(索引访问) | 丰富方法(Add(), Remove()等) |
| 性能 | 更高访问速度(直接内存寻址) | 略低(需动态管理) |
Q3:列表(List)与数组列表(ArrayList)有什么区别?
| 特性 | List<T> | ArrayList |
|---|---|---|
| 类型安全 | 泛型强类型(编译时检查) | 存储object类型(需装箱拆箱) |
| 性能 | 更高(避免装箱拆箱开销) | 较低(值类型需装箱) |
| 内存效率 | 更优(直接存储值类型) | 较差(堆内存占用高) |
| 推荐场景 | 现代C#开发首选 | 遗留代码兼容(.NET 1.x) |
Q4:List的常用操作有哪些?
- 创建 :
List<int> list = new List<int>(); - 添加 :
list.Add(10);(末尾追加) - 插入 :
list.Insert(0, 5);(在索引0处插入5) - 删除 :
list.RemoveAt(0);(删除索引0的元素) - 其他 :
list.Remove(10)(删除第一个匹配值)list.Clear()(清空)list.Contains(5)(检查是否存在)
Foreach 相关问题
Q1:集合迭代的原理和工作流程是什么?
1. 集合迭代的核心原理
foreach 循环的本质是通过 迭代器(Iterator) 模式实现的,依赖以下两个接口:
IEnumerable:声明集合支持迭代(包含GetEnumerator()方法,返回IEnumerator对象)。IEnumerator:实现迭代逻辑(包含MoveNext()、Current、Reset()方法)。
2. foreach 的执行流程
-
获取迭代器 :通过
集合对象.GetEnumerator()获取IEnumerator实例。 -
遍历元素 :
- 调用
MoveNext()方法移动到下一个元素,若返回true,则通过Current属性获取当前元素。 - 重复此步骤,直到
MoveNext()返回false(遍历结束)。
- 调用
-
释放资源 :遍历完成后,自动调用
IDisposable.Dispose()释放迭代器占用的资源(若迭代器实现了IDisposable)。
Q2:foreach 是如何实现的?
csharp
// 以下代码:
foreach (var item in collection) { ... }
// 编译器转换为:
IEnumerator enumerator = collection.GetEnumerator();
try {
while (enumerator.MoveNext()) {
var item = (T)enumerator.Current;
...
}
}
finally {
(enumerator as IDisposable)?.Dispose();
}
Q3:IEnumerator 与 IEnumerable 是什么?
enum 关键字:用于声明一个枚举类型。
csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace HelloWord
{
public enum ShapeType
{
Circle,
Rectangle
}
}
-
枚举成员 :
Circle 和 Rectangle 是 ShapeType 枚举的成员,它们是命名的常量。默认情况下,枚举成员的值从 0 开始依次递增,即 Circle 的值为 0,Rectangle 的值为 1。
-
IEnumerable接口:- 定义:
public interface IEnumerable { IEnumerator GetEnumerator(); } - 作用:标识集合可被迭代。
- 定义:
-
IEnumerator接口:-
定义:
csharppublic interface IEnumerator { bool MoveNext(); object Current { get; } void Reset(); } -
作用:提供遍历集合的状态控制 (
MoveNext())和当前元素访问 (Current)。
-
(一)IEnumerable
- 定义 :
IEnumerable是一个由可枚举类实现的接口,整个集合在C#中可以实现IEnumerable接口,它返回IEnumerator。IEnumerable有一个称为GetEnumerator的方法,此实现仅在类内部完成。迭代过程使得在集合中遍历变得更容易,它充当一个对象。GetEnumerator是用于实现IEnumerator接口的方法。 - 作用 :
IEnumerable通常被称为通用接口,因为代码编写得非常小,只需要一次性实现。它用抽象化用于数组中所有元素的迭代,使用一个IEnumerator迭代器来迭代所有元素。由于它是一个泛型类,所以很容易对数组中的元素进行迭代,提供了一个通用接口,可用于所有非通用类。
(二)IEnumerator
- 定义 :
IEnumerator有两种方法来实现集合中所有元素的迭代,它有MoveNext和Reset两个方法,以及一个Current属性。MoveNext方法指出迭代还没有到达集合的最后一个元素,Reset方法指出在上一次迭代完成后再次开始迭代,直到数组的最后一个元素,Current属性给出当前元素作为迭代的结果。 - 作用 :
IEnumerator对其元素有一些特定的访问权限,它只提供对其所有数组的只读访问。IEnumerator调用Current属性,负责返回列表中当前正在使用的元素,使用MoveNext和Reset方法以及Current属性来迭代对象。
(三)两者的区别
IEnumerable是模块,而IEnumerator是对象。IEnumerable是用于迭代的序列,IEnumerator则像是序列的一个游标。多个IEnumerator可以遍历同一个IEnumerable,并且不会改变IEnumerable的状态,而IEnumerator本身就是多状态的,每次调用MoveNext(),当前游标都会向前移动一个元素。IEnumerable是一个可以迭代对象集合的接口,而IEnumerator是一个实现IEnumerator接口并提供遍历集合的方法的类。IEnumerable提供了一个返回IEnumerator对象的方法(GetEnumerator),而IEnumerator提供了在集合中移动(MoveNext)和检索当前对象(Current)的方法。IEnumerable用于创建可以迭代的集合,而IEnumerator用于遍历这些集合。IEnumerable用于通用接口,而IEnumerator用于非通用接口。IEnumerable可以返回IEnumerator,但IEnumerator无法返回IEnumerable。
Q4:如何使用 yield return 简化迭代器?
yield return 是 C# 提供的语法糖,用于简化迭代器的实现。通过在方法中使用 yield return,编译器会自动生成迭代器类,无需手动实现 IEnumerator 接口。
csharp
public static IEnumerable<int> GetNumbers() {
yield return 1; // 返回当前元素并暂停,下次调用 MoveNext() 时继续执行
yield return 2;
yield return 3;
}
// 使用 foreach 遍历:
foreach (int num in GetNumbers()) {
Console.WriteLine(num); // 输出:1, 2, 3
}
Q5:如何使用 Benchmark 做性能基准测试?
-
工具介绍 :
BenchmarkDotNet是一个常用的性能测试库,可精准测量代码执行时间、内存消耗等指标。 -
使用步骤 :
- 安装 NuGet 包:
BenchmarkDotNet。 - 在类中添加
[Benchmark]特性标记测试方法。 - 调用
BenchmarkRunner.Run<测试类>()执行测试。
- 安装 NuGet 包:
csharp
using BenchmarkDotNet.Attributes;
using System.Collections.Generic;
public class ListBenchmark {
private List<int> list = new List<int> { 1, 2, 3, 4, 5 };
[Benchmark]
public void ForeachLoop() {
int sum = 0;
foreach (int item in list) {
sum += item;
}
}
[Benchmark]
public void ForLoop() {
int sum = 0;
for (int i = 0; i < list.Count; i++) {
sum += list[i];
}
}
}
// 执行测试:
BenchmarkRunner.Run<ListBenchmark>();
- 输出结果:会显示每个方法的平均执行时间、标准差、内存分配等数据,用于对比性能差异。
LINQ 相关问题
Q1:声明式语法与命令式语法有什么区别?
| 特性 | 命令式语法 | 声明式语法 (LINQ) |
|---|---|---|
| 编程范式 | 描述"如何做"(详细步骤) | 描述"做什么"(定义结果) |
| 代码示例 | for循环+if条件过滤 |
Where() + OrderBy() |
| 可读性 | 较低(需理解实现细节) | 更高(接近自然语言) |
| 维护性 | 修改复杂(需调整逻辑流程) | 修改简单(仅调整表达式) |
Q2:LINQ 是什么?
-
定义:Language Integrated Query(语言集成查询)
-
LINQ的组成部分:
- LINQ to Objects:用于对象的查询。
- LINQ to XML:对XML数据的查询。
- LINQ to ADO.NET:对数据库的查询。
- LINQ to DataSets:对数据集的查询。
- LINQ to Entities:ORM对象的查询。
- LINQ to SQL:简易ORM框架。
-
两种形式:
csharp// 1. 查询表达式(类SQL语法) var results = from item in collection where item.Age > 18 orderby item.Name select item; // 2. 方法链(扩展方法) var results = collection.Where(i => i.Age > 18) .OrderBy(i => i.Name);
Q3:LINQ 方法的意义(these 方法)
基本查询方法
- Where :用于限定输入集合中的元素,将符合条件的元素组织成一个序列结果。
.Where(x => x.Score > 90) - Select :用于根据输入序列中的元素创建相应的输出序列中的元素,输出序列中的元素类型可以与输入序列中的元素类型相同,也可以不同。
.Select(x => x.Email) - SelectMany :与
Select类似,但可以根据输入序列中的每一个元素,在输出序列中创建相应的零个或多个元素。
元素选择与投影
- First/FirstOrDefault :返回序列中的第一个元素,
FirstOrDefault在序列为空时返回默认值(对于引用类型为null,对于值类型为该类型的默认值)。 - Single/SingleOrDefault :返回序列中的唯一元素,如果序列为空或包含多个元素,则
Single会抛出异常,而SingleOrDefault在序列为空时返回默认值。
分页与跳过元素
- Take:从输入序列中返回指定数量的元素,常用于分页。
- Skip:从输入序列中跳过指定数量的元素,返回由序列中剩余的元素所组成的新序列。
- SkipWhile:从输入序列中跳过满足一定条件指定数量的元素。
排序
- OrderBy :对输入序列中的元素进行升序排序。
.OrderBy(x => x.Name) - OrderByDescending:对输入序列中的元素进行降序排序。
- ThenBy 和 ThenByDescending :用于在
OrderBy或OrderByDescending之后,根据另一个条件对序列进行进一步排序。
集合操作
- Concat:连接两个序列,生成一个新序列。
- Union:将两个序列中的元素合并成一个新的序列,新序列将自动去除重复的元素。
- Intersect:将两个输入序列中的重复元素挑选出来,生成一个新的集合,即求交集。
- Except:返回两个序列中存在于第一个序列但不存在于第二个序列的元素所组成的新序列。
分组与去重
- GroupBy :根据某个键对数据进行分组。
.GroupBy(x => x.Department) - Distinct:去除一个序列中的重复元素。
转换与聚合
- Cast :将一个类型为
IEnumerable的集合对象转换为IEnumerable<T>类型的集合对象。 - OfType :与
Cast类似,但更加安全,仅会将能够成功转换的元素进行转换。 - AsEnumerable :将一个实现了
IEnumerable<T>接口的对象转换成一个标准的IEnumerable<T>接口对象。 - Aggregate:对序列中的元素进行累积操作,如求和、求积等。
特定操作
- Join:根据某个公共键将两个集合连接起来。
- GroupJoin :也用于连接两个输入序列,但与
Join不同,它允许将outer序列元素与对应的inner序列元素作为组一次性处理。 - Reverse:生成一个与输入序列中元素相同,但元素排列顺序相反的新序列。
| 方法 | 作用 | 示例 |
|---|---|---|
Any()/All() |
存在性检查 | .Any(x => x.IsVIP) |
Contains() |
包含检查 | .Contains("admin@example.com") |
Where 方法的原理是什么?
-
本质 :
Where是 LINQ 的筛选操作符,属于 延迟执行(Deferred Execution) 的方法。 -
实现逻辑 :
- 接收一个
Func<T, bool>类型的谓词函数(Lambda 表达式)作为筛选条件。 - 返回一个 迭代器(Iterator),而非立即执行筛选并生成新集合。
- 当遍历结果(如通过
foreach或ToList()等终端操作)时,才会逐一遍历原始集合,筛选符合条件的元素。
- 接收一个
-
示例
csharpList<int> numbers = new List<int> { 1, 2, 3, 4, 5 }; var evenNumbers = numbers.Where(n => n % 2 == 0); // 此时未执行筛选 foreach (int n in evenNumbers) { // 遍历触发实际筛选 Console.WriteLine(n); // 输出:2, 4 }
Q4:LINQ 的核心概念与用法
-
延迟执行:查询定义时不立即执行,实际使用时才计算
csharpvar query = data.Where(...); // 未执行 var result = query.ToList(); // 此时执行 -
组合查询:可链式组合多个操作
csharpvar results = data .Where(u => u.Age >= 20) .OrderBy(u => u.JoinDate) .Select(u => u.Email); -
数据库集成:通过Entity Framework连接数据库
csharpvar users = dbContext.Users .Where(u => u.City == "Beijing") .ToList();
数据处理相关问题
Q1:如何从集合中读取数据?
-
基础方法:
csharp// 读取整个集合 var allData = data.ToList(); // 读取首元素 var first = data.First(); // 条件读取 var admins = data.Where(d => d.Role == "Admin");
Q2:如何进行数据过滤与排序?
| 操作 | 方法 | 示例 |
|---|---|---|
| 过滤 | Where() |
.Where(x => x.Price > 100) |
| 升序排序 | OrderBy() |
.OrderBy(x => x.Date) |
| 降序排序 | OrderByDescending() |
.OrderByDescending(x => x.Score) |
| 多级排序 | ThenBy()/ThenByDescending() |
.OrderBy(x=>x.Department).ThenBy(x=>x.Name) |
Q3:如何使用量词方法(All/Contains)?
-
全称量词
All():检查是否所有元素满足条件csharpbool allAdults = users.All(u => u.Age >= 18); -
存在量词
Any():检查是否存在满足条件的元素csharpbool hasAdmin = users.Any(u => u.IsAdmin); -
包含检查
Contains():检查元素是否存在csharpbool exists = emails.Contains("contact@example.com");
Q4:什么是数据提取(Projection)?
-
使用
Select()转换数据结构:csharp// 提取单个字段 var names = users.Select(u => u.Name); // 创建匿名对象 var profiles = users.Select(u => new { u.Id, u.Name, AgeGroup = u.Age/10*10 }); // 转换数据类型 var idStrings = products.Select(p => p.Id.ToString());
Q5:如何转换数据格式?
| 转换目标 | 方法 | 示例 |
|---|---|---|
| 列表 | ToList() |
.Where(...).ToList() |
| 数组 | ToArray() |
.Select(...).ToArray() |
| 字典 | ToDictionary() |
.ToDictionary(k => k.Id) |
| 分组字典 | ToLookup() |
.ToLookup(k => k.Category) |
| 自定义转换 | Select() + 构造函数 |
.Select(d => new Report(d)) |
进程与线程相关问题
Q1:进程与线程有什么区别?
| 特性 | 进程 | 线程 |
|---|---|---|
| 资源分配 | 独立内存空间(>2MB) | 共享进程内存(<1MB) |
| 隔离性 | 崩溃不影响其他进程 | 崩溃导致整个进程终止 |
| 创建开销 | 大(需分配独立资源) | 小(复用进程资源) |
| 通信方式 | IPC(管道、套接字等) | 共享内存、同步原语 |
| 数量关系 | 1个进程包含≥1个线程 | 1个线程只属于1个进程 |
比喻:进程是工厂,线程是工厂中的工人。工厂提供场地资源(进程内存),工人(线程)共享场地并协作完成任务。
Q2:多线程能做什么?
- 性能提升 :
- CPU密集型:并行计算(图像处理/数据分析)
- I/O密集型:异步处理(网络请求/文件读写)
- 响应性优化 :
- UI线程保持响应(后台线程处理任务)
- 实时数据更新(股票行情/游戏物理引擎)
- 资源利用 :
- 多核CPU利用率最大化(任务并行分配)
- 避免阻塞主线程(耗时操作后台执行)
Q3:前台线程与后台线程的区别?
| 特性 | 前台线程 | 后台线程 |
|---|---|---|
| 进程终止 | 阻止进程结束 | 自动随进程终止 |
| 应用场景 | 关键任务(数据保存) | 辅助任务(日志记录) |
| 创建方式 | Thread.IsBackground=false |
Thread.IsBackground=true |
| 默认类型 | 手动创建的线程 | 线程池线程 |
csharp
var frontThread = new Thread(Work) { IsBackground = false };
frontThread.Start(); // 进程需等待此线程结束
Q4:什么是线程池?
-
核心机制:预创建的线程集合(避免频繁创建销毁开销)
-
工作流程:
任务队列
线程池
分配空闲线程
执行任务
返回线程池 -
优势:
- 降低线程创建开销(复用线程)
- 自动管理并发数量(默认最大1023线程)
-
使用方式:
csharpThreadPool.QueueUserWorkItem(state => { Console.WriteLine("线程池任务执行"); });
Q5:如何开启和结束线程?
-
开启线程:
csharpvar thread = new Thread(() => { // 任务逻辑 }); thread.Start(); // 启动线程 -
安全结束线程:
csharp// 使用取消令牌 var cts = new CancellationTokenSource(); var thread = new Thread(() => { while (!cts.Token.IsCancellationRequested) { // 可中断的工作 } }); thread.Start(); // 请求取消 cts.Cancel(); // 等待线程结束 thread.Join();
禁止 :
Thread.Abort()(已废弃,会导致资源泄漏)
Q6:什么是资源竞争?
-
发生条件:多线程同时读写共享资源
-
典型症状:
- 数据损坏(账户余额错误)
- 逻辑异常(计数器不同步)
-
示例:
csharpint count = 0; Parallel.For(0, 1000, i => { count++; // 多线程同时修改 → 结果<1000 });
Q7:如何避免资源竞争?
-
锁机制(互斥访问):
csharpprivate object _lock = new object(); lock (_lock) { // 临界区代码 count++; } -
原子操作(简单值类型):
csharpInterlocked.Increment(ref count); -
线程安全集合:
csharpvar safeDict = new ConcurrentDictionary<string, int>(); safeDict.TryAdd("key", 1);
异步编程相关问题
Q1:异步编程是什么?
-
本质:释放线程等待I/O时的计算资源
-
同步 vs 异步:
线程阻塞
释放线程
回调
同步调用
等待I/O完成
异步调用
执行其他任务
恢复执行 -
优势:
- 高并发:单线程处理千级I/O请求(如Node.js)
- 低资源:避免线程阻塞浪费内存(1线程≈1MB)
Q2:Task有什么用?
-
核心能力:
- 封装异步操作状态(未完成/已完成/故障)
- 支持任务组合(
Task.WhenAll/WhenAny) - 提供取消机制(
CancellationToken)
-
使用场景:
csharp// 执行异步I/O Task<string> downloadTask = httpClient.GetStringAsync(url); // 组合任务 var tasks = new List<Task>(); tasks.Add(Task.Run(() => ProcessData())); await Task.WhenAll(tasks);
Q3:如何使用async/await模式?
-
标准模式:
csharppublic async Task<string> GetDataAsync() { // 异步I/O不阻塞线程 var data = await File.ReadAllTextAsync("file.txt"); // 返回自动包装为Task<string> return data.ToUpper(); } -
关键规则:
async修饰方法:允许使用await- 返回类型:
Task(无结果)或Task<T>(有结果) - 避免
async void:仅用于事件处理程序
Q4:异步与多线程是一回事吗?
| 维度 | 多线程 | 异步 |
|---|---|---|
| 核心目标 | 并行计算(利用多核CPU) | 避免I/O等待(释放线程) |
| 资源消耗 | 高(线程栈内存) | 极低(任务对象≈100字节) |
| 适用场景 | CPU密集型任务(图像处理) | I/O密集型任务(网络请求) |
| 实现基础 | 操作系统线程调度 | 编译器状态机转换 |
关系:异步操作可能使用线程池,但本质是I/O完成端口(IOCP)等系统机制。
Q5:并发与并行有什么区别?
-
并发:逻辑上同时处理多任务(单核交替执行)
时间片切换
任务1
任务2
任务1
任务2 -
并行:物理上同时执行多任务(多核同步执行)
任务1
核心1
任务2
核心2 -
技术实现:
- 并发:
async/await(单线程处理多I/O) - 并行:
Parallel.For/Task.Run(多线程计算)
- 并发:
垃圾回收(GC)相关问题
- 堆内存与栈内存
- 栈内存 :存储基本数据类型(如
int、bool)和对象引用,遵循先进后出(LIFO)原则,由系统自动分配和释放,速度快。 - 堆内存 :存储对象实例(如
new创建的对象),分配和释放由垃圾回收器(GC)管理,生命周期较长,可能产生垃圾。
- 栈内存 :存储基本数据类型(如
- 对象实例在内存中的存储
- 对象引用存储在栈中,指向堆内存中的实际对象数据。当栈中的引用被删除或指向其他对象时,堆中的原对象若不再被任何引用指向,即成为垃圾。
Q1:什么是垃圾(Garbage)?
-
定义:程序中不再被任何对象引用的内存对象。
-
产生场景:
csharpvar obj = new MyObject(); // 创建对象 obj = null; // 对象失去引用 → 成为垃圾
Q2:垃圾回收(GC)是什么?
-
核心机制:CLR(.NET运行时)自动管理内存的子系统,负责:
- 跟踪对象引用
- 回收无引用对象内存
- 压缩堆空间(减少内存碎片)
-
工作流程:
创建对象
对象失去引用
GC标记阶段
GC回收阶段
内存压缩
Q3:GC 如何工作?(分代回收机制)
| 代(Generation) | 对象特征 | 收集频率 | 回收算法 |
|---|---|---|---|
| Gen 0 | 新创建对象 | 高频 | 复制收集 |
| Gen 1 | 幸存于Gen0收集的对象 | 中频 | 标记-清除 |
| Gen 2 | 长期存活对象(如静态变量) | 低频 | 标记-压缩 |
关键点:约90%对象在Gen0被回收,避免全堆扫描
Q4:垃圾回收有什么好处?
- 避免内存泄漏:自动回收无用对象
- 减少代码复杂度 :无需手动
free/delete - 提升安全性:防止野指针访问
- 优化内存布局:压缩减少碎片
Q5:析构函数(Destructor)与终结器(Finalizer)
| 特性 | 析构函数(C++风格) | 终结器(C#) |
|---|---|---|
| 执行时机 | 对象销毁时立即执行 | GC回收前执行(时间不确定) |
| 语法 | ~ClassName() |
~ClassName() |
| 推荐替代 | 无 | 实现IDisposable接口 |
Q6:为什么不能依赖终结器回收资源?
-
延迟性:GC触发时间不可控(可能耗尽资源)
-
性能代价:
- 对象需额外进入终结队列
- 至少两次GC才能回收
-
示例风险:
csharp~FileHandler() { _file.Close(); // 可能延迟数分钟执行 → 文件被锁 }
非托管资源处理
Q1:托管资源 vs 非托管资源
| 类型 | 托管资源 | 非托管资源 |
|---|---|---|
| 管理方 | CLR垃圾回收器 | 需手动释放 |
| 示例 | class对象、数组 |
文件句柄、数据库连接、Socket |
| 释放要求 | 自动回收 | 必须显式释放 |
Q2:Disposable 模式的作用
-
核心目标:提供确定性的资源释放机制
-
关键接口 :
IDisposablecsharppublic interface IDisposable { void Dispose(); } -
解决的问题:
- 及时释放非托管资源(避免资源泄漏)
- 提前释放托管资源(减少GC压力)
Q3:如何实现 Disposable 模式?
-
标准实现模板:
csharppublic class Resource : IDisposable { private bool _disposed; // 公共Dispose方法 public void Dispose() { Dispose(true); GC.SuppressFinalize(this); // 避免重复执行终结器 } // 受保护的虚方法(允许子类扩展) protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { // 释放托管资源 _managedResource?.Dispose(); } // 释放非托管资源 CloseHandle(_unmanagedHandle); _disposed = true; } // 终结器(安全后备) ~Resource() => Dispose(false); }
Q4:使用模式(using语句)
-
语法糖 :自动调用
Dispose()csharp// 标准写法 using (var resource = new Resource()) { // 使用资源 } // 此处自动调用resource.Dispose() // C# 8.0+ 简化写法 using var resource = new Resource(); // 作用域结束时自动Dispose
Q5:常见非托管资源处理示例
-
文件处理:
csharpusing var file = File.Open("data.txt", FileMode.Open); // 自动关闭文件句柄 -
数据库连接:
csharpusing var conn = new SqlConnection(connectionString); conn.Open(); // 自动关闭连接 -
图形句柄:
csharpusing var brush = new SolidBrush(Color.Red); // 自动释放GDI+资源
