现代.NET开发不仅要求代码功能正确,更需追求智能、高效、内存可控且具备扩展性的实现。无论您是为了优化性能还是可维护性,.NET(尤其是最新版本的C#)提供了远超基础的工具。然而,许多开发者仍在使用2010年代初的集合和多线程编码方式。
以下五个技巧并非语法糖,而是真正体现工程成熟度的实践。正确使用它们,您的代码会立刻显得出自经验丰富的.NET开发者之手。
大多数开发者这样创建字典:
var lookup = new Dictionary<string, string>();
这对于小型集合没问题,但若需添加大量项,会触发多次内部扩容和重新哈希操作,这些操作开销大且可避免。
更优做法:预估项数量并作为容量参数传入:
var lookup = new Dictionary<string, string>(expectedItemCount);
这减少了内存分配和哈希表重建。若处理可预测负载(如配置键、预加载数据),此优化能提供更好的扩展性和更确定的性能。
无需为共享字典的线程安全访问重复造轮子。ConcurrentDictionary<TKey, TValue>
是处理并行访问的首选结构,内置锁和原子操作。
常见错误:
var dict = new Dictionary<string, int>();
await Task.WhenAll(inputs.Select(async item =>
{
// 多线程同时添加时可能抛出异常
dict[item] = await ComputeAsync(item);
}));
改进方案:
var dict = new ConcurrentDictionary<string, int>();
await Task.WhenAll(inputs.Select(async item =>
{
dict[item] = await ComputeAsync(item);
}));
这避免了竞态条件和不必要的锁,为并发读写提供了可扩展的路径,而Dictionary<TKey, TValue>
无法安全实现此功能。
无需在每次返回空值时分配新数组或列表(例如在循环或LINQ中每次迭代分配内存),.NET提供了预装箱的空单例。
低效做法:
return new string[0];
高效做法:
return Array.Empty<string>();
对于从LINQ风格API返回空集合的情况,同样适用Enumerable.Empty<T>()
。这些方法使用缓存的不可变实例,既减少分配又降低GC压力。
在查找密集的操作中,您可能为了快速检查备用键而构建字典:
旧方式:
var byId = items.ToDictionary(x => x.Id);
if (byId.TryGetValue(id, out var match))
{
return match;
}
C# 13新方式:
if (items.TryGetAlternateLookup(x => x.Id, id, out var match))
{
return match;
}
这完全避免了字典分配,无需物化单独集合即可执行查找,代码更简洁且内存效率更高,特别适用于热路径或解析场景。
使用List<T>
时,移除项后内存不会自动回收。许多开发者忽略的是:即使项数为零,List<T>
仍保留内部数组。或者,当List<T>
在循环迭代完成后项数固定,但在后续调用栈/生命周期中仍保留额外内存作为容量(Capacity)。
问题模式:
var list = new List<int>();
// ... 添加数千项
Console.WriteLine(list.Capacity);
// 仍持有内部数组的内存(无需调用Clear也会保留额外内存)
list.Clear();
解决方案:
list.TrimExcess();
或者,若构建已知大小的List<T>
:
var list = new List<string>(expectedSize);
这避免了列表增长时的内部扩容。两种技巧都是低投入高回报的改动,能显著提升生产环境中内存使用的可预测性。
此类细微调整常区分出“仅能运行”的代码与“可扩展”的代码。无论您是为高级面试做准备,还是重构大型代码库,这些现代C#技巧都体现了经验带来的打磨。频繁使用它们,您将不仅写出更好的代码,还能打造更智能、更精简、更易维护的软件。
这五个技巧并非隐藏功能,却常被忽视。一旦开始使用,您不仅会写出更优代码,还会更像系统工程师一样思考,您的软件也将因此脱颖而出。