我刚刚注意到该方法有 11 个重载 string.Concat()
public static string Concat(IEnumerable<string> values);
public static string Concat<T>(IEnumerable<T> values);
public static string Concat(object arg0);
public static string Concat(params object[] args);
public static string Concat(params string[] values);
public static string Concat(object arg0, object arg1);
public static string Concat(string str0, string str1);
public static string Concat(object arg0, object arg1, object arg2);
public static string Concat(string str0, string str1, string str2);
public static string Concat(object arg0, object arg1, object arg2, object arg3);
public static string Concat(string str0, string str1, string str2, string str3);
这是什么原因?两者
public static string Concat(params object[] args);
public static string Concat<T>(IEnumerable<T> values);
应该是唯一需要的,因为它们同样方便/强大。 MSDN对此没有给出答案,如果您从框架中删除 9 个“重复”重载,则没有人会注意到。
最佳答案
此实现决策的主要动机是性能。
正如您正确指出的那样,可能只有两个:
public static string Concat(params object[] args);
public static string Concat<T>(IEnumerable<T> values);
如果 C# 实现了“params enumerable”特性——可变参数方法可以有一个
IEnumerable<T>
而不是 T[]
作为扩展参数——那么我们可以只得到一个。或者,我们可能会丢失可枚举重载而只使用对象数组版本。假设我们做了后者。你说
string x = Foo();
string y = Bar();
string z = x + y;
会发生什么?在只有可变参数的世界中
ToString
这只能编码为string x = Foo();
string y = Bar();
object[] array = new string[2];
array[0] = x;
array[1] = y;
string z = string.Concat(array);
所以:让我们回顾一下。大概每个调用分配一个字符串。然后我们分配一个短期数组,将引用复制到它,将它传递给可变参数方法,等等。需要编写该方法来处理任意大小的数组,处理空数组,等等。
我们不仅向零代堆添加了新的短期垃圾;我们还在活性分析图中创建了两个可能需要遍历的新边。我们可能通过增加压力减少了收集之间的时间,或者通过增加边缘增加了收集的成本,或者,最有可能的是,收集变得更加频繁和更加昂贵:双重打击。
但是等等,还有更多。我们必须考虑调用
Concat
的实现是什么方法看起来像。对象数组是——出人意料——对象数组,而不是字符串数组。那么我们需要做什么呢?被调用者需要将每个转换为字符串。调用
ToString
在各个?不,那可能会崩溃。首先检查 null,然后调用 ToString
.我们已经传入了字符串,但被调用者不知道这一点。
ToString
是字符串的标识,但 编译器不知道 ,并且调用是虚拟化的,因此抖动也不能轻易优化掉。因此,我们又进行了一些不必要的几纳秒检查和间接访问。更不用说我们需要检查数组是否为空,获取数组的长度,创建一个循环来遍历数组的每个元素,等等。这些开销非常小,但它们是每个级联的,它们可能会增加实际花费的时间和内存压力。
许多程序的性能取决于字符串操作和内存压力。我们如何消除或减轻这些成本?
我们可以观察到大多数字符串连接是两个字符串,因此创建一个专门用于处理这种情况的重载是有意义的:
static string Concat(string, string)
现在我们可以将上面的片段编码为:
string x = Foo();
string y = Bar();
string z = string.Concat(x, y);
现在没有创建数组,因此没有创建额外的垃圾,没有收集压力,引用图中没有新的边。在被调用方中,需要检查字符串是否为空,但我们不需要调用
ToString
在实现中,因为我们有类型系统来强制操作数已经是字符串,我们不需要检查数组是否为空,我们不需要根据数组长度检查循环变量,等等。因此,我们有一个很好的理由来进行两个重载:一个采用 params 数组,另一个采用恰好两个字符串。
现在我们对另一种常见且性能更高的场景重复该分析。每个额外的重载旨在为常见场景生成更有效的替代方案。随着更常见的场景被识别出来,可以变得更快,资源占用更少,就有动力产生更多的重载,并修复编译器,以便它们生成利用这些重载的代码。最终结果是大约十几个看似多余的过载,每个过载都可以针对高性能进行调整;这些涵盖了实际程序中最常见的情况。
如果您对这个主题感兴趣,我已经写了一系列关于我如何在 2006 年重新设计字符串连接优化器的短篇文章。
https://ericlippert.com/2013/06/17/string-concatenation-behind-the-scenes-part-one/
关于c# - string.Concat 11 次重载的原因,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/40528758/