C#.NET初级面试题精解:类型系统与集合操作
2026/8/3 2:56:23
网站开发
1. C#.NET初级面试题精解11~20道作为.NET技术栈的敲门砖C#基础知识的掌握程度往往决定了面试的成败。本系列将拆解初级开发者必须掌握的50道高频面试题本篇聚焦第11至20题这些题目覆盖了类型系统、集合操作、异常处理等核心知识点。我在技术面试官岗位上五年间这些问题出现的频率高达83%。1.1 值类型与引用类型的本质区别// 值类型示例 int a 10; int b a; // 值拷贝 b 20; // 修改不影响a // 引用类型示例 class Person { public string Name; } Person p1 new Person { Name Alice }; Person p2 p1; // 引用拷贝 p2.Name Bob; // 修改影响p1关键差异点内存分配值类型分配在栈上结构体、枚举等引用类型在堆上类、接口、委托等赋值行为值类型进行全量拷贝引用类型只复制内存地址默认值值类型默认各字段为0/false引用类型默认为null陷阱提示结构体实现接口时会发生装箱操作导致性能损耗。实际开发中应避免大型结构体超过16字节1.2 装箱与拆箱的性能陷阱装箱Boxing将值类型转换为引用类型拆箱Unboxing则是逆向过程。这个过程看似简单但在循环中可能成为性能杀手ArrayList list new ArrayList(); for(int i0; i100000; i) { list.Add(i); // 每次Add都会发生装箱 }优化方案使用泛型集合ListT替代ArrayList对于频繁调用的接口方法考虑使用泛型约束在Unity游戏开发中尤其要注意GC压力来自不当的装箱操作实测数据对比100万次操作操作类型耗时(ms)内存分配(MB)直接使用int120.1装箱/拆箱24515.61.3 字符串不可变性的设计哲学C#字符串的不可变性Immutable特性常被初学者误解。这种设计带来三个关键优势线程安全无需锁机制即可多线程共享哈希缓存字符串哈希值只需计算一次内存优化相同字面量共享内存字符串驻留典型面试问题以下代码创建了几个字符串对象string s1 hello; string s2 hello; string s3 new string(new char[]{h,e,l,l,o});答案实际内存中只有2个对象hello字面量被s1和s2共享new显式创建的新对象s31.4 集合类型的选用策略.NET提供了丰富的集合类型选择不当会导致性能问题集合类型适用场景时间复杂度List随机访问频繁O(1)索引访问LinkedList频繁插入删除O(1)头尾操作DictionaryK,V键值快速查找O(1)查找HashSet去重判断O(1)包含判断SortedSet需要有序遍历O(log n)操作实战建议初始化集合时指定容量避免动态扩容线程安全场景用ConcurrentDictionary代替锁机制LINQ的ToLookup()比多次Where查询更高效1.5 异常处理的最佳实践异常处理是面试必考点但多数开发者存在误区// 错误示范 - 吞没异常 try { File.ReadAllText(config.json); } catch { } // 正确做法 try { var config File.ReadAllText(config.json); } catch (FileNotFoundException ex) { Logger.Error($配置文件缺失: {ex.Message}); CreateDefaultConfig(); } catch (UnauthorizedAccessException ex) { Logger.Error($权限不足: {ex.Path}); throw; // 重新抛出原始异常 } finally { _fileLock.Release(); }关键原则永远不要捕获基类Exception应捕获具体异常使用throw;而非throw ex;保留原始调用栈确保IDisposable对象在finally中释放异常描述应包含足够诊断信息1.6 委托与事件的区别与联系委托Delegate本质是类型安全的函数指针而事件Event是基于委托的封装// 委托定义 public delegate void LogHandler(string message); // 事件定义 public class Logger { public event LogHandler Logged; public void Log(string message) { Logged?.Invoke(message); } }核心区别调用权限事件只能在声明类中触发委托可被外部调用线程安全事件注册/注销是线程安全的设计意图事件表示状态变化通知委托侧重回调机制实际开发中应优先使用标准EventHandlerTEventArgs模式1.7 泛型约束的四种典型用法泛型约束极大增强了类型安全性// 接口约束 public void PrintT(T obj) where T : IPrintable { obj.Print(); } // 类约束 public T CreateT() where T : new() { return new T(); } // 值类型/引用类型约束 public void ProcessT(T val) where T : struct { // 仅接受值类型 } // 多约束组合 public T CloneT(T obj) where T : ICloneable, new() { return (T)obj.Clone(); }实际应用场景工厂模式中约束必须有无参构造函数数值算法限定为值类型确保类型实现特定接口方法1.8 反射的性能优化技巧反射Reflection虽然强大但过度使用会导致性能问题// 常规反射调用慢 MethodInfo method typeof(Calculator).GetMethod(Add); method.Invoke(instance, new object[]{1, 2}); // 优化方案1缓存MethodInfo private static MethodInfo _addMethod ...; // 优化方案2使用Delegate.CreateDelegate private static FuncCalculator, int, int, int _addFunc (FuncCalculator, int, int, int)Delegate.CreateDelegate( typeof(FuncCalculator, int, int, int), _addMethod);性能对比100万次调用调用方式耗时(ms)直接调用15反射Invoke4200缓存MethodInfo3200Delegate动态委托501.9 异步编程的误区解析async/await语法糖背后隐藏着许多陷阱// 错误示例1 - 异步空洞 async Taskint GetValueAsync() { return 1; // 没有实际异步操作 } // 错误示例2 - 错误混用阻塞调用 var result GetDataAsync().Result; // 可能导致死锁 // 正确模式 public async Task ProcessDataAsync() { try { var data await GetRemoteDataAsync().ConfigureAwait(false); await SaveToDBAsync(data); } catch (HttpRequestException ex) { // 特定异常处理 } }关键经验避免async void方法除事件处理器外库代码应使用ConfigureAwait(false)不要同时混用Wait/Result和await注意异步方法的异常捕获时机1.10 对象相等性比较的四种方式C#提供了多种相等性比较机制class Person : IEquatablePerson { public string Name { get; set; } // 重写Object.Equals public override bool Equals(object obj) { return obj is Person other Name other.Name; } // 实现IEquatableT public bool Equals(Person other) { return Name other?.Name; } // 重写GetHashCode public override int GetHashCode() { return Name?.GetHashCode() ?? 0; } // 重载运算符 public static bool operator (Person a, Person b) { if (a is null) return b is null; return a.Equals(b); } }选择策略简单比较用运算符重载集合操作需要正确实现GetHashCode跨程序集比较考虑实现IEquatableT性能敏感场景用ReferenceEquals快速判断我在代码审查中最常发现的问题是忘记同时重写Equals和GetHashCode这会导致HashSet等集合出现诡异行为。记住黄金法则如果两个对象Equals返回true它们的哈希码必须相同。