string.Concat的11个重载原因

本文关键字:重载 11个 Concat string | 更新日期: 2023-09-27 18:17:21

我刚刚注意到string.Concat()方法有11个重载

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个"重复"的重载,没有人会注意到。

string.Concat的11个重载原因

这个实现决策的主要动机是性能。

正如您正确注意到的,只能有两个:

public static string Concat(params object[] args);
public static string Concat<T>(IEnumerable<T> values);

如果c#实现了"参数枚举"功能——即可变方法可以有IEnumerable<T>而不是T[]作为扩展参数——那么我们可以减少到只有一个。或者,可以不使用enumerable重载,只使用对象数组版本。

假设我们做了后者。你说

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);

让我们复习一下。大概每个调用分配一个字符串。然后我们分配一个短周期数组,将引用复制到它,将其传递给variadic方法,等等。需要编写该方法来处理任何大小的数组,处理空数组,等等。

我们不仅在零代堆中添加了新的短期垃圾;我们还在活度分析图中创建了两条可能需要遍历的新边。我们可能通过增加压力减少了收集之间的时间间隔,或者通过增加边增加了收集的成本,或者,最有可能的是,收集变得更频繁和更昂贵:双重打击。

等等,还有更多。我们必须考虑被称为Concat的方法的实现是什么样子的。

对象数组是一个对象数组,而不是字符串数组。那么我们需要做什么呢?被调用方需要将每个转换为字符串。通过调用ToString每个?不,那可能会崩溃。首先检查null,然后调用ToString

我们传入了字符串,但是被调用者不知道。ToString是字符串的标识,但是编译器不知道,并且调用是虚拟化的,所以抖动也不能很容易地优化它。所以我们又进行了几纳秒的不必要的检查和暗示。更不用说我们需要检查数组是否为空,获取数组的长度,对数组的每个元素进行循环,等等。

这些费用非常小,但它们是每次连接,它们可能会增加实际花费的时间和内存压力。

很多程序的性能都取决于字符串操作和内存压力。我们如何消除或减少这些成本?

我们可以观察到大多数字符串连接都是两个字符串,因此创建一个专门处理这种情况的重载是有意义的:
static string Concat(string, string)
现在我们可以将上面的片段编码为:
string x = Foo();
string y = Bar();
string z = string.Concat(x, y);

现在没有创建数组,所以没有额外的垃圾创建,没有收集压力,参考图中没有新的边。在调用程序中,需要检查字符串是否为空,但我们不需要在实现中调用ToString,因为我们有类型系统来强制操作数已经是字符串,我们不需要检查数组是否为空,我们不需要根据数组长度检查循环变量,等等。

因此,我们有很好的理由使用两个重载:一个接受一个参数数组,另一个接受恰好两个字符串。

现在我们对另一个常见且性能更高的场景重复该分析。每个额外的过载都旨在为常见场景生成更有效的替代方案。随着越来越多的常见场景被识别出来,它们可以更快、更少地占用资源,因此有动机产生更多的重载,并修复编译器,以便它们生成利用这些重载的代码。最终的结果是大约十二个看似冗余的过载,每个都可以调优以获得高性能;这些包含了在实际程序中最常见的情况。

如果您对这个主题感兴趣,我已经写了一个简短的系列文章,介绍我在2006年如何重新设计字符串连接优化器。

https://ericlippert.com/2013/06/17/string-concatenation-behind-the-scenes-part-one/

(IEnumerable<String>)(IEnumerable<T>)的重载是不相等的。

  • IEnumerable<String>意味着任何字符串序列/列表都可以直接使用,而不需要产生任何运行时强制转换或字符串转换费用,并且通过使用此重载,调用者可以确保这一点。
  • IEnumerable<T> -也将是Object[]或任何不同类型序列的过载)。

虽然IEnumerable<T>更通用,但不幸的是,c#支持使用params的可变参数意味着它必须被键入为数组(例如String[]Object[]),因此必须分别添加第三和第四重载。

确实,params Object[]params String[]版本可以正确地代替Object arg0, Object arg1String arg0, String arg1重载,但是使用params意味着将在运行时分配一个新的数组,这是次优的,特别是在你想要最小化分配的闭环情况下;所以作为一个优化,如果你只有1、2、3或4个参数(这可能是95%的时间),那么这些参数可以在堆栈上传递。

巧合的是,几周前我问了一个类似的问题(关于params参数):c#中的"params"是否总是会在每次调用时分配一个新的数组?——这也讨论了为什么会有大量的重载。

如果c#编译器支持params IEnumerable(考虑到IEnumerable在c#中已经具有特权状态),并且JIT支持基于堆栈的可变参数而不是在堆上使用数组,那就太好了。