.NET中删除空白字符串的10大方法

我们有无数方法可用于删除字符串中的所有空白,但是哪个更快呢?

介绍

我们有无数方法可用于删除字符串中的所有空白。大部分都能够在绝大多数的用例中很好工作,但在某些对时间敏感的应用程序中,是否采用最快的方法可能就会造成天壤之别。

如果你问空白是什么,那说起来还真是有些乱。许多人认为空白就是SPACE 字符(UnicodeU+0020,ASCII 32,HTML ),但它实际上还包括使得版式水平和垂直出现空格的所有字符。事实上,这是一整类定义为Unicode字符数据库的字符。

本文所说的空白,不但指的是它的正确定义,同时也包括string.Replace(” “, “”)方法。

这里的基准方法,将删除所有头尾和中间的空白。这就是文章标题中“所有空白”的含义。

.NET中删除空白字符串的10大方法

背景

这篇文章一开始是出于我的好奇心。事实上,我并不需要用最快的算法来删除字符串中的空白。

检查空白字符

检查空白字符很简单。所有你需要的代码就是:


  1. char wp = ' '
  2. char a = 'a'
  3. Assert.True(char.IsWhiteSpace(wp)); 
  4. Assert.False(char.IsWhiteSpace(a)); 

但是,当我实现手动优化删除方法时,我意识到这并不像预期得那么好。一些源代码在微软的参考源代码库的char.cs挖掘找到:


  1. public static bool IsWhiteSpace(char c) { 
  2.     if (IsLatin1(c)) { 
  3.         return (IsWhiteSpaceLatin1(c)); 
  4.     } 
  5.     return CharUnicodeInfo.IsWhiteSpace(c); 
  6.  
  7. 然后CharUnicodeInfo.IsWhiteSpace成了: 
  8.  
  9. internal static bool IsWhiteSpace(char c) 
  10.     UnicodeCategory uc = GetUnicodeCategory(c); 
  11.     // In Unicode 3.0, U+2028 is the only character which is under the category "LineSeparator". 
  12.     // And U+2029 is th eonly character which is under the category "ParagraphSeparator". 
  13.     switch (uc) { 
  14.         case (UnicodeCategory.SpaceSeparator): 
  15.         case (UnicodeCategory.LineSeparator): 
  16.         case (UnicodeCategory.ParagraphSeparator): 
  17.             return (true); 
  18.     } 
  19.  
  20.     return (false); 

GetUnicodeCategory()方法调用InternalGetUnicodeCategory()方法,而且实际上相当快,但现在我们依次已经有了4个方法调用!以下这段代码是由一位评论者提供的,可用于快速实现定制版本和JIT默认内联:


  1. // whitespace detection method: very fast, a lot faster than Char.IsWhiteSpace 
  2. [MethodImpl(MethodImplOptions.AggressiveInlining)] // if it's not inlined then it will be slow!!! 
  3. public static bool isWhiteSpace(char ch) { 
  4.     // this is surprisingly faster than the equivalent if statement 
  5.     switch (ch) { 
  6.         case '\u0009'case '\u000A'case '\u000B'case '\u000C'case '\u000D'
  7.         case '\u0020'case '\u0085'case '\u00A0'case '\u1680'case '\u2000'
  8.         case '\u2001'case '\u2002'case '\u2003'case '\u2004'case '\u2005'
  9.         case '\u2006'case '\u2007'case '\u2008'case '\u2009'case '\u200A'
  10.         case '\u2028'case '\u2029'case '\u202F'case '\u205F'case '\u3000'
  11.             return true
  12.         default
  13.             return false
  14.     } 

删除字符串的不同方法

我用各种不同的方法来实现删除字符串中的所有空白。

分离合并法

这是我一直在用的一个非常简单的方法。根据空格字符分离字符串,但不包括空项,然后将产生的碎片重新合并到一起。这方法听上去有点傻乎乎的,而事实上,乍一看,很像是一个非常浪费的解决方式:


  1. public static string TrimAllWithSplitAndJoin(string str) { 
  2.     return string.Concat(str.Split(default(string[]), StringSplitOptions.RemoveEmptyEntries)); 

LINQ

这是优雅地声明式地实现这个过程的方法:


  1. public static string TrimAllWithLinq(string str) { 
  2.     return new string(str.Where(c => !isWhiteSpace(c)).ToArray()); 

正则表达式

正则表达式是非常强大的力量,任何程序员都应该意识到这一点。


  1. static Regex whitespace = new Regex(@"\s+", RegexOptions.Compiled); 
  2.  
  3. public static string TrimAllWithRegex(string str) { 
  4.     return whitespace.Replace(str, ""); 

字符数组原地转换法

该方法将输入的字符串转换成字符数组,然后原地扫描字符串去除空白字符(不创建中间缓冲区或字符串)。最后,经过“删减”的数组会产生新的字符串。


  1. public static string TrimAllWithInplaceCharArray(string str) { 
  2.     var len = str.Length; 
  3.     var src = str.ToCharArray(); 
  4.     int dstIdx = 0
  5.     for (int i = 0; i < len; i++) { 
  6.         var ch = src[i]; 
  7.         if (!isWhiteSpace(ch)) 
  8.             src[dstIdx++] = ch; 
  9.     } 
  10.     return new string(src, 0, dstIdx); 

字符数组复制法

这种方法类似于字符数组原地转换法,但它使用Array.Copy复制连续非空白“字符串”的同时跳过空格。最后,它将创建一个适当尺寸的字符数组,并用相同的方式返回一个新的字符串。


  1. public static string TrimAllWithCharArrayCopy(string str) { 
  2.     var len = str.Length; 
  3.     var src = str.ToCharArray(); 
  4.     int srcIdx = 0, dstIdx = 0, count = 0
  5.     for (int i = 0; i < len; i++) { 
  6.         if (isWhiteSpace(src[i])) { 
  7.             count = i - srcIdx; 
  8.             Array.Copy(src, srcIdx, src, dstIdx, count); 
  9.             srcIdx += count + 1
  10.             dstIdx += count; 
  11.             len--; 
  12.         } 
  13.     } 
  14.     if (dstIdx < len) 
  15.         Array.Copy(src, srcIdx, src, dstIdx, len - dstIdx); 
  16.     return new string(src, 0, len); 

循环交换法

用代码实现循环,并使用StringBuilder类,通过依靠StringBuilder的内在优化来创建新的字符串。为了避免任何其他因素对本实施产生干扰,不调用其他的方法,并且通过缓存到本地变量避免访问类成员。最后通过设置StringBuilder.Length将缓冲区调整到合适大小。


  1. // Code suggested by http://www.codeproject.com/Members/TheBasketcaseSoftware 
  2. public static string TrimAllWithLexerLoop(string s) { 
  3.     int length = s.Length; 
  4.     var buffer = new StringBuilder(s); 
  5.     var dstIdx = 0; 
  6.     for (int index = 0; index < s.Length; index++) { 
  7.         char ch = s[index]; 
  8.         switch (ch) { 
  9.             case '\u0020'case '\u00A0'case '\u1680'case '\u2000'case '\u2001'
  10.             case '\u2002'case '\u2003'case '\u2004'case '\u2005'case '\u2006'
  11.             case '\u2007'case '\u2008'case '\u2009'case '\u200A'case '\u202F'
  12.             case '\u205F'case '\u3000'case '\u2028'case '\u2029'case '\u0009'
  13.             case '\u000A'case '\u000B'case '\u000C'case '\u000D'case '\u0085'
  14.                 length--; 
  15.                 continue
  16.             default
  17.                 break
  18.         } 
  19.         buffer[dstIdx++] = ch; 
  20.     } 
  21.     buffer.Length = length; 
  22.     return buffer.ToString();; 

循环字符法

这种方法几乎和前面的循环交换法相同,不过它采用if语句来调用isWhiteSpace(),而不是乱七八糟的switch伎俩 :)。


  1. public static string TrimAllWithLexerLoopCharIsWhitespce(string s) { 
  2.     int length = s.Length; 
  3.     var buffer = new StringBuilder(s); 
  4.     var dstIdx = 0
  5.     for (int index = 0; index < s.Length; index++) { 
  6.         char currentchar = s[index]; 
  7.         if (isWhiteSpace(currentchar)) 
  8.             length--; 
  9.         else 
  10.             buffer[dstIdx++] = currentchar; 
  11.     } 
  12.     buffer.Length = length; 
  13.     return buffer.ToString();; 

原地改变字符串法(不安全)

这种方法使用不安全的字符指针和指针运算来原地改变字符串。我不推荐这个方法,因为它打破了.NET框架在生产中的基本约定:字符串是不可变的。


  1. public static unsafe string TrimAllWithStringInplace(string str) { 
  2.     fixed (char* pfixed = str) { 
  3.         char* dst = pfixed; 
  4.         for (char* p = pfixed; *p != 0; p++) 
  5.             if (!isWhiteSpace(*p)) 
  6.                 *dst++ = *p; 
  7.  
  8.         /*// reset the string size 
  9.             * ONLY IT DIDN'T WORK! A GARBAGE COLLECTION ACCESS VIOLATION OCCURRED AFTER USING IT 
  10.             * SO I HAD TO RESORT TO RETURN A NEW STRING INSTEAD, WITH ONLY THE PERTINENT BYTES 
  11.             * IT WOULD BE A LOT FASTER IF IT DID WORK THOUGH... 
  12.         Int32 len = (Int32)(dst - pfixed); 
  13.         Int32* pi = (Int32*)pfixed; 
  14.         pi[-1] = len; 
  15.         pfixed[len] = '\0';*/ 
  16.         return new string(pfixed, 0, (int)(dst - pfixed)); 
  17.     } 

原地改变字符串法V2(不安全)

这种方法几乎和前面那个相同,不过此处使用类似数组的指针访问。我很好奇,不知道这两种哪种存储访问会更快。


  1. public static unsafe string TrimAllWithStringInplaceV2(string str) { 
  2.     var len = str.Length; 
  3.     fixed (char* pStr = str) { 
  4.         int dstIdx = 0
  5.         for (int i = 0; i < len; i++) 
  6.             if (!isWhiteSpace(pStr[i])) 
  7.                 pStr[dstIdx++] = pStr[i]; 
  8.         // since the unsafe string length reset didn't work we need to resort to this slower compromise 
  9.         return new string(pStr, 0, dstIdx); 
  10.     } 

String.Replace(“”,“”)

这种实现方法很天真,由于它只替换空格字符,所以它不使用空白的正确定义,因此会遗漏很多其他的空格字符。虽然它应该算是本文中最快的方法,但功能不及其他。

但如果你只需要去掉真正的空格字符,那就很难用纯.NET写出胜过string.Replace的代码。大多数字符串方法将回退到手动优化本地C ++代码。而String.Replace本身将用comstring.cpp调用C ++方法:


  1. FCIMPL3(Object*, 
  2.     COMString::ReplaceString, 
  3.     StringObject* thisRefUNSAFE, 
  4.     StringObject* oldValueUNSAFE, 
  5.     StringObject* newValueUNSAFE) 

下面是基准测试套件方法:


  1. public static string TrimAllWithStringReplace(string str) { 
  2.     // This method is NOT functionaly equivalent to the others as it will only trim "spaces" 
  3.     // Whitespace comprises lots of other characters 
  4.     return str.Replace(" """); 

许可证

这篇文章,以及任何相关的源代码和文件,依据The Code Project Open License (CPOL)的许可。


作者:小峰

来源:51CTO

上一篇:Android内存泄漏检测工具:LeakCanary


下一篇:C# String 前面不足位数补零的方法