.NET是一个功能强大的平台,但有时真正的力量在于知道如何正确使用其功能,或者何时完全不使用它们。在本系列中,我们将探讨5个实用技巧,这些技巧不仅能让你的代码更简洁、运行更快,还能揭示即使是经验丰富的开发者也会遇到的性能陷阱、内存低效问题和不良实践。
这是为.NET开发者准备的一系列经过实战检验的实用技巧的第一篇。内容将涵盖语言特性、性能优化技术和编码理念,没有废话,只有实用的要点。
处理数组、字符串或缓冲区的切片时,并非一定要进行内存分配。Span和ReadOnlySpan提供了内存安全的、栈分配的数据视图,在解析、读取文件或操作缓冲区时,能显著提升性能。
例如:如果你需要反复解析或切片字符串或字节数组,Span可以通过在栈上操作内存切片,帮助你避免不必要的内存分配,而不是创建子字符串或副本。
之前(使用string.Split()——内存开销大):
var line = "123,John Doe,5000";
var parts = line.Split(',');
int id = int.Parse(parts[]);
string name = parts[];
int salary = int.Parse(parts[]);
之后(使用ReadOnlySpan——无内存分配):
ReadOnlySpan<char> line = "123,John Doe,5000";
int firstComma = line.IndexOf(',');
int secondComma = line.Slice(firstComma + ).IndexOf(',') + firstComma + ;
var idSpan = line.Slice(, firstComma);
var nameSpan = line.Slice(firstComma + , secondComma - firstComma - );
var salarySpan = line.Slice(secondComma + );
int id = int.Parse(idSpan);
string name = nameSpan.ToString();
int salary = int.Parse(salarySpan);
这样可以避免分配新的数组或子字符串,让你直接、安全且高效地操作内存。
重要提示:Span是一个ref结构体,它存在于栈上,而非堆上。你不应将其用作类中的字段,也不应在lambda表达式或异步方法中捕获它。它适用于短期的、高性能的操作。
如果你需要在生命周期较长的上下文中实现类似功能,可以考虑使用Memory或ReadOnlyMemory,它们在堆上是安全的。
这是一个常见的LINQ反模式:
var user = users.Where(u => u.Id == ).FirstOrDefault();
这种写法会创建一个不必要的中间迭代器,只为了获取一个项。相反,直接使用带有谓词的FirstOrDefault:
var user = users.FirstOrDefault(u => u.Id == );
这样更简洁,性能也更好,尤其是在处理大型集合时。当迭代数千个元素时,每毫秒都很重要;除非必要,否则避免构建中间集合。
使用int.Parse()或类似的Parse()方法可能看起来很简单,但它们基于异常机制,这意味着如果输入无效,它们会抛出异常。这在性能方面代价很高,尤其是在循环或高吞吐量代码中。
// 不好:可能会抛出异常
int value = int.Parse(input);
相反,优先使用TryParse(),它可以避免异常并提升性能:
if (int.TryParse(input, out int value))
{
// 可以安全地使用'value'
}
这在用户输入、文件解析或网络场景中尤为重要,因为在这些场景中可能会出现无效数据。当每秒需要进行多次解析时,两者的差异会变得很明显。
重要原因:在.NET中,抛出和捕获异常的代价很高,不仅体现在运行时成本上,还体现在内存分配上。TryParse让你能够编写防御性的、高性能的代码。
下面的代码可能可以编译,但从设计上来说是有问题的:
public MyService()
{
LoadDataAsync(); // 危险!
}
private async void LoadDataAsync()
{
await Task.Delay();
// 异步逻辑...
}
异步void方法无法被等待,异常可能会未被捕获,而且执行时机也变得不可预测。相反,可以使用以下选项之一:
如果你需要在对象创建时执行异步逻辑,可以将其作为工厂模式的一部分,或者使用返回Task的静态方法。
当处理短字符串或固定长度的字符串时,Span和stackalloc可以通过完全避免堆分配来超越StringBuilder的性能:
Span<char> buffer = stackalloc char[];
bool success = int.TryFormat(, buffer, out int charsWritten);
string result = new string(buffer.Slice(, charsWritten));
这速度极快,并且不会造成内存压力,非常适合格式化数字、生成标识符或进行底层解析。
当你确实需要动态调整大小或处理长生命周期的字符串时,再使用StringBuilder。否则,栈分配的缓冲区通常在性能上更具优势。
每个.NET开发者都有知识盲区,都有我们不加质疑就采用的模式,或者因为看似过于高级或不必要而忽略的语言特性。上述技巧虽然只是小小的改变,但累积起来可能会产生巨大的影响,尤其是在高性能或对内存敏感的应用程序中。
这是这个不断扩展的系列的第一篇文章。在接下来的文章中,我们将继续探讨被忽视的语言特性、隐藏的成本和最佳实践。