document/面试题 C# .md
2025-05-18 15:14:53 +08:00

14 KiB
Raw Blame History

  1. C# 中值类型和引用类型的区别是什么? 答案:

值类型:存储数据本身,分配在栈上,通常具有较高的性能。常见的值类型有 int、float、char、struct。 引用类型:存储对数据的引用,分配在堆上。常见的引用类型有 string、class、array、delegate。 区别:

值类型变量直接存储数据,而引用类型变量存储的是指向数据的引用。 值类型赋值会复制数据,引用类型赋值会复制引用。 在堆上创建的对象的生命周期由垃圾回收器管理,而栈上的数据会在方法执行完毕后自动销毁。 2. C# 中的 static 关键字有什么作用? 答案:

静态成员static 修饰符用于声明类的静态成员(字段、方法、属性等)。静态成员属于类本身,而不是类的实例。静态成员在程序启动时初始化,并且在整个应用程序生命周期内共享。 静态类static 修饰符也可用于声明静态类。静态类只能包含静态成员,并且不能实例化。 示例:

public static class MyStaticClass
{
    public static int Counter = 0;
    
    public static void IncrementCounter()
    {
        Counter++;
    }
}


  1. C# 中的 async 和 await 是如何工作的? 答案:

async用于修饰方法表示该方法是异步的。异步方法可以包含 await 关键字。 await用于等待一个异步操作的完成。await 会暂停方法的执行,直到异步任务完成,之后恢复执行。 async 和 await 使得异步编程变得更加简洁和易读。在执行异步操作时,线程不会被阻塞,从而提高了应用的响应性。

示例:

 
public async Task<int> CalculateSumAsync(int a, int b)
{
    await Task.Delay(1000);  // 模拟异步操作
    return a + b;
}
  1. C# 中 ref 和 out 的区别是什么? 答案:

ref参数在传递之前必须已经初始化方法内的修改会影响到方法外的值。 out参数无需初始化方法内部必须给 out 参数赋值,方法外的值将被修改。 示例:

 
public void RefMethod(ref int x)
{
    x = x + 1; // 需要初始化
}

public void OutMethod(out int x)
{
    x = 10; // 不需要初始化
}
  1. 什么是委托Delegate 答案: 委托是对方法的引用,允许将方法作为参数传递。委托可以用来实现事件机制或回调函数。委托类似于函数指针,但它是类型安全的。

示例:

 
public delegate void MyDelegate(string message);

public void DisplayMessage(string message)
{
    Console.WriteLine(message);
}

MyDelegate del = new MyDelegate(DisplayMessage);
del("Hello, world!");
  1. 解释 C# 中的事件Event和委托Delegate的关系。 答案:

委托:委托是一个引用类型,用来定义方法的签名,可以指向一个或多个方法。 事件:事件基于委托,用于通知订阅者某些事情已经发生。事件是一种封装机制,通常用于实现发布/订阅模式。 事件的主要作用是防止直接调用委托,而是通过事件的触发机制来通知订阅者。

示例:

 
public delegate void Notify();  // 定义委托

public class Publisher
{
    public event Notify OnNotify;  // 定义事件
    
    public void TriggerEvent()
    {
        OnNotify?.Invoke();  // 触发事件
    }
}
  1. C# 中的 LINQ 是什么? 答案: LINQLanguage Integrated Query是一种用于查询集合数据的强大工具它使得你可以用一种声明性的方法在内存中处理数据如数组、列表、数据库等。LINQ 提供了多种操作符,如 Where、Select、GroupBy、OrderBy 等,用来处理和筛选数据。

示例:

 
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 num in evenNumbers)
{
    Console.WriteLine(num);
}
  1. C# 中的垃圾回收机制Garbage Collection是如何工作的 答案: C# 使用自动垃圾回收GC机制来管理内存。GC 负责回收不再使用的对象释放它们占用的内存空间。GC 会定期检查堆上的对象并回收那些不再被引用的对象。C# 中的对象在堆上创建,并且它们的生命周期由垃圾回收器控制。

Generational GCC# 使用分代垃圾回收策略,分为 3 代0、1、2 代)。年轻代的对象容易被回收,而老年代的对象通常会存活更长时间。 Dispose 和 Finalize实现 IDisposable 接口可以让对象在垃圾回收之前释放资源Finalize 用于定义清理资源的逻辑。 9. C# 中的 using 语句的作用是什么? 答案: using 语句用于确保实现了 IDisposable 接口的对象在使用完毕后正确释放资源。using 语句会在代码块结束时自动调用对象的 Dispose 方法。

 
using (var stream = new FileStream("file.txt", FileMode.Open))
{
    // 使用 stream 进行操作
}  // stream.Dispose() 会自动被调用
  1. C# 中的扩展方法Extension Method是什么 答案: 扩展方法是 C# 中的一种特殊方法,它允许向现有类型添加新功能,而无需修改原始类型的代码。扩展方法通常定义为静态方法,并且第一个参数是 this 修饰符,指示它是扩展哪个类型。
 
public static class StringExtensions
{
    public static bool IsCapitalized(this string str)
    {
        return char.IsUpper(str[0]);
    }
}

class Program
{
    static void Main()
    {
        string text = "Hello";
        Console.WriteLine(text.IsCapitalized());  // 使用扩展方法
    }
}

垃圾回收机制Garbage Collection是如何工作的

在 C# 中垃圾回收机制Garbage Collection简称 GC负责自动管理内存确保不再使用的对象能够被回收以便释放内存并避免内存泄漏。垃圾回收是 .NET 平台的核心特性之一,它简化了内存管理,开发人员不需要手动释放内存。然而,理解其工作原理对于优化应用程序的性能和减少内存使用非常重要。

  1. 垃圾回收的基本概念 垃圾回收机制的主要目的是释放不再使用的内存以便为新的对象腾出空间。GC 在运行时自动跟踪和管理托管堆中的对象,确保已经不再引用的对象能够被销毁,释放其占用的内存。

托管堆Managed Heap 是垃圾回收管理的内存区域,所有通过 new 关键字创建的对象(如类实例)都存储在托管堆上。托管堆是 .NET 平台自动管理的内存区域,垃圾回收器会负责清理这些对象。

  1. 垃圾回收的工作流程 垃圾回收的过程是由 .NET 运行时CLRCommon Language Runtime自动执行的通常包括以下步骤

2.1 标记Mark
垃圾回收器会首先遍历所有的活动仍然被引用的对象并标记它们为存活对象。这些存活对象通过根GC Roots进行引用根对象包括

局部变量和方法参数
静态字段
当前线程的栈
其他存活对象引用的对象
通过从这些根对象开始,垃圾回收器会遍历所有直接或间接引用的对象,并标记它们为“活跃”的。

2.2 清除Sweep
在标记阶段完成后,垃圾回收器会遍历堆中的所有对象,检查哪些对象没有被标记为存活对象。对于这些未被标记的对象,垃圾回收器会将它们标记为“死对象”并准备删除。

2.3 压缩Compact
清除不再使用的对象后,垃圾回收器将释放的内存位置合并并压缩堆。这个过程包括将存活对象重新排列,使得堆中的内存空间不再被碎片化。压缩过程有时会导致对象的地址发生变化,但这是由垃圾回收器自动处理的。

  1. 垃圾回收的代Generations
    垃圾回收器将堆内存分为多个代Generation目的是提高性能并减少回收的频率。GC 使用代的概念来优化回收过程,根据对象的生命周期将对象分配到不同的代。常见的有三个代:

3.1 第 0 代Generation 0
新创建的对象:大多数新对象都会被分配到第 0 代。
垃圾回收频繁:第 0 代的对象生命周期较短,因此垃圾回收器会频繁回收这一代的对象。
性能优化:垃圾回收器通过频繁清理第 0 代中的对象来减少不必要的内存占用。
3.2 第 1 代Generation 1
存活对象:如果一个对象在第 0 代的回收过程中仍然存活,它会被提升到第 1 代。
回收频率较低:相对于第 0 代,第 1 代的垃圾回收会更少发生,因为这些对象通常存活时间较长。
3.3 第 2 代Generation 2
长期存活的对象:第 2 代包含存活时间较长的对象,例如缓存对象、连接池对象等。通常较少发生垃圾回收。
回收频率最低:当第 2 代发生垃圾回收时,意味着系统正在经历较大的内存压力。
3.4 代间晋升
当对象从一个代晋升到另一个代时它们的生命周期变得更长。GC 会尽可能将存活的对象提升到较高的代,以减少回收频率。
4. 垃圾回收的触发条件 垃圾回收的触发可以由多种因素触发:

4.1 内存压力 当系统内存不足时垃圾回收会被触发。GC 会尝试回收不再使用的对象,以释放内存。

4.2 显式调用 开发人员可以通过调用 GC.Collect() 显式触发垃圾回收。一般来说,不建议频繁调用此方法,除非你有特别的理由进行显式垃圾回收。

GC.Collect(); // 显式触发垃圾回收

4.3 堆满 当托管堆的内存达到阈值时,也会触发垃圾回收。堆的大小通常由 .NET 运行时的配置决定,但垃圾回收器会根据需要扩展堆的大小。

  1. 垃圾回收的优化 虽然垃圾回收机制大大简化了内存管理,但它仍然有可能对性能产生影响,尤其是在大量对象创建和销毁时。以下是一些优化建议:

5.1 减少短生命周期的对象 尽量避免创建生命周期短暂的对象,尤其是在高频率的循环中。这些对象将频繁地进入第 0 代垃圾回收,可能导致性能问题。

5.2 使用对象池Object Pool 对于那些高频次创建销毁的对象,可以使用对象池(如 ObjectPool来重复使用对象减少垃圾回收的频率。

5.3 避免大对象分配 大对象(通常大于 85 KB会直接分配到大对象堆LOHLarge Object Heap它不属于常规的代管理。大对象堆的回收较为复杂可能导致长时间的暂停。如果可能尽量避免频繁分配大对象。

5.4 减少内存碎片
当你处理大型数据集或长生命周期的对象时,要注意内存碎片的影响。适时压缩堆可以有效减少碎片。

  1. 垃圾回收的暂停时间GC Pauses
    在垃圾回收过程中CLR 会停止应用程序的执行,这通常称为 GC 停顿GC Pause。GC 停顿的时间取决于多种因素,包括堆的大小、代的数量以及是否发生了大对象堆的回收。

在较大的应用程序中,频繁的垃圾回收可能会导致性能瓶颈,尤其是在需要响应快速的实时应用中。为了减少这种停顿,.NET 提供了 后台垃圾回收Background GC它在后台线程中执行垃圾回收操作以减少应用程序停顿的时间。

  1. GC 类型的选择
    .NET 提供了两种主要的垃圾回收模式:
    7.1 工作站垃圾回收Workstation GC
    适用于客户端应用程序(如桌面应用程序)。
    在较小的系统上工作良好。
    启动时有较短的暂停时间,但吞吐量较低。
    7.2 服务器垃圾回收Server GC
    适用于服务器应用程序,能够充分利用多核处理器。
    增强了吞吐量和多核支持,适用于高并发、大规模应用。
    启动时有较长的暂停时间,但吞吐量更高。

  2. C# 定时框架 Quartz.NET快 s .net /kwɔːts ,使用Cron(克阮特 krɑn)表达式

  3. 线程池。概念 1种管理和复用线程的机制旨在减少线程创建和销毁的开销提高多线程应用程序的性能和资源利用率。 是1种高效的多线程管理机制通过复用线程、减少开销和优化资源利用提升了多线程应用程序的性能 核心1. 线程复用:线程池维护一组预先创建的线程,任务到来时从池中分配线程执行,任务完成后线程返回池中,而不是销毁。 1. 减少开销:创建和销毁线程是昂贵的操作,线程池通过复用线程减少了这些开销。 2. 资源控制:线程池限制了同时运行的线程数量,避免系统资源被过度占用。 线程池的工作原理 1.任务队列:当任务提交到线程池时,如果所有线程都在忙碌,任务会被放入队列中等待执行。 2.线程分配:线程池中的空闲线程会从队列中取出任务并执行。 3.线程回收:任务完成后,线程不会销毁,而是返回线程池等待下一个任务。

  4. 线程池的优点 性能提升:减少了线程创建和销毁的开销,提高了任务执行的效率。 资源管理:限制了并发线程的数量,避免系统资源耗尽。 简化编程:开发者无需手动管理线程的生命周期,只需提交任务即可。

  5. 线程池的缺点 任务排队延迟:如果任务过多,可能导致任务在队列中等待时间过长。 不适合长时间任务:线程池中的线程数量有限,长时间任务可能占用线程,影响其他任务的执行。 调试困难:由于线程是复用的,调试多线程问题可能更加复杂。

  6. .NET 中的线程池 在.NET中线程池通过ThreadPool类Task.Run将任务提交到线程池。

C# 依赖注入框架 Autofac