100道C#高频经典面试题带解析答案
以下是100道C#高频经典面试题及其详细解析,涵盖基础语法、面向对象编程、集合、异步编程、LINQ等多个方面,旨在帮助初学者和有经验的开发者全面准备C#相关面试。
🧑 博主简介:CSDN博客专家、CSDN平台优质创作者,高级开发工程师,数学专业,10年以上C/C++, C#, Java等多种编程语言开发经验,拥有高级工程师证书;擅长C/C++、C#等开发语言,熟悉Java常用开发技术,能熟练应用常用数据库SQL server,Oracle,mysql,postgresql等进行开发应用,熟悉DICOM医学影像及DICOM协议,业余时间自学JavaScript,Vue,qt,python等,具备多种混合语言开发能力。撰写博客分享知识,致力于帮助编程爱好者共同进步。欢迎关注、交流及合作,提供技术支持与解决方案。
技术合作请加本人wx(注明来自csdn):xt20160813
基础语法
1. 什么是C#,它的主要特点是什么?
答案:
C#(发音为C Sharp)是一种由微软开发的现代、通用、面向对象的编程语言,作为.NET框架的一部分,主要用于开发Windows应用、Web应用、移动应用等。其主要特点包括:
- 面向对象:支持类、对象、继承、多态、封装等面向对象的特性。
- 类型安全:强类型语言,提供类型检查,减少运行时错误。
- 丰富的类库:提供大量的标准库,简化开发。
- 跨平台:通过.NET Core和.NET 5/6等实现跨平台开发。
- 异步编程:内置支持异步编程模型(async/await),提高应用性能。
- 垃圾回收:自动内存管理,减少内存泄漏和错误。
2. C#中struct
和class
的区别是什么?
答案:
-
类型:
struct
是值类型,存储在栈上。class
是引用类型,存储在堆上。
-
继承:
struct
不支持继承(除了实现接口)。class
支持继承,可以有基类和派生类。
-
默认构造函数:
struct
有隐式的无参构造函数,不能显式定义。class
可以定义显式的无参构造函数。
-
用途:
struct
适用于小型、不可变的数据结构,如点、颜色等。class
适用于需要复杂行为和状态管理的对象。
3. 什么是属性(Property)?与字段(Field)相比有什么区别?
答案:
- **字段(Field)**是类中用于存储数据的变量,通常声明为私有(private)。
- **属性(Property)**是一种类成员,通过get和set访问器控制对字段的访问,提供数据的封装和验证。
区别:
- 封装性:属性可以控制读写权限,添加逻辑验证,而字段直接暴露数据,缺乏封装。
- 数据隐藏:通过属性隐藏内部字段,实现更灵活的接口设计。
- 兼容性:属性可以在不改变外部接口的情况下修改内部实现。
示例:
private int _age;
public int Age
{
get { return _age; }
set
{
if (value >= 0)
_age = value;
}
}
4. C#中的readonly
和const
有什么区别?
答案:
-
const
:- 在编译时确定值。
- 必须初始化,且只能使用字面量。
- 默认静态(static)的。
- 只能用于基本类型和字符串。
-
readonly
:- 在运行时确定值。
- 可以在声明时或构造函数中初始化。
- 不是默认静态的,除非显式声明为
static readonly
。 - 可以用于复杂类型。
示例:
public const double PI = 3.14159;
public readonly DateTime creationTime;
public MyClass()
{
creationTime = DateTime.Now;
}
5. 什么是委托(Delegate)?与事件(Event)有什么关系?
答案:
-
**委托(Delegate)**是C#中一种类型安全的函数指针,允许将方法作为参数传递或赋值给变量。
示例:
public delegate void Notify(string message); public void ShowMessage(string msg) { Console.WriteLine(msg); } Notify notifier = ShowMessage; notifier("Hello, World!");
-
**事件(Event)**是基于委托的一种机制,用于在对象之间传递通知。事件通常用于发布-订阅模式,允许多个订阅者响应某个动作。
关系:事件使用委托作为其底层类型,定义了事件处理的方法签名。
示例:
public event Notify OnNotify; public void TriggerEvent(string msg) { OnNotify?.Invoke(msg); }
6. 什么是interface
,它与抽象类有什么区别?
答案:
-
interface
:- 定义一组方法和属性的签名,不包含任何实现。
- 类或结构体可以实现多个接口,实现接口中的所有成员。
- 不支持字段和构造函数。
-
抽象类:
- 可以包含抽象方法(无实现)和具体方法(有实现)。
- 只能单继承,一个类只能继承一个抽象类。
- 可以包含字段和构造函数。
区别:
- 实现:接口仅定义契约,不提供实现;抽象类可以部分实现。
- 继承:类可以实现多个接口,但只能继承一个抽象类。
- 成员:接口不能包含字段,抽象类可以。
7. C#中的enum
如何使用?有什么限制?
答案:
-
使用:
- 定义一组命名的整型常量,用于提高代码的可读性和维护性。
示例:
public enum Days { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday }; Days today = Days.Monday;
-
限制:
- 基础类型默认为
int
,但可以指定其他整数类型(byte, sbyte, short, ushort, int, uint, long, ulong)。 - 枚举成员的名称必须唯一,不能重复。
- 不能包含成员方法,只能包含成员字段。
- 基础类型默认为
8. 什么是partial
类,它的作用是什么?
答案:
-
partial
类:允许将一个类、结构体或接口的定义分布在多个文件中。编译器会将所有部分组合成一个完整的类型。 -
作用:
- 便于多人协作开发,分工编写不同部分。
- 支持代码生成工具生成部分代码,开发者编写手动部分。
- 提高代码的组织性和可维护性。
示例:
文件1:
public partial class MyClass
{
public void MethodA() { /* ... */ }
}
文件2:
public partial class MyClass
{
public void MethodB() { /* ... */ }
}
9. 什么是索引器(Indexer)?如何定义和使用?
答案:
-
**索引器(Indexer)**允许对象像数组一样通过索引访问其内部数据。它使用
this
关键字,并带有参数列表。 -
定义:
public class SampleCollection { private int[] arr = new int[100]; public int this[int i] { get { return arr[i]; } set { arr[i] = value; } } }
-
使用:
SampleCollection collection = new SampleCollection(); collection[0] = 42; int value = collection[0];
10. 什么是扩展方法(Extension Method)?如何定义和使用?
答案:
-
**扩展方法(Extension Method)**允许向现有的类型添加新方法,而无需修改该类型的源代码或创建派生类型。
-
定义:
- 必须在静态静态类中定义静态方法,第一个参数使用
this
关键字表示要扩展的类型。
示例:
public static class StringExtensions { public static bool IsNullOrEmpty(this string str) { return string.IsNullOrEmpty(str); } }
- 必须在静态静态类中定义静态方法,第一个参数使用
-
使用:
string s = ""; bool result = s.IsNullOrEmpty(); // 调用扩展方法
面向对象编程
11. 什么是封装(Encapsulation)?
答案:
封装是面向对象编程的基本特性之一,通过将数据(字段)和操作数据的方法(函数)绑定在一起,并隐藏内部实现细节,只暴露必要的接口,以提高代码的模块性和安全性。封装通过访问修饰符(如private
, public
, protected
)实现数据隐藏,防止外部直接访问和修改对象的内部状态。
示例:
public class Person
{
private string name; // 隐藏字段
public string Name // 公开属性
{
get { return name; }
set { name = value; }
}
public void Display()
{
Console.WriteLine($"Name: {name}");
}
}
12. 什么是继承(Inheritance)?C#如何实现继承?
答案:
-
**继承(Inheritance)**是面向对象编程的关键特性,允许一个类(子类)继承另一个类(基类)的属性和方法,从而实现代码复用和层次化设计。
-
C#实现:
- 使用冒号(:)符号,子类继承基类。
示例:
public class Animal { public void Eat() { Console.WriteLine("Eating"); } } public class Dog : Animal { public void Bark() { Console.WriteLine("Barking"); } }
使用:
Dog dog = new Dog(); dog.Eat(); // 继承自Animal dog.Bark(); // Dog自身的方法
13. 什么是多态(Polymorphism)?它有哪些形式?
答案:
-
**多态(Polymorphism)**指对象在不同情境下表现出的多种形态,是面向对象编程的核心特性之一。多态使得同一个接口或方法可以有不同的实现。
-
主要形式:
-
编译时多态(Static Polymorphism):
- 通过方法重载(Overloading)和运算符重载(Operator Overloading)实现。
-
运行时多态(Dynamic Polymorphism):
- 通过方法重写(Overriding)和接口实现,实现基类引用指向派生类对象时,调用派生类的实现。
-
示例:
public class Animal
{
public virtual void Speak()
{
Console.WriteLine("Animal speaks");
}
}
public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Meow");
}
}
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Woof");
}
}
// 使用多态
Animal myAnimal = new Dog();
myAnimal.Speak(); // 输出: Woof
myAnimal = new Cat();
myAnimal.Speak(); // 输出: Meow
14. 什么是抽象类(Abstract Class)?如何使用?
答案:
-
**抽象类(Abstract Class)**是一种不能被实例化的类,用于提供基类的一部分实现,并定义一些必须由派生类实现的抽象方法。抽象类通过
abstract
关键字声明。 -
使用场景:
- 当需要定义一个通用的基类,并希望派生类实现具体的行为时。
- 提供共享代码和接口。
-
定义和使用:
public abstract class Shape { public abstract double Area(); // 抽象方法 public void Display() { Console.WriteLine($"The area is {Area()}"); } } public class Circle : Shape { public double Radius { get; set; } public Circle(double radius) { Radius = radius; } public override double Area() { return Math.PI * Radius * Radius; } } // 使用 Shape myCircle = new Circle(5); myCircle.Display(); // 输出: The area is 78.53981633974483
15. C#中的sealed
关键字有什么作用?
答案:
-
sealed
关键字用于类或类成员,限制其进一步继承或重写。 -
作用:
-
封闭类继承:当用于类时,表示该类不能被继承。
示例:
public sealed class FinalClass { } // 下面的代码将导致编译错误 public class DerivedClass : FinalClass { }
-
封闭方法重写:当用于虚方法时,表示该方法不能被派生类重写。
示例:
public class BaseClass { public virtual void Display() { } } public class DerivedClass : BaseClass { public sealed override void Display() { } } // 下面的代码将导致编译错误 public class FurtherDerivedClass : DerivedClass { public override void Display() { } }
-
16. 什么是接口继承?如何实现多接口继承?
答案:
-
**接口继承(Interface Inheritance)**指一个接口可以继承一个或多个其他接口,继承后的接口包含所有基接口的方法和属性。
-
多接口继承:C#中,类或结构体可以实现多个接口,通过逗号分隔接口名称。
-
示例:
public interface IPrintable { void Print(); } public interface IScannable { void Scan(); } public class MultiFunctionPrinter : IPrintable, IScannable { public void Print() { Console.WriteLine("Printing..."); } public void Scan() { Console.WriteLine("Scanning..."); } }
17. 什么是静态构造函数(Static Constructor)?
答案:
-
**静态构造函数(Static Constructor)**是用于初始化类的静态成员的特殊构造函数。它在类第一次被访问之前自动调用,且仅调用一次。
-
特点:
- 无访问修饰符,不能有参数。
- 不能直接调用。
- 适用于初始化静态字段或执行一次性的类级别初始化操作。
-
示例:
public class Configuration { public static readonly string AppName; static Configuration() { AppName = "MyApplication"; // 其他初始化操作 } } // 使用 Console.WriteLine(Configuration.AppName); // 输出: MyApplication
18. 什么是泛型(Generics)?它的优势是什么?
答案:
-
**泛型(Generics)**允许在类、结构体、接口、方法等的定义中使用类型参数,使代码更具重用性和类型安全性。
-
优势:
- 类型安全:在编译时检查类型,减少运行时错误。
- 性能:避免装箱和拆箱,提高性能,尤其在集合操作中。
- 代码复用:编写一次泛型代码,适用于多种数据类型。
-
示例:
public class GenericList<T> { private T[] items = new T[100]; private int index = 0; public void Add(T item) { items[index++] = item; } public T Get(int i) { return items[i]; } } // 使用 GenericList<int> intList = new GenericList<int>(); intList.Add(1); Console.WriteLine(intList.Get(0)); // 输出: 1 GenericList<string> stringList = new GenericList<string>(); stringList.Add("Hello"); Console.WriteLine(stringList.Get(0)); // 输出: Hello
19. 什么是命名空间(Namespace)?它的作用是什么?
答案:
-
**命名空间(Namespace)**是用于组织代码的逻辑容器,防止命名冲突,提升代码的可读性和维护性。
-
作用:
- 组织代码:将相关类、接口、结构体等分组,便于管理。
- 避免命名冲突:不同命名空间中的同名类型互不影响。
- 便于维护:清晰的结构使代码更易于维护和理解。
-
示例:
namespace Company.Project.Module { public class MyClass { // ... } } // 使用 using Company.Project.Module; MyClass obj = new MyClass();
20. 什么是using
语句,有什么作用?
答案:
using
语句有两种主要用途:-
命名空间导入:通过
using
关键字引入命名空间,使得在代码中可以直接使用该命名空间下的类型,而无需全名限定。示例:
using System.Text; StringBuilder sb = new StringBuilder();
-
资源管理:用于确保实现了
IDisposable
接口的对象在使用完毕后被正确释放,自动调用Dispose
方法,避免资源泄漏。示例:
using (FileStream fs = new FileStream("file.txt", FileMode.Open)) { // 使用文件流 } // 自动调用 fs.Dispose()
-
集合与泛型
21. C#中有哪些常用的集合类型?
答案:
C#中常用的集合类型主要分为两类:非泛型集合和泛型集合。
-
非泛型集合(位于
System.Collections
命名空间):ArrayList
Hashtable
Queue
Stack
SortedList
-
泛型集合(位于
System.Collections.Generic
命名空间):List<T>
Dictionary<TKey, TValue>
Queue<T>
Stack<T>
LinkedList<T>
HashSet<T>
SortedDictionary<TKey, TValue>
SortedSet<T>
此外,还有并发集合(System.Collections.Concurrent
)和只读集合(System.Collections.ObjectModel
)等。
22. 什么是List<T>
,它的特点是什么?
答案:
-
**
List<T>
**是一个泛型动态数组,位于System.Collections.Generic
命名空间,用于存储同一类型的对象,提供了动态扩展、索引访问等功能。 -
特点:
- 动态大小:可以根据需要自动调整大小。
- 泛型:类型安全,避免运行时类型错误。
- 丰富的方法:提供诸如
Add
,Remove
,Find
,Sort
等多种方法,简化操作。 - 索引访问:支持通过索引快速访问元素。
- 性能:底层使用数组,提供较高的访问性能。
-
示例:
List<int> numbers = new List<int>(); numbers.Add(1); numbers.Add(2); numbers.Add(3); Console.WriteLine(numbers[1]); // 输出: 2 numbers.Remove(2); Console.WriteLine(numbers.Count); // 输出: 2
23. 什么是Dictionary<TKey, TValue>
,它有哪些常用方法?
答案:
-
**
Dictionary<TKey, TValue>
**是一个基于哈希表实现的泛型集合,存储键值对,提供快速的查找、插入和删除操作。 -
特点:
- 键唯一:每个键必须唯一,但不同键可以对应相同的值。
- 快速查找:通过键进行高效查找。
- 泛型:类型安全,键和值可以是任何类型。
-
常用方法:
Add(TKey key, TValue value)
:添加键值对。Remove(TKey key)
:根据键移除键值对。TryGetValue(TKey key, out TValue value)
:尝试获取指定键的值。ContainsKey(TKey key)
:检查是否包含指定键。Clear()
:清空字典。Keys
和Values
属性:获取所有键和值的集合。
-
示例:
Dictionary<string, int> ages = new Dictionary<string, int>(); ages.Add("Alice", 30); ages.Add("Bob", 25); if (ages.TryGetValue("Alice", out int age)) { Console.WriteLine($"Alice's age is {age}"); } foreach (var key in ages.Keys) { Console.WriteLine(key); }
24. 什么是HashSet<T>
,它的用途是什么?
答案:
-
**
HashSet<T>
**是一个泛型集合,存储唯一的元素,基于哈希表实现,主要用于集合操作如并集、交集和差集。 -
特点:
- 元素唯一:自动去重,确保集合中没有重复元素。
- 高效的查找:提供快速的添加、删除和查找操作。
- 不保持顺序:元素的存储顺序不固定。
-
用途:
- 去重:从数据集中快速移除重复项。
- 集合操作:执行并集、交集、差集等操作。
- 高效查找:在需要频繁查找操作的场景中使用。
-
示例:
HashSet<int> numbers = new HashSet<int>(); numbers.Add(1); numbers.Add(2); numbers.Add(2); // 重复元素,添加失败 Console.WriteLine(numbers.Count); // 输出: 2 HashSet<int> otherNumbers = new HashSet<int> { 2, 3, 4 }; numbers.IntersectWith(otherNumbers); foreach (var num in numbers) { Console.WriteLine(num); // 输出: 2 }
25. C#中如何遍历Dictionary
集合?
答案:
可以通过foreach
循环遍历Dictionary<TKey, TValue>
,访问每个键值对。常见的遍历方式包括:
-
遍历键值对:
Dictionary<string, int> ages = new Dictionary<string, int> { { "Alice", 30 }, { "Bob", 25 } }; foreach (KeyValuePair<string, int> kvp in ages) { Console.WriteLine($"Name: {kvp.Key}, Age: {kvp.Value}"); }
-
遍历键:
foreach (string key in ages.Keys) { Console.WriteLine($"Name: {key}"); }
-
遍历值:
foreach (int value in ages.Values) { Console.WriteLine($"Age: {value}"); }
26. 什么是协变(Covariance)和逆变(Contravariance)?
答案:
-
**协变(Covariance)和逆变(Contravariance)**是泛型类型参数中的类型转换规则,用于实现更灵活的类型系统。
-
协变:
- 允许将派生类型的泛型接口赋值给基类型的泛型接口。
- 适用于输出方向(返回类型)。
- 使用
out
关键字。
示例:
IEnumerable<string> strings = new List<string>(); IEnumerable<object> objects = strings; // 协变
-
逆变:
- 允许将基类型的泛型接口赋值给派生类型的泛型接口。
- 适用于输入方向(参数类型)。
- 使用
in
关键字。
示例:
Action<object> actObject = obj => Console.WriteLine(obj); Action<string> actString = actObject; // 逆变
-
应用:
- 提高泛型代码的灵活性和可重用性。
- 常用于委托和接口,如
IEnumerable<out T>
、IComparer<in T>
。
27. C#中的Queue<T>
和Stack<T>
有什么区别?
答案:
-
**
Queue<T>
**是先进先出(FIFO)的数据结构,适用于需要按顺序处理元素的场景。主要操作:
Enqueue(T item)
:添加元素到队列尾部。Dequeue()
:移除并返回队列头部元素。Peek()
:返回队列头部元素,但不移除。
-
**
Stack<T>
**是后进先出(LIFO)的数据结构,适用于需要逆序处理元素的场景。主要操作:
Push(T item)
:添加元素到栈顶。Pop()
:移除并返回栈顶元素。Peek()
:返回栈顶元素,但不移除。
-
示例:
Queue<int> queue = new Queue<int>(); queue.Enqueue(1); queue.Enqueue(2); Console.WriteLine(queue.Dequeue()); // 输出: 1 Stack<int> stack = new Stack<int>(); stack.Push(1); stack.Push(2); Console.WriteLine(stack.Pop()); // 输出: 2
28. 什么是LinkedList<T>
,它的优势和劣势是什么?
答案:
-
**
LinkedList<T>
**是一个基于双向链表实现的泛型集合,支持快速的插入和删除操作。 -
优势:
- 快速插入和删除:在任何位置插入或删除元素的时间复杂度为O(1),只需调整指针。
- 动态大小:无需预先分配固定大小。
-
劣势:
- 内存消耗高:每个节点需要额外存储前后指针。
- 访问速度慢:无法通过索引快速访问元素,需从头或尾遍历。
- 缓存局部性差:由于元素分散存储,可能导致缓存命中率低。
-
适用场景:
- 需要频繁在中间位置插入或删除元素的场景。
- 不需要随机访问元素的场景。
-
示例:
LinkedList<string> linkedList = new LinkedList<string>(); linkedList.AddLast("First"); linkedList.AddLast("Second"); linkedList.AddFirst("Zero"); foreach (var item in linkedList) { Console.WriteLine(item); } // 输出: // Zero // First // Second
29. 什么是只读集合(ReadOnly Collection)?如何创建?
答案:
-
**只读集合(ReadOnly Collection)**是无法修改的集合视图,提供对基础集合的只读访问。
-
创建方式:
- 使用
ReadOnlyCollection<T>
包装一个现有的IList<T>
。
- 使用
-
用途:
- 提供安全的只读访问,防止外部代码修改集合。
- 保护内部数据结构,增强封装性。
-
示例:
using System.Collections.ObjectModel; List<int> list = new List<int> { 1, 2, 3 }; ReadOnlyCollection<int> readOnly = new ReadOnlyCollection<int>(list); Console.WriteLine(readOnly[0]); // 输出: 1 // readOnly.Add(4); // 编译错误,无法修改
30. 什么是ArrayList
,为什么更推荐使用泛型集合如List<T>
?
答案:
-
**
ArrayList
**是非泛型的动态数组集合,属于System.Collections
命名空间,能够存储任何类型的对象。 -
原因不推荐使用
ArrayList
:- 类型不安全:由于存储为
object
,需要进行装箱和拆箱操作,容易引发运行时错误。 - 性能低下:频繁的装箱和拆箱导致性能下降,尤其在处理值类型时。
- 缺乏泛型优势:无法利用泛型带来的类型检查和性能优化。
- 类型不安全:由于存储为
-
推荐使用:
- 使用**
List<T>
**,提供类型安全、无需装箱、性能更高、支持泛型特性。
- 使用**
-
示例:
ArrayList arrayList = new ArrayList(); arrayList.Add(1); arrayList.Add("Two"); // 需要进行类型检查或转换 foreach (var item in arrayList) { if (item is int) Console.WriteLine((int)item); else if (item is string) Console.WriteLine((string)item); } // 使用泛型集合 List<int> list = new List<int> { 1, 2, 3 }; foreach (int num in list) { Console.WriteLine(num); // 无需转换,类型安全 }
异常处理
31. C#中如何实现异常处理?常用的关键字有哪些?
答案:
-
异常处理通过
try-catch-finally
语句块实现,捕获和处理运行时错误,保证程序的稳定性。 -
关键字:
try
:包裹可能引发异常的代码块。catch
:捕获特定类型的异常并进行处理。finally
:无论是否发生异常,都会执行的代码块,通常用于资源释放。throw
:引发异常或重新抛出捕获的异常。
-
示例:
try { int[] numbers = {1, 2, 3}; Console.WriteLine(numbers[5]); // 可能引发IndexOutOfRangeException } catch (IndexOutOfRangeException ex) { Console.WriteLine("索引越界错误: " + ex.Message); } catch (Exception ex) { Console.WriteLine("其他错误: " + ex.Message); } finally { Console.WriteLine("无论是否发生异常,都会执行。"); }
32. 如何自定义异常类?
答案:
-
步骤:
- 创建一个继承自
Exception
或其派生类的类。 - 实现至少一个构造函数,通常包括无参构造函数、带消息参数的构造函数,以及支持序列化的构造函数。
- 创建一个继承自
-
示例:
[Serializable] public class MyCustomException : Exception { public MyCustomException() { } public MyCustomException(string message) : base(message) { } public MyCustomException(string message, Exception inner) : base(message, inner) { } protected MyCustomException(SerializationInfo info, StreamingContext context) : base(info, context) { } } // 使用自定义异常 public void DoSomething(int value) { if (value < 0) throw new MyCustomException("值不能为负数"); }
33. 什么是finally
块,它的作用是什么?
答案:
-
finally
块是try-catch
结构的一部分,用于包含无论是否发生异常都需要执行的代码,如资源释放、清理操作等。 -
作用:
- 确保资源被正确释放,避免资源泄漏。
- 执行必要的清理操作,无论异常是否被捕获。
-
特点:
finally
块是可选的,可以单独与try
配合使用。- 如果有
return
语句,finally
块仍会执行。
-
示例:
try { // 可能引发异常的代码 } catch (Exception ex) { // 异常处理 } finally { // 清理操作 Console.WriteLine("执行 finally 块"); }
34. 什么是throw
和throw ex
的区别?
答案:
-
throw
:- 重新抛出当前捕获的异常,保留原始的堆栈跟踪信息。
-
throw ex
:- 抛出一个新的异常实例,导致原始的堆栈跟踪信息丢失,显示异常发生的位置为
throw ex
处。
- 抛出一个新的异常实例,导致原始的堆栈跟踪信息丢失,显示异常发生的位置为
-
区别:
- 使用
throw
能够保留原始异常的上下文,便于调试和错误追踪。 - 使用
throw ex
会丢失原始的堆栈跟踪,降低异常信息的可用性。
- 使用
-
示例:
try { // 可能引发异常 } catch (Exception ex) { throw; // 保留堆栈信息 // throw ex; // 丢失原始堆栈信息 }
35. 什么是自定义异常消息,如何实现?
答案:
-
自定义异常消息是指在抛出异常时,提供一个自定义的错误信息,以便更清晰地描述异常的原因。
-
实现方法:
- 在抛出异常时,通过构造函数传递自定义的错误消息。
- 自定义异常类中可以添加额外的属性或方法,以提供更详细的信息。
-
示例:
public class InvalidAgeException : Exception { public InvalidAgeException(string message) : base(message) { } } public void SetAge(int age) { if (age < 0 || age > 120) throw new InvalidAgeException("年龄必须在0到120之间。"); // 设置年龄 } // 使用 try { SetAge(-5); } catch (InvalidAgeException ex) { Console.WriteLine(ex.Message); // 输出: 年龄必须在0到120之间。 }
异步编程
36. C#中如何实现异步编程?关键字有哪些?
答案:
-
异步编程允许程序在执行耗时操作时不阻塞主线程,提高应用响应性和性能。
-
关键字:
async
:标记方法为异步方法,允许使用await
关键字。await
:暂停异步方法的执行,直到等待的任务完成,然后继续执行。Task
和Task<T>
:表示异步操作的结果和状态。async void
:用于事件处理器,不推荐在其他场景使用,因无法捕获异常。
-
示例:
public async Task<int> GetDataAsync() { // 模拟异步操作 await Task.Delay(1000); return 42; } public async void DisplayData() { int result = await GetDataAsync(); Console.WriteLine(result); } // 使用 DisplayData(); // 输出: 42 (延迟1秒)
37. 什么是Task
和Task<T>
?
答案:
-
Task
:- 表示一个异步操作,不返回任何结果。
- 用于表示执行中的操作。
-
Task<T>
:- 表示一个异步操作,返回一个类型为
T
的结果。 - 允许在异步操作完成后获取结果。
- 表示一个异步操作,返回一个类型为
-
区别:
Task
用于无返回值的异步方法。Task<T>
用于有返回值的异步方法。
-
示例:
// 使用 Task public async Task SaveDataAsync(string data) { await Task.Delay(1000); // 模拟保存数据 Console.WriteLine("数据已保存"); } // 使用 Task<T> public async Task<int> ComputeValueAsync() { await Task.Delay(1000); // 模拟计算 return 100; } // 调用 await SaveDataAsync("Sample Data"); int value = await ComputeValueAsync(); Console.WriteLine(value); // 输出: 100
38. 什么是async
和await
关键字?
答案:
-
async
:- 标记方法为异步方法,允许使用
await
关键字。 - 方法返回类型通常为
Task
、Task<T>
或void
。
- 标记方法为异步方法,允许使用
-
await
:- 用于等待一个异步操作完成,非阻塞地暂停方法执行,待等待的任务完成后继续执行。
- 只能在标记为
async
的方法中使用。
-
工作原理:
- 当
await
等待的任务未完成时,方法会挂起,释放当前线程。 - 任务完成后,方法会恢复执行,继续后续代码。
- 当
-
示例:
public async Task<string> FetchDataAsync() { using (HttpClient client = new HttpClient()) { string result = await client.GetStringAsync("https://example.com"); return result; } } public async void DisplayData() { string data = await FetchDataAsync(); Console.WriteLine(data); }
39. 如何处理异步方法中的异常?
答案:
-
使用
try-catch
块:在异步方法中,可以使用try-catch
结构捕获并处理异常。示例:
public async Task<string> GetDataAsync() { try { // 可能引发异常的异步操作 return await File.ReadAllTextAsync("nonexistentfile.txt"); } catch (FileNotFoundException ex) { Console.WriteLine("文件未找到: " + ex.Message); return null; } }
-
捕获任务异常:
- 当使用
Task
时,可以通过await
抛出任务的异常,随后被try-catch
捕获。 - 如果直接使用
Task
,可以通过访问Task.Exception
属性获取异常信息。
- 当使用
-
注意事项:
- 避免使用
async void
,因其异常无法被外部捕获。 - 在异步事件处理器中,异常需要在内部处理,以防止程序崩溃。
- 避免使用
40. 什么是ConfigureAwait(false)
,它的作用是什么?
答案:
-
**
ConfigureAwait(false)
**用于告诉编译器在等待异步操作完成后,不需要恢复到原来的同步上下文(如UI线程)。 -
作用:
- 提高应用性能,减少上下文切换的开销。
- 避免死锁问题,尤其在库代码和服务端应用中。
-
使用场景:
- 在非UI线程、后台处理任务或库代码中使用异步方法。
-
示例:
public async Task<string> GetDataAsync() { using (HttpClient client = new HttpClient()) { // 异步调用,但不需要恢复到原同步上下文 string result = await client.GetStringAsync("https://example.com").ConfigureAwait(false); return result; } }
LINQ(Language Integrated Query)
41. 什么是LINQ,它的主要用途是什么?
答案:
-
**LINQ(Language Integrated Query)**是C#中的一组语言特性,允许开发者在语言层面上对数据进行查询、过滤、排序和转换等操作,无需使用外部查询语言(如SQL)。
-
主要用途:
- 提高代码的可读性和简洁性。
- 在各种数据源(集合、数据库、XML等)上执行统一的查询操作。
- 利用强类型和编译时检查,减少运行时错误。
-
示例:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 }; var evenNumbers = from num in numbers where num % 2 == 0 select num; foreach (var n in evenNumbers) { Console.WriteLine(n); // 输出: 2, 4 }
42. 什么是延迟执行(Deferred Execution)?
答案:
-
**延迟执行(Deferred Execution)**指LINQ查询在创建时并不立即执行,而是当查询的结果被实际迭代(如使用
foreach
)或调用终结操作(如ToList
)时才执行。 -
作用:
- 提高性能,避免不必要的计算。
- 支持动态查询,查询操作可以随数据源的变化而变化。
-
示例:
List<int> numbers = new List<int> { 1, 2, 3 }; var query = numbers.Where(n => n > 1); // 查询未执行 numbers.Add(4); foreach (var n in query) { Console.WriteLine(n); // 输出: 2, 3, 4 }
43. IEnumerable<T>
和IQueryable<T>
有什么区别?
答案:
-
IEnumerable<T>
:- 定义在
System.Collections.Generic
命名空间。 - 支持对内存中对象集合的迭代,适用于LINQ to Objects。
- 查询在本地执行,返回结果集。
- 定义在
-
IQueryable<T>
:- 定义在
System.Linq
命名空间。 - 继承自
IEnumerable<T>
,支持LINQ到其他数据源(如数据库、XML)。 - 查询表达式可以被转换为数据源特定的查询,如SQL,优化查询性能。
- 定义在
-
区别:
IEnumerable<T>
适合操作内存中的集合,IQueryable<T>
适合延伸到外部数据源。IQueryable<T>
支持表达式树,允许在数据源端进行查询优化。
44. 什么是LINQ的标准查询运算符?
答案:
LINQ的标准查询运算符是指一系列扩展方法,用于对数据源进行查询操作。这些运算符分为几类:
-
过滤:
Where
:筛选符合条件的元素。OfType
:筛选特定类型的元素。Distinct
:移除重复元素。
-
排序:
OrderBy
、OrderByDescending
:按升序或降序排序。ThenBy
、ThenByDescending
:在已有排序的基础上进行次级排序。
-
分组:
GroupBy
:将元素按键值分组。
-
投影:
Select
:选择或转换元素。SelectMany
:将多维数据展开为一维。
-
集合操作:
Join
:连接两个数据源。GroupJoin
:进行分组连接。Union
、Intersect
、Except
:集合的并、交、差操作。
-
量化:
All
、Any
:检查所有或任意元素是否满足条件。Count
、LongCount
:计算元素数量。Min
、Max
、Sum
、Average
:计算最小值、最大值、总和、平均值。
-
元素操作:
First
、FirstOrDefault
:获取第一个元素或默认值。Last
、LastOrDefault
:获取最后一个元素或默认值。Single
、SingleOrDefault
:获取唯一的元素或默认值。
-
转换:
ToList
、ToArray
:将查询结果转换为列表或数组。ToDictionary
:将查询结果转换为字典。
-
生成:
Empty
:返回一个空的序列。
45. 如何使用LINQ实现分组操作?
答案:
使用GroupBy
运算符可以将元素按指定的键进行分组,返回一个分组的集合,每个分组中包含键和相关联的元素。
示例:
public class Student
{
public string Name { get; set; }
public string Grade { get; set; }
}
List<Student> students = new List<Student>
{
new Student { Name = "Alice", Grade = "A" },
new Student { Name = "Bob", Grade = "B" },
new Student { Name = "Charlie", Grade = "A" },
new Student { Name = "David", Grade = "C" }
};
// 按成绩分组
var grouped = students.GroupBy(s => s.Grade);
foreach (var group in grouped)
{
Console.WriteLine($"Grade: {group.Key}");
foreach (var student in group)
{
Console.WriteLine($" - {student.Name}");
}
}
// 输出:
// Grade: A
// - Alice
// - Charlie
// Grade: B
// - Bob
// Grade: C
// - David
46. 什么是LINQ的延迟执行和立即执行?
答案:
-
延迟执行(Deferred Execution):
- LINQ查询在定义时不立即执行,直到其结果被迭代或调用终结运算符(如
ToList
,ToArray
)时才执行。 - 有利于性能优化和动态查询。
- LINQ查询在定义时不立即执行,直到其结果被迭代或调用终结运算符(如
-
立即执行(Immediate Execution):
- LINQ查询在定义时立即执行,获取结果并存储。
- 常用的终结运算符触发立即执行,如
ToList
,ToArray
,Count
,First
等。
-
示例:
List<int> numbers = new List<int> { 1, 2, 3 }; // 延迟执行 var query = numbers.Where(n => n > 1); // 查询未执行 numbers.Add(4); foreach (var num in query) { Console.WriteLine(num); // 输出: 2, 3, 4 } // 立即执行 var list = numbers.Where(n => n > 1).ToList(); // 查询立即执行并存储结果 numbers.Add(5); foreach (var num in list) { Console.WriteLine(num); // 输出: 2, 3, 4 }
47. 如何在LINQ中使用匿名类型?
答案:
-
**匿名类型(Anonymous Types)**允许在LINQ查询中创建临时类型,包含一组只读属性,无需预先定义类。
-
使用场景:
- 当查询结果需要特定的项目集合,但不想创建新的类时。
- 提高查询的灵活性和简洁性。
-
示例:
var products = new List<Product> { new Product { Name = "Apple", Category = "Fruit", Price = 1.2 }, new Product { Name = "Carrot", Category = "Vegetable", Price = 0.8 }, new Product { Name = "Banana", Category = "Fruit", Price = 1.1 } }; var fruitProducts = from p in products where p.Category == "Fruit" select new { p.Name, p.Price }; foreach (var item in fruitProducts) { Console.WriteLine($"Name: {item.Name}, Price: {item.Price}"); } // 输出: // Name: Apple, Price: 1.2 // Name: Banana, Price: 1.1
48. 如何使用LINQ进行连接操作?
答案:
使用Join
运算符可以在两个序列之间根据某个键进行内连接(Inner Join),返回匹配的元素对。
示例:
public class Student
{
public int ID { get; set; }
public string Name { get; set; }
}
public class Score
{
public int StudentID { get; set; }
public int ScoreValue { get; set; }
}
List<Student> students = new List<Student>
{
new Student { ID = 1, Name = "Alice" },
new Student { ID = 2, Name = "Bob" },
new Student { ID = 3, Name = "Charlie" }
};
List<Score> scores = new List<Score>
{
new Score { StudentID = 1, ScoreValue = 85 },
new Score { StudentID = 2, ScoreValue = 90 },
new Score { StudentID = 1, ScoreValue = 88 }
};
// 内连接
var studentScores = from s in students
join sc in scores on s.ID equals sc.StudentID
select new { s.Name, sc.ScoreValue };
foreach (var item in studentScores)
{
Console.WriteLine($"Name: {item.Name}, Score: {item.ScoreValue}");
}
// 输出:
// Name: Alice, Score: 85
// Name: Alice, Score: 88
// Name: Bob, Score: 90
49. 什么是LINQ的方法语法和查询语法?有什么区别?
答案:
-
查询语法(Query Syntax):
- 类似SQL的语法,将查询表达为
from
,where
,select
等关键字的组合。 - 更具可读性,适合熟悉SQL的开发者。
- 类似SQL的语法,将查询表达为
-
方法语法(Method Syntax):
- 基于扩展方法链式调用的语法,使用LINQ的标准查询运算符。
- 更灵活,支持复杂的查询操作。
-
区别:
- 查询语法在简单查询中更直观和易读。
- 方法语法在复杂查询或需要自定义方法时更为强大。
-
示例:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 }; // 查询语法 var querySyntax = from n in numbers where n > 2 select n; // 方法语法 var methodSyntax = numbers.Where(n => n > 2); // 两者效果相同 foreach (var num in querySyntax) { Console.WriteLine(num); // 输出: 3,4,5 } foreach (var num in methodSyntax) { Console.WriteLine(num); // 输出: 3,4,5 }
50. 什么是梯形(Trapezoidal)规则?
答案:
梯形规则通常用于数值积分,是一种通过梯形近似来估计定积分的方法。由于梯形规则与C#中的LINQ无直接关联,可能此问题意图有误。
更可能的面试问题: 什么是LINQ的SelectMany
运算符?
答案:
-
SelectMany
运算符用于将每个元素的子集合扁平化为一个单一的序列,常用于多层嵌套集合的查询。 -
用途:
- 处理多维数据结构,如列表中的列表。
- 实现复合集合的平面化。
-
示例:
var listOfLists = new List<List<int>> { new List<int> {1, 2, 3}, new List<int> {4, 5}, new List<int> {6, 7, 8, 9} }; var flattened = listOfLists.SelectMany(subList => subList); foreach (var num in flattened) { Console.WriteLine(num); // 输出: 1,2,3,4,5,6,7,8,9 }
委托与事件
51. 什么是委托(Delegate)?它有什么用途?
答案:
-
**委托(Delegate)**是C#中一种类型安全的函数指针,允许将方法作为参数传递或赋值给变量。委托定义了方法的签名(返回类型和参数列表),确保委托只能指向符合签名的方法。
-
用途:
- 回调函数:在异步操作或事件处理中使用回调方法。
- 事件处理:作为事件的基础类型,用于发布-订阅模式。
- LINQ:通过委托实现查询表达式中的选择和过滤逻辑。
- 策略模式:动态选择算法或行为。
-
示例:
public delegate void Notify(string message); public class Process { public event Notify OnCompleted; public void Start() { Console.WriteLine("Process started."); // 处理逻辑 OnCompleted?.Invoke("Process finished."); } } // 使用 class Program { static void Main(string[] args) { Process process = new Process(); process.OnCompleted += ShowMessage; process.Start(); } static void ShowMessage(string msg) { Console.WriteLine(msg); } } // 输出: // Process started. // Process finished.
52. 什么是多播委托(Multicast Delegate)?
答案:
-
**多播委托(Multicast Delegate)**是能够封装多个方法的委托实例。当调用多播委托时,所有封装的方法都会按添加顺序依次执行。委托的
+
和-
运算符用于组合或移除方法。 -
特点:
- 方法链:可以将多个方法组合成一个委托链。
- 执行顺序:按添加顺序依次执行各方法。
- 返回值:多播委托的返回值为最后一个方法的返回值,通常多播委托用于
void
返回类型的方法。
-
示例:
public delegate void Notify(string message); public class Publisher { public event Notify OnNotify; public void SendMessage(string msg) { OnNotify?.Invoke(msg); } } public class Subscriber { public void ReceiveMessage(string msg) { Console.WriteLine($"Received: {msg}"); } } // 使用 class Program { static void Main(string[] args) { Publisher publisher = new Publisher(); Subscriber subscriber1 = new Subscriber(); Subscriber subscriber2 = new Subscriber(); publisher.OnNotify += subscriber1.ReceiveMessage; publisher.OnNotify += subscriber2.ReceiveMessage; publisher.SendMessage("Hello Subscribers!"); // 输出: // Received: Hello Subscribers! // Received: Hello Subscribers! } }
53. C#中如何实现事件(Event)?
答案:
-
**事件(Event)**是基于委托的一种机制,用于在对象之间传递通知,遵循发布-订阅模式。事件允许多个订阅者响应某个动作。
-
实现步骤:
- 定义一个委托类型,用于描述事件处理方法的签名。
- 在发布者类中声明事件,类型为该委托类型。
- 发布者在适当的时候触发事件,通过调用事件委托。
- 订阅者通过
+=
运算符订阅事件,提供处理方法。
-
示例:
public delegate void ThresholdReachedEventHandler(object sender, ThresholdReachedEventArgs e); public class ThresholdReachedEventArgs : EventArgs { public int Threshold { get; set; } public DateTime TimeReached { get; set; } } public class Counter { private int count = 0; private int threshold; public Counter(int threshold) { this.threshold = threshold; } public event ThresholdReachedEventHandler ThresholdReached; public void Add(int x) { count += x; if (count >= threshold) { OnThresholdReached(new ThresholdReachedEventArgs { Threshold = threshold, TimeReached = DateTime.Now }); } } protected virtual void OnThresholdReached(ThresholdReachedEventArgs e) { ThresholdReached?.Invoke(this, e); } } // 使用 class Program { static void Main(string[] args) { Counter c = new Counter(10); c.ThresholdReached += c_ThresholdReached; c.Add(3); c.Add(4); c.Add(5); // 触发事件 } static void c_ThresholdReached(object sender, ThresholdReachedEventArgs e) { Console.WriteLine($"Threshold of {e.Threshold} was reached at {e.TimeReached}."); } } // 输出: // Threshold of 10 was reached at 2023-09-28 10:00:00.
54. 什么是委托链(Delegate Chain)?
答案:
-
委托链是指多个方法通过多播委托链接在一起,形成的一条调用链。当调用委托链时,所有链接的方法按顺序被依次调用。
-
特点:
- 支持多播,可以同时执行多个方法。
- 常用于事件处理、回调函数等场景。
- 可以通过
+=
添加方法,通过-=
移除方法。
-
示例:
public delegate void Notify(string message); public void Method1(string msg) { Console.WriteLine("Method1: " + msg); } public void Method2(string msg) { Console.WriteLine("Method2: " + msg); } // 构建委托链 Notify notify = Method1; notify += Method2; notify("Hello Delegate Chain"); // 输出: // Method1: Hello Delegate Chain // Method2: Hello Delegate Chain
55. C#中的委托与函数指针有什么区别?
答案:
-
委托(Delegate):
- 是一种类型安全的、面向对象的函数引用。
- 可以封装多个方法,支持多播。
- 支持异步调用、事件处理。
- 与C#的类型系统紧密集成,具有安全性和灵活性。
-
函数指针(Function Pointer):
- 是一种低级别的、非类型安全的指针,指向内存中的函数地址。
- 直接操作内存地址,存在安全风险。
- C# 9.0引入了
delegate*
语法,用于高性能场景,但仍不如委托安全。
-
区别:
- 类型安全:委托是类型安全的,函数指针不具备。
- 灵活性:委托更灵活,支持多播和异步操作。
- 安全性:委托提供更高的安全性,函数指针可能导致安全漏洞。
线程与并发
56. 什么是线程(Thread)?如何在C#中创建一个线程?
答案:
-
**线程(Thread)**是程序执行的最小单位,一个进程可以包含多个线程,共享进程的资源,如内存空间和文件句柄。
-
在C#中创建线程:
- 使用
System.Threading.Thread
类,传入需要执行的方法,并调用Start
方法启动线程。
- 使用
-
示例:
using System; using System.Threading; class Program { static void Main(string[] args) { Thread t = new Thread(new ThreadStart(PrintNumbers)); t.Start(); // 主线程继续执行 for (int i = 0; i < 5; i++) { Console.WriteLine("Main Thread: " + i); Thread.Sleep(500); } t.Join(); // 等待子线程完成 } static void PrintNumbers() { for (int i = 0; i < 5; i++) { Console.WriteLine("Child Thread: " + i); Thread.Sleep(500); } } } // 输出(顺序可能不同): // Child Thread: 0 // Main Thread: 0 // Child Thread: 1 // Main Thread: 1 // ...
57. 什么是线程池(Thread Pool)?它的优势是什么?
答案:
-
**线程池(Thread Pool)**是预先创建并管理的一组线程,供应用程序重复利用,避免频繁创建和销毁线程的开销。
-
优势:
- 性能提升:重用线程,减少线程创建和销毁带来的性能开销。
- 资源管理:集中管理线程资源,避免过多线程导致资源耗尽。
- 简化编程:通过高级API(如
Task
,ThreadPool
)简化异步编程模型。
-
在C#中的使用:
System.Threading.ThreadPool
类。- 高级抽象如
Task
和async/await
自动利用线程池。
-
示例:
using System; using System.Threading; class Program { static void Main(string[] args) { ThreadPool.QueueUserWorkItem(DoWork); ThreadPool.QueueUserWorkItem(DoWork); Console.WriteLine("Main thread continues..."); Thread.Sleep(1000); // 等待线程池线程完成 } static void DoWork(object state) { Console.WriteLine("ThreadPool thread: " + Thread.CurrentThread.ManagedThreadId); } } // 可能输出: // Main thread continues... // ThreadPool thread: 3 // ThreadPool thread: 4
58. 什么是锁(Lock)?如何在C#中实现锁机制?
答案:
-
**锁(Lock)**是一种同步机制,用于保护共享资源,防止多个线程同时访问和修改,导致数据竞态(Race Condition)。
-
在C#中实现锁机制:
- 使用
lock
关键字(语法糖,基于Monitor
类)。 lock
关键字确保被保护的代码块在同一时间只能被一个线程访问。
- 使用
-
示例:
using System; using System.Threading; class Counter { private int count = 0; private object lockObj = new object(); public void Increment() { lock (lockObj) { count++; Console.WriteLine("Count: " + count); } } } class Program { static void Main(string[] args) { Counter counter = new Counter(); for (int i = 0; i < 5; i++) { new Thread(counter.Increment).Start(); } } } // 输出: Count: 1 // Count: 2 // Count: 3 // Count: 4 // Count: 5
-
注意事项:
- 锁定对象应私有且只用于锁定,不应锁定
this
或公共对象。 - 尽量缩小锁定范围,避免死锁和性能问题。
- 锁定对象应私有且只用于锁定,不应锁定
59. 什么是死锁(Deadlock)?如何避免?
答案:
-
**死锁(Deadlock)**是指两个或多个线程相互等待对方释放资源,导致所有线程永久阻塞,无法继续执行。
-
产生条件(Coffman条件):
- 互斥:至少有一个资源必须处于非共享模式。
- 保持和等待:至少有一个线程保持一个资源并等待获取另一个被其他线程占用的资源。
- 不剥夺:资源在未释放前,不能被强行剥夺。
- 循环等待:存在一个线程环,环中每个线程都在等待下一个线程持有的资源。
-
避免策略:
- 资源排序:为所有资源定义一个全局顺序,线程必须按顺序请求资源,避免循环等待。
- 避免保持和等待:线程在请求新资源前,释放当前持有的所有资源。
- 使用超时:线程在请求资源时设置超时,如果超时则放弃并重试。
- 减少锁的范围:尽量缩小锁定代码块的范围,减少持有锁的时间。
- 使用死锁检测:定期检查系统中是否存在死锁,采取恢复措施。
-
示例避免资源排序:
class ResourceA { } class ResourceB { } class DeadlockDemo { private ResourceA resourceA = new ResourceA(); private ResourceB resourceB = new ResourceB(); public void Method1() { lock (resourceA) { Console.WriteLine("Thread 1: Locked ResourceA"); Thread.Sleep(100); lock (resourceB) { Console.WriteLine("Thread 1: Locked ResourceB"); } } } public void Method2() { lock (resourceA) // 改为先锁定ResourceA,再锁定ResourceB { Console.WriteLine("Thread 2: Locked ResourceA"); Thread.Sleep(100); lock (resourceB) { Console.WriteLine("Thread 2: Locked ResourceB"); } } } } // 在主线程中启动两个线程调用Method1和Method2,避免死锁
60. 什么是lock
与Monitor
的区别和联系?
答案:
-
联系:
lock
关键字是C#的语法糖,底层实现基于System.Threading.Monitor
类。- 两者都用于实现线程同步,确保共享资源的互斥访问。
-
区别:
- 简洁性:
lock
语法更简洁,自动处理进入和退出锁的过程,包括异常时的释放锁。 - 功能性:
Monitor
类提供了更丰富的功能,如Pulse
和Wait
方法,用于线程间的信号传递和协作。 - 异常处理:使用
lock
时,无需显式释放锁,Monitor
需要确保在finally
块中调用Monitor.Exit
释放锁。
- 简洁性:
-
示例:
-
使用
lock
:private object lockObj = new object(); public void SafeMethod() { lock (lockObj) { // 临界区 } }
-
使用
Monitor
:private object lockObj = new object(); public void SafeMethod() { bool lockTaken = false; try { Monitor.Enter(lockObj, ref lockTaken); // 临界区 } finally { if (lockTaken) Monitor.Exit(lockObj); } }
-
异步与并发
61. 什么是async
方法的返回类型,可以有哪些类型?
答案:
-
async
方法需要有特定的返回类型,用于表示异步操作的结果或状态。 -
可能的返回类型:
-
Task
:- 用于没有返回值的异步方法。
示例:
public async Task DoWorkAsync() { await Task.Delay(1000); Console.WriteLine("Work completed."); }
-
Task<T>
:- 用于有返回值的异步方法。
示例:
public async Task<int> GetNumberAsync() { await Task.Delay(1000); return 42; }
-
void
:- 用于异步事件处理器,不建议在其他情况下使用,因为无法等待或捕获异常。
示例:
public async void OnButtonClick(object sender, EventArgs e) { await Task.Delay(1000); Console.WriteLine("Button clicked."); }
-
62. 什么是CancellationToken
,如何在异步操作中使用?
答案:
-
**
CancellationToken
**是一种机制,用于通知异步操作或任务取消其执行。它通过CancellationTokenSource
传递,并在需要的地方检查取消请求。 -
使用步骤:
- 创建
CancellationTokenSource
:发起取消请求的源。 - 获取
CancellationToken
:从CancellationTokenSource
获取令牌。 - 传递
CancellationToken
:将令牌传递给异步方法或任务。 - 在异步方法中检查取消:通过
token.IsCancellationRequested
或token.ThrowIfCancellationRequested()
主动检查并响应取消请求。 - 发起取消:调用
CancellationTokenSource.Cancel()
发起取消。
- 创建
-
示例:
using System; using System.Threading; using System.Threading.Tasks; class Program { static async Task Main(string[] args) { CancellationTokenSource cts = new CancellationTokenSource(); Task task = LongRunningOperationAsync(cts.Token); // 取消任务 cts.Cancel(); try { await task; } catch (OperationCanceledException) { Console.WriteLine("Operation was canceled."); } } static async Task LongRunningOperationAsync(CancellationToken token) { for (int i = 0; i < 10; i++) { token.ThrowIfCancellationRequested(); Console.WriteLine($"Working... {i}"); await Task.Delay(1000); } } } // 输出: // Working... 0 // 操作被取消后,捕获异常并输出: // Operation was canceled.
63. 什么是Task Parallel Library
(TPL)?
答案:
-
**Task Parallel Library(TPL)**是.NET框架提供的一组用于简化并行和异步编程的库,位于
System.Threading.Tasks
命名空间。 -
主要特性:
- 任务(Task):抽象并行操作的单位,支持组合、等待和取消。
- 数据并行:通过
Parallel
类实现对集合的并行操作,如Parallel.For
,Parallel.ForEach
。 - PLINQ(Parallel LINQ):对LINQ查询进行并行化处理,提升查询性能。
- 任务调度:自动管理线程池,优化资源使用。
-
优势:
- 简化并行编程模型。
- 提高代码的可读性和可维护性。
- 利用多核处理器提高应用性能。
-
示例:
using System; using System.Threading.Tasks; class Program { static void Main(string[] args) { Task.Run(() => DoWork()); Parallel.For(0, 10, i => { Console.WriteLine($"Parallel task {i}"); }); Console.ReadLine(); } static void DoWork() { Console.WriteLine("Task is running..."); } } // 输出: // Task is running... // Parallel task 0 // Parallel task 1 // ... // Parallel task 9
64. 什么是async
和await
的配对使用模式?
答案:
-
配对使用模式:
- 方法使用
async
修饰,标记为异步方法。 - 在异步方法内部,使用
await
关键字等待一个返回Task
或Task<T>
的异步操作完成。 - 异步方法的调用方可以选择使用
await
等待其完成,或继续执行其他操作。
- 方法使用
-
关键点:
async
修饰符使方法能够使用await
,改变方法的编译方式,使其返回一个任务。await
关键字释放当前线程,等待任务完成后继续执行,确保异步操作的非阻塞性。
-
示例:
public async Task<int> CalculateSumAsync(int a, int b) { await Task.Delay(1000); // 模拟耗时操作 return a + b; } public async Task DisplaySumAsync() { int sum = await CalculateSumAsync(5, 10); Console.WriteLine($"Sum: {sum}"); } // 使用 await DisplaySumAsync(); // 输出: Sum: 15 (延迟1秒)
65. 什么是异步流(Asynchronous Streams)?
答案:
-
**异步流(Asynchronous Streams)**是C# 8.0引入的一种特性,允许异步地遍历数据序列,结合
async
和yield
实现异步的迭代器。 -
用途:
- 处理大规模或无限的数据流,节省内存和提高效率。
- 结合I/O操作,如从网络或文件异步读取数据。
-
关键字:
async
和await
。IAsyncEnumerable<T>
和IAsyncEnumerator<T>
接口。await foreach
语法。
-
示例:
public async IAsyncEnumerable<int> GetNumbersAsync() { for (int i = 0; i < 5; i++) { await Task.Delay(500); // 模拟异步操作 yield return i; } } public async Task DisplayNumbersAsync() { await foreach (var num in GetNumbersAsync()) { Console.WriteLine(num); } } // 使用 await DisplayNumbersAsync(); // 输出(每500ms输出一个数字): // 0 // 1 // 2 // 3 // 4
66. 什么是TPL中的Task.WhenAll
和Task.WhenAny
?
答案:
-
Task.WhenAll
:- 接收一组任务,返回一个在所有任务完成时完成的任务。
- 如果任何一个任务失败,则
WhenAll
任务也会失败。 - 通常用于等待多个任务同时完成。
-
Task.WhenAny
:- 接收一组任务,返回一个在任意一个任务完成时完成的任务。
- 允许在最先完成的任务时进行响应。
-
示例:
using System; using System.Threading.Tasks; class Program { static async Task Main(string[] args) { Task<int> task1 = Task.Run(() => { Task.Delay(1000).Wait(); return 1; }); Task<int> task2 = Task.Run(() => { Task.Delay(2000).Wait(); return 2; }); Task<int[]> allTasks = Task.WhenAll(task1, task2); int[] results = await allTasks; Console.WriteLine($"Results: {string.Join(", ", results)}"); // 输出: Results: 1, 2 Task firstTask = Task.WhenAny(task1, task2); await firstTask; Console.WriteLine("First task completed."); } }
67. 如何使用async
和await
处理并行任务?
答案:
可以通过启动多个异步任务,并使用await Task.WhenAll
等待所有任务并行完成,实现并行异步操作。
示例:
public async Task ParallelTasksAsync()
{
Task<int> task1 = Task.Run(() => {
Task.Delay(1000).Wait();
return 1;
});
Task<int> task2 = Task.Run(() => {
Task.Delay(1500).Wait();
return 2;
});
Task<int> task3 = Task.Run(() => {
Task.Delay(500).Wait();
return 3;
});
int[] results = await Task.WhenAll(task1, task2, task3);
Console.WriteLine($"Results: {string.Join(", ", results)}"); // 输出: Results: 1, 2, 3
}
// 使用
await ParallelTasksAsync();
68. 什么是ValueTask
,它与Task
有什么区别?
答案:
-
**
ValueTask
**是C# 7.0引入的一种新的异步返回类型,用于替代Task
,尤其在方法可能频繁完成同步而无需异步操作时,提供更高的性能。 -
区别:
- 性能:
ValueTask
避免了在频繁同步完成的情况创建大量Task
对象,减少堆分配和GC压力。 - 多次使用:
ValueTask
只能作为单次异步操作使用,不能被多次等待或转换为Task
。 - 语义:
ValueTask
表达异步操作可能是同步完成的,需谨慎使用。
- 性能:
-
使用场景:
- 高性能库或框架中用于优化异步方法返回类型。
- 方法可能在大多数情况下同步完成,偶尔异步。
-
示例:
public async ValueTask<int> GetValueAsync(bool returnSync) { if (returnSync) { return 42; // 同步完成 } else { await Task.Delay(1000); return 99; } } // 使用 int value1 = await GetValueAsync(true); // 快速返回 int value2 = await GetValueAsync(false); // 异步等待
69. 什么是防火墙(Firewall)?
答案:
**防火墙(Firewall)**通常指网络安全设备或软件,用于监控和控制进出网络的数据流。它基于预定义的安全规则,允许或阻止特定类型的网络流量,以保护内部网络免受未授权访问和网络攻击。
-
作用:
- 保护计算机和网络免受恶意攻击。
- 控制和过滤进出网络的数据流。
- 监控网络活动,检测异常行为。
-
类型:
- 网络防火墙:部署在网络边界,过滤进出网络的数据包。
- 主机型防火墙:安装在单个计算机上,控制该计算机的网络流量。
- 应用层防火墙:针对特定应用程序的流量进行过滤和监控。
-
特点:
- 基于规则的过滤(如IP地址、端口号、协议)。
- 支持状态检测,理解连接的上下文。
- 提供日志记录和报警功能。
注意:由于此问题与C#面试关联不大,可能属于误提或基础网络知识相关面试问题。
70. 什么是线程安全(Thread Safety)?
答案:
-
**线程安全(Thread Safety)**指代码或数据结构能够在多线程环境中安全地执行,不会引发数据竞态(Race Condition)或不一致。
-
实现线程安全的方法:
- 使用锁(Lock):通过
lock
关键字或Monitor
类,实现对共享资源的互斥访问。 - 使用并发集合:如
ConcurrentDictionary
,ConcurrentQueue
等,内置线程安全机制。 - 不可变对象:设计对象为不可变,避免并发修改。
- 使用原子操作:通过
Interlocked
类提供的原子操作方法,如Interlocked.Increment
。 - 避免共享状态:尽量减少或避免多个线程访问同一数据。
- 使用锁(Lock):通过
-
示例:
public class ThreadSafeCounter { private int count = 0; private object lockObj = new object(); public void Increment() { lock (lockObj) { count++; } } public int GetCount() { lock (lockObj) { return count; } } }
异常处理与调试
71. 如何在C#中进行调试?
答案:
-
使用集成开发环境(IDE):
- Visual Studio提供丰富的调试工具,如断点、步进执行、监视变量、调用堆栈等。
-
断点(Breakpoints):
- 在代码行左侧点击设置断点,使程序在执行到该行时暂停,便于检查状态。
-
步进执行:
- Step Into (F11):进入方法内部逐行执行。
- Step Over (F10):执行方法但不进入内部。
- Step Out (Shift+F11):从当前方法退出。
-
监视和即时窗口:
- 监视变量的值,通过“Watch”窗口或“Immediate”窗口查询和修改变量。
-
条件断点和日志断点:
- 设置断点满足特定条件时触发,或仅记录日志而不中断执行。
-
异常设置:
- 配置IDE在异常抛出时自动中断,便于捕捉未处理的异常。
-
远程调试:
- 在不同机器上运行的应用程序进行调试。
72. 如何捕获未处理的异常?
答案:
-
在控制台应用程序中:
- 使用
AppDomain.CurrentDomain.UnhandledException
事件捕获未处理的异常。
示例:
class Program { static void Main(string[] args) { AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler; throw new Exception("未处理的异常"); } static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e) { Console.WriteLine("捕获到未处理的异常: " + ((Exception)e.ExceptionObject).Message); } }
- 使用
-
在应用程序域中:
- 使用
Application.ThreadException
事件(适用于WinForms应用)。
- 使用
-
订阅TaskScheduler.UnobservedTaskException(适用于异步任务中的未观察异常)。
73. 什么是finally
块在异常处理中的作用?
答案:
-
finally
块包含在try-catch-finally
结构中,用于在try
块中执行完毕后,无论是否发生异常,都执行的清理代码。 -
作用:
- 确保资源被正确释放,如关闭文件、释放锁、销毁对象等。
- 保证必要的操作在异常后也能被执行,维持程序的稳定性。
-
示例:
try { // 可能引发异常的代码 } catch (Exception ex) { Console.WriteLine("捕获异常: " + ex.Message); } finally { // 清理操作,无论是否发生异常都会执行 Console.WriteLine("执行 finally 块"); }
74. 如何自定义异常类?
答案:
-
步骤:
- 创建一个继承自
Exception
或其派生类的类。 - 实现至少一个构造函数,通常包括无参构造函数、带消息参数的构造函数,以及支持序列化的构造函数(可选)。
- 创建一个继承自
-
示例:
[Serializable] public class InvalidInputException : Exception { public InvalidInputException() { } public InvalidInputException(string message) : base(message) { } public InvalidInputException(string message, Exception inner) : base(message, inner) { } protected InvalidInputException(SerializationInfo info, StreamingContext context) : base(info, context) { } } // 使用 public void ValidateInput(int input) { if (input < 0) throw new InvalidInputException("输入值不能为负数。"); }
75. 什么是try-finally
结构?
答案:
-
try-finally
结构是一种异常处理结构,包含try
代码块和finally
代码块。其中,finally
代码块在try
代码块执行完毕后,无论是否发生异常,都会执行。 -
作用:
- 确保必要的清理操作在异常发生时也能被执行。
- 比较适用于无需捕获异常但需要执行清理操作的场景。
-
示例:
try { // 执行操作,可能引发异常 Console.WriteLine("执行 try 块"); throw new Exception("错误"); } finally { // 执行清理操作 Console.WriteLine("执行 finally 块"); } // 输出: // 执行 try 块 // 执行 finally 块 // 抛出异常
设计模式
76. 什么是设计模式?C#中常用的设计模式有哪些?
答案:
-
**设计模式(Design Patterns)**是经验丰富的软件开发者在解决特定问题时总结出的通用解决方案,提供了系统化、可复用的设计方法。
-
常用的设计模式按其用途分类,主要包括:
-
创建型模式:
- 单例模式(Singleton)
- 工厂方法模式(Factory Method)
- 抽象工厂模式(Abstract Factory)
- 建造者模式(Builder)
- 原型模式(Prototype)
-
结构型模式:
- 适配器模式(Adapter)
- 装饰器模式(Decorator)
- 组合模式(Composite)
- 外观模式(Facade)
- 代理模式(Proxy)
- 桥接模式(Bridge)
- 享元模式(Flyweight)
-
行为型模式:
- 策略模式(Strategy)
- 观察者模式(Observer)
- 状态模式(State)
- 模板方法模式(Template Method)
- 职责链模式(Chain of Responsibility)
- 命令模式(Command)
- 迭代器模式(Iterator)
- 中介者模式(Mediator)
- 备忘录模式(Memento)
- 解释器模式(Interpreter)
- 访问者模式(Visitor)
-
-
应用场景:
- 提高代码的复用性和可维护性。
- 提供解决特定设计问题的标准方法。
- 增强系统的灵活性和扩展性。
77. 什么是单例模式(Singleton Pattern)?如何实现?
答案:
-
**单例模式(Singleton Pattern)**确保一个类只有一个实例,并提供一个全局访问点。
-
实现方式:
- 私有构造函数:防止外部创建实例。
- 静态字段:保存单例实例。
- 公共静态属性或方法:提供获取实例的途径。
- 线程安全:确保多线程环境下只有一个实例。
-
C#实现(线程安全的懒加载方式):
public sealed class Singleton { private static readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton()); public static Singleton Instance { get { return lazy.Value; } } private Singleton() { // 私有构造函数,防止外部实例化 } public void DoSomething() { Console.WriteLine("Singleton instance is doing something."); } } // 使用 Singleton.Instance.DoSomething();
说明:
Lazy<T>
提供延迟初始化,并确保线程安全。sealed
关键字防止类被继承,确保单例特性不被破坏。
78. 什么是工厂方法模式(Factory Method Pattern)?
答案:
-
**工厂方法模式(Factory Method Pattern)**属于创建型设计模式,通过定义一个创建对象的接口,让子类决定实例化哪一个类。使得类的实例化延迟到子类。
-
目的:
- 提高代码的可扩展性和灵活性。
- 解耦对象的创建与使用。
-
关键点:
- 抽象类或接口定义工厂方法。
- 具体工厂类实现工厂方法,创建具体产品实例。
-
示例:
// 产品接口 public interface IProduct { void Operation(); } // 具体产品A public class ConcreteProductA : IProduct { public void Operation() { Console.WriteLine("Operation of ConcreteProductA"); } } // 具体产品B public class ConcreteProductB : IProduct { public void Operation() { Console.WriteLine("Operation of ConcreteProductB"); } } // 工厂接口 public abstract class Creator { public abstract IProduct FactoryMethod(); } // 具体工厂A public class ConcreteCreatorA : Creator { public override IProduct FactoryMethod() { return new ConcreteProductA(); } } // 具体工厂B public class ConcreteCreatorB : Creator { public override IProduct FactoryMethod() { return new ConcreteProductB(); } } // 使用 class Program { static void Main(string[] args) { Creator creatorA = new ConcreteCreatorA(); IProduct productA = creatorA.FactoryMethod(); productA.Operation(); // 输出: Operation of ConcreteProductA Creator creatorB = new ConcreteCreatorB(); IProduct productB = creatorB.FactoryMethod(); productB.Operation(); // 输出: Operation of ConcreteProductB } }
79. 什么是观察者模式(Observer Pattern)?
答案:
-
**观察者模式(Observer Pattern)**属于行为型设计模式,定义了一种一对多的依赖关系,当一个对象(被观察者)状态发生变化时,所有依赖于它的对象(观察者)都会被自动通知和更新。
-
目的:
- 实现松散耦合,减少对象之间的依赖。
- 支持动态添加和移除观察者。
-
关键点:
- Subject(被观察者):维护一组观察者,并提供注册、注销、通知的方法。
- Observer(观察者):定义一个更新接口,供被观察者调用。
-
示例:
using System; using System.Collections.Generic; // 观察者接口 public interface IObserver { void Update(string message); } // 被观察者类 public class Subject { private List<IObserver> observers = new List<IObserver>(); public void Attach(IObserver observer) { observers.Add(observer); } public void Detach(IObserver observer) { observers.Remove(observer); } public void Notify(string message) { foreach (var observer in observers) { observer.Update(message); } } } // 具体观察者 public class ConcreteObserver : IObserver { private string name; public ConcreteObserver(string name) { this.name = name; } public void Update(string message) { Console.WriteLine($"{name} received message: {message}"); } } // 使用 class Program { static void Main(string[] args) { Subject subject = new Subject(); IObserver observer1 = new ConcreteObserver("Observer1"); IObserver observer2 = new ConcreteObserver("Observer2"); subject.Attach(observer1); subject.Attach(observer2); subject.Notify("Hello Observers!"); // 输出: // Observer1 received message: Hello Observers! // Observer2 received message: Hello Observers! } }
80. 什么是装饰器模式(Decorator Pattern)?
答案:
-
**装饰器模式(Decorator Pattern)**属于结构型设计模式,允许动态地向对象添加职责,提供比继承更灵活的扩展方式。
-
目的:
- 动态地组合对象的功能。
- 避免大量的子类,提升系统的灵活性。
-
关键点:
- 组件接口:定义具体组件和装饰器的共同行为。
- 具体组件:实现组件接口,表示被装饰的对象。
- 装饰器基类:包含一个组件接口的引用,转发行为。
- 具体装饰器:继承装饰器基类,添加额外功能。
-
示例:
// 组件接口 public interface INotifier { void Send(string message); } // 具体组件 public class EmailNotifier : INotifier { public void Send(string message) { Console.WriteLine($"Sending Email: {message}"); } } // 装饰器基类 public abstract class NotifierDecorator : INotifier { protected INotifier notifier; public NotifierDecorator(INotifier notifier) { this.notifier = notifier; } public virtual void Send(string message) { notifier.Send(message); } } // 具体装饰器A public class SMSNotifier : NotifierDecorator { public SMSNotifier(INotifier notifier) : base(notifier) { } public override void Send(string message) { base.Send(message); Console.WriteLine($"Sending SMS: {message}"); } } // 具体装饰器B public class PushNotifier : NotifierDecorator { public PushNotifier(INotifier notifier) : base(notifier) { } public override void Send(string message) { base.Send(message); Console.WriteLine($"Sending Push Notification: {message}"); } } // 使用 class Program { static void Main(string[] args) { INotifier notifier = new EmailNotifier(); notifier = new SMSNotifier(notifier); notifier = new PushNotifier(notifier); notifier.Send("Hello Decorators!"); // 输出: // Sending Email: Hello Decorators! // Sending SMS: Hello Decorators! // Sending Push Notification: Hello Decorators! } }
81. 什么是工厂模式(Factory Pattern)的优点和缺点?
答案:
-
优点:
- 解耦:客户端无需知道具体的创建类,只需依赖于抽象接口,减少类之间的耦合。
- 可扩展性:容易引入新的产品类,只需创建新的工厂方法或工厂类,无需修改现有代码。
- 单一职责:将对象的创建逻辑集中到工厂,符合单一职责原则。
-
缺点:
- 增加类的数量:需要为每个产品类创建对应的工厂,可能导致类数量增加,增加复杂性。
- 维护成本:随着产品种类的增多,工厂方法或工厂类的维护变得更加困难。
- 设计复杂性:对于简单的对象创建,使用工厂模式可能显得冗余。
-
适用场景:
- 需要创建的对象数量庞大且复杂,难以通过简单的构造函数完成。
- 客户端不依赖于具体的对象类,依赖于抽象接口。
- 需要在运行时动态决定创建哪种类型的对象。
82. 什么是单例模式的饿汉式(Eager Initialization)和懒汉式(Lazy Initialization)?
答案:
-
饿汉式(Eager Initialization):
- 在类加载时就创建单例实例,确保线程安全。
- 实现简单,但可能造成资源浪费,尤其在单例未被使用的情况下。
实现示例:
public sealed class Singleton { private static readonly Singleton instance = new Singleton(); public static Singleton Instance { get { return instance; } } private Singleton() { // 私有构造函数 } }
-
懒汉式(Lazy Initialization):
- 在首次需要实例时创建,延迟实例化,节约资源。
- 需保证线程安全,可以通过
Lazy<T>
或使用锁(lock
)实现。
使用
Lazy<T>
的实现示例:public sealed class Singleton { private static readonly Lazy<Singleton> lazy = new Lazy<Singleton>(() => new Singleton()); public static Singleton Instance { get { return lazy.Value; } } private Singleton() { // 私有构造函数 } }
83. 什么是策略模式(Strategy Pattern)?
答案:
-
**策略模式(Strategy Pattern)**属于行为型设计模式,定义了一系列算法,将每个算法封装起来,使它们可以互换。策略模式使得算法的变化独立于使用算法的客户端。
-
目的:
- 提高算法的可复用性和灵活性。
- 避免在类中使用大量的条件语句。
- 支持动态切换算法。
-
关键点:
- 策略接口:定义算法的统一接口。
- 具体策略类:实现策略接口的具体算法。
- 上下文类:持有策略接口的引用,通过上下文类执行策略。
-
示例:
// 策略接口 public interface ICompressionStrategy { void Compress(string fileName); } // 具体策略A public class ZipCompression : ICompressionStrategy { public void Compress(string fileName) { Console.WriteLine($"Compressing {fileName} using ZIP compression."); } } // 具体策略B public class RarCompression : ICompressionStrategy { public void Compress(string fileName) { Console.WriteLine($"Compressing {fileName} using RAR compression."); } } // 上下文类 public class CompressionContext { private ICompressionStrategy strategy; public void SetStrategy(ICompressionStrategy strategy) { this.strategy = strategy; } public void CreateArchive(string fileName) { if (strategy == null) { Console.WriteLine("Compression strategy not set."); return; } strategy.Compress(fileName); } } // 使用 class Program { static void Main(string[] args) { CompressionContext context = new CompressionContext(); context.SetStrategy(new ZipCompression()); context.CreateArchive("file1.txt"); // 输出: Compressing file1.txt using ZIP compression. context.SetStrategy(new RarCompression()); context.CreateArchive("file2.txt"); // 输出: Compressing file2.txt using RAR compression. } }
84. 解释一下观察者模式中的Subject
和Observer
角色。
答案:
-
Subject(被观察者):
- 是观察者模式中的核心实体,保持一组观察者的引用,允许观察者注册和注销。
- 负责在自身状态发生变化时,通知所有注册的观察者。
-
Observer(观察者):
- 是观察者模式中的依赖实体,实现特定的接口,以接收来自被观察者的通知。
- 通常包含一个更新方法,用于在被观察者状态变化时执行相应的操作。
-
关系:
- 一个Subject可以有多个Observer。
- Observers依赖于Subject,但Subject不依赖于具体的Observer,实现松散耦合。
-
示例:
// 已在观察者模式第15题中展示
85. 什么是装饰器模式与代理模式的区别?
答案:
-
装饰器模式(Decorator Pattern):
- 目的:动态地向对象添加额外的职责或功能,而无需修改原有类。
- 结构:基于组合,通过装饰器类包裹被装饰对象,实现功能的扩展。
- 应用场景:需要在运行时灵活地为对象添加或移除功能。
-
代理模式(Proxy Pattern):
- 目的:提供一个替代对象,控制对另一个对象的访问。
- 结构:代理类实现与目标对象相同的接口,通过代理控制对目标对象的访问。
- 应用场景:需要对目标对象的访问进行控制,如权限验证、懒加载、远程代理等。
-
区别:
- 意图不同:装饰器用于功能增强,代理用于控制访问。
- 关系:装饰器通常保留对被装饰对象的引用,而代理通常保持对目标对象的引用,执行访问控制。
-
示例:
- 装饰器:为图像对象添加滤镜效果。
- 代理:为远程服务提供本地代理,控制网络访问。
异步编程进阶
86. 什么是async void
,它的使用场景是什么?
答案:
-
**
async void
**是异步方法的一个返回类型,表示该方法不返回任务,异步操作无法被调用方等待或进行异常捕获。 -
使用场景:
- 主要用于事件处理器,因为事件处理程序需要返回
void
。
- 主要用于事件处理器,因为事件处理程序需要返回
-
注意事项:
- 避免在其他场景中使用
async void
,因其无法等待或捕获异常,容易导致未处理的异常和程序崩溃。 - Prefer using
Task
orTask<T>
作为异步方法的返回类型。
- 避免在其他场景中使用
-
示例:
public async void Button_Click(object sender, EventArgs e) { try { await SomeAsyncOperation(); } catch (Exception ex) { // 异常可以在这里捕获 Console.WriteLine(ex.Message); } }
87. 什么是同步上下文(SynchronizationContext)?
答案:
-
**同步上下文(SynchronizationContext)**是.NET中的一个抽象类,用于捕获和管理异步操作的上下文信息,如线程调度和回调执行的位置。
-
作用:
- 决定异步操作完成后回调执行的线程或上下文。
- 支持在特定的上下文(如UI线程)中执行回调,避免线程切换问题。
-
应用:
- 在GUI应用(如WPF、WinForms)中,确保UI更新在主线程执行。
- 在ASP.NET中,管理请求上下文,确保异步操作的正确执行。
-
示例:
// 在WPF应用中,UI线程有自己的同步上下文 public async void LoadData() { // 整个方法在UI线程执行 var data = await GetDataAsync(); // await后,继续在UI线程执行 // 更新UI控件 myLabel.Content = data; }
88. 如何取消一个异步任务?
答案:
-
使用
CancellationToken
:- 创建
CancellationTokenSource
,获取CancellationToken
。 - 将
CancellationToken
传递给异步方法或任务。 - 在需要取消时,调用
CancellationTokenSource.Cancel()
方法。 - 在异步方法中,定期检查
CancellationToken
是否请求取消,并适当响应。
- 创建
-
示例:
using System; using System.Threading; using System.Threading.Tasks; class Program { static async Task Main(string[] args) { CancellationTokenSource cts = new CancellationTokenSource(); Task task = LongRunningOperationAsync(cts.Token); // 等待一段时间后取消 await Task.Delay(2000); cts.Cancel(); try { await task; } catch (OperationCanceledException) { Console.WriteLine("任务已取消。"); } } static async Task LongRunningOperationAsync(CancellationToken token) { for (int i = 0; i < 10; i++) { token.ThrowIfCancellationRequested(); Console.WriteLine($"执行步骤 {i}"); await Task.Delay(1000); } } } // 输出(约2秒后): // 执行步骤 0 // 执行步骤 1 // 任务已取消。
89. 什么是任务取消(Task Cancellation)中的协作式取消?
答案:
-
**协作式取消(Cooperative Cancellation)**指任务自己检查是否有取消请求,并主动中止执行。任务必须遵循取消策略,通过检查
CancellationToken
并适时退出。 -
特点:
- 任务需要设计为可取消,频繁检查取消请求。
- 提高取消的响应性和优雅性,避免强制中断导致的不一致状态。
-
实现方法:
- 使用
CancellationToken
,在任务内部定期检查token.IsCancellationRequested
或调用token.ThrowIfCancellationRequested()
。
- 使用
-
示例:
public async Task DoWorkAsync(CancellationToken token) { for (int i = 0; i < 100; i++) { // 检查取消请求 if (token.IsCancellationRequested) { Console.WriteLine("任务取消中..."); break; } // 执行工作 Console.WriteLine($"工作步骤 {i}"); await Task.Delay(500, token); // 支持取消的延迟 } Console.WriteLine("任务完成。"); } // 使用 CancellationTokenSource cts = new CancellationTokenSource(); Task workTask = DoWorkAsync(cts.Token); // 取消任务 cts.CancelAfter(3000); // 3秒后取消 await workTask; // 输出: // 工作步骤 0 // 工作步骤 1 // 工作步骤 2 // 工作步骤 3 // 任务取消中... // 任务完成。
90. 什么是并行LINQ(PLINQ)?
答案:
-
**并行LINQ(PLINQ)**是LINQ的一个扩展,允许在多核处理器上并行执行查询操作,通过分段处理和线程分配提高查询性能。
-
特点:
- 利用多核优势,加速大规模数据处理。
- 支持查询操作符,如
Where
,Select
,OrderBy
等。 - 自动优化线程使用,无需手动管理线程。
-
使用方式:
- 将数据源转换为
AsParallel()
,启用并行查询。 - 可选择性地使用
WithDegreeOfParallelism
指定并行度。
- 将数据源转换为
-
示例:
using System; using System.Linq; class Program { static void Main(string[] args) { var numbers = Enumerable.Range(1, 1000000); // 并行LINQ查询 var evenNumbers = numbers.AsParallel() .Where(n => n % 2 == 0) .ToList(); Console.WriteLine($"找到 {evenNumbers.Count} 个偶数。"); } }
-
注意事项:
- 并非所有查询都适合并行执行,需考虑数据量、操作复杂性和开销。
- 并行查询可能导致结果顺序不同,除非使用
AsOrdered()
保证顺序。 - 处理副作用操作时需谨慎,避免数据竞态。
异常处理进阶
91. 什么是try-catch-finally
结构中的catch
参数?
答案:
-
catch
参数是表示被捕获异常的变量,可以用于访问异常的信息,如消息、堆栈跟踪等。 -
语法:
catch (Exception ex) { // 使用 ex 变量 Console.WriteLine(ex.Message); Console.WriteLine(ex.StackTrace); }
-
特点:
- 可以捕获特定类型的异常,通过定义为特定异常类的类型。
- 支持基于异常类型的多层次捕获,优先捕获更具体的异常类型。
-
示例:
try { int[] array = {1, 2, 3}; Console.WriteLine(array[5]); // 引发IndexOutOfRangeException } catch (IndexOutOfRangeException ex) { Console.WriteLine("捕获到索引越界异常: " + ex.Message); } catch (Exception ex) { Console.WriteLine("捕获到其他异常: " + ex.Message); }
92. 如何在C#中创建自定义的异常链?
答案:
-
**异常链(Exception Chaining)**是通过在抛出新异常时,将原始异常作为内部异常传递,从而保留异常的上下文信息。
-
实现方法:
- 在自定义异常类的构造函数中接受一个
Exception
类型的参数,传递给基类的构造函数。 - 使用
throw new Exception("message", ex);
语法抛出带有内部异常的异常对象。
- 在自定义异常类的构造函数中接受一个
-
示例:
public class DataProcessingException : Exception { public DataProcessingException() { } public DataProcessingException(string message) : base(message) { } public DataProcessingException(string message, Exception inner) : base(message, inner) { } } public void ProcessData() { try { // 可能引发异常的操作 int x = int.Parse("abc"); // 引发FormatException } catch (FormatException ex) { throw new DataProcessingException("数据格式错误。", ex); } } // 使用 try { ProcessData(); } catch (DataProcessingException ex) { Console.WriteLine(ex.Message); // 输出: 数据格式错误。 Console.WriteLine("内部异常: " + ex.InnerException.Message); // 输出: 内部异常: Input string was not in a correct format. }
93. 什么是Throw
关键字的用法?
答案:
-
throw
关键字用于在代码中主动引发异常,可以在try
块中抛出新的异常或在catch
块中重新抛出捕获的异常。 -
用法:
-
抛出新异常:
throw new ArgumentNullException(nameof(parameter), "参数不能为空。");
-
重新抛出捕获的异常:
catch (Exception ex) { // 处理部分逻辑 throw; // 重抛当前捕获的异常,保留堆栈信息 }
-
-
区别:
throw;
:在catch
块中重新抛出当前异常,保留原始堆栈信息。throw ex;
:在catch
块中抛出一个新的异常,丢失原始的堆栈信息。
-
注意事项:
- 应优先使用
throw;
重新抛出异常,以保留异常的完整上下文。 - 不应在非异常处理上下文中随意使用
throw
,以免造成程序崩溃。
- 应优先使用
94. 如何在C#中使用try-catch
嵌套?
答案:
-
**嵌套
try-catch
**指在一个try
块或catch
块内部,再次使用try-catch
结构,以处理不同层次或类型的异常。 -
应用场景:
- 处理复杂操作中不同部分可能引发的多种异常。
- 在特定异常处理逻辑中,需要进一步捕获和处理异常。
-
示例:
try { // 外层操作 try { // 内层操作,可能引发异常 int x = int.Parse("invalid"); } catch (FormatException ex) { Console.WriteLine("内层捕获: " + ex.Message); throw; // 重新抛出异常给外层处理 } } catch (Exception ex) { Console.WriteLine("外层捕获: " + ex.Message); } // 输出: // 内层捕获: Input string was not in a correct format. // 外层捕获: Input string was not in a correct format.
95. 什么是Exception
类的常用属性和方法?
答案:
-
常用属性:
- Message:描述异常的错误消息。
- StackTrace:包含调用堆栈信息,便于定位异常发生的位置。
- InnerException:如果异常是由另一个异常引起的,
InnerException
包含原始异常信息。 - Source:引发异常的程序集或应用程序的名称。
- HelpLink:指向有关异常的帮助文档的链接。
-
常用方法:
- ToString():返回包含异常类型、消息和堆栈跟踪的完整字符串表示。
- GetBaseException():返回发生异常的原始异常。
- GetType():获取异常的类型。
-
示例:
try { int x = 10 / 0; // 引发DivideByZeroException } catch (Exception ex) { Console.WriteLine("异常消息: " + ex.Message); Console.WriteLine("堆栈跟踪: " + ex.StackTrace); Console.WriteLine("内部异常: " + ex.InnerException); Console.WriteLine("异常类型: " + ex.GetType()); Console.WriteLine("完整信息: " + ex.ToString()); } // 输出: // 异常消息: Attempted to divide by zero. // 堆栈跟踪: at Program.Main(String[] args) in C:\Path\To\Program.cs:line XX // 内部异常: // 异常类型: System.DivideByZeroException // 完整信息: System.DivideByZeroException: Attempted to divide by zero. // at Program.Main(String[] args) in C:\Path\To\Program.cs:line XX
96. 解释一下try-catch
中的捕获顺序。
答案:
-
捕获顺序指多个
catch
块按顺序检查和捕获异常的过程。 -
规则:
- 从上到下:
catch
块按照从上到下的顺序进行匹配,首先匹配到符合的异常类型时,执行对应的catch
块。 - 从具体到通用:应先捕获更具体的异常类型,再捕获更通用的异常类型,避免通用异常块提前捕获所有异常,导致具体异常块无法执行。
- 唯一匹配:每个
catch
块只能捕获一次,且同一异常类型在同一try
块中不能重复捕获。
- 从上到下:
-
示例:
try { // 引发异常 int x = int.Parse("abc"); // FormatException } catch (FormatException ex) { Console.WriteLine("捕获到FormatException: " + ex.Message); } catch (Exception ex) { Console.WriteLine("捕获到Exception: " + ex.Message); }
输出:
捕获到FormatException: Input string was not in a correct format.
注意:
如果将catch (Exception ex)
放在前面,会导致FormatException
被通用的异常块捕获,FormatException
的专用异常块将无法执行,编译器会报错。
97. C#中如何使用finally
块保证资源释放?
答案:
-
资源释放:在
finally
块中编写释放资源的代码,确保资源在使用完毕后被正确释放,无论是否发生异常。 -
示例:
FileStream fs = null; try { fs = new FileStream("file.txt", FileMode.Open); // 读取文件内容 } catch (IOException ex) { Console.WriteLine("IO异常: " + ex.Message); } finally { if (fs != null) fs.Close(); // 确保文件流被关闭 }
-
使用
using
语句简化:using
语句是一种语法糖,自动在作用域结束时调用Dispose
方法,确保资源释放。
示例:
try { using (FileStream fs = new FileStream("file.txt", FileMode.Open)) { // 读取文件内容 } // 自动调用 fs.Dispose() } catch (IOException ex) { Console.WriteLine("IO异常: " + ex.Message); }
98. 什么是异常过滤器(Exception Filters)?C#如何实现?
答案:
-
**异常过滤器(Exception Filters)**是C# 6.0引入的一种机制,允许在
catch
块前添加条件,以决定是否捕获特定异常。它通过when
关键字实现。 -
作用:
- 提高异常处理的精确性,只在满足特定条件时捕获异常。
- 避免在
catch
块中嵌套if
语句,提升代码可读性。
-
语法:
try { // 可能引发异常的代码 } catch (Exception ex) when (ex.Message.Contains("specific")) { // 仅当异常消息包含 "specific" 时执行 Console.WriteLine("捕获到特定异常: " + ex.Message); } catch (Exception ex) { // 其他异常处理 Console.WriteLine("捕获到其他异常: " + ex.Message); }
-
示例:
try { int x = int.Parse("invalid"); // 引发FormatException } catch (FormatException ex) when (ex.Message.Contains("Input")) { Console.WriteLine("捕获到特定的FormatException: " + ex.Message); } catch (FormatException ex) { Console.WriteLine("捕获到其他FormatException: " + ex.Message); }
输出:
捕获到特定的FormatException: Input string was not in a correct format.
99. 如何在C#中实现自定义的异常处理逻辑?
答案:
在C#中实现自定义的异常处理逻辑主要涉及创建自定义异常类,并在适当的地方抛出和捕获这些异常。自定义异常允许开发者提供更具描述性的错误信息,增强代码的可读性和可维护性。以下是实现自定义异常处理逻辑的步骤及详细解析:
步骤一:创建自定义异常类
- 继承自
Exception
类:自定义异常类通常继承自System.Exception
,但也可以继承自更具体的异常类(如ApplicationException
)。 - 添加构造函数:至少实现以下三个构造函数:
- 无参构造函数。
- 接受异常消息的构造函数。
- 接受异常消息和内部异常的构造函数(用于异常链)。
- 实现序列化构造函数(可选):如果需要跨应用域传递异常,需实现序列化构造函数。
示例:
using System;
using System.Runtime.Serialization;
[Serializable]
public class InvalidAgeException : Exception
{
public InvalidAgeException()
{
}
public InvalidAgeException(string message)
: base(message)
{
}
public InvalidAgeException(string message, Exception inner)
: base(message, inner)
{
}
protected InvalidAgeException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
步骤二:在适当的位置抛出自定义异常
在业务逻辑中,当检测到特定的错误条件时,抛出自定义异常以提供更具语义化的错误信息。
示例:
public class Person
{
private int age;
public int Age
{
get { return age; }
set
{
if (value < 0 || value > 120)
{
throw new InvalidAgeException("年龄必须在0到120之间。");
}
age = value;
}
}
}
步骤三:捕获和处理自定义异常
在调用代码中,通过try-catch
结构捕获自定义异常,并进行相应的处理。
示例:
class Program
{
static void Main(string[] args)
{
Person person = new Person();
try
{
person.Age = -5; // 引发InvalidAgeException
}
catch (InvalidAgeException ex)
{
Console.WriteLine("捕获到自定义异常: " + ex.Message);
// 可以执行额外的处理,如记录日志、提示用户等
}
catch (Exception ex)
{
Console.WriteLine("捕获到其他异常: " + ex.Message);
}
}
// 输出:
// 捕获到自定义异常: 年龄必须在0到120之间。
}
高级应用:异常链(Exception Chaining)
通过在新异常中包含内部异常,可以保留异常的原始上下文,便利问题的追踪和调试。
示例:
public void ProcessData(string input)
{
try
{
int age = int.Parse(input); // 可能引发FormatException
// 其他处理逻辑
}
catch (FormatException ex)
{
throw new InvalidAgeException("输入的年龄格式不正确。", ex);
}
}
class Program
{
static void Main(string[] args)
{
try
{
ProcessData("invalid_number");
}
catch (InvalidAgeException ex)
{
Console.WriteLine("捕获到自定义异常: " + ex.Message);
Console.WriteLine("内部异常: " + ex.InnerException.Message);
}
}
// 输出:
// 捕获到自定义异常: 输入的年龄格式不正确。
// 内部异常: Input string was not in a correct format.
}
注意事项:
- 保持异常类的命名清晰:异常类应清晰表达错误的类型和原因,例如
InvalidAgeException
。 - 避免使用过多的自定义异常:仅在有利于提升代码可读性和可维护性的情况下使用自定义异常。
- 确保自定义异常具备序列化能力:如果异常需要跨应用域传递,应确保异常类可序列化。
100. 如何在C#中有效地记录和追踪异常信息?
答案:
有效的异常记录和追踪对于提高应用程序的稳定性和可维护性至关重要。以下是C#中实现有效异常记录和追踪的一些方法:
1. 使用日志框架
选择合适的日志框架(如NLog
、log4net
、Serilog
等),并根据项目需求进行配置。日志框架提供丰富的功能,如不同级别的日志记录、多目标输出、格式化等。
示例(使用Serilog):
using Serilog;
class Program
{
static void Main(string[] args)
{
// 配置Serilog
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File("logs\\app.log", rollingInterval: RollingInterval.Day)
.CreateLogger();
try
{
// 执行操作,可能引发异常
int x = int.Parse("invalid");
}
catch (Exception ex)
{
// 记录异常
Log.Error(ex, "发生了一个错误");
}
finally
{
// 关闭并刷新日志
Log.CloseAndFlush();
}
}
// 输出到控制台和文件:
// [Error] 发生了一个错误
// Exception details...
}
2. 使用异常过滤器
通过when
关键字,在catch
块中设置条件,记录特定异常的信息或采取特定的行动。
示例:
try
{
// 可能引发异常的代码
}
catch (Exception ex) when (ex is FormatException || ex is OverflowException)
{
Log.Error(ex, "数据格式错误");
}
3. 捕获堆栈跟踪信息
使用ex.StackTrace
属性,记录引发异常的具体位置和调用路径,帮助快速定位问题。
示例:
catch (Exception ex)
{
Log.Error($"异常信息: {ex.Message}, 堆栈跟踪: {ex.StackTrace}");
}
4. 使用全局异常处理
对于跨界面或跨应用域的应用(如WPF、WinForms、ASP.NET),使用全局异常处理机制,捕获未处理的异常,并记录日志。
ASP.NET Core 示例:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = 500;
context.Response.ContentType = "text/plain";
var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
if (exceptionHandlerPathFeature?.Error != null)
{
Log.Error(exceptionHandlerPathFeature.Error, "未处理的异常");
await context.Response.WriteAsync("服务器内部错误");
}
});
});
// 其他中间件
}
5. 定制异常信息
在自定义异常类中添加额外的信息,如错误代码、相关数据字段,有助于更细粒度的异常处理和日志记录。
示例:
[Serializable]
public class ValidationException : Exception
{
public int ErrorCode { get; set; }
public string FieldName { get; set; }
public ValidationException(string message, int errorCode, string fieldName)
: base(message)
{
ErrorCode = errorCode;
FieldName = fieldName;
}
// 其他构造函数
}
// 使用
try
{
// 可能引发验证异常的操作
throw new ValidationException("输入无效", 1001, "Age");
}
catch (ValidationException ex)
{
Log.Warn($"验证错误 - Field: {ex.FieldName}, Code: {ex.ErrorCode}, Message: {ex.Message}");
}
6. 监控和报警
集成监控工具(如ELK stack、Azure Application Insights、Sentry)来实时监控异常,设置报警机制,及时响应潜在的问题。
示例(使用Application Insights):
// 在Startup.cs中配置
public void ConfigureServices(IServiceCollection services)
{
services.AddApplicationInsightsTelemetry(Configuration["ApplicationInsights:InstrumentationKey"]);
}
// 在catch块中记录异常
catch (Exception ex)
{
TelemetryClient telemetry = new TelemetryClient();
telemetry.TrackException(ex);
}
总结
通过有效的异常记录和追踪,可以大幅提升应用程序的稳定性、可维护性和可调试性。结合合适的日志框架、全局异常处理机制和监控工具,开发者可以系统化地管理和响应异常,确保应用程序的健壮性和可靠性。
以上为100道C#高频经典面试题的解析答案,希望这些内容能帮助您更好地理解和掌握C#相关的知识,为面试做好充分的准备!
需要深入学习C#开发技术的同学可以订阅C#开发从入门到精通系列专栏学习
https://blog.csdn.net/martian665/category_8983778.html,
创作不易,希望大家能够关注、收藏和点赞支持一下哦。