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