C#面试核心知识点详解

  • ~7.24K 字
  1. 1. 前言
  2. 2. 值类型与引用类型
  3. 3. 接口和抽象类
  4. 4. 泛型
  5. 5. 委托与事件
  6. 6. 匿名方法与 Lambda 表达式

前言

深入浅出讲解C#面试中的核心知识点,包括值类型与引用类型、泛型、委托事件、异步编程、LINQ等重要概念

值类型与引用类型

  • 值类型:直接存储数据本身,如int、bool、struct、enum等,存储在栈(Stack)上(或作为引用类型的字段时存储在堆上)。

  • 引用类型:储存数据的引用(内存地址),实际数据在堆(Heap)上,如string、class、array、delegate等。

  • 区别:

    • 赋值行为:值类型赋值时赋值数据本身;引用类型赋值时赋值时复制引用(指向同一堆内存)。
    • 内存管理:值类型超出作用域自动释放;引用类型由GC(垃圾回收器)管理生命周期。
  • 代码举例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 值类型示例
    int a = 10;
    int b = a; // 复制值,b独立于a
    b = 20;
    Console.WriteLine(a); // 输出10(a不受影响)

    // 引用类型示例
    class Person { public int Age; }
    Person p1 = new Person { Age = 20 };
    Person p2 = p1; // 复制引用,p1和p2指向同一对象
    p2.Age = 30;
    Console.WriteLine(p1.Age); // 输出30(p1受影响)
  • 方法参数传递,值类型和引用类型的传递方式区别

    • 值类型:方法内修改不会影响外部变量。
    • 引用类型:默认按引用类型传递的引用地址,方法内部修改对象内容会影响外部(指向同一对象)
  • 装箱(Boxing)和拆箱(Unboxing)

    • 装箱:将值类型转换为引用类型(object或接口)的过程

      • 过程:在堆上分配内存 → 复制值类型数据到堆 → 返回堆对象的引用
    • 拆箱:将引用类型(装箱后的对象)转换回值类型的过程

      • 过程:检查对象类型是否匹配 → 从堆中提取值 → 复制到栈上
    • 性能影响

      • 装箱和拆箱都涉及内存分配和复制操作,开销较大
      • 频繁装箱会增加GC压力(堆上产生大量临时对象)
      • 应尽量避免:使用泛型集合(如List<T>)代替非泛型集合(如ArrayList)
    • 代码示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      // 装箱示例
      int num = 123;
      object obj = num; // 装箱:int → object,在堆上创建对象

      // 拆箱示例
      int value = (int)obj; // 拆箱:object → int,需显式类型转换

      // 性能对比:避免装箱
      // 不推荐:非泛型集合会导致装箱
      ArrayList list1 = new ArrayList();
      list1.Add(1); // 装箱
      int x = (int)list1[0]; // 拆箱

      // 推荐:泛型集合避免装箱
      List<int> list2 = new List<int>();
      list2.Add(1); // 无装箱
      int y = list2[0]; // 无拆箱
    • 常见装箱场景

      • 值类型赋值给object或接口变量
      • 值类型作为参数传递给接受object的方法(如Console.WriteLine(123)
      • 值类型存入非泛型集合(ArrayList、Hashtable等)

接口和抽象类

  • 接口:仅定义方法、属性、事件或索引器的签名(无实现)。Interface定义,成员默认public。

  • 抽象类:可包含抽象成员和具体成员的类,不能实例化。abstract定义,成员可指定访问修饰符。

  • 接口和抽象类区别

    维度 接口 抽象类
    继承方式 支持多继承(一个类可实现多个接口) 单继承(一个类只能继承一个抽象类)
    成员实现 所有成员无实现(纯契约) 可混合抽象成员(无实现)和具体成员
    访问修饰符 成员默认public,不可修改 成员可指定public/protected等
    构造函数 无构造函数 有构造函数(供派生类调用)
    字段 不能包含字段(仅属性/方法等) 可以包含字段
    版本兼容 新增成员会导致所有实现类必须修改 可新增具体成员,不影响派生类
  • 什么时候用接口?什么时候用抽象类?

    • 优先用接口场景:
      1. 需要继承多能力时
      2. 定义跨多个不相关类的通用功能(如IDisposable接口)
      3. 纯契约设计,不提供默认实现(如服务接口)
    • 优先用抽象类场景:
      1. 多个相关类共享部分实现逻辑(提取公共代码)
      2. 需要定义字段或构造函数时
      3. 希望控制继承层次(单继承体系),且可能后续扩展具体方法
  • 接口和抽象类在依赖注入中的应用场景

    • 接口是依赖注入的核心,用于定义服务契约,实现依赖倒置(高层模块依赖抽象)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      // 接口定义服务
      public interface IUserService { void AddUser(); }

      // 具体实现
      public class UserService : IUserService { ... }

      // 注入时依赖接口,而非具体类
      public class UserController
      {
      private readonly IUserService _userService;
      public UserController(IUserService userService) // 依赖注入
      {
      _userService = userService;
      }
      }
    • 抽象类较少直接用于依赖注入,但若多个服务共享基础逻辑,可作为基类

泛型

  • 什么是泛型?为什么使用泛型?

    • 定义:泛型是.NET 2.0 引入的特性,允许在定义类、接口、方法、委托时使用未指定的 “类型参数”,在使用时再指定具体类型(如 List<T> 中的 T 就是类型参数)
    • 作用
      • 类型安全:避免运行时类型转换错误(如非泛型 ArrayList 存储 object,需强制转换,可能抛 InvalidCastException)
      • 性能优化:减少装箱 / 拆箱操作(值类型存入非泛型集合时会装箱,取出时拆箱,泛型直接操作具体类型)。
      • 代码复用:用一套逻辑支持多种类型(如 List<int>List<string> 共享 List<T> 的实现)。
  • 泛型类型参数的约束(Constraints)有哪些?如何使用?
    约束用于限制类型参数的范围,确保其满足特定条件(如必须是引用类型、必须有默认构造函数等)。常见约束:

    • where T : class:T 必须是引用类型(类、接口、委托等)。
    • where T : struct:T 必须是值类型(int、struct 等,不包括 Nullable<T>)。
    • where T : new():T 必须有公共无参构造函数(需放在其他约束最后)。
    • where T : 基类名:T 必须是指定基类或其派生类。
    • where T : 接口名:T 必须实现指定接口。
    • where T : U:T 必须是 U 的派生类型(泛型参数间的约束)。

    示例:

    1
    2
    3
    4
    5
    6
    7
    public class MyClass<T> where T : MyBase, new() 
    {
    public T CreateInstance()
    {
    return new T(); // 因有 new() 约束,可实例化
    }
    }
  • 泛型方法与泛型类的区别?

    • 泛型类:在类级别声明类型参数,类的所有成员(方法、属性等)可使用该参数(如 List<T>)。
    • 泛型方法:在方法级别声明类型参数,仅该方法可用,可独立于类的泛型参数(即使类非泛型,也可定义泛型方法)。
  • 泛型类型在运行时的表现(泛型类型的实例化)?

    • 值类型:对不同值类型的泛型实例(如 List<int>List<double>),CLR 会生成不同的原生代码(因值类型大小不同)。
    • 引用类型:对不同引用类型的泛型实例(如 List<string>List<object>),CLR共享一套原生代码(因引用类型在内存中都是指针,大小相同)。

委托与事件

  • 委托(Delegate):委托是一种类型安全的函数指针,可以引用一个或多个具有相同签名的方法

    • 定义:使用 delegate 关键字定义,指定返回类型和参数列表
    • 特点:
      • 类型安全:编译时检查方法签名是否匹配
      • 支持多播:一个委托可以引用多个方法(通过 += 添加,-= 移除)
      • 可以作为参数传递或作为返回值
    • 常见内置委托:
      • Action<T>:无返回值的委托(可带0-16个参数)
      • Func<T, TResult>:有返回值的委托(可带0-16个参数)
      • Predicate<T>:返回bool的委托(用于条件判断)
  • 事件(Event):事件是对委托的封装,用于实现发布-订阅模式

    • 定义:使用 event 关键字修饰委托
    • 特点:
      • 只能在类内部触发(通过 Invoke?.Invoke()
      • 外部只能订阅(+=)或取消订阅(-=),不能直接赋值或触发
      • 提供了更好的封装性,防止外部误触发事件
  • 委托与事件的区别

    • 委托是类型,事件是成员(事件是委托实例的包装器)
    • 委托可被外部赋值(=)和调用,事件只能被订阅/取消订阅
    • 事件通常用于类之间的松耦合通信(如UI事件、观察者模式)
  • 代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    // 委托示例
    public delegate void LogHandler(string message);

    public class Logger
    {
    public LogHandler OnLog; // 委托字段

    public void Log(string msg)
    {
    OnLog?.Invoke(msg); // 外部可直接赋值和调用(不安全)
    }
    }

    // 事件示例
    public class Button
    {
    public event EventHandler Clicked; // 事件(基于EventHandler委托)

    public void Click()
    {
    Clicked?.Invoke(this, EventArgs.Empty); // 只能在类内部触发
    }
    }

    // 使用示例
    var button = new Button();
    button.Clicked += (sender, e) => Console.WriteLine("按钮被点击"); // 订阅事件
    // button.Clicked = null; // 编译错误!事件不能直接赋值
    button.Click(); // 输出:按钮被点击

    // 多播委托示例
    Action<string> notify = msg => Console.WriteLine($"通知1: {msg}");
    notify += msg => Console.WriteLine($"通知2: {msg}");
    notify("测试"); // 输出两行:通知1和通知2
  • 委托的应用场景

    • 回调函数:如 LINQ 的 Where(x => x > 10)、异步编程的回调
    • 事件处理:UI 框架中的按钮点击、数据变更通知等
    • 策略模式:动态选择算法(如排序策略)
    • 链式调用:如中间件管道(ASP.NET Core 的 Use 方法)

匿名方法与 Lambda 表达式

  • 匿名方法(Anonymous Method):C# 2.0 引入的特性,用于内联定义委托的实现(无需单独定义方法)

    • 语法:delegate (参数列表) { 方法体 }
    • 特点:
      • 可访问外部作用域的变量(形成闭包)
      • 可省略参数列表(如果委托不需要参数)
  • Lambda 表达式(Lambda Expression):C# 3.0 引入的更简洁的匿名方法语法

    • 语法:
      • 表达式 Lambda:(参数) => 表达式(单行返回值)
      • 语句 Lambda:(参数) => { 语句块 }(多行代码块)
    • 特点:
      • 更简洁:省略 delegate 关键字和返回类型
      • 支持表达式树:可转换为 Expression<Func<T>> 类型(用于 LINQ to SQL 等场景)
      • 广泛用于 LINQ、事件订阅、异步编程等
  • 匿名方法 vs Lambda 表达式

    维度 匿名方法 Lambda 表达式
    语法简洁性 冗长(需 delegate 关键字) 简洁(仅需 => 符号)
    表达式树 不支持 支持(可转换为 Expression<T>
    参数推断 需显式声明参数类型 可推断参数类型(如 x => x > 5
    适用场景 已被 Lambda 取代(兼容旧代码) 现代 C# 的首选方式
  • 代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 匿名方法示例
    List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
    var even1 = numbers.FindAll(delegate(int x) { return x % 2 == 0; });

    // Lambda 表达式示例(等价于上面的匿名方法)
    var even2 = numbers.FindAll(x => x % 2 == 0); // 表达式 Lambda

    // 语句 Lambda(多行)
    var even3 = numbers.FindAll(x =>
    {
    Console.WriteLine($"检查 {x}");
    return x % 2 == 0;
    });

    // 闭包示例:Lambda 捕获外部变量
    int threshold = 3;
    var greaterThanThreshold = numbers.Where(x => x > threshold).ToList();
    // threshold 被"捕获"到 Lambda 中,即使在方法外部修改 threshold,Lambda 内部的值也会同步

    // 表达式树示例(用于 LINQ to SQL)
    Expression<Func<int, bool>> expr = x => x > 10;
    // 可分析 expr 的结构:expr.Body、expr.Parameters 等(用于动态查询生成)
  • 闭包(Closure)的注意事项

    • Lambda 和匿名方法会捕获外部变量的引用(而非值)

    • 如果在循环中使用 Lambda,需注意变量捕获的时机:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      // 错误示例:循环变量被捕获
      var actions = new List<Action>();
      for (int i = 0; i < 3; i++)
      {
      actions.Add(() => Console.WriteLine(i)); // 捕获的是 i 的引用
      }
      actions.ForEach(a => a()); // 输出:3 3 3(循环结束后 i = 3)

      // 正确示例:使用局部变量
      var actions2 = new List<Action>();
      for (int i = 0; i < 3; i++)
      {
      int temp = i; // 每次循环创建新的局部变量
      actions2.Add(() => Console.WriteLine(temp));
      }
      actions2.ForEach(a => a()); // 输出:0 1 2
  • Lambda 表达式的应用场景

    • LINQ 查询:var result = list.Where(x => x > 10).Select(x => x * 2);
    • 事件订阅:button.Click += (s, e) => Console.WriteLine("点击");
    • 异步编程:await Task.Run(() => DoWork());
    • 函数式编程:高阶函数(接受或返回函数的函数)