你的 C# 代码真的够快吗?90% 的开发都忽略的 10 个致命错误

304 阅读5分钟

前言

开发高性能的C#应用程序时,我们常常将注意力集中在算法优化和架构设计上,却忽略了那些看似微不足道的代码细节。

事实上,真正拖慢你应用速度的,往往不是复杂的业务逻辑,而是那些"隐形杀手"——一些细微且常见的C#错误。这些模式不仅会显著降低性能,增加云服务成本,还会造成意想不到的垃圾回收(GC)压力。

本文将揭示10个由微软官方文档证实的性能陷阱,帮助你打造更快、更高效的C#应用。

正文

错误1:看似无辜的.ToList()?它正在吞噬你的内存

// 之前(低效——强制立即枚举并分配内存)  
var activeUsers = GetUsers().Where(u => u.IsActive).ToList();  
if (activeUsers.Any())  
{  
    // 逻辑处理  
}  
// 更好的方式(延迟执行,避免双重枚举)  
var activeUsersQuery = GetUsers().Where(u => u.IsActive);  
if (activeUsersQuery.Any())  
{  
    // 逻辑处理  
}

注意:每个.ToList()都会分配内存并强制立即迭代。除非必须将数据具体化,否则请使用IEnumerable进行延迟处理。

错误2:在事件处理程序之外使用async void

// 之前(不好——无法等待或处理异常)  
public async void SaveDataAsync()  
{  
    await db.SaveChangesAsync();  
}  
// 更好的方式(异步方法始终返回Task)  
public async Task SaveDataAsync()  
{  
    await db.SaveChangesAsync();  
}

注意:async void会破坏错误处理机制,且无法被等待——可能导致无声崩溃。除非在UI事件处理程序中(根据微软指南),否则请使用async Task。

错误3:在频繁循环中对值类型进行装箱操作

// 不好的方式  
object sum = 0;  
for (int i = 0; i < 10000; i++) sum = (int)sum + i;  
// 好的方式  
int sum = 0;  
for (int i = 0; i < 10000; i++) sum += i;

注意:装箱操作会严重影响性能并增加GC压力。应保持值类型的值类型特性。

[Benchmark]  
public void BoxingTest() 
{  
    object sum = 0;  
    for (int i = 0; i < 10000; i++) sum = (int)sum + i;  
}

原因:即使是5毫秒与50毫秒的实际差异,其影响也不容忽视。

错误4:在循环中过度使用字符串拼接

// 之前(缓慢——重复进行堆分配)  
string result = "";  
foreach (var word in words)  
{  
    result += word + " ";  
}  
// 更好的方式(更快——最小化分配)  
var builder = new StringBuilder();  
foreach (var word in words)  
{  
    builder.Append(word).Append(' ');  
}  
string result = builder.ToString();  
// 最佳方式(零分配,使用Span<char>实现高性能)  
Span<char> buffer = stackalloc char[1024];  
var pos = 0;  

foreach (var word in words)  
{  
    word.AsSpan().CopyTo(buffer.Slice(pos));  
    pos += word.Length;  
    buffer[pos++] = ' ';  
}  

string result = new string(buffer.Slice(0, pos));

注意:字符串是不可变的;使用+=会产生大量分配。Span可避免堆压力和GC,非常适合快速、低延迟的循环。

错误5:在性能关键的API中忽略ValueTask

// 之前(效率较低)  
public async Task<int> GetCachedValueAsync()  
{  
    if (cache.HasValue)  
        return cache.Value;  

    return await ComputeValueAsync();  
}  
// 更好的方式  
public ValueTask<int> GetCachedValueAsync()  
{  
    if (cache.HasValue)  
        return new ValueTask<int>(cache.Value);  

    return new ValueTask<int>(ComputeValueAsync());  
}

注意:当结果已可用时,ValueTask可避免堆分配。非常适合“缓存优先”的异步流程。

错误6:对字符串和数组进行切片时不使用AsSpan()或AsMemory()

// 之前(低效——分配子字符串)  
string prefix = input.Substring(0, 5);  
// 更好的方式(零分配——不复制子字符串)  
ReadOnlySpan<char> prefix = input.AsSpan(0, 5);

注意:Substring()会分配内存,在循环中造成GC压力。Span在原地工作——零分配,在解析、文件处理和协议处理方面性能更佳。

错误7:不池化HttpClient或Regex等昂贵对象

// 之前(不好——导致套接字耗尽或每次都重新编译Regex)  
var client = new HttpClient();  
var match = new Regex(@"\d+").Match(input);  
// 更好的方式(重用昂贵对象——线程安全且经过优化)  
private static readonly HttpClient _httpClient = new HttpClient();  
private static readonly Regex _regex = new Regex(@"\d+", RegexOptions.Compiled | RegexOptions.CultureInvariant);

注意:重新创建HttpClient或Regex会浪费资源。池化可避免套接字问题和重新编译,提高性能和可靠性。

错误8:阻塞异步代码(例如使用.Result、.Wait())

// 之前(危险——导致死锁和线程池饥饿)  
var result = httpClient.GetAsync(url).Result;  
// 更好的方式(非阻塞,全程异步)  
var result = await httpClient.GetAsync(url);

注意:使用.Result或.Wait()进行阻塞可能导致死锁和线程饥饿。全程使用async/await可避免超时并确保可扩展性。

错误9:数据库或网络调用不使用批处理

// 之前(N+1查询或多个缓慢的网络调用)  
foreach (var id in orderIds)  
{  
    var order = await db.Orders.FindAsync(id);  
    results.Add(order);  
}  
// 更好的方式(批处理——一次往返而非多次)  
var orders = await db.Orders.Where(o => orderIds.Contains(o.Id)).ToListAsync();

注意:循环中重复的I/O会导致缓慢、频繁的操作。正如微软所建议的,批处理可减少往返次数,改善延迟并提高可扩展性。

错误10:“即发即弃”的异步调用没有安全保障

// 之前(即发即弃——异常会被吞噬)  
DoSomethingAsync();  
// 更好的方式(附加到带有异常处理的安全后台任务)  
_ = Task.Run(async () =>  
{  
    try  
    {  
        await DoSomethingAsync();  
    }  
    catch (Exception ex)  
    {  
        logger.LogError(ex, "后台任务失败。");  
    }  
});

注意:未等待的异步调用可能会无声失败或导致服务崩溃。正如微软所建议的,将它们包装在带有try/catch的Task.Run中,或使用BackgroundService以确保安全执行。

总结

通过识别和修复这10个常见的C#性能陷阱,可以显著提升应用程序的性能和可靠性。从避免不必要的内存分配,到正确使用异步编程模式,再到优化I/O操作,每一个小的改进都能带来可观的性能提升。

记住,高性能的应用程序不仅仅是关于算法和架构,更在于对细节的关注和持续的优化。遵循微软官方的最佳实践,让大家的C#代码真正发挥其潜力。

关键词

C#、性能优化、内存分配、GC压力、LINQ、异步编程、装箱、字符串拼接、ValueTask、Span、HttpClient、正则表达式、阻塞调用、批处理、即发即弃任务

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!

作者:寒冰

出处:mp.weixin.qq.com/s/3imd6IZWsY4topdA0TKdtw?scene=1&click_id=59

声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!