我刚刚注意到方法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“重复”重载,没有人会注意到这一点。
发布于 2016-11-10 21:30:12
执行决策的主要动机是绩效。
正如你正确地指出的,可能只有两个:
public static string Concat(params object[] args);
public static string Concat<T>(IEnumerable<T> values);如果C#实现了"params枚举“特性--即变量方法可以有一个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);所以:让我们回顾一下。据推测,调用每个分配一个字符串。然后,我们分配一个短暂的数组,复制对它的引用,将它传递给变量方法,等等。该方法需要编写来处理任意大小的数组,处理空数组,等等。
我们不仅在gen 0堆中添加了新的短命垃圾,还在活性分析图中创建了两个可能需要遍历的新边。我们可能通过增加压力来减少收藏之间的时间,或者通过增加边缘来增加收藏成本,或者,最有可能的是,收藏变得更加频繁和昂贵:双重打击。
但是等等还有更多。我们必须考虑被调用的Concat方法的实现情况。
对象数组是一个对象数组,而不是字符串数组。那我们需要做什么呢?被叫者需要将每个被调用者转换为一个字符串。通过在每一个上调用ToString?不,那可能会坠毁。首先检查null,然后调用ToString。
我们传递了弦乐,但被叫人不知道。ToString是字符串的标识,但是编译器不知道,并且调用是虚拟化的,因此抖动也不能轻松地优化它。因此,我们采取了另一个不必要的几纳秒的检查和间接。更不用说,我们需要检查数组是否为null,获取数组的长度,对数组的每个元素进行循环,以此类推。
这些费用非常小,但它们是每次串接,它们可以加起来的实时时间和记忆压力。
很多程序的性能取决于字符串操作和内存压力。我们如何才能消除或减轻这些成本?
我们可以观察到,大多数字符串连接是两个字符串,因此创建一个专门用于处理这种情况的重载是有意义的:
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/
发布于 2016-11-10 21:13:31
(IEnumerable<String>)和(IEnumerable<T>)重载不是等效的。
IEnumerable<String>意味着可以直接使用任何字符串序列/列表,而无需支付任何运行时强制转换或字符串转换费用,通过使用此重载,调用方可以得到保证。IEnumerable<T> -也是Object[]或任何不同类型的序列的重载)。虽然IEnumerable<T>更通用,但不幸的是,C#支持使用params的各种参数意味着必须将其作为数组(例如String[]或Object[])进行类型化,因此必须分别作为第三重载和第四重载添加。
诚然,params Object[]和params String[]版本可以正确地替代Object arg0, Object arg1和String arg0, String arg1重载,但是使用params意味着在运行时分配一个新的数组,这是次优的,特别是在希望最小化分配的紧循环情况下;因此,作为优化,如果您只有1、2、3或4个参数(可能是95%的时间),那么这些参数就可以传递到堆栈上。
巧合的是,几周前我问了一个类似的问题(关于params参数):在C#中,总是会在每次调用时分配一个新数组? --这也讨论了为什么会有大量的重载。
如果C#编译器支持params IEnumerable (考虑到IEnumerable在C#中已经具有特权状态),如果JIT支持基于堆栈的各种参数,而不是在堆上使用数组,那就更好了。
https://stackoverflow.com/questions/40528758
复制相似问题