admin管理员组文章数量:1027634
25个被低估的C#开发技巧:从性能优化到代码优雅的实战指南
我构建过从企业级应用到性能关键型系统的各种项目,然而,在这些年里,我注意到一件奇怪的事情——每个人都在谈论相同的最佳实践。
- • 保持代码 DRY(不要重复自己)。
- • 使用依赖注入。
- • 遵循 SOLID 原则。
不要误会我的意思——这些实践确实至关重要,但有些实践却被忽视了。它们并不新鲜,也不炫酷,但在扩展系统、提高可维护性或凌晨 3 点调试问题时,它们却能带来巨大的改变。
今天,我想分享 25 个 C# 实践,这些实践讨论得不够多。它们是区分经验丰富的 C# 开发者和仅遵循教科书的开发者的习惯。
1. 结构体(Struct)不仅仅是为了性能——它们还能减少错误
大多数开发者都知道,C# 中的结构体是值类型,而类是引用类型。大多数关于结构体的讨论都围绕性能优势——如何通过传递结构体避免堆分配,以及它们不需要垃圾回收。但还有一个更大、较少被提及的优势:结构体可以防止整类错误。
假设你正在处理一个接受金额值的 API。常见的做法是使用 decimal
类型:
public void ProcessPayment(decimal amount) { ... }
这虽然可行,但容易出错。有人可能会传递税率而不是金额。
相反,将值包装在结构体中会更清晰:
代码语言:javascript代码运行次数:0运行复制public readonly struct Money
{
public decimal Amount { get; }
public Money(decimal amount)
{
if (amount < ) throw new ArgumentException("Amount cannot be negative.");
Amount = amount;
}
public static implicit operator Money(decimal amount) => new Money(amount);
}
现在,你的 API 在类型级别上强制执行意图:
代码语言:javascript代码运行次数:0运行复制public void ProcessPayment(Money amount) { ... }
编译器不会让你意外传递税率或随机的 decimal
。在这种情况下,结构体不仅提高了性能,还减少了开发者犯错的可能性。
2. 异步代码不仅仅是关于 async
和 await
——它还涉及控制执行上下文
当人们讨论 C# 中的异步编程时,他们大多关注 async
和 await
。但这只是表面。真正的力量在于理解执行上下文。
以下是一个常见的错误:
代码语言:javascript代码运行次数:0运行复制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
。
以下是大多数人的做法:
代码语言:javascript代码运行次数:0运行复制public class UserService
{
public User? GetUser(int id)
{
return _repository.FindById(id);
}
}
现在,每个 GetUser
的调用者都必须检查 null
:
var user = _userService.GetUser();
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
错误。然而,很少有 C# 开发者始终如一地实现它。
4. Span<T>
和 Memory<T>
是游戏规则的改变者——即使你不编写高性能代码
Span<T>
和 Memory<T>
通常在高性能应用程序的上下文中讨论,但它们的真正好处是编写更安全、更高效的代码——而不需要指针或不安全块的复杂性。
考虑以下简单示例:
代码语言:javascript代码运行次数:0运行复制public void ProcessBuffer(byte[] data)
{
for (int i = ; i < data.Length; i++)
{
data[i] = (byte)(data[i] + );
}
}
使用 Span<T>
,这变得更安全且更灵活:
public void ProcessBuffer(Span<byte> data)
{
for (int i = ; i < data.Length; i++)
{
data[i] = (byte)(data[i] + );
}
}
为什么这更好?
- • 它消除了不必要的堆分配。
- • 它适用于数组和栈分配的内存。
- • 它防止了大型缓冲区的意外复制,从而在大型应用程序中提高了性能。
通过 Span<T>
和 Memory<T>
,你可以安全高效地操作数据。但由于它们通常与“低级”性能优化相关联,大多数 C# 开发者并未充分探索它们的潜力。
5. 使用 readonly struct
实现真正的不可变性和性能
许多 C# 开发者使用不可变类来确保线程安全性和可预测性。但在某些场景中,不可变结构体更胜一筹。
普通结构体仍然可能被无意修改:
代码语言:javascript代码运行次数:0运行复制public struct Point
{
public int X;
public int Y;
}
即使结构体是值类型,如果通过引用传递,它们仍然可以被修改。为了强制不可变性,使用 readonly struct
:
public readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
}
现在,Point
在创建后无法被修改,确保了更好的性能和安全性。
6. 使用 CallerMemberName
改进日志记录和调试
日志记录是调试和监控的重要组成部分,但许多开发者手动将方法名称传递到日志中:
代码语言:javascript代码运行次数:0运行复制public void ProcessOrder()
{
_logger.Log("Processing order in ProcessOrder");
}
相反,使用 [CallerMemberName]
自动捕获方法名称:
public void LogMessage(string message, [CallerMemberName] string caller = "")
{
Console.WriteLine($"{caller}: {message}");
}
现在,你可以简单地调用:
代码语言:javascript代码运行次数:0运行复制LogMessage("Processing order");
它会自动打印:
代码语言:javascript代码运行次数:0运行复制ProcessOrder: Processing order
这个小技巧减少了手动错误,提高了日志记录的准确性。
7. 使用 Dictionary<TKey, Lazy<TValue>>
实现高效缓存
在实现缓存时,许多开发者会立即将值存储在字典中:
代码语言:javascript代码运行次数:0运行复制private Dictionary<int, User> _userCache = new();
但这意味着每个条目都会预先计算,即使它从未被使用。更好的方法是使用 Lazy<T>
进行延迟初始化:
private Dictionary<int, Lazy<User>> _userCache = new();
现在,值仅在访问时创建:
代码语言:javascript代码运行次数:0运行复制var user = _userCache[userId].Value; // 仅在第一次访问时计算
这提高了效率,尤其是在从 API 或数据库加载数据时。
8. 在依赖注入中使用 KeyedService
处理多实现
有时,你有多个接口实现,但标准的依赖注入无法轻松选择特定实现。
代码语言:javascript代码运行次数:0运行复制public interface INotification
{
void Send(string message);
}
public class EmailNotification : INotification { ... }
public class SmsNotification : INotification { ... }
与其使用 IEnumerable<INotification>
并手动过滤,不如使用 .NET 8 引入的键控依赖注入:
builder.Services.AddKeyedSingleton<INotification, EmailNotification>("Email");
builder.Services.AddKeyedSingleton<INotification, SmsNotification>("SMS");
然后,像这样解析它:
代码语言:javascript代码运行次数:0运行复制var smsNotifier = serviceProvider.GetRequiredKeyedService<INotification>("SMS");
这简化了服务解析,避免了不必要的条件逻辑。
9. 使用 Span<T>
避免不必要的字符串分配
在 C# 中,字符串操作通常会导致隐藏的内存分配,尤其是在大规模应用程序中。考虑以下示例:
代码语言:javascript代码运行次数:0运行复制string input = "John,Doe,Developer";
var parts = input.Split(',');
每次调用 Split()
都会分配一个新的字符串数组。相反,使用 Span<T>
:
ReadOnlySpan<char> input = "John,Doe,Developer";
var firstName = input.Slice(, ); // "John"
这种方法避免了不必要的分配,并且速度显著更快。
10. 在异步方法中正确使用 CancellationToken
许多开发者忘记在异步方法中传播 CancellationToken
,导致应用程序无响应。
错误做法:
代码语言:javascript代码运行次数:0运行复制public async Task FetchData()
{
await Task.Delay(); // 无法取消
}
更好的方法:
代码语言:javascript代码运行次数:0运行复制public async Task FetchData(CancellationToken token)
{
await Task.Delay(, token);
}
这确保了如果用户取消操作,它会立即停止,而不是等待。
11. 使用 Enumerable.Range()
简化循环
与其使用手动循环,不如使用 Enumerable.Range()
编写更简洁、更具表现力的代码:
foreach (var i in Enumerable.Range(, ))
{
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 引入了记录类型用于不可变类型,但 C# 10 进一步改进了它,引入了 record struct
:
public readonly record struct Coordinates(int X, int Y);
这提供了:
- • ✅ 不可变性
- • ✅ 值语义(结构体行为)
- • ✅ 更高效的内存使用
非常适合 DTO、事件数据和缓存场景。
14. 使用 string.Create()
优化字符串构建
在构建大型字符串时,与其使用 StringBuilder
,不如使用 string.Create()
,它直接写入内存:
var str = string.Create(, 'X', (span, ch) =>
{
span.Fill(ch);
});
这避免了中间分配,使其非常适合性能关键型应用程序。
15. 使用 nameof()
替代硬编码字符串
在方法名称、属性名称或异常消息中使用硬编码字符串容易出错:
代码语言:javascript代码运行次数:0运行复制throw new ArgumentException("Invalid parameter: customerId");
相反,使用 nameof()
:
throw new ArgumentException($"Invalid parameter: {nameof(customerId)}");
如果变量名称更改,nameof()
会自动更新,减少了维护工作。
16. 使用 ConditionalWeakTable
将数据与对象关联
许多开发者将元数据存储在字典中,如果对象未被移除,可能会导致内存泄漏。
与其使用:
代码语言:javascript代码运行次数:0运行复制Dictionary<MyClass, string> _metadata = new();
不如使用 ConditionalWeakTable<T, TValue>
,它在对象被垃圾回收时自动移除数据:
private static readonly ConditionalWeakTable<MyClass, string> _metadata = new();
这确保了没有内存泄漏,非常适合缓存计算值。
17. 使用 Task.WhenAll
替代多个 await
调用
如果你有多个异步操作,避免顺序等待它们:
代码语言:javascript代码运行次数:0运行复制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+ 中,使用插值字符串处理器来避免分配:
代码语言:javascript代码运行次数:0运行复制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
避免异常开销
在向字典添加元素时,开发者通常会先检查键是否存在:
代码语言:javascript代码运行次数:0运行复制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>(); // 无堆分配
}
ValueTask<T>
在结果已经可用时避免了不必要的内存分配,提高了性能。
24. 使用 ConfigureAwait(false)
避免异步代码中的死锁
在编写库中的异步代码时,始终使用 ConfigureAwait(false)
以防止 UI 死锁:
await Task.Delay().ConfigureAwait(false);
这告诉运行时不要捕获原始的同步上下文,提高了性能并避免了桌面和 Web 应用程序中的死锁。
25. 使用 BlockingCollection<T>
处理生产者-消费者场景
如果多个线程需要并行处理数据,使用普通队列会导致竞争条件:
代码语言:javascript代码运行次数:0运行复制Queue<int> queue = new();
queue.Enqueue(); // 无线程安全性
相反,使用 BlockingCollection<T>
实现线程安全的生产者-消费者模式:
var queue = new BlockingCollection<int>();
queue.Add();
int item = queue.Take();
这确保了安全的并发访问,提高了多线程性能。
最后总结
- • 结构体不仅仅是为了性能——它们可以防止整类错误。
- • 异步执行上下文与
async
和await
同样重要。 - • 避免
null
不仅仅是使用Nullable<T>
——它还涉及返回有意义的默认值。 - •
Span<T>
和Memory<T>
不仅仅是为了性能——它们使内存管理更安全、更简单。 - •
readonly struct
提高了不可变性。 - •
CallerMemberName
简化了日志记录。 - •
Lazy<T>
优化了缓存。 - •
KeyedService
简化了依赖注入。 - •
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# 项目中,立即看到改进!
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。原始发表:2025-05-01,如有侵权请联系 cloudcommunity@tencent 删除性能优化c#技巧开发性能25个被低估的C#开发技巧:从性能优化到代码优雅的实战指南
我构建过从企业级应用到性能关键型系统的各种项目,然而,在这些年里,我注意到一件奇怪的事情——每个人都在谈论相同的最佳实践。
- • 保持代码 DRY(不要重复自己)。
- • 使用依赖注入。
- • 遵循 SOLID 原则。
不要误会我的意思——这些实践确实至关重要,但有些实践却被忽视了。它们并不新鲜,也不炫酷,但在扩展系统、提高可维护性或凌晨 3 点调试问题时,它们却能带来巨大的改变。
今天,我想分享 25 个 C# 实践,这些实践讨论得不够多。它们是区分经验丰富的 C# 开发者和仅遵循教科书的开发者的习惯。
1. 结构体(Struct)不仅仅是为了性能——它们还能减少错误
大多数开发者都知道,C# 中的结构体是值类型,而类是引用类型。大多数关于结构体的讨论都围绕性能优势——如何通过传递结构体避免堆分配,以及它们不需要垃圾回收。但还有一个更大、较少被提及的优势:结构体可以防止整类错误。
假设你正在处理一个接受金额值的 API。常见的做法是使用 decimal
类型:
public void ProcessPayment(decimal amount) { ... }
这虽然可行,但容易出错。有人可能会传递税率而不是金额。
相反,将值包装在结构体中会更清晰:
代码语言:javascript代码运行次数:0运行复制public readonly struct Money
{
public decimal Amount { get; }
public Money(decimal amount)
{
if (amount < ) throw new ArgumentException("Amount cannot be negative.");
Amount = amount;
}
public static implicit operator Money(decimal amount) => new Money(amount);
}
现在,你的 API 在类型级别上强制执行意图:
代码语言:javascript代码运行次数:0运行复制public void ProcessPayment(Money amount) { ... }
编译器不会让你意外传递税率或随机的 decimal
。在这种情况下,结构体不仅提高了性能,还减少了开发者犯错的可能性。
2. 异步代码不仅仅是关于 async
和 await
——它还涉及控制执行上下文
当人们讨论 C# 中的异步编程时,他们大多关注 async
和 await
。但这只是表面。真正的力量在于理解执行上下文。
以下是一个常见的错误:
代码语言:javascript代码运行次数:0运行复制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
。
以下是大多数人的做法:
代码语言:javascript代码运行次数:0运行复制public class UserService
{
public User? GetUser(int id)
{
return _repository.FindById(id);
}
}
现在,每个 GetUser
的调用者都必须检查 null
:
var user = _userService.GetUser();
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
错误。然而,很少有 C# 开发者始终如一地实现它。
4. Span<T>
和 Memory<T>
是游戏规则的改变者——即使你不编写高性能代码
Span<T>
和 Memory<T>
通常在高性能应用程序的上下文中讨论,但它们的真正好处是编写更安全、更高效的代码——而不需要指针或不安全块的复杂性。
考虑以下简单示例:
代码语言:javascript代码运行次数:0运行复制public void ProcessBuffer(byte[] data)
{
for (int i = ; i < data.Length; i++)
{
data[i] = (byte)(data[i] + );
}
}
使用 Span<T>
,这变得更安全且更灵活:
public void ProcessBuffer(Span<byte> data)
{
for (int i = ; i < data.Length; i++)
{
data[i] = (byte)(data[i] + );
}
}
为什么这更好?
- • 它消除了不必要的堆分配。
- • 它适用于数组和栈分配的内存。
- • 它防止了大型缓冲区的意外复制,从而在大型应用程序中提高了性能。
通过 Span<T>
和 Memory<T>
,你可以安全高效地操作数据。但由于它们通常与“低级”性能优化相关联,大多数 C# 开发者并未充分探索它们的潜力。
5. 使用 readonly struct
实现真正的不可变性和性能
许多 C# 开发者使用不可变类来确保线程安全性和可预测性。但在某些场景中,不可变结构体更胜一筹。
普通结构体仍然可能被无意修改:
代码语言:javascript代码运行次数:0运行复制public struct Point
{
public int X;
public int Y;
}
即使结构体是值类型,如果通过引用传递,它们仍然可以被修改。为了强制不可变性,使用 readonly struct
:
public readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
}
现在,Point
在创建后无法被修改,确保了更好的性能和安全性。
6. 使用 CallerMemberName
改进日志记录和调试
日志记录是调试和监控的重要组成部分,但许多开发者手动将方法名称传递到日志中:
代码语言:javascript代码运行次数:0运行复制public void ProcessOrder()
{
_logger.Log("Processing order in ProcessOrder");
}
相反,使用 [CallerMemberName]
自动捕获方法名称:
public void LogMessage(string message, [CallerMemberName] string caller = "")
{
Console.WriteLine($"{caller}: {message}");
}
现在,你可以简单地调用:
代码语言:javascript代码运行次数:0运行复制LogMessage("Processing order");
它会自动打印:
代码语言:javascript代码运行次数:0运行复制ProcessOrder: Processing order
这个小技巧减少了手动错误,提高了日志记录的准确性。
7. 使用 Dictionary<TKey, Lazy<TValue>>
实现高效缓存
在实现缓存时,许多开发者会立即将值存储在字典中:
代码语言:javascript代码运行次数:0运行复制private Dictionary<int, User> _userCache = new();
但这意味着每个条目都会预先计算,即使它从未被使用。更好的方法是使用 Lazy<T>
进行延迟初始化:
private Dictionary<int, Lazy<User>> _userCache = new();
现在,值仅在访问时创建:
代码语言:javascript代码运行次数:0运行复制var user = _userCache[userId].Value; // 仅在第一次访问时计算
这提高了效率,尤其是在从 API 或数据库加载数据时。
8. 在依赖注入中使用 KeyedService
处理多实现
有时,你有多个接口实现,但标准的依赖注入无法轻松选择特定实现。
代码语言:javascript代码运行次数:0运行复制public interface INotification
{
void Send(string message);
}
public class EmailNotification : INotification { ... }
public class SmsNotification : INotification { ... }
与其使用 IEnumerable<INotification>
并手动过滤,不如使用 .NET 8 引入的键控依赖注入:
builder.Services.AddKeyedSingleton<INotification, EmailNotification>("Email");
builder.Services.AddKeyedSingleton<INotification, SmsNotification>("SMS");
然后,像这样解析它:
代码语言:javascript代码运行次数:0运行复制var smsNotifier = serviceProvider.GetRequiredKeyedService<INotification>("SMS");
这简化了服务解析,避免了不必要的条件逻辑。
9. 使用 Span<T>
避免不必要的字符串分配
在 C# 中,字符串操作通常会导致隐藏的内存分配,尤其是在大规模应用程序中。考虑以下示例:
代码语言:javascript代码运行次数:0运行复制string input = "John,Doe,Developer";
var parts = input.Split(',');
每次调用 Split()
都会分配一个新的字符串数组。相反,使用 Span<T>
:
ReadOnlySpan<char> input = "John,Doe,Developer";
var firstName = input.Slice(, ); // "John"
这种方法避免了不必要的分配,并且速度显著更快。
10. 在异步方法中正确使用 CancellationToken
许多开发者忘记在异步方法中传播 CancellationToken
,导致应用程序无响应。
错误做法:
代码语言:javascript代码运行次数:0运行复制public async Task FetchData()
{
await Task.Delay(); // 无法取消
}
更好的方法:
代码语言:javascript代码运行次数:0运行复制public async Task FetchData(CancellationToken token)
{
await Task.Delay(, token);
}
这确保了如果用户取消操作,它会立即停止,而不是等待。
11. 使用 Enumerable.Range()
简化循环
与其使用手动循环,不如使用 Enumerable.Range()
编写更简洁、更具表现力的代码:
foreach (var i in Enumerable.Range(, ))
{
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 引入了记录类型用于不可变类型,但 C# 10 进一步改进了它,引入了 record struct
:
public readonly record struct Coordinates(int X, int Y);
这提供了:
- • ✅ 不可变性
- • ✅ 值语义(结构体行为)
- • ✅ 更高效的内存使用
非常适合 DTO、事件数据和缓存场景。
14. 使用 string.Create()
优化字符串构建
在构建大型字符串时,与其使用 StringBuilder
,不如使用 string.Create()
,它直接写入内存:
var str = string.Create(, 'X', (span, ch) =>
{
span.Fill(ch);
});
这避免了中间分配,使其非常适合性能关键型应用程序。
15. 使用 nameof()
替代硬编码字符串
在方法名称、属性名称或异常消息中使用硬编码字符串容易出错:
代码语言:javascript代码运行次数:0运行复制throw new ArgumentException("Invalid parameter: customerId");
相反,使用 nameof()
:
throw new ArgumentException($"Invalid parameter: {nameof(customerId)}");
如果变量名称更改,nameof()
会自动更新,减少了维护工作。
16. 使用 ConditionalWeakTable
将数据与对象关联
许多开发者将元数据存储在字典中,如果对象未被移除,可能会导致内存泄漏。
与其使用:
代码语言:javascript代码运行次数:0运行复制Dictionary<MyClass, string> _metadata = new();
不如使用 ConditionalWeakTable<T, TValue>
,它在对象被垃圾回收时自动移除数据:
private static readonly ConditionalWeakTable<MyClass, string> _metadata = new();
这确保了没有内存泄漏,非常适合缓存计算值。
17. 使用 Task.WhenAll
替代多个 await
调用
如果你有多个异步操作,避免顺序等待它们:
代码语言:javascript代码运行次数:0运行复制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+ 中,使用插值字符串处理器来避免分配:
代码语言:javascript代码运行次数:0运行复制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
避免异常开销
在向字典添加元素时,开发者通常会先检查键是否存在:
代码语言:javascript代码运行次数:0运行复制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>(); // 无堆分配
}
ValueTask<T>
在结果已经可用时避免了不必要的内存分配,提高了性能。
24. 使用 ConfigureAwait(false)
避免异步代码中的死锁
在编写库中的异步代码时,始终使用 ConfigureAwait(false)
以防止 UI 死锁:
await Task.Delay().ConfigureAwait(false);
这告诉运行时不要捕获原始的同步上下文,提高了性能并避免了桌面和 Web 应用程序中的死锁。
25. 使用 BlockingCollection<T>
处理生产者-消费者场景
如果多个线程需要并行处理数据,使用普通队列会导致竞争条件:
代码语言:javascript代码运行次数:0运行复制Queue<int> queue = new();
queue.Enqueue(); // 无线程安全性
相反,使用 BlockingCollection<T>
实现线程安全的生产者-消费者模式:
var queue = new BlockingCollection<int>();
queue.Add();
int item = queue.Take();
这确保了安全的并发访问,提高了多线程性能。
最后总结
- • 结构体不仅仅是为了性能——它们可以防止整类错误。
- • 异步执行上下文与
async
和await
同样重要。 - • 避免
null
不仅仅是使用Nullable<T>
——它还涉及返回有意义的默认值。 - •
Span<T>
和Memory<T>
不仅仅是为了性能——它们使内存管理更安全、更简单。 - •
readonly struct
提高了不可变性。 - •
CallerMemberName
简化了日志记录。 - •
Lazy<T>
优化了缓存。 - •
KeyedService
简化了依赖注入。 - •
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# 项目中,立即看到改进!
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。原始发表:2025-05-01,如有侵权请联系 cloudcommunity@tencent 删除性能优化c#技巧开发性能本文标签: 25个被低估的C开发技巧从性能优化到代码优雅的实战指南
版权声明:本文标题:25个被低估的C#开发技巧:从性能优化到代码优雅的实战指南 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://it.en369.cn/jiaocheng/1747426344a2165826.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论