C#面试问题

  • ~6.86K 字
  1. 1. csharp基础
    1. 1.1. 值类型与引用类型、装箱与拆箱
    2. 1.2. ref和out的区别
    3. 1.3. 接口和抽象类
    4. 1.4. 关键字using有什么用
    5. 1.5. IEnumerable 和 IQueryable 有什么区别
    6. 1.6. Stack 和 Heap
  2. 2. 泛型
  3. 3. 委托
    1. 3.1. 什么是委托?与事件有何不同?
    2. 3.2. 委托应用场景
  4. 4. 匿名方法与 Lambda 表达式

二四、二五年找工作比较频繁,去过很多家公司面试,有的公司还会有笔试,之前是有记过笔记,但都是零零散散的,现整理一些的面试问题与回答,供大家参考。

csharp基础

值类型与引用类型、装箱与拆箱

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

  • 引用类型:储存数据的引用(内存地址),实际数据在堆(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受影响)
  • 作为方法参数传递,值类型和引用类型的传递方式区别

    • 值类型:修改传递进来的参数不会改变外部变量本身,除非使用ref或者out标记参数
    • 引用类型:按引用类型传递的引用地址,方法内部修改对象内容会改变外部变量(指向同一对象)
  • 装箱和拆箱

    • 装箱:将值类型转换为引用类型的过程
    • 拆箱:将引用类型转换回值类型的过程

ref和out的区别

  • ref:调用前变量必须先初始化,方法内可以不改
  • out:调用前变量可以不初始化,方法执行结束前必须赋值

接口和抽象类

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

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

  • 接口和抽象类区别

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

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

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

    • 抽象类较少直接用于依赖注入,但若多个服务共享基础逻辑,可作为基类

关键字using有什么用

  1. 为命名空间创建别名,或导入在其他命名空间中定义的类
  2. 用于实现了 IDisposable 接口的对象,如Stream、SqlConnection、HttpClient

IEnumerable 和 IQueryable 有什么区别

  • IEnumerable
    • 将所有值加载到内存,再对数据过滤或排序
    • 大数据量下性能极差
  • IQueryable
    • 构建一个延后的表达式树,直到枚举时才执行,只查需要的数据
    • 使用表达式树解析为 SQL
    • 适用于大数据量、数据库

Stack 和 Heap

  • Stack
    • 存储值类型、引用类型变量的地址
    • 速度快,简单的移动指针
    • 线程独有
    • 空间小
    • 系统自动管理内存
  • Heap
    • 存储引用类型的真实数据
    • 速度慢,需要内存分配、垃圾回收
    • 所有线程共享
    • 空间大,受限于物理内存
    • 由 GC 自动清理不再使用的对象

泛型

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

    • 定义:泛型是.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()
      • 外部只能订阅(+=)或取消订阅(-=),不能直接赋值或触发
      • 提供了更好的封装性,防止外部误触发事件
  • 委托与事件的区别

    • 委托是类型(任何有访问权限/可见的类都可以调用),事件是成员(只能由定义它们的类提出,确保封装),只能类内部触发
    • 委托可被外部赋值(=)和调用,事件只能被订阅/取消订阅
    • 委托通常用于回调、函数参数,事件通常用于类之间的松耦合通信(如消息通知、观察者模式)
    代码举例
    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 框架中的按钮点击、数据变更通知等
  • 策略模式:动态选择算法(如排序策略)
  • 多播任务:一次触发多个方法

匿名方法与 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());
    • 函数式编程:高阶函数(接受或返回函数的函数)