二四、二五年找工作比较频繁,去过很多家公司面试,有的公司还会有笔试,之前是有记过笔记,但都是零零散散的,现整理一些的面试问题与回答,供大家参考。
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,不可修改 成员可指定访问修饰符 构造函数 无构造函数 有构造函数(供派生类调用) 字段 不能包含字段(仅属性/方法等) 可以包含字段 版本兼容 新增成员会导致所有实现类必须修改 可新增具体成员,不影响派生类 什么时候用接口?什么时候用抽象类?
- 优先用接口场景:
- 需要继承多能力时
- 定义跨多个不相关类的通用功能(如IDisposable接口)
- 纯契约设计,不提供默认实现(如服务接口)
- 优先用抽象类场景:
- 多个相关类共享部分实现逻辑(提取公共代码)
- 需要定义字段或构造函数时
- 希望控制继承层次(单继承体系),且可能后续扩展具体方法
- 优先用接口场景:
接口和抽象类在依赖注入中的应用场景
接口是依赖注入的核心,用于定义服务契约,实现依赖倒置(高层模块依赖抽象)
抽象类较少直接用于依赖注入,但若多个服务共享基础逻辑,可作为基类
关键字using有什么用
- 为命名空间创建别名,或导入在其他命名空间中定义的类
- 用于实现了 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>的实现)。
- 定义:泛型是.NET 2.0 引入的特性,允许在定义类、接口、方法、委托时使用未指定的 “类型参数”,在使用时再指定具体类型(如
泛型类型参数的约束(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
7public 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:
(参数) => { 语句块 }(多行代码块)
- 表达式 Lambda:
- 特点:
- 更简洁:省略
delegate关键字和返回类型 - 支持表达式树:可转换为
Expression<Func<T>>类型(用于 LINQ to SQL 等场景) - 广泛用于 LINQ、事件订阅、异步编程等
- 更简洁:省略
- 语法:
匿名方法 vs Lambda 表达式
维度 匿名方法 Lambda 表达式 语法简洁性 冗长(需 delegate关键字)简洁(仅需 =>符号)表达式树 不支持 支持(可转换为 Expression<T>)参数推断 需显式声明参数类型 可推断参数类型(如 x => x > 5)适用场景 已被 Lambda 取代(兼容旧代码) 现代 C# 的首选方式
1 | // 匿名方法示例 |
闭包(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()); - 函数式编程:高阶函数(接受或返回函数的函数)
- LINQ 查询: