LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

25个被忽视的C#实践:从性能优化到代码质量的全面提升

admin
2025年2月21日 13:4 本文热度 320

我开发过从企业级应用程序到性能关键型系统的各种项目,然而在这些年里,我注意到一件奇怪的事情——每个人都在谈论相同的最佳实践。

  • 保持代码 DRY(不要重复自己)。
  • 使用依赖注入。
  • 遵循 SOLID 原则。

今天,我想分享 25 个 C# 实践中被谈论得不够多的技巧。这些习惯将经验丰富的 C# 开发者与那些只遵循教科书的人区分开来。


1. 结构体(Struct)不仅仅是为了性能——它们还能减少 Bug

大多数开发者都知道 C# 中的结构体是值类型,而类是引用类型。大多数关于结构体的讨论都围绕性能优势——如何通过传递结构体避免堆分配,以及它们不需要垃圾回收。但还有一个更大、鲜为人知的优势:结构体可以防止整类 Bug。

假设你正在处理一个接受金额值的 API。常见的做法是使用 decimal 类型:

public void ProcessPayment(decimal amount) { ... }

这虽然可行,但容易出错。有人可能会传递税率而不是金额。

相反,将值包装在结构体中会更清晰:

public readonlystructMoney
{
    publicdecimal Amount {get;}
    publicMoney(decimal amount)
    {
        if(amount <0)thrownewArgumentException("Amount cannot be negative.");
        Amount = amount;
    }
    publicstaticimplicitoperatorMoney(decimal amount)=>newMoney(amount);
}

现在,你的 API 在类型级别上强制执行意图:

public void ProcessPayment(Money amount) { ... }

编译器不会让你意外传递税率或随机的 decimal。在这种情况下,结构体不仅提高了性能,还减少了开发者犯错的可能性。


2. 异步代码不仅仅是 async 和 await——它还关乎控制执行上下文

当人们讨论 C# 中的异步编程时,大多集中在 async 和 await 上。但这只是表面。真正的力量在于理解执行上下文。

以下是一个常见错误:

public async Task<int> GetDataAsync()
{
    var data = await _database.GetRecordsAsync();
    return data.Count;
}

乍一看,这似乎没问题。但如果 _database.GetRecordsAsync() 正在进行繁重的 I/O 工作,它会捕获同步上下文,可能导致 UI 应用程序中的死锁或高性能系统中不必要的上下文切换。

更好的方法是使用 ConfigureAwait(false),当你不需要在同一上下文中恢复时:

public async Task<int> GetDataAsync()
{
    var data = await _database.GetRecordsAsync().ConfigureAwait(false);
    return data.Count;
}

这个小改动可以显著提高性能,尤其是在服务器应用程序中。然而,尽管这是一个最佳实践,但在性能关键领域之外的讨论中却很少被提及。


3. 避免 null 不仅仅是使用 Nullable<T>

每个 C# 开发者都曾面对可怕的 NullReferenceException。这就是为什么 C# 8.0 引入了可空引用类型。然而,即使有了这些功能,开发者仍然过度依赖 null

以下是大多数人的做法:

public class UserService
{
    public User? GetUser(int id)
    {
        return _repository.FindById(id);
    }
}

现在,每个 GetUser 的调用者都必须检查 null

var user = _userService.GetUser(1);
if (user != null)
{
    Console.WriteLine(user.Name);
}

这种方法会使代码变得杂乱。相反,更好的方法是返回一个 Option<T> 或特殊的“空”对象:

public sealed class NoUser : User { }
public static readonly User NoUserInstance = new NoUser();

现在,该方法永远不会返回 null

public User GetUser(int id)
{
    return _repository.FindById(id) ?? NoUserInstance;
}

这个简单的改动消除了 null 检查,并减少了意外的 NullReferenceException Bug。然而,很少有 C# 开发者始终如一地实现它。


4. Span<T> 和 Memory<T> 是游戏规则改变者——即使你不编写高性能代码

Span<T> 和 Memory<T> 通常在高性能应用程序的上下文中讨论,但它们的真正好处是编写更安全、更高效的代码——而无需指针或不安全块的复杂性。

考虑以下简单示例:

public voidProcessBuffer(byte[] data)
{
    for(int i =0; i < data.Length; i++)
    {
        data[i]=(byte)(data[i]+1);
    }
}

使用 Span<T>,这变得更安全且更灵活:

public voidProcessBuffer(Span<byte> data)
{
    for(int i =0; i < data.Length; i++)
    {
        data[i]=(byte)(data[i]+1);
    }
}

为什么这更好?

  • 它消除了不必要的堆分配。
  • 它适用于数组和栈分配的内存。
  • 它防止了大型缓冲区的意外复制,从而在大规模应用程序中提高了性能。

通过 Span<T> 和 Memory<T>,你可以安全高效地操作数据。但由于它们通常与“低级”性能优化相关联,大多数 C# 开发者并未探索它们的全部潜力。


5. 使用 readonly struct 实现真正的不可变性和性能

许多 C# 开发者使用不可变类来确保线程安全性和可预测性。但在某些场景中,不可变结构体甚至更好。

普通结构体仍可能被意外修改:

public struct Point
{
    public int X;
    public int Y;
}

即使结构体是值类型,如果通过引用传递,它们仍然可以被修改。为了强制执行不可变性,请使用 readonly struct

public readonlystructPoint
{
    publicint X {get;}
    publicint Y {get;}
    publicPoint(int x,int y)=>(X, Y)=(x, y);
}

现在,Point 在创建后无法修改,确保了更好的性能和安全性。


6. 使用 CallerMemberName 实现更好的日志记录和调试

日志记录是调试和监控的重要组成部分,但许多开发者手动将方法名称传递到日志中:

public void ProcessOrder()
{
    _logger.Log("Processing order in ProcessOrder");
}

相反,使用 [CallerMemberName] 自动捕获方法名称:

public void LogMessage(string message, [CallerMemberName] string caller = "")
{
    Console.WriteLine($"{caller}{message}");
}

现在,你可以简单地调用:

LogMessage("Processing order");

它会自动打印:

ProcessOrder: Processing order

这个小技巧减少了手动错误并提高了日志记录的准确性。


7. 使用 Dictionary<TKey, Lazy<TValue>> 实现高效缓存

在实现缓存时,许多开发者会立即将值存储在字典中:

private Dictionary<int, User> _userCache = new();

但这意味着即使从未使用过,每个条目也会预先计算。更好的方法是使用 Lazy<T> 进行延迟初始化:

private Dictionary<int, Lazy<User>> _userCache = new();

现在,值仅在访问时创建:

var user = _userCache[userId].Value; // 仅在第一次访问时计算

这提高了效率,尤其是在从 API 或数据库加载数据时。


8. 在依赖注入中使用 KeyedService 实现多实现

有时,你有多个接口实现,但标准依赖注入不允许你轻松选择特定的实现。

public interfaceINotification
{
    voidSend(string message);
}
publicclassEmailNotification:INotification{...}
publicclassSmsNotification:INotification{...}

与其使用 IEnumerable<INotification> 并手动过滤,不如使用 .NET 8 引入的键控依赖注入:

builder.Services.AddKeyedSingleton<INotification, EmailNotification>("Email");
builder.Services.AddKeyedSingleton<INotification, SmsNotification>("SMS");

然后,像这样解析它:

var smsNotifier = serviceProvider.GetRequiredKeyedService<INotification>("SMS");

这简化了服务解析,避免了不必要的条件逻辑。


9. 使用 Span<T> 避免不必要的字符串分配

在 C# 中,字符串操作通常会导致隐藏的内存分配,尤其是在大规模应用程序中。考虑以下示例:

string input = "John,Doe,Developer";
var parts = input.Split(',');

每次调用 Split() 都会分配一个新的字符串数组。相反,使用 Span<T>

ReadOnlySpan<char> input = "John,Doe,Developer";
var firstName = input.Slice(0, 4); // "John"

这种方法避免了不必要的分配,并且速度显著更快。


10. 在异步方法中正确使用 CancellationToken

许多开发者忘记在异步方法中传播 CancellationToken,导致应用程序无响应。

错误做法:

public async Task FetchData()
{
    await Task.Delay(5000); // 无法取消
}

更好的方法:

public async Task FetchData(CancellationToken token)
{
    await Task.Delay(5000, token);
}

这确保了如果用户取消操作,它会立即停止,而不是等待。


11. 使用 Enumerable.Range() 实现更简洁的循环

与其使用手动循环,不如使用 Enumerable.Range() 实现更简洁、更具表现力的代码:

foreach (var i in Enumerable.Range(1, 10))
{
    Console.WriteLine(i);
}

这种方法更具可读性和功能性,减少了与循环相关的错误。


12. 优先使用 TryParse 而不是 Parse 以避免异常

异常是昂贵的。与其使用 int.Parse()(在失败时抛出异常):

int value = int.Parse("notANumber"); // 抛出异常

不如使用 TryParse() 来避免不必要的异常处理:

if (int.TryParse("notANumber", out int value))
{
    Console.WriteLine($"Valid number: {value}");
}

这提高了性能,并避免了不必要的 try-catch 块。


13. 使用 record struct 实现高性能不可变类型

C# 9 引入了记录(record)用于不可变类型,但 C# 10 进一步改进了它,引入了 record struct

public readonly record struct Coordinates(int X, int Y);

这提供了:

  • ✅ 不可变性
  • ✅ 值语义(结构体行为)
  • ✅ 更高效的内存使用

非常适合 DTO、事件数据和缓存场景。


14. 使用 string.Create() 优化字符串构建

在构建大型字符串时,与其使用 StringBuilder,不如使用 string.Create(),它直接写入内存:

var str = string.Create(5, 'X', (span, ch) =>
{
    span.Fill(ch);
});

这避免了中间分配,使其非常适合性能关键型应用程序。


15. 使用 nameof() 而不是硬编码字符串

在方法名称、属性名称或异常消息中使用硬编码字符串容易出错:

throw new ArgumentException("Invalid parameter: customerId");

相反,使用 nameof()

throw new ArgumentException($"Invalid parameter: {nameof(customerId)}");

如果变量名称更改,nameof() 会自动更新,减少了维护工作量。


16. 使用 ConditionalWeakTable 将数据与对象关联

许多开发者将元数据存储在字典中,如果对象未被移除,可能会导致内存泄漏。

与其使用:

Dictionary<MyClass, string> _metadata = new();

不如使用 ConditionalWeakTable<T, TValue>,它在对象被垃圾回收时自动移除数据:

private static readonly ConditionalWeakTable<MyClass, string> _metadata = new();

这确保了没有内存泄漏,非常适合缓存计算值。


17. 使用 Task.WhenAll 而不是多次 await 调用

如果你有多个异步操作,避免顺序等待它们:

await Task1();
await Task2();
await Task3();

相反,使用 Task.WhenAll() 并行运行它们:

await Task.WhenAll(Task1(), Task2(), Task3());

这通过并发运行任务显著减少了执行时间。


18. 使用 sealed 关键字提升性能

默认情况下,C# 类可以被继承,这会由于虚方法分派而增加额外的性能开销。

如果一个类不打算被继承,请将其标记为 sealed

public sealed class MyClass
{
    public void DoWork() { /* 快速执行 */ }
}

这允许 JIT 编译器优化方法调用,提高性能。


19. 使用 Stopwatch 而不是 DateTime 进行性能测量

在测量执行时间时,开发者通常使用 DateTime

var start = DateTime.Now;
// 某些操作
var elapsed = DateTime.Now - start;

这是不准确的,因为 DateTime.Now 受系统时钟变化的影响。相反,使用 Stopwatch

var stopwatch = Stopwatch.StartNew();
// 某些操作
stopwatch.Stop();
Console.WriteLine($"Elapsed time: {stopwatch.ElapsedMilliseconds} ms");

Stopwatch 使用高分辨率计时器,使其更加准确。


20. 使用插值字符串处理器实现高效的字符串格式化

使用 $"{var1} {var2}" 进行日志记录很常见,但它会分配不必要的字符串。

在 .NET 6+ 中,使用插值字符串处理器来避免分配:

public void Log(LogLevel level, [InterpolatedStringHandler] ref LogInterpolatedStringHandler message)
{
    Console.WriteLine(message);
}

这允许零分配日志记录,提高了高负载应用程序的性能。


21. 使用 Parallel.ForEachAsync 实现真正的异步并行

开发者通常使用 Parallel.ForEach,但它不支持 async/await。相反,使用 Parallel.ForEachAsync

await Parallel.ForEachAsync(myCollection, async (item, token) =>
{
    await ProcessItemAsync(item);
});

这允许真正的并行异步执行,在处理 I/O 密集型操作时提高了性能。


22. 使用 Dictionary.TryAdd 避免异常开销

在向字典添加元素时,开发者通常会先检查键是否存在:

if (!dict.ContainsKey(key))
{
    dict.Add(key, value);
}

更好的方法是使用 TryAdd(),它避免了双重查找开销:

dict.TryAdd(key, value);

这既更快又更高效。


23. 使用 ValueTask<T> 减少高性能代码中的分配

Task<T> 很好,但它总是分配内存。如果一个方法经常返回已完成的任务,请使用 ValueTask<T>

public ValueTask<int> GetCachedDataAsync()
{
    return new ValueTask<int>(42); // 无堆分配
}

ValueTask<T> 在结果已经可用时避免了不必要的内存分配,提高了性能。


24. 使用 ConfigureAwait(false) 避免异步代码中的死锁

在编写库中的异步代码时,始终使用 ConfigureAwait(false) 以防止 UI 死锁:

await Task.Delay(1000).ConfigureAwait(false);

这告诉运行时不要捕获原始的同步上下文,提高了性能并避免了桌面和 Web 应用程序中的死锁。


25. 使用 BlockingCollection<T> 实现生产者-消费者场景

如果多个线程需要并行处理数据,使用普通队列会导致竞争条件:

Queue<int> queue = new();
queue.Enqueue(10); // 无线程安全

相反,使用 BlockingCollection<T> 实现线程安全的生产者-消费者模式:

var queue = new BlockingCollection<int>();
queue.Add(10);
int item = queue.Take();

这确保了安全的并发访问,提高了多线程性能。


最后总结

  • 结构体不仅仅是为了性能——它们可以防止整类 Bug。
  • 异步执行上下文与 async 和 await 同样重要。
  • 避免 null 不仅仅是使用 Nullable<T>——它还关乎返回有意义的默认值。
  • Span<T>

     和 Memory<T> 不仅仅是为了性能——它们使内存管理更安全、更容易。
  • readonly struct

     提高了不可变性。
  • CallerMemberName

     简化了日志记录。
  • Lazy<T>

     优化了缓存。
  • 键控服务简化了依赖注入。
  • Span<T>

     避免了不必要的分配。
  • CancellationToken

     防止了应用程序无响应。
  • TryParse()

     消除了不必要的异常。
  • record struct

     是高性能 DTO 的理想选择。
  • string.Create()

     优化了字符串构建。
  • nameof()

     使代码更易于维护。
  • ConditionalWeakTable

     防止了内存泄漏。
  • Task.WhenAll

     减少了执行时间。
  • sealed

     提升了性能。
  • Stopwatch

     提高了计时准确性。
  • 插值字符串处理器优化了日志记录。
  • Parallel.ForEachAsync

     实现了真正的异步并行。
  • TryAdd()

     避免了不必要的字典查找。
  • ValueTask<T>

     减少了内存分配。
  • ConfigureAwait(false)

     防止了死锁。
  • BlockingCollection<T>

     提高了多线程性能。

从今天开始将这些技术应用到你的 C# 项目中,立即看到改进!🚀


阅读原文:https://mp.weixin.qq.com/s/H3SpEMFgmxUc9mQArSmRqA


该文章在 2025/2/21 13:07:12 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved