C#笔试面试题全面解析与实战
简介:C#是一种面向对象的编程语言,广泛应用于游戏开发、桌面应用和Web服务等领域。本资料“C#笔试面试题大全”涵盖C#基础语法、面向对象编程、接口与抽象类、异常处理、泛型、委托与事件、Lambda表达式与LINQ、异步编程、内存管理、.NET框架与Core、单元测试、设计模式等核心技术点。通过系统学习与实战演练,可帮助开发者全面提升C#技术水平,增强笔试与面试竞争力,适用于各阶段的C#学习者和求职者。
1. C#基础语法详解
本章将带领读者深入理解C#语言的基本语法结构,从最基础的变量定义开始,逐步过渡到控制结构与输入输出操作。理解这些内容是构建C#面向对象编程能力的基石。
1.1 变量、常量与数据类型
在C#中,每个变量都必须声明其类型。C#是一种静态类型语言,这意味着变量类型在编译时就必须确定。
int age = 25; // 整型变量
double height = 1.75; // 双精度浮点型
string name = "Tom"; // 字符串类型
bool isStudent = true; // 布尔类型
int:用于存储整数值;double:用于高精度浮点运算;string:表示文本;bool:值只能是true或false。
常量使用 const 关键字定义,值在编译时确定,不可更改:
const double PI = 3.14159;
2. 面向对象编程(OOP)详解
面向对象编程(Object-Oriented Programming,简称 OOP)是现代软件开发中最重要的编程范式之一。它以对象为中心,通过封装、继承和多态等机制来构建模块化、可扩展、易于维护的代码结构。在 C# 中,OOP 是语言的核心特性之一,广泛应用于各种应用程序开发,包括控制台程序、Web 应用、桌面应用和游戏开发等。本章将深入讲解 C# 中的 OOP 原理、实现方式以及设计原则,帮助开发者构建良好的面向对象思维和编码习惯。
2.1 面向对象的基本概念
面向对象编程的核心理念是将现实世界中的事物抽象为“对象”,并通过类来描述这些对象的属性和行为。C# 是一种强类型的面向对象语言,其语法设计充分体现了 OOP 的三大基本特性: 封装、继承和多态 。理解这些概念是掌握 C# OOP 编程的关键。
2.1.1 类与对象的关系
类(Class)是对象的模板或蓝图,它定义了对象的属性和行为。而对象(Object)是类的一个具体实例。类是抽象的,而对象是具体的。例如:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public void SayHello()
{
Console.WriteLine($"Hello, my name is {Name}, I am {Age} years old.");
}
}
上面定义了一个 Person 类,它包含两个属性 Name 和 Age ,以及一个方法 SayHello 。接下来,我们可以创建一个 Person 类的对象:
Person person = new Person();
person.Name = "Tom";
person.Age = 25;
person.SayHello(); // 输出:Hello, my name is Tom, I am 25 years old.
这段代码中, person 是 Person 类的一个实例,我们通过它访问类的成员变量和方法。
类与对象的对比表格:
| 概念 | 说明 |
|---|---|
| 类 | 对象的模板,定义了属性和行为 |
| 对象 | 类的具体实例,拥有真实的数据 |
| 实例化 | 使用 new 关键字创建对象的过程 |
| 成员变量 | 类中定义的数据 |
| 成员方法 | 类中定义的行为 |
2.1.2 封装、继承与多态的三大特性
面向对象编程的三大核心特性分别是封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism),它们共同构成了 OOP 的基石。
1. 封装(Encapsulation)
封装是指将数据和行为包装在类中,对外提供访问接口,隐藏内部实现细节。通过使用访问修饰符(如 private 、 protected 、 public 等),可以控制类成员的可访问性。
例如,下面是一个封装良好的类:
public class BankAccount
{
private decimal balance;
public void Deposit(decimal amount)
{
if (amount > 0)
balance += amount;
}
public void Withdraw(decimal amount)
{
if (amount > 0 && balance >= amount)
balance -= amount;
}
public decimal GetBalance()
{
return balance;
}
}
在这个例子中, balance 被设为 private ,只能通过 Deposit 和 Withdraw 方法来修改,确保了数据的安全性。
2. 继承(Inheritance)
继承允许一个类(派生类)继承另一个类(基类)的成员,从而实现代码复用。继承是实现代码组织和层次结构的重要手段。
例如:
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Some sound");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Woof!");
}
}
在这个例子中, Dog 类继承自 Animal 类,并重写了 MakeSound 方法。
3. 多态(Polymorphism)
多态是指同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在 C# 中,多态主要通过方法重写( override )和接口实现来体现。
例如:
Animal animal1 = new Animal();
Animal animal2 = new Dog();
animal1.MakeSound(); // 输出:Some sound
animal2.MakeSound(); // 输出:Woof!
虽然 animal2 的变量类型是 Animal ,但由于它实际指向的是 Dog 对象,因此调用的是 Dog 类的 MakeSound 方法。
2.2 C#中的类与对象定义
在 C# 中,类是面向对象编程的基本构建块。类可以包含字段、属性、方法、构造函数、析构函数等多个成员。理解类的定义和对象的创建是编写 C# 程序的基础。
2.2.1 类的声明与成员变量
类的声明使用 class 关键字,后跟类名和类体。类体中可以包含字段、属性、方法等成员。
public class Car
{
// 字段
private string model;
private int year;
// 属性
public string Model
{
get { return model; }
set { model = value; }
}
public int Year
{
get { return year; }
set { year = value; }
}
}
- 字段(Field) :类的内部数据,通常设为
private,通过属性访问。 - 属性(Property) :用于访问字段,提供封装机制,增强数据安全性。
2.2.2 构造函数与析构函数
构造函数用于初始化对象的状态,析构函数则用于释放对象所占用的资源。
构造函数示例:
public class Student
{
public string Name { get; set; }
public int Age { get; set; }
// 构造函数
public Student(string name, int age)
{
Name = name;
Age = age;
}
}
// 使用构造函数创建对象
Student student = new Student("Alice", 20);
构造函数可以重载,例如:
public Student() : this("Unknown", 0) { }
析构函数示例:
public class Resource
{
~Resource()
{
Console.WriteLine("Resource is being released.");
}
}
析构函数通常用于清理非托管资源,但 C# 的垃圾回收机制会自动处理内存释放,因此在大多数情况下无需手动实现析构函数。
2.2.3 方法的重载与访问修饰符
方法重载是指在同一个类中定义多个具有相同名称但参数不同的方法。这是实现多态的一种方式。
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
public double Add(double a, double b)
{
return a + b;
}
}
访问修饰符控制类成员的访问权限,常用的有:
| 修饰符 | 说明 |
|---|---|
| public | 公共的,任何位置都可以访问 |
| private | 私有的,只能在类内部访问 |
| protected | 受保护的,只能在类及其派生类中访问 |
| internal | 内部的,只能在同一程序集内访问 |
2.3 OOP在C#中的实践
在实际开发中,面向对象编程不仅仅是类和对象的使用,还涉及静态成员、值类型与引用类型的区别、 this 与 base 关键字等高级用法。这些特性在构建复杂系统时尤为重要。
2.3.1 静态类与静态成员的使用
静态类和静态成员属于类本身,而不是类的实例。它们在程序运行期间只加载一次,适用于工具类、常量定义等场景。
public static class MathUtils
{
public static double Pi = 3.14159;
public static double CalculateCircleArea(double radius)
{
return Pi * radius * radius;
}
}
调用静态方法:
double area = MathUtils.CalculateCircleArea(5);
⚠️ 注意:静态类不能实例化,不能包含实例成员,只能包含静态成员。
2.3.2 值类型与引用类型的差异
在 C# 中,数据类型分为值类型(Value Type)和引用类型(Reference Type):
| 类型 | 示例 | 存储方式 | 特点 |
|---|---|---|---|
| 值类型 | int, double, struct | 栈内存 | 值直接存储,拷贝时复制值 |
| 引用类型 | class, string, array | 堆内存 | 存储引用地址,拷贝时复制引用 |
示例对比:
int a = 10;
int b = a;
b = 20;
Console.WriteLine(a); // 输出 10,a 和 b 是两个独立的值
Person p1 = new Person { Name = "Tom" };
Person p2 = p1;
p2.Name = "Jerry";
Console.WriteLine(p1.Name); // 输出 Jerry,p1 和 p2 指向同一个对象
2.3.3 this关键字与base关键字的作用
this关键字用于引用当前类的实例成员。base关键字用于调用基类的构造函数或方法。
public class BaseClass
{
public BaseClass()
{
Console.WriteLine("BaseClass constructor.");
}
}
public class DerivedClass : BaseClass
{
public DerivedClass() : base()
{
Console.WriteLine("DerivedClass constructor.");
}
}
使用 this 的示例:
public class Person
{
public string Name { get; set; }
public void Print()
{
Console.WriteLine(this.Name);
}
}
2.4 面向对象设计原则
良好的面向对象设计需要遵循一些通用的设计原则,这些原则有助于提高代码的可维护性、可扩展性和可测试性。其中, 单一职责原则 和 开放封闭原则 是最基础也是最重要的两个原则。
2.4.1 单一职责原则(SRP)
单一职责原则指出:一个类应该只有一个引起它变化的原因。换句话说,一个类只负责一项职责。
✅ 正确示例:
public class ReportGenerator
{
public void GenerateReport() { /* 生成报告 */ }
}
public class ReportPrinter
{
public void PrintReport() { /* 打印报告 */ }
}
❌ 错误示例:
public class ReportManager
{
public void GenerateReport() { /* 生成报告 */ }
public void PrintReport() { /* 打印报告 */ }
}
将两个职责合并在一个类中,违反了 SRP 原则,增加了类的耦合度和维护成本。
2.4.2 开放封闭原则(OCP)
开放封闭原则指出:软件实体(类、模块、函数等)应该是 对扩展开放,对修改关闭 。也就是说,应该通过添加新代码来扩展功能,而不是修改已有代码。
✅ 正确示例:
public abstract class Shape
{
public abstract double Area();
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double Area() => Width * Height;
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double Area() => Math.PI * Radius * Radius;
}
通过继承 Shape 类并实现 Area 方法,可以轻松扩展新的图形类型,而无需修改原有代码。
本章内容到此结束。我们从面向对象的基本概念入手,逐步深入介绍了类与对象的定义、OOP 的三大特性、C# 中的构造函数与析构函数、静态类、值类型与引用类型、关键字的使用,以及面向对象的设计原则。这些内容构成了 C# 面向对象编程的核心知识体系,为后续章节的继承、多态、接口等高级特性打下了坚实基础。
3. 类、对象、继承与多态实现
面向对象编程(OOP)的核心特性之一是 继承与多态 ,它们是构建可扩展、可维护和可重用代码的关键机制。在C#中,类之间的继承关系允许我们复用已有的代码结构,并通过多态实现灵活的接口调用。本章将深入探讨类的继承机制、多态的实现方式、对象的生命周期管理,并通过一个实际案例展示如何在项目中使用这些机制。
3.1 类的继承机制
3.1.1 继承的基本语法与实现
在C#中, 继承 是指一个类(称为 派生类 或 子类 )可以从另一个类(称为 基类 或 父类 )继承其成员(如字段、方法、属性等)。这种机制使得代码复用变得简单而高效。
// 基类定义
public class Animal
{
public void Eat()
{
Console.WriteLine("Animal is eating.");
}
}
// 派生类定义
public class Dog : Animal
{
public void Bark()
{
Console.WriteLine("Dog is barking.");
}
}
代码逻辑分析:
Animal是基类,包含一个公共方法Eat()。Dog是派生类,通过:符号继承Animal类。Dog类中新增了一个方法Bark(),并继承了Animal的所有公共成员。- 派生类可以访问基类的
public和protected成员。
使用示例:
Dog dog = new Dog();
dog.Eat(); // 继承自 Animal
dog.Bark(); // Dog 自身定义的方法
参数说明:
Dog dog = new Dog();:创建Dog实例。dog.Eat():调用继承自基类的方法。dog.Bark():调用派生类自身的方法。
3.1.2 protected访问修饰符的使用
protected 是一种访问修饰符,它允许类成员被其自身和派生类访问,但对外部类不可见。
public class Animal
{
protected string Name;
public Animal(string name)
{
Name = name;
}
protected void Sleep()
{
Console.WriteLine($"{Name} is sleeping.");
}
}
public class Dog : Animal
{
public Dog(string name) : base(name)
{
}
public void Rest()
{
Sleep(); // 可以访问基类的 protected 方法
}
}
代码逻辑分析:
Name字段和Sleep()方法被声明为protected,意味着只能在Animal类及其派生类中访问。Dog类的Rest()方法中调用了Sleep(),这在编译时是合法的。- 如果尝试在外部类中访问这些成员,则会引发编译错误。
使用示例:
Dog dog = new Dog("Buddy");
dog.Rest(); // 输出:Buddy is sleeping.
表格:访问修饰符对比
| 修饰符 | 同一类 | 派生类 | 外部类 |
|---|---|---|---|
private |
✅ | ❌ | ❌ |
internal |
✅ | ✅(同一程序集) | ❌(不同程序集) |
protected |
✅ | ✅ | ❌ |
public |
✅ | ✅ | ✅ |
3.1.3 base关键字在继承中的应用
base 关键字用于访问基类的成员,尤其是在构造函数中调用基类的构造方法时非常常见。
public class Animal
{
public Animal(string name)
{
Console.WriteLine($"Animal constructor: {name}");
}
}
public class Dog : Animal
{
public Dog(string name) : base(name)
{
Console.WriteLine("Dog constructor");
}
}
代码逻辑分析:
Dog的构造函数通过base(name)调用了Animal的构造函数。- 这确保了基类在派生类初始化之前完成必要的构造逻辑。
使用示例:
Dog dog = new Dog("Max");
// 输出:
// Animal constructor: Max
// Dog constructor
流程图:构造函数调用顺序
graph TD
A[Dog 构造函数] --> B[调用 base(name)]
B --> C[执行 Animal 构造函数]
C --> D[继续执行 Dog 构造函数]
3.2 多态的实现方式
3.2.1 方法的重写(override)与虚方法(virtual)
多态性 (Polymorphism)允许对象在运行时根据其实际类型来决定调用哪个方法。C#中通过 虚方法 ( virtual )和 重写 ( override )实现运行时多态。
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Animal sound");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Woof!");
}
}
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("Meow!");
}
}
代码逻辑分析:
MakeSound()方法在Animal中被声明为virtual,表示它可以在派生类中被重写。Dog和Cat类分别重写了MakeSound()方法,提供了不同的实现。- 通过基类引用指向不同子类实例时,会根据实际对象调用对应的方法。
使用示例:
Animal animal1 = new Dog();
Animal animal2 = new Cat();
animal1.MakeSound(); // 输出:Woof!
animal2.MakeSound(); // 输出:Meow!
3.2.2 抽象方法与抽象类
抽象类 ( abstract class )是一种不能被实例化的类,通常用于定义派生类必须实现的 抽象方法 。
public abstract class Animal
{
public abstract void MakeSound();
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Woof!");
}
}
代码逻辑分析:
Animal类被声明为abstract,并包含一个抽象方法MakeSound()。- 所有派生类必须实现该方法,否则也会成为抽象类。
- 抽象类可以包含非抽象成员,也可以有构造函数。
使用示例:
Animal animal = new Dog();
animal.MakeSound(); // 输出:Woof!
对比:虚方法 vs 抽象方法
| 特性 | 虚方法(virtual) | 抽象方法(abstract) |
|---|---|---|
| 所在类 | 普通类 | 抽象类 |
| 是否必须重写 | 否 | 是 |
| 是否有实现体 | 有 | 无 |
| 可否实例化 | 可以 | 不可以 |
3.2.3 接口多态与运行时多态
除了类继承实现的多态外,C#还支持通过 接口 实现多态。接口允许类实现多个接口,从而实现 多重继承 的多态行为。
public interface ISound
{
void MakeSound();
}
public class Dog : ISound
{
public void MakeSound()
{
Console.WriteLine("Woof!");
}
}
public class Cat : ISound
{
public void MakeSound()
{
Console.WriteLine("Meow!");
}
}
代码逻辑分析:
ISound是一个接口,定义了MakeSound()方法。Dog和Cat实现了该接口,并提供了不同的实现。- 可以通过接口引用调用具体实现。
使用示例:
ISound sound1 = new Dog();
ISound sound2 = new Cat();
sound1.MakeSound(); // 输出:Woof!
sound2.MakeSound(); // 输出:Meow!
流程图:接口多态调用流程
graph TD
A[ISound 接口引用] --> B[指向 Dog 实例]
A --> C[指向 Cat 实例]
B --> D[调用 Dog 的 MakeSound()]
C --> E[调用 Cat 的 MakeSound()]
3.3 对象的生命周期管理
3.3.1 new操作符与对象创建
在C#中, new 操作符用于创建类的实例,并调用相应的构造函数。
public class Person
{
public Person()
{
Console.WriteLine("Person created.");
}
}
// 创建对象
Person person = new Person(); // 输出:Person created.
代码逻辑分析:
new Person()创建了一个Person类的实例。- 构造函数
Person()在对象创建时自动调用。 new操作符分配内存并初始化对象。
使用示例:
Person p1 = new Person();
Person p2 = new Person();
// 输出两次:Person created.
3.3.2 垃圾回收对对象生命周期的影响
C# 使用 垃圾回收器 (Garbage Collector, GC)来自动管理内存。当对象不再被引用时,GC会回收其占用的内存。
public class Person
{
~Person()
{
Console.WriteLine("Person destroyed.");
}
}
// 创建对象
Person p = new Person();
p = null; // 断开引用
GC.Collect(); // 强制回收
代码逻辑分析:
~Person()是析构函数,在对象被销毁时调用。- 将引用设置为
null表示该对象不再使用。 - 调用
GC.Collect()可以手动触发垃圾回收(通常不推荐频繁调用)。
表格:对象生命周期阶段
| 阶段 | 描述 |
|---|---|
| 创建 | 使用 new 操作符创建对象 |
| 使用 | 对象被程序访问和操作 |
| 释放 | 对象不再被引用,等待GC回收 |
| 销毁 | GC调用析构函数并释放内存 |
3.4 实践案例:实现一个简单的继承结构
3.4.1 定义基类与派生类
我们来设计一个动物继承结构,包含一个基类 Animal ,以及两个派生类 Dog 和 Cat 。
public abstract class Animal
{
public abstract void Speak();
}
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Woof!");
}
}
public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Meow!");
}
}
代码逻辑分析:
Animal是抽象类,包含抽象方法Speak()。Dog和Cat实现了各自的Speak()方法。- 抽象类定义了接口规范,派生类提供具体实现。
3.4.2 使用多态调用不同子类的方法
我们可以使用多态机制,通过统一的接口调用不同子类的方法。
List<Animal> animals = new List<Animal>
{
new Dog(),
new Cat()
};
foreach (var animal in animals)
{
animal.Speak();
}
输出结果:
Woof!
Meow!
代码逻辑分析:
- 使用
List<Animal>存储不同子类的实例。 foreach循环遍历集合,调用Speak()方法。- 根据实际对象类型,调用相应的实现。
表格:多态调用示例
| 对象类型 | Speak() 实现 |
|---|---|
| Dog | Woof! |
| Cat | Meow! |
通过这个案例,我们展示了如何利用继承与多态构建灵活的面向对象结构。在实际项目中,这种模式可以大大提升代码的可扩展性和维护性。
4. 接口与抽象类区别与应用
4.1 接口的定义与使用
4.1.1 接口的基本语法
在C#中,接口(Interface)是一种定义行为契约的类型,它不提供具体实现,而是定义一组方法、属性、事件或索引器的签名。接口的实现由实现该接口的类或结构体来完成。
基本语法:
public interface IAnimal
{
void Speak(); // 方法签名
string Name { get; } // 属性定义
}
说明:
IAnimal是一个接口,定义了两个成员:一个方法Speak和一个只读属性Name。- 接口成员默认是
public的,不能显式指定访问修饰符。 - 接口中不能包含字段,但可以定义属性、方法、事件和索引器。
4.1.2 接口中方法、属性与事件的实现
当一个类实现接口时,必须实现接口中定义的所有成员,并且必须使用 public 修饰符。
示例:
public class Dog : IAnimal
{
public string Name => "Buddy"; // 实现Name属性
public void Speak()
{
Console.WriteLine("Woof!");
}
}
代码逻辑分析:
Dog类实现了IAnimal接口。Name属性使用表达式体定义,返回常量字符串"Buddy"。Speak方法输出"Woof!",表示狗的叫声。
表格:接口成员实现要求
| 接口成员类型 | 实现要求 |
|---|---|
| 方法 | 必须实现,且访问权限为 public |
| 属性 | 必须实现,且访问权限为 public |
| 事件 | 必须实现,用于通知机制 |
| 索引器 | 可选实现,但需遵循接口定义 |
4.2 抽象类的特性与用途
4.2.1 抽象类与抽象方法
抽象类(Abstract Class)是不能被实例化的类,它用于作为其他类的基类。抽象类中可以包含抽象方法(没有实现的方法)和具体方法(有实现的方法)。
基本语法:
public abstract class Animal
{
public abstract void Speak(); // 抽象方法,无实现
public virtual void Eat()
{
Console.WriteLine("Eating...");
}
}
说明:
Animal是一个抽象类,包含一个抽象方法Speak和一个虚方法Eat。- 抽象方法必须在派生类中实现。
- 虚方法可以被重写,但不是强制性的。
派生类实现:
public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Meow!");
}
}
代码逻辑分析:
Cat类继承自Animal抽象类。- 实现了抽象方法
Speak,输出"Meow!"。 - 继承了
Eat方法,可以直接调用。
4.2.2 抽象类与接口的对比分析
| 特性 | 接口 | 抽象类 |
|---|---|---|
| 成员实现 | 无实现(默认) | 可以有实现 |
| 多重继承 | 支持 | 不支持 |
| 构造函数 | 不允许 | 允许 |
| 字段 | 不允许 | 允许 |
| 访问修饰符 | 默认 public | 可自定义 |
| 应用场景 | 行为契约,多态设计 | 基类共享代码和状态 |
mermaid流程图:接口与抽象类的对比分析
graph TD
A[接口] --> B[无实现]
A --> C[支持多重继承]
A --> D[无字段]
E[抽象类] --> F[可部分实现]
E --> G[不支持多重继承]
E --> H[允许字段]
4.3 接口与抽象类在设计中的应用
4.3.1 接口实现多重继承
C#不支持类的多重继承,但可以通过接口实现类似的效果。
示例:
public interface IFlyable
{
void Fly();
}
public interface ISwimmable
{
void Swim();
}
public class Duck : IFlyable, ISwimmable
{
public void Fly()
{
Console.WriteLine("Duck is flying.");
}
public void Swim()
{
Console.WriteLine("Duck is swimming.");
}
}
代码逻辑分析:
Duck类同时实现了IFlyable和ISwimmable接口。- 通过接口实现了类的“多重行为继承”。
- 每个接口定义了不同的行为,类分别实现。
表格:接口多重继承的优势
| 优势点 | 描述 |
|---|---|
| 灵活性 | 可以组合多个接口行为 |
| 松耦合 | 类与接口之间无继承依赖 |
| 可扩展性 | 新增接口不影响已有类 |
4.3.2 抽象类在框架设计中的作用
抽象类通常用于框架设计中,作为一组相关类的基类,提供共享的代码和结构。
示例:
public abstract class DatabaseProvider
{
public abstract void Connect();
public void LogConnection()
{
Console.WriteLine("Connection established.");
}
}
public class SqlProvider : DatabaseProvider
{
public override void Connect()
{
Console.WriteLine("Connecting to SQL Server...");
}
}
代码逻辑分析:
DatabaseProvider是一个抽象类,定义了抽象方法Connect和一个具体方法LogConnection。SqlProvider实现了Connect方法,输出连接信息。- 所有继承该抽象类的子类都可以复用
LogConnection方法。
mermaid流程图:抽象类在框架中的使用
classDiagram
DatabaseProvider <|-- SqlProvider
DatabaseProvider <|-- OracleProvider
class DatabaseProvider{
+abstract void Connect()
+void LogConnection()
}
class SqlProvider{
+void Connect()
}
class OracleProvider{
+void Connect()
}
4.4 实战演练:接口与抽象类在项目中的应用
4.4.1 使用接口解耦模块
在大型项目中,接口常用于模块之间的解耦,使得系统更具可维护性和可测试性。
示例:
public interface IEmailService
{
void SendEmail(string to, string body);
}
public class SmtpEmailService : IEmailService
{
public void SendEmail(string to, string body)
{
Console.WriteLine($"Sending email to {to} via SMTP: {body}");
}
}
public class UserService
{
private readonly IEmailService _emailService;
public UserService(IEmailService emailService)
{
_emailService = emailService;
}
public void RegisterUser(string email)
{
// 用户注册逻辑
_emailService.SendEmail(email, "Welcome to our system!");
}
}
代码逻辑分析:
UserService不直接依赖具体的邮件发送实现,而是依赖IEmailService接口。- 这样在测试或更换邮件服务时,只需更换实现类,无需修改
UserService。 - 实现了依赖倒置原则(DIP)和控制反转(IoC)。
4.4.2 抽象类在数据访问层的设计实践
抽象类常用于定义数据访问层的通用操作,如数据库连接、事务管理等。
示例:
public abstract class RepositoryBase
{
protected string ConnectionString { get; set; }
protected RepositoryBase(string connectionString)
{
ConnectionString = connectionString;
}
public abstract void Add(object entity);
public void LogOperation(string message)
{
Console.WriteLine($"Operation: {message}");
}
}
public class ProductRepository : RepositoryBase
{
public ProductRepository(string connectionString) : base(connectionString)
{
}
public override void Add(object entity)
{
// 添加产品逻辑
LogOperation("Product added.");
}
}
代码逻辑分析:
RepositoryBase抽象类定义了所有仓库类共有的属性和方法。Add是抽象方法,由子类实现。LogOperation是具体方法,供子类复用。ProductRepository实现了具体的添加逻辑,并调用了基类的日志方法。
表格:接口与抽象类在项目中的典型应用场景
| 场景 | 使用接口的优势 | 使用抽象类的优势 |
|---|---|---|
| 行为抽象 | 明确契约,便于扩展 | 提供默认实现,减少冗余代码 |
| 模块解耦 | 依赖注入,松耦合 | 封装公共逻辑,便于维护 |
| 框架基础类设计 | 多重继承能力 | 提供基础状态和行为 |
本章深入探讨了接口与抽象类的定义、语法、实现方式及其在实际项目中的应用场景。通过多个代码示例和对比分析,帮助读者理解两者的核心区别和使用时机,为构建灵活、可维护的C#项目提供了理论和实践基础。
5. 异常处理机制与自定义异常
5.1 异常处理基础
5.1.1 try-catch-finally结构
C# 中的异常处理机制是构建健壮、稳定和可维护应用程序的关键组成部分。异常处理的基本结构由 try 、 catch 和 finally 块组成,允许程序在运行时捕获并处理错误。
try-catch-finally 的基本语法结构:
try
{
// 可能引发异常的代码
}
catch (Exception ex)
{
// 处理异常的代码
Console.WriteLine("发生异常:" + ex.Message);
}
finally
{
// 无论是否发生异常,都会执行的清理代码
Console.WriteLine("清理资源...");
}
参数说明与逻辑分析:
- try 块 :用于包裹可能抛出异常的代码逻辑。一旦在
try块中发生异常,程序会立即跳转到匹配的catch块。 - catch 块 :用于捕获特定类型的异常。
catch (Exception ex)可以捕获所有类型的异常,但更推荐捕获具体的异常类型(如NullReferenceException)以提高代码的可读性和可维护性。 - finally 块 :无论是否发生异常,该块中的代码都会被执行,常用于资源释放(如关闭文件流、数据库连接等),确保程序不会因异常而泄漏资源。
示例代码:读取文件内容并处理可能的异常
using System;
using System.IO;
class Program
{
static void Main()
{
string path = @"C:\example.txt";
StreamReader reader = null;
try
{
reader = new StreamReader(path);
string content = reader.ReadToEnd();
Console.WriteLine(content);
}
catch (FileNotFoundException ex)
{
Console.WriteLine("文件未找到:" + ex.Message);
}
catch (IOException ex)
{
Console.WriteLine("读取文件时发生IO异常:" + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("发生未知异常:" + ex.Message);
}
finally
{
if (reader != null)
{
reader.Close();
Console.WriteLine("文件流已关闭");
}
}
}
}
代码逻辑分析:
- 文件读取 :程序尝试打开指定路径的文件并读取内容。
- 异常分类捕获 :
- 如果文件不存在,会抛出FileNotFoundException,由第一个catch捕获。
- 如果在读取过程中发生 IO 错误(如磁盘读写失败),则会进入第二个catch。
- 对于其他未明确分类的异常,最后一个通用catch会处理。 - 资源清理 :
finally块确保无论是否发生异常,StreamReader都会被关闭,防止资源泄露。
流程图:异常处理执行流程
graph TD
A[开始执行try块] --> B{是否发生异常?}
B -->|否| C[继续执行try后代码]
B -->|是| D[查找匹配的catch块]
D --> E[执行catch块]
C --> F[执行finally块]
E --> F
F --> G[结束异常处理]
5.1.2 常见异常类型(如NullReferenceException、IndexOutOfRangeException等)
C# 提供了丰富的内置异常类型来应对不同场景下的错误情况。掌握这些常见异常类型有助于开发者更快定位问题。
常见异常类型列表:
| 异常类型 | 描述 |
|---|---|
NullReferenceException |
尝试访问未初始化的对象成员 |
IndexOutOfRangeException |
访问数组时索引超出范围 |
FormatException |
字符串格式不正确导致转换失败 |
IOException |
输入输出操作失败(如文件读写错误) |
ArgumentException |
方法传入的参数不合法 |
InvalidOperationException |
调用对象处于无效状态时的方法 |
示例代码:演示不同异常的发生场景
class Program
{
static void Main()
{
try
{
object obj = null;
obj.ToString(); // 触发 NullReferenceException
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers[5]); // 触发 IndexOutOfRangeException
int.Parse("abc"); // 触发 FormatException
throw new ArgumentException("参数无效"); // 主动抛出 ArgumentException
}
catch (NullReferenceException ex)
{
Console.WriteLine("空引用异常:" + ex.Message);
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine("索引越界异常:" + ex.Message);
}
catch (FormatException ex)
{
Console.WriteLine("格式转换异常:" + ex.Message);
}
catch (ArgumentException ex)
{
Console.WriteLine("参数异常:" + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("未知异常:" + ex.Message);
}
}
}
执行逻辑说明:
- 空引用异常 :尝试调用
null对象的ToString()方法,触发NullReferenceException。 - 索引越界异常 :访问数组中不存在的索引(5),触发
IndexOutOfRangeException。 - 格式转换异常 :将字符串
"abc"转换为整数失败,触发FormatException。 - 参数异常 :通过
throw手动抛出ArgumentException,并被捕获。
注意事项:
- 异常捕获顺序 :
catch块应按异常的具体程度由高到低排列,避免通用异常捕获提前截断具体异常。 - 不要过度捕获异常 :仅在必要时捕获异常,避免掩盖错误根源。
- 避免空的 catch 块 :应记录或处理异常,否则会隐藏错误信息,增加调试难度。
5.2 自定义异常类的实现
5.2.1 创建自定义异常类
虽然 C# 提供了丰富的内置异常类型,但在实际开发中,为了提高代码的可读性和业务语义的清晰性,常常需要创建自定义异常类。
自定义异常类的实现步骤:
- 继承
Exception类 :所有自定义异常类都应继承自System.Exception或其子类。 - 提供构造函数 :至少应包含默认构造函数、带有消息参数的构造函数以及带有消息和内部异常的构造函数。
- 命名规范 :通常以
Exception结尾,如InvalidUserInputException。
示例代码:定义一个自定义异常类
using System;
public class InvalidUserInputException : Exception
{
public InvalidUserInputException() : base() { }
public InvalidUserInputException(string message) : base(message) { }
public InvalidUserInputException(string message, Exception innerException)
: base(message, innerException) { }
}
代码逻辑分析:
- 构造函数 :
- 无参构造函数:供反序列化使用。
- 带
message参数的构造函数:用于提供异常信息。 - 带
message和innerException的构造函数:支持异常链,记录导致当前异常的原始异常。 - 用途 :当用户输入非法数据时抛出该异常,便于上层逻辑识别并处理。
5.2.2 throw关键字与异常抛出机制
在 C# 中, throw 关键字用于显式抛出异常。它不仅可以抛出内置异常,也可以抛出自定义异常。
示例代码:在方法中使用 throw 抛出自定义异常
class Program
{
static void ValidateUserInput(string input)
{
if (string.IsNullOrEmpty(input))
{
throw new InvalidUserInputException("用户输入不能为空");
}
}
static void Main()
{
try
{
ValidateUserInput(""); // 传递空字符串
}
catch (InvalidUserInputException ex)
{
Console.WriteLine("捕获到自定义异常:" + ex.Message);
}
}
}
执行逻辑分析:
- 验证输入 :方法
ValidateUserInput检查用户输入是否为空。 - 抛出异常 :如果为空,抛出
InvalidUserInputException。 - 捕获异常 :
Main方法中通过catch捕获并处理该异常。
注意事项:
- 避免在 catch 块中直接 throw :若需保留堆栈信息,应使用
throw;,而不是throw ex;。 - 异常信息应明确 :异常消息应清晰描述错误原因,便于调试和日志记录。
5.3 异常处理的最佳实践
5.3.1 异常捕获与日志记录
在实际项目中,异常捕获不仅用于程序流程控制,还应结合日志记录机制进行错误追踪和分析。
推荐做法:
- 使用日志框架 :如
NLog、log4net、Serilog等记录异常信息。 - 记录异常堆栈信息 :使用
ex.StackTrace记录完整的调用堆栈。 - 避免在 catch 块中吞异常 :即使不处理异常,也应记录或重新抛出。
示例代码:结合日志记录异常信息
using System;
using NLog;
class Program
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
static void Main()
{
try
{
int.Parse("abc"); // 触发异常
}
catch (FormatException ex)
{
logger.Error(ex, "格式转换异常");
}
}
}
执行逻辑说明:
- 日志记录 :使用
NLog的logger.Error()方法记录异常类型、消息和堆栈信息。 - 调试辅助 :开发人员可通过日志快速定位异常发生的位置和原因。
5.3.2 避免异常滥用与性能影响
尽管异常处理是强大的工具,但不合理的使用会导致性能下降甚至掩盖真正的问题。
异常处理的性能考量:
- 避免使用异常控制流程 :异常机制涉及堆栈展开,性能开销较大。
- 仅在真正异常情况下使用 try-catch :如网络中断、文件丢失等不可控因素。
- 合理使用 finally 块释放资源 :避免在 catch 中重复释放资源。
对比示例:异常控制 vs 条件判断
// 不推荐:用异常控制流程
try
{
int.Parse(input);
}
catch (FormatException)
{
Console.WriteLine("输入无效");
}
// 推荐:使用条件判断
if (!int.TryParse(input, out int value))
{
Console.WriteLine("输入无效");
}
执行逻辑分析:
- 异常控制 :若输入非法,频繁抛出异常会显著影响性能。
- 条件判断 :
int.TryParse()提供了更高效、安全的方式处理转换失败的情况。
5.4 实战案例:在业务逻辑中优雅处理异常
5.4.1 在数据访问层封装异常处理
在分层架构中,数据访问层(DAL)常涉及数据库连接、查询、事务等操作,这些操作容易发生异常。合理封装异常处理可以提高代码的可维护性和稳定性。
示例:封装数据访问异常
public class DatabaseAccessException : Exception
{
public DatabaseAccessException(string message, Exception innerException)
: base(message, innerException) { }
}
public class UserRepository
{
public User GetUserById(int id)
{
try
{
// 模拟数据库查询
if (id <= 0)
throw new ArgumentException("ID必须大于0");
// 假设数据库连接失败
throw new IOException("数据库连接失败");
}
catch (Exception ex)
{
throw new DatabaseAccessException("获取用户信息失败", ex);
}
}
}
代码说明:
- 自定义异常封装 :将底层异常(如
IOException)封装为业务相关的DatabaseAccessException。 - 异常链保留 :通过构造函数传入原始异常,保留完整的堆栈信息,便于调试。
5.4.2 使用全局异常处理机制(Global Exception Handling)
在 ASP.NET Core 等 Web 应用中,可以使用全局异常处理机制统一捕获未处理的异常,返回友好的错误信息并记录日志。
示例:在 ASP.NET Core 中配置全局异常处理
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
逻辑说明:
- 开发环境 :启用
UseDeveloperExceptionPage()显示详细的异常信息。 - 生产环境 :使用
UseExceptionHandler()捕获未处理异常,并重定向到/Home/Error页面。 - 统一响应 :可以在
/Home/Error中返回统一的 JSON 错误格式或 HTML 页面,提升用户体验。
全局异常中间件示例:
app.Use(async (context, next) =>
{
try
{
await next();
}
catch (Exception ex)
{
context.Response.StatusCode = 500;
await context.Response.WriteAsync("发生内部服务器错误");
// 记录日志
Console.WriteLine($"全局异常:{ex.Message}\n{ex.StackTrace}");
}
});
执行逻辑:
- 全局捕获 :所有未被捕获的异常都会进入该中间件。
- 状态码设置 :返回 500 错误码。
- 日志记录 :打印异常信息和堆栈跟踪,便于后续分析。
本章内容深入探讨了 C# 异常处理机制的核心要素,包括基础结构、自定义异常、最佳实践与实战应用。通过合理使用 try-catch-finally 、创建自定义异常类、结合日志系统及全局异常处理,开发者可以显著提升应用程序的健壮性与可维护性。
6. 委托与事件通信机制
6.1 委托的基本概念与使用
6.1.1 委托的定义与实例化
委托(Delegate)在C#中是一种类型安全的函数指针,它允许我们将方法作为参数传递给另一个方法,或者作为返回值,甚至可以将多个方法组合成一个调用链。委托是实现事件机制和回调函数的基础。
定义委托语法:
public delegate 返回类型 委托名(参数列表);
示例:
public delegate int CalculateDelegate(int a, int b);
class Program
{
static int Add(int a, int b)
{
return a + b;
}
static void Main(string[] args)
{
// 实例化委托
CalculateDelegate calc = new CalculateDelegate(Add);
// 通过委托调用方法
int result = calc(10, 20);
Console.WriteLine("Result: " + result); // 输出:Result: 30
}
}
参数说明 :
-CalculateDelegate:定义了一个返回int类型,接受两个int参数的委托类型。
-calc:委托实例,指向Add方法。
-calc(10, 20):调用委托,实际上是调用Add方法。
C# 2.0 之后还支持直接赋值方式简化委托实例化:
CalculateDelegate calc = Add;
多播委托(Multicast Delegate)
委托可以绑定多个方法,使用 += 操作符进行绑定,调用时会依次执行所有绑定的方法。
public delegate void NotifyDelegate(string message);
class Program
{
static void LogMessage(string message)
{
Console.WriteLine("Log: " + message);
}
static void ShowMessage(string message)
{
Console.WriteLine("Show: " + message);
}
static void Main()
{
NotifyDelegate notify = LogMessage;
notify += ShowMessage;
notify("Hello, C# Delegate!");
// 输出:
// Log: Hello, C# Delegate!
// Show: Hello, C# Delegate!
}
}
注意 :多播委托的返回值将被忽略,建议使用
void返回类型。
6.1.2 Action与Func内置委托类型
C# 提供了通用的内置委托类型 Action 和 Func ,它们可以避免手动定义委托,提升代码简洁性和可维护性。
| 类型 | 描述 | 示例 |
|---|---|---|
Action |
无返回值,可接受0~16个参数 | Action<string> print = Console.WriteLine; |
Func<T> |
有返回值,可接受0~16个参数 | Func<int, int, int> add = (a, b) => a + b; |
示例:
class Program
{
static void Main()
{
// Action 示例
Action<string> log = message => Console.WriteLine("Action: " + message);
log("This is an Action delegate.");
// Func 示例
Func<int, int, int> multiply = (x, y) => x * y;
int result = multiply(5, 4);
Console.WriteLine("Func Result: " + result); // 输出:Func Result: 20
}
}
说明 :
-Action<string>表示一个接受一个字符串参数,无返回值的方法。
-Func<int, int, int>表示接受两个int参数,返回int的方法。
6.2 事件的定义与触发
6.2.1 事件的声明与注册
事件(Event)本质上是一种特殊的委托,用于在对象之间实现松耦合的通信机制。它通常用于响应用户操作(如按钮点击)、系统通知或数据变化等场景。
事件声明语法:
public event 委托类型 事件名;
示例:定义一个按钮点击事件
public delegate void ButtonClickedHandler(string buttonName);
public class Button
{
public event ButtonClickedHandler Clicked;
public void OnClick(string name)
{
Clicked?.Invoke(name); // 使用 ?. 防止空引用
}
}
class Program
{
static void HandleClick(string name)
{
Console.WriteLine(name + " button was clicked.");
}
static void Main()
{
Button btn = new Button();
btn.Clicked += HandleClick;
btn.OnClick("Submit"); // 触发事件
}
}
说明 :
-btn.Clicked += HandleClick;:注册事件处理方法。
-Clicked?.Invoke(name);:触发事件,如果没有任何订阅者则不会抛出异常。
6.2.2 事件与委托的关系
事件是基于委托实现的,但它们之间有以下区别:
| 特性 | 委托 | 事件 |
|---|---|---|
| 使用范围 | 可在任意类中定义和调用 | 通常用于封装对象的行为通知 |
| 调用方式 | 可直接调用 | 由发布者调用 |
| 注册方式 | 使用 += 或 = |
使用 += ,不能使用 = |
| 封装性 | 不具备封装性 | 具备封装性,外部不能随意触发 |
结论 :事件是对委托的封装,提供了更好的访问控制和安全性。
6.3 委托与事件在实际项目中的应用
6.3.1 实现观察者模式
观察者模式(Observer Pattern)是一种经典的设计模式,适用于一对多的依赖关系。当一个对象的状态发生变化时,所有依赖它的对象都会被通知并自动更新。
示例:使用委托和事件实现观察者模式
public class Stock
{
public string Symbol { get; set; }
public decimal Price { get; private set; }
public event EventHandler PriceChanged;
public void UpdatePrice(decimal newPrice)
{
if (newPrice != Price)
{
Price = newPrice;
OnPriceChanged();
}
}
protected virtual void OnPriceChanged()
{
PriceChanged?.Invoke(this, EventArgs.Empty);
}
}
class Program
{
static void Stock_PriceChanged(object sender, EventArgs e)
{
var stock = (Stock)sender;
Console.WriteLine($"Stock {stock.Symbol} price changed to {stock.Price}");
}
static void Main()
{
Stock stock = new Stock { Symbol = "AAPL" };
stock.PriceChanged += Stock_PriceChanged;
stock.UpdatePrice(150.25m); // 输出:Stock AAPL price changed to 150.25
stock.UpdatePrice(152.50m); // 输出:Stock AAPL price changed to 152.50
}
}
说明 :
-Stock类作为主题(Subject),通过PriceChanged事件通知观察者。
-Stock_PriceChanged是观察者的处理逻辑。
6.3.2 窗体应用程序中的事件通信
在Windows Forms或WPF应用程序中,事件广泛用于界面交互。例如按钮点击、文本框内容变化、窗口加载等。
示例:在WinForm中绑定按钮点击事件
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show("Button clicked!");
}
// 在构造函数或Load事件中注册
button1.Click += button1_Click;
说明 :
-sender表示事件源(通常是按钮对象)。
-e表示事件参数,包含事件相关信息。
6.4 高级应用:使用Lambda表达式简化委托调用
6.4.1 Lambda表达式与委托的结合
Lambda 表达式是一种简化匿名方法的语法,常用于 LINQ 查询、委托绑定等场景。
示例:使用Lambda表达式绑定事件
button1.Click += (sender, e) =>
{
MessageBox.Show("This is a lambda event handler.");
};
示例:使用Lambda表达式简化Func委托
Func<int, int, int> sum = (x, y) => x + y;
int result = sum(3, 5);
Console.WriteLine(result); // 输出:8
说明 :
- Lambda 表达式(x, y) => x + y等价于一个方法返回x + y。
- 更加简洁,适合逻辑简单、临时使用的场景。
6.4.2 事件订阅与匿名方法
C# 支持使用匿名方法或Lambda表达式订阅事件,无需单独定义方法。
示例:使用匿名方法订阅事件
button1.Click += delegate (object sender, EventArgs e)
{
MessageBox.Show("Anonymous method clicked.");
};
示例:使用Lambda表达式订阅事件
button1.Click += (sender, e) =>
{
Console.WriteLine("Lambda expression clicked.");
};
优点 :
- 无需额外定义方法,提升代码可读性和开发效率。
- 特别适用于事件处理逻辑简单、一次性使用的情况。本章通过深入讲解委托与事件的定义、使用方式及其在实际项目中的应用场景,帮助开发者掌握事件驱动编程的核心机制。下一章将进入泛型与集合类的使用,进一步提升代码的复用性与性能。
简介:C#是一种面向对象的编程语言,广泛应用于游戏开发、桌面应用和Web服务等领域。本资料“C#笔试面试题大全”涵盖C#基础语法、面向对象编程、接口与抽象类、异常处理、泛型、委托与事件、Lambda表达式与LINQ、异步编程、内存管理、.NET框架与Core、单元测试、设计模式等核心技术点。通过系统学习与实战演练,可帮助开发者全面提升C#技术水平,增强笔试与面试竞争力,适用于各阶段的C#学习者和求职者。
更多推荐



所有评论(0)