我在自己的时间里一直在修补小函数,试图找到重构它们的方法(我最近阅读了Martin Fowler的书 Refactoring: Improving the Design of Existing Code )。我在更新它附近的代码库的另一部分时找到了以下函数MakeNiceString()
,它看起来像是一个很好的候选人。事实上,没有真正的理由来替换它,但是它足够小并且做了一些小的事情,因此它很容易理解,但仍能从中获得“良好”的体验。
private static string MakeNiceString(string str)
{
char[] ca = str.ToCharArray();
string result = null;
int i = 0;
result += System.Convert.ToString(ca[0]);
for (i = 1; i <= ca.Length - 1; i++)
{
if (!(char.IsLower(ca[i])))
{
result += " ";
}
result += System.Convert.ToString(ca[i]);
}
return result;
}
static string SplitCamelCase(string str)
{
string[] temp = Regex.Split(str, @"(?<!^)(?=[A-Z])");
string result = String.Join(" ", temp);
return result;
}
第一个函数MakeNiceString()
是我在工作中更新的一些代码中找到的函数。该函数的目的是将 ThisIsAString 转换为这是一个字符串。它在代码中的六个位置使用,并且在整个方案中非常微不足道。
我将第二个函数纯粹作为学术练习构建,以确定使用正则表达式是否需要更长时间。
嗯,结果如下:
有10次迭代:
MakeNiceString took 2649 ticks SplitCamelCase took 2502 ticks
然而,它在长途旅行中发生了巨大的变化:
10,000次迭代:
MakeNiceString took 121625 ticks SplitCamelCase took 443001 ticks
MakeNiceString()
重构
MakeNiceString()
的过程始于简单地删除正在发生的转化。这样做会产生以下结果:
MakeNiceString took 124716 ticks ImprovedMakeNiceString took 118486
这是重构#1之后的代码:
private static string ImprovedMakeNiceString(string str)
{ //Removed Convert.ToString()
char[] ca = str.ToCharArray();
string result = null;
int i = 0;
result += ca[0];
for (i = 1; i <= ca.Length - 1; i++)
{
if (!(char.IsLower(ca[i])))
{
result += " ";
}
result += ca[i];
}
return result;
}
StringBuilder
我的第二项任务是使用
StringBuilder
代替String
。由于String
是不可变的,因此在整个循环中创建了不必要的副本。使用它的基准如下:代码:
static string RefactoredMakeNiceString(string str)
{
char[] ca = str.ToCharArray();
StringBuilder sb = new StringBuilder((str.Length * 5 / 4));
int i = 0;
sb.Append(ca[0]);
for (i = 1; i <= ca.Length - 1; i++)
{
if (!(char.IsLower(ca[i])))
{
sb.Append(" ");
}
sb.Append(ca[i]);
}
return sb.ToString();
}
这导致以下基准:
MakeNiceString Took: 124497 Ticks //Original SplitCamelCase Took: 464459 Ticks //Regex ImprovedMakeNiceString Took: 117369 Ticks //Remove Conversion RefactoredMakeNiceString Took: 38542 Ticks //Using StringBuilder
将for
循环更改为foreach
循环会产生以下基准测试结果:
static string RefactoredForEachMakeNiceString(string str)
{
char[] ca = str.ToCharArray();
StringBuilder sb1 = new StringBuilder((str.Length * 5 / 4));
sb1.Append(ca[0]);
foreach (char c in ca)
{
if (!(char.IsLower(c)))
{
sb1.Append(" ");
}
sb1.Append(c);
}
return sb1.ToString();
}
RefactoredForEachMakeNiceString Took: 45163 Ticks
正如您所看到的那样,维护方面,foreach
循环将是最容易维护的并且具有“最干净”的外观。它比for
循环略慢,但更容易遵循。
Regex
我在循环开始之前将正则表达式移到了右边,希望因为它只编译一次,它会执行得更快。我发现的(我确定我在某个地方有一个错误)是不会发生这样的事情:
static void runTest5()
{
Regex rg = new Regex(@"(?<!^)(?=[A-Z])", RegexOptions.Compiled);
for (int i = 0; i < 10000; i++)
{
CompiledRegex(rg, myString);
}
}
static string CompiledRegex(Regex regex, string str)
{
string result = null;
Regex rg1 = regex;
string[] temp = rg1.Split(str);
result = String.Join(" ", temp);
return result;
}
MakeNiceString Took 139363 Ticks SplitCamelCase Took 489174 Ticks ImprovedMakeNiceString Took 115478 Ticks RefactoredMakeNiceString Took 38819 Ticks RefactoredForEachMakeNiceString Took 44700 Ticks CompiledRegex Took 227021 Ticks
或者,如果您更喜欢毫秒:
MakeNiceString Took 38 ms SplitCamelCase Took 123 ms ImprovedMakeNiceString Took 33 ms RefactoredMakeNiceString Took 11 ms RefactoredForEachMakeNiceString Took 12 ms CompiledRegex Took 63 ms
所以百分比增益是:
MakeNiceString 38 ms Baseline SplitCamelCase 123 ms 223% slower ImprovedMakeNiceString 33 ms 13.15% faster RefactoredMakeNiceString 11 ms 71.05% faster RefactoredForEachMakeNiceString 12 ms 68.42% faster CompiledRegex 63 ms 65.79% slower
(请检查我的数学)
最后,我将用RefactoredForEachMakeNiceString()
替换那里的内容,而我正在使用它,我会将其重命名为有用的内容,例如SplitStringOnUpperCase
。
要进行基准测试,我只需为每个方法调用调用一个新的Stopwatch
:
string myString = "ThisIsAUpperCaseString";
Stopwatch sw = new Stopwatch();
sw.Start();
runTest();
sw.Stop();
static void runTest()
{
for (int i = 0; i < 10000; i++)
{
MakeNiceString(myString);
}
}
感谢您迄今为止的回复。我已经插入了@Jon Skeet提出的所有建议,并希望得到关于我已经提出的更新问题的反馈。
NB :这个问题旨在探索在C#中重构字符串处理函数的方法。我复制/粘贴了第一个代码
as is
。我很清楚你可以删除第一种方法中的System.Convert.ToString()
,我就是这么做的。如果有人意识到删除System.Convert.ToString()
的任何影响,那么知道也会有所帮助。
答案 0 :(得分:17)
1)使用StringBuilder,最好设置合理的初始容量(例如字符串长度* 5/4,每四个字符允许一个额外的空格)。
2)尝试使用foreach循环而不是for循环 - 它可能更简单
3)您不需要首先将字符串转换为字符数组 - foreach将在字符串上工作,或者使用索引器。
4)不要在任何地方进行额外的字符串转换 - 调用Convert.ToString(char)然后附加该字符串是没有意义的;不需要单个字符串
5)对于第二个选项,只需在方法之外构建一次正则表达式。尝试使用RegexOptions.Compiled。
编辑:好的,完整的基准测试结果。我已经尝试了一些其他的东西,并且还使用相当多的迭代执行代码以获得更准确的结果。这只能在Eee PC上运行,所以毫无疑问它会在“真正的”PC上运行得更快,但我怀疑广泛的结果是合适的。首先是代码:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
class Benchmark
{
const string TestData = "ThisIsAUpperCaseString";
const string ValidResult = "This Is A Upper Case String";
const int Iterations = 1000000;
static void Main(string[] args)
{
Test(BenchmarkOverhead);
Test(MakeNiceString);
Test(ImprovedMakeNiceString);
Test(RefactoredMakeNiceString);
Test(MakeNiceStringWithStringIndexer);
Test(MakeNiceStringWithForeach);
Test(MakeNiceStringWithForeachAndLinqSkip);
Test(MakeNiceStringWithForeachAndCustomSkip);
Test(SplitCamelCase);
Test(SplitCamelCaseCachedRegex);
Test(SplitCamelCaseCompiledRegex);
}
static void Test(Func<string,string> function)
{
Console.Write("{0}... ", function.Method.Name);
Stopwatch sw = Stopwatch.StartNew();
for (int i=0; i < Iterations; i++)
{
string result = function(TestData);
if (result.Length != ValidResult.Length)
{
throw new Exception("Bad result: " + result);
}
}
sw.Stop();
Console.WriteLine(" {0}ms", sw.ElapsedMilliseconds);
GC.Collect();
}
private static string BenchmarkOverhead(string str)
{
return ValidResult;
}
private static string MakeNiceString(string str)
{
char[] ca = str.ToCharArray();
string result = null;
int i = 0;
result += System.Convert.ToString(ca[0]);
for (i = 1; i <= ca.Length - 1; i++)
{
if (!(char.IsLower(ca[i])))
{
result += " ";
}
result += System.Convert.ToString(ca[i]);
}
return result;
}
private static string ImprovedMakeNiceString(string str)
{ //Removed Convert.ToString()
char[] ca = str.ToCharArray();
string result = null;
int i = 0;
result += ca[0];
for (i = 1; i <= ca.Length - 1; i++)
{
if (!(char.IsLower(ca[i])))
{
result += " ";
}
result += ca[i];
}
return result;
}
private static string RefactoredMakeNiceString(string str)
{
char[] ca = str.ToCharArray();
StringBuilder sb = new StringBuilder((str.Length * 5 / 4));
int i = 0;
sb.Append(ca[0]);
for (i = 1; i <= ca.Length - 1; i++)
{
if (!(char.IsLower(ca[i])))
{
sb.Append(" ");
}
sb.Append(ca[i]);
}
return sb.ToString();
}
private static string MakeNiceStringWithStringIndexer(string str)
{
StringBuilder sb = new StringBuilder((str.Length * 5 / 4));
sb.Append(str[0]);
for (int i = 1; i < str.Length; i++)
{
char c = str[i];
if (!(char.IsLower(c)))
{
sb.Append(" ");
}
sb.Append(c);
}
return sb.ToString();
}
private static string MakeNiceStringWithForeach(string str)
{
StringBuilder sb = new StringBuilder(str.Length * 5 / 4);
bool first = true;
foreach (char c in str)
{
if (!first && char.IsUpper(c))
{
sb.Append(" ");
}
sb.Append(c);
first = false;
}
return sb.ToString();
}
private static string MakeNiceStringWithForeachAndLinqSkip(string str)
{
StringBuilder sb = new StringBuilder(str.Length * 5 / 4);
sb.Append(str[0]);
foreach (char c in str.Skip(1))
{
if (char.IsUpper(c))
{
sb.Append(" ");
}
sb.Append(c);
}
return sb.ToString();
}
private static string MakeNiceStringWithForeachAndCustomSkip(string str)
{
StringBuilder sb = new StringBuilder(str.Length * 5 / 4);
sb.Append(str[0]);
foreach (char c in new SkipEnumerable<char>(str, 1))
{
if (char.IsUpper(c))
{
sb.Append(" ");
}
sb.Append(c);
}
return sb.ToString();
}
private static string SplitCamelCase(string str)
{
string[] temp = Regex.Split(str, @"(?<!^)(?=[A-Z])");
string result = String.Join(" ", temp);
return result;
}
private static readonly Regex CachedRegex = new Regex("(?<!^)(?=[A-Z])");
private static string SplitCamelCaseCachedRegex(string str)
{
string[] temp = CachedRegex.Split(str);
string result = String.Join(" ", temp);
return result;
}
private static readonly Regex CompiledRegex =
new Regex("(?<!^)(?=[A-Z])", RegexOptions.Compiled);
private static string SplitCamelCaseCompiledRegex(string str)
{
string[] temp = CompiledRegex.Split(str);
string result = String.Join(" ", temp);
return result;
}
private class SkipEnumerable<T> : IEnumerable<T>
{
private readonly IEnumerable<T> original;
private readonly int skip;
public SkipEnumerable(IEnumerable<T> original, int skip)
{
this.original = original;
this.skip = skip;
}
public IEnumerator<T> GetEnumerator()
{
IEnumerator<T> ret = original.GetEnumerator();
for (int i=0; i < skip; i++)
{
ret.MoveNext();
}
return ret;
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
现在结果:
BenchmarkOverhead... 22ms
MakeNiceString... 10062ms
ImprovedMakeNiceString... 12367ms
RefactoredMakeNiceString... 3489ms
MakeNiceStringWithStringIndexer... 3115ms
MakeNiceStringWithForeach... 3292ms
MakeNiceStringWithForeachAndLinqSkip... 5702ms
MakeNiceStringWithForeachAndCustomSkip... 4490ms
SplitCamelCase... 68267ms
SplitCamelCaseCachedRegex... 52529ms
SplitCamelCaseCompiledRegex... 26806ms
正如您所看到的,字符串索引器版本是赢家 - 它也是非常简单的代码。
希望这会有所帮助......不要忘记,我一直没想到其他选择!
答案 1 :(得分:3)
您可能希望尝试将Regex
对象实例化为类成员,并在创建时使用RegexOptions.Compiled
选项。
目前,您正在使用Split
的静态Regex
成员,并且不会缓存正则表达式。使用实例成员对象而不是静态方法应该可以进一步提高性能(从长远来看)。
答案 2 :(得分:2)
使用StringBuilder
代替连接。每个连接都在创建一个新的字符串实例并丢弃旧的。
答案 3 :(得分:2)
尝试重构,以便在第二种方法中用于拆分字符串的正则表达式存储在静态方法中,并使用RegexOptions.Compiled选项构建。有关此内容的更多信息:http://msdn.microsoft.com/en-us/library/8zbs0h2f.aspx。
我没有测试这个理论,但我想每次重新创建正则表达式都会非常费时。
答案 4 :(得分:2)
这是对ctacke对Jon Skeet的回答的评论的回应(评论的时间不长)
我一直以为foreach很漂亮 众所周知,比一个慢 循环,因为它必须使用迭代器。
实际上,不,在这种情况下,foreach会更快。索引访问是边界检查(即,我在循环中检查三次进入范围:一次在for()中,一次在两次ca [i]中),这使得for循环比foreach慢。
如果C#编译器检测到特定语法:
for(i = 0; i < ca.Length; i++)
然后它将执行临时优化,删除内部边界检查,使for()循环更快。但是,因为在这里我们必须将ca [0]视为特殊情况(为了防止输出上的前导空格),我们无法触发优化。
答案 5 :(得分:2)
我知道他们对RegEx的看法,用它来解决问题,现在你有两个问题,但我仍然是粉丝,只是为了笑,这是一个RegEx版本。 RegEx,带有一点启动,易于阅读,代码更少,并且可以让您轻松捕捉其他分隔符(就像我使用逗号一样)。
s1 = MakeNiceString( "LookOut,Momma,There'sAWhiteBoatComingUpTheRiver" ) );
private string MakeNiceString( string input )
{
StringBuilder sb = new StringBuilder( input );
int Incrementer = 0;
MatchCollection mc;
const string SPACE = " ";
mc = Regex.Matches( input, "[A-Z|,]" );
foreach ( Match m in mc )
{
if ( m.Index > 0 )
{
sb.Insert( m.Index + Incrementer, SPACE );
Incrementer++;
}
}
return sb.ToString().TrimEnd();
}
答案 6 :(得分:2)
我的第一次重构是将方法的名称更改为更具描述性的名称。 MakeNiceString imo不是一个能告诉我这个方法有什么用的名字。
PascalCaseToSentence怎么样?不喜欢这个名字,但它比MakeNiceString更好。
答案 7 :(得分:1)
这是一个稍微优化的版本。我从以前的海报中获取了建议,但也以块方式附加到字符串构建器。这可能允许字符串构建器一次复制4个字节,具体取决于字的大小。我还删除了字符串分配,只需用str.length替换它。
static string RefactoredMakeNiceString2(string str)
{
char[] ca = str.ToCharArray();
StringBuilder sb = new StringBuilder(str.Length);
int start = 0;
for (int i = 0; i < ca.Length; i++)
{
if (char.IsUpper(ca[i]) && i != 0)
{
sb.Append(ca, start, i - start);
sb.Append(' ');
start = i;
}
}
sb.Append(ca, start, ca.Length - start);
return sb.ToString();
}
答案 8 :(得分:0)
您的解决方案的正则表达式版本与原始代码的结果不相同。也许代码的较大上下文避免了它们不同的区域。原始代码将为任何非小写字符的内容添加空格。例如,"This\tIsATest"
在原始版本中会变为"This \t Is A Test"
,但在"This\t Is A Test"
中会变为正则表达式版本。
(?<!^)(?=[^a-z])
你想要的模式是否更接近,但即便如此,它仍然忽略了i18n的问题。以下模式应该注意:
(?<!^)(?=\P{Ll})
答案 9 :(得分:-2)
在C#中(.Net,真的)当你追加一个字符串时,后台会发生一些事情。现在,我忘记了具体细节,但它类似于:
string A = B + C;
A += D;
A += E;
// ... rinse-repeat for 10,000 iterations
对于上面的每一行,.NET将: 1)为A分配一些新的内存。 2)将字符串B复制到新内存中。 3)扩展内存以保持C. 4)将字符串C附加到A。
字符串A越长,花费的时间就越长。除此之外,执行此操作的次数越多,A获得的时间越长,所需的时间就越长。
但是,使用StringBuilder,您不会分配新内存,因此您可以跳过该问题。
如果你说:
StringBuilder A = new StringBuilder();
A.Append(B);
A.Append(C);
// .. rinse/repeat for 10,000 times...
string sA = A.ToString();
StringBuilder A = new StringBuilder();
A.Append(B);
A.Append(C);
// .. rinse/repeat for 10,000 times...
string sA = A.ToString();
StringBuilder(编辑:固定描述)在内存中有一个字符串。它不需要为每个添加的子字符串重新分配整个字符串。当您发出ToString()时,该字符串已经以正确的格式附加。
一次拍摄而不是一个需要更长时间的循环。
我希望这有助于回答为什么花费的时间少得多。