你以为自己的C#代码很快?再好好想想

作者:微信公众号:【架构师老卢】
7-29 8:21
13

你觉得自己的C#代码速度够快?那可未必。拖慢你代码速度的并非算法——而是那些“隐形杀手”:一些细微且常见的C#错误,即便是经验丰富的开发者也容易忽略。这10种模式(有微软官方文档为证)正在拖慢你的应用、增加云服务账单,还会造成你意想不到的GC压力。

🔥 影响重大的错误

🚫 错误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进行延迟处理。

参考资料:Microsoft LINQ陷阱

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

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

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

参考资料:Microsoft异步编程

📈 GC与内存分配陷阱

🌀 错误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毫秒的实际差异,其影响也不容忽视。

参考资料:Microsoft设计指南——装箱

🔁 错误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,非常适合快速、低延迟的循环。

参考资料:Microsoft StringBuilder文档

🎯 错误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可避免堆分配。非常适合“缓存优先”的异步流程。

参考资料:Microsoft ValueTask文档

🧵 异步与线程问题

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

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

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

📚 参考资料:

Microsoft文档——使用Span和Memory提升性能

🎯 错误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会浪费资源。池化可避免套接字问题和重新编译,提高性能和可靠性。

📚 参考资料:

Microsoft文档——HttpClient使用指南

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

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

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

📚 参考资料:

Microsoft文档——异步编程最佳实践

🎯 错误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会导致缓慢、频繁的操作。正如微软所建议的,批处理可减少往返次数,改善延迟并提高可扩展性。

📚 参考资料:

Microsoft模式与实践——最小化往返次数

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

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

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

相关留言评论
昵称:
邮箱:
阅读排行