在C#中,字符串拼接操作不当是常见的性能陷阱。由于字符串的不可变性,反复使用 + 或 += 操作符会导致大量临时字符串对象的创建和垃圾回收。对于需要大量拼接的场景,应优先使用 StringBuilder。
var sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
sb.Append("Hello ");
}
string result = sb.ToString();
这种方法通过一个可变的字符缓冲区进行操作,有效避免了不必要的内存分配。
LINQ(Language Integrated Query)极大地提升了代码的可读性和表达力,但在性能关键的代码路径中,其抽象层可能带来额外的开销。例如,numbers.Max() 方法实际上会遍历集合两次。在数据量极大时,一个简单的手动循环通常更高效。
int max = int.MinValue;
foreach (var num in numbers)
{
if (num > max) max = num;
}
当集合大小在初始化时就已经确定,使用数组比 List<T> 更有优势。列表的动态扩容机制会带来额外的内存分配和复制开销,而数组则没有这些负担。
int[] numbers = new int[1000];
为了处理数组或字符串切片等场景而不进行内存分配,可以使用 Span<T> 和 Memory<T>。它们提供了对连续内存区域的类型安全访问,是进行高性能处理的有力工具。
Span<int> span = new int[] { 1, 2, 3, 4 };
// 对span进行切片操作不会创建新数组
装箱(将值类型转换为 object)和拆箱会产生堆内存分配,应尽量避免。使用泛型集合(如 List<int>)替代非泛型集合(如 ArrayList)是避免此问题的有效方法。
对于可以并行执行的CPU密集型任务,Parallel.For 或 Parallel.ForEach 可以充分利用多核处理器的计算能力,显著缩短执行时间。
Parallel.For(0, 1000, i => ProcessItem(i));
在编写异步代码时,如果后续代码不需要回到原始的同步上下文(例如在UI线程中),应在 await 时使用 ConfigureAwait(false)。这可以减少不必要的上下文切换,提升性能并有助于避免死锁。
await SomeAsyncMethod().ConfigureAwait(false);
async void 方法应仅限于事件处理程序使用,因为它会“冒火”异常(难以捕获),并且无法被等待。标准的异步方法应返回 Task 或 Task<T>。
async Task DoWorkAsync() { }
对于需要频繁根据键查找值的场景,Dictionary<TKey, TValue> 提供了接近 O(1) 的查找时间复杂度,远优于在列表中进行线性搜索(O(n))。
var dict = new Dictionary<int, string>();
dict[1] = "First";
string value = dict[1];
为了确保结构体的不可变性并帮助编译器进行优化,可以将其声明为 readonly。这能避免在方法调用时产生防御性拷贝。
readonly struct Point { public int X { get; } public int Y { get; } }
异常处理的机制决定了其开销远大于普通的流程控制。不应使用抛出和捕获异常的方式来处理正常的业务逻辑分支。例如,检查字典中是否存在某个键,应使用 TryGetValue。
// 推荐做法
if (dict.TryGetValue(key, out int value))
{
// 使用 value
}
对于需要长时间运行的CPU密集型计算,为了避免阻塞调用线程(如UI线程),可以使用 Task.Run 将其转移到线程池线程中执行。
对于频繁进行数据库调用的应用,启用连接池至关重要。它通过重用已建立的数据库连接,避免了频繁创建和销毁连接带来的巨大开销。通常只需在连接字符串中设置 Pooling=true(默认即为true)即可。
对于小的、不可变的数据模型,使用结构体(struct)可以减少堆内存分配和垃圾回收的压力。
对于方法内临时使用的小型数组,可以使用 stackalloc 关键字在栈上分配内存。这完全避免了堆分配和后续的GC,但需注意栈空间有限,仅适用于非常小的数组。
Span<int> numbers = stackalloc int[10];
对于执行时间短的后台任务,应优先使用线程池(ThreadPool)而非手动创建新线程(new Thread),因为线程池能高效地管理线程的生命周期,减少线程创建和销毁的开销。
处理大量大型对象(大于85KB)的应用可能会产生大对象堆(LOH)碎片。在特定时机(如应用启动后、或处理完一批大型对象后),可以主动请求压缩LOH以减少内存占用。
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(); // 触发一次GC以执行压缩
对于初始化成本高昂的对象,可以使用 Lazy<T> 来延迟其创建,直到第一次被实际访问。这有助于加快应用的启动速度。
private static readonly Lazy<MyExpensiveService> _service = new Lazy<MyExpensiveService>(() => new MyExpensiveService());
public static MyExpensiveService Instance => _service.Value;
当需要异步地生成或消费一个数据序列时(例如分页查询数据库),IAsyncEnumerable<T> 提供了流式处理的能力,无需等待所有数据都加载到内存中,提升了响应性和内存效率。
async IAsyncEnumerable<int> GetNumbersAsync()
{
for (int i = 0; i < 100; i++)
{
await Task.Delay(10); // 模拟异步操作
yield return i;
}
}
最后,也是最重要的原则:先分析,后优化。在投入时间进行代码级优化之前,务必使用性能剖析工具(如 .NET 内置的 dotnet-counters、dotnet-trace,或 JetBrains 的 dotTrace、微软的 PerfView 等)准确定位真正的性能瓶颈。盲目优化往往事倍功半。
高效的 .NET 代码并不总是依赖于使用最新、最炫的特性,更多是在日常开发中,基于对云原生环境下应用特点的理解,做出的一系列明智且务实的选择。将这些技巧融入你的编码习惯,能有效提升应用的性能与可维护性。