65.9K
CodeProject 正在变化。 阅读更多。
Home

在 .NET 中移除字符串所有空白字符的最快方法

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (41投票s)

2015年7月30日

CPOL

16分钟阅读

viewsIcon

234684

downloadIcon

729

有无数种方法可以从字符串中移除所有空白字符,但哪一种最快呢?

引言

有无数种方法可以从字符串中移除所有空白字符。大多数方法对于绝大多数用例都能完美工作,但在某些对时间敏感的应用程序中,选择最快的方法可能会带来天壤之别(当然,如果你不是一个夜猫子的话)。

关于空白字符到底是什么,存在一些困惑。许多人认为空白字符等同于 `SPACE` 字符(Unicode `U+0020`,ASCII `32`,HTML ` `),但实际上它包括所有在排版中用于提供水平或垂直空间的字符。事实上,正如 Unicode 字符数据库所定义的那样,它是一个完整的字符类别。更多信息可以在 空白字符 的维基百科条目中找到。

本文使用了空白字符的正确定义,同时还包含了 `string.Replace(" ", "")` 方法进行比较,以展示天真的实现(尽管这对其他正确处理空白字符而非仅仅是空格的方法不公平)。

本文测试的方法将移除所有字符串开头、结尾以及中间的空白字符。这正是标题中“所有空白字符”的含义。 :-)

注意:我已将文章标题从“...移除字符串所有空白字符...”改为“...移除字符串所有空白字符...”是因为有些读者在理解“trim”一词时遇到了困难。对他们来说,“trim”的意思仅仅是移除边缘空白,虽然字典的解释不同,但如果能让事情更清楚,何乐而不为呢?(顺便说一句,电影行业在“trim”一词的使用上与我原标题中的意思完全相同:移除电影胶片的边缘中间部分)。

背景

本文实际上源于我的好奇心。我根本不需要最快的算法来移除字符串中的所有空白字符,任何能正确工作的方法都可以,但当我开始在 Stack Overflow 上看到文章建议使用正则表达式,并且可能会比其他大多数方法更快时,我感到很惊讶。“因为微软肯定对它进行了速度优化”,“它在处理更大的数据集时效果最好”等等。我不相信,所以我选择通过实验来验证(对于好奇的人来说,引发我这个想法的 SO 问题之一是:移除字符串所有空白字符的有效方法?

检查空白字符

检查空白字符再简单不过了。只需要

char wp = ' ';
char a = 'a';
Assert.True(char.IsWhiteSpace(wp));
Assert.False(char.IsWhiteSpace(a));

但当我实现手动优化的移除空白方法时,我意识到它的性能并不如预期。稍作源码挖掘,发现在微软的引用源代码库的 `char.cs` 中有这段代码:

public static bool IsWhiteSpace(char c) {
    if (IsLatin1(c)) {
        return (IsWhiteSpaceLatin1(c));
    }
    return CharUnicodeInfo.IsWhiteSpace(c);
}

然后 `CharUnicodeInfo.IsWhiteSpace` 实际上是这样的:

internal static bool IsWhiteSpace(char c)
{
    UnicodeCategory uc = GetUnicodeCategory(c);
    // In Unicode 3.0, U+2028 is the only character which is under the category "LineSeparator".
    // And U+2029 is th eonly character which is under the category "ParagraphSeparator".
    switch (uc) {
        case (UnicodeCategory.SpaceSeparator):
        case (UnicodeCategory.LineSeparator):
        case (UnicodeCategory.ParagraphSeparator):
            return (true);
    }
 
    return (false);
}

`GetUnicodeCategory()` 方法调用 `InternalGetUnicodeCategory()` 方法,实际上相当快,但现在我们已经连续调用了 4 个方法(JIT 肯定会内联其中一些,但……)!根据一位评论者的建议,我实现了一个自定义版本,旨在非常快速且默认由 JIT 内联。

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

移除字符串的不同方法

我实现了各种不同的方法来移除字符串中的所有空白字符(这包括中间的空白字符)。

拆分并连接

这是我多年来使用的一个非常简单的方法。你将字符串按空白字符拆分(不包含空条目),然后将结果片段重新连接起来。这听起来不是很聪明,事实上,乍一看,这似乎是一种非常浪费的解决问题的方式。

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

LINQ

这是以近乎声明式的方式实现此过程的一个非常优雅的方法。

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

正则表达式

有时候我觉得可以用一个巨大的正则表达式来写完Titanfall! :-) 它是任何程序员都应该知道的最强大的工具之一。另一方面,也许我们想要实现的目标如此简单,以至于我们会为如此基本的事情浪费很多能力。

static Regex whitespace = new Regex(@"\s+", RegexOptions.Compiled);

public static string TrimAllWithRegex(string str) {
    return whitespace.Replace(str, "");
}

注意:我将“空白字符” `Regex` 对象缓存到方法外部,以便重复使用。每次实例化一个新的 `Regex` 对象都有相当大的开销。Regex 还使用 `RegexOptions.Compiled` 参数创建,这极大地加快了速度!

原地字符数组

此方法将输入字符串转换为字符数组,然后扫描字符串,就地移除空白字符(不创建中间缓冲区或字符串)。最后,从“修剪后”的数组创建一个新字符串,因为就地替换字符可能会在字符串末尾留下空白字符。

public static string TrimAllWithInplaceCharArray(string str) {
    var len = str.Length;
    var src = str.ToCharArray();
    int dstIdx = 0;
    for (int i = 0; i < len; i++) {
        var ch = src[i];
        if (!isWhiteSpace(ch))
            src[dstIdx++] = ch;
    }
    return new string(src, 0, dstIdx);
}

注意:如果有一种方法可以就地更改 .NET 数组的长度,我相信此方法可以更快,因为它无需创建新数组并在最后调用 `Array.Copy`。 事实证明,我可以使用一个接受字符数组、索引和字符计数的新字符串构造函数,而无需创建中间数组!这显著影响了此方法(以及 **字符数组复制** 方法)的速度。

字符数组复制

此方法与“原地字符数组”方法类似,但它使用 `Array.Copy` 来复制连续的非空白字符“字符串”,同时跳过空白字符,实际上它几乎是“原地字符数组”方法的反向版本。最后,它创建一个大小合适的字符数组,并以相同的方式从中返回一个新字符串。

public static string TrimAllWithCharArrayCopy(string str) {
    var len = str.Length;
    var src = str.ToCharArray();
    int srcIdx = 0, dstIdx = 0, count = 0;
    for (int i = 0; i < len; i++) {
        if (isWhiteSpace(src[i])) {
            count = i - srcIdx;
            Array.Copy(src, srcIdx, src, dstIdx, count);
            srcIdx += count + 1;
            dstIdx += count;
            len--;
        }
    }
    if (dstIdx < len)
        Array.Copy(src, srcIdx, src, dstIdx, len - dstIdx);
    return new string(src, 0, len);
}

词法分析器循环开关

此代码实现了一个循环,并使用 `StringBuilder` 类来尝试创建新字符串,依赖于 `StringBuilder` 的固有优化。为了避免任何其他因素干扰此实现,不进行其他方法调用,并通过缓存到局部变量来避免类成员访问。最后,通过设置 `StringBuilder.Length` 将缓冲区调整到合适的长度。

// Code suggested by https://codeproject.org.cn/Members/TheBasketcaseSoftware
public static string TrimAllWithLexerLoop(string s) {
    int length = s.Length;
    var buffer = new StringBuilder(s);
    var dstIdx = 0;
    for (int index = 0; index < s.Length; index++) {
        char ch = s[index];
        switch (ch) {
            case '\u0020': case '\u00A0': case '\u1680': case '\u2000': case '\u2001':
            case '\u2002': case '\u2003': case '\u2004': case '\u2005': case '\u2006':
            case '\u2007': case '\u2008': case '\u2009': case '\u200A': case '\u202F':
            case '\u205F': case '\u3000': case '\u2028': case '\u2029': case '\u0009':
            case '\u000A': case '\u000B': case '\u000C': case '\u000D': case '\u0085':
                length--;
                continue;
            default:
                break;
        }
        buffer[dstIdx++] = ch;
    }
    buffer.Length = length;
    return buffer.ToString();;
}

注意:此代码由成员 Basketcase Software 在评论中建议,并尽可能忠实于其原始意图实现。未进行进一步优化。方法名称来源于算法最初存在某种词法范围,但在修复一些错误后,词法范围实际上不再需要了,但名称保留了下来,而且听起来不错!(也许应该改为“原地 StringBuilder”……我持开放态度!)

词法分析器循环字符

此方法与之前的词法分析器循环方法(几乎)相同,但它使用一个带有 `isWhiteSpace()` 调用 的 `if` 语句,而不是肮脏的 `switch` 技巧 :-)。它应该执行得完全相同。如果不是,则可能是由于 `if` 语句或方法调用(尽管 JIT 几乎肯定会内联它)。

public static string TrimAllWithLexerLoopCharIsWhitespce(string s) {
    int length = s.Length;
    var buffer = new StringBuilder(s);
    var dstIdx = 0;
    for (int index = 0; index < s.Length; index++) {
        char currentchar = s[index];
        if (isWhiteSpace(currentchar))
            length--;
        else
            buffer[dstIdx++] = currentchar;
    }
    buffer.Length = length;
    return buffer.ToString();;
}

字符串原地(不安全)

此方法使用不安全字符指针和指针算术来就地修改一个原本不可变的字符串。不建议在生产环境中使用此方法,因为它打破了 .NET 框架的一个基本约定:字符串是不可变的。我试图更深入地操作,实际上在不实例化新字符串的情况下更改字符串的逻辑长度,通过访问内部原生 `string` 结构中的私有 `m_stringLength` 字段,但这导致在 Win64 操作系统上的垃圾回收器中发生后续的访问冲突(我认为它会在 Windows 的 Win32 版本上工作)。

public static unsafe string TrimAllWithStringInplace(string str) {
    fixed (char* pfixed = str) {
        char* dst = pfixed;
        for (char* p = pfixed; *p != 0; p++)
            if (!isWhiteSpace(*p))
                *dst++ = *p;

        /*// reset the string size
            * ONLY IT DIDN'T WORK! A GARBAGE COLLECTION ACCESS VIOLATION OCCURRED AFTER USING IT
            * SO I HAD TO RESORT TO RETURN A NEW STRING INSTEAD, WITH ONLY THE PERTINENT BYTES
            * IT WOULD BE A LOT FASTER IF IT DID WORK THOUGH...
        Int32 len = (Int32)(dst - pfixed);
        Int32* pi = (Int32*)pfixed;
        pi[-1] = len;
        pfixed[len] = '\0';*/
        return new string(pfixed, 0, (int)(dst - pfixed));
    }
}

注意:我将上面注释掉的“原地”字符串长度重置尝试留给好奇的人!

字符串原地 V2(不安全)

此方法几乎与前一种方法相同,但使用了类似数组的指针访问。我想知道哪种内存访问方式更快。

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

String.Replace("", "")

这是一个天真的实现,它不使用空白字符的正确定义,因为它只替换空格字符,遗漏了许多其他空白字符。它应该是这里最快的方法,但它在功能上并不等同于其他方法。即使它在数量级上优于其他任何方法(它并没有),这种比较也是不公平的。

另一方面,如果你真正需要的是移除空格字符,那么在纯 .NET 中编写任何能够优于 `string.Replace` 的代码将非常非常困难。 .NET 工程师非常(我指的是非常)努力地确保字符串在框架中的性能表现非凡。大多数字符串方法都会回退到手动优化的 C++ 代码。String.Replace 本身会调用 `comstring.cpp` 中的一个 C++ 方法,该方法签名如下(基于 SSCLI2.0 源代码):

FCIMPL3(Object*, 
    COMString::ReplaceString, 
    StringObject* thisRefUNSAFE, 
    StringObject* oldValueUNSAFE, 
    StringObject* newValueUNSAFE)

然后诉诸于各种肮脏的 :-) 指针和内存访问技巧来使其真正快速!

以下是基准测试套件中的方法:

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

关注点

由于我们可能会创建大量巨大的字符串,我们很容易遇到 .NET 的内存限制。这些限制对于普通场景来说不是问题,但在某些关键应用程序中可能会出现。这些是我们可能面临的限制:

  • 进程内存限制 2GiB (x86) 或 4GiB (x64)(目标:AnyCPU 或 x86)
  • 单个对象内存大小限制 2GiB(.NET 版本 1.0 到 4.0)

为了规避这些问题,项目将编译目标平台设置为 x64,框架设置为 4.5。这将解除所有先前的内存限制,你只会受到可用操作系统内存的困扰。

关于这个主题的一篇非常好的文章是 .NET 进程中的内存限制

同样重要的是要注意,由于两种不安全方法都会就地修改原始字符串,这打破了 .NET 的“字符串约定”,我必须在每次测试运行后对源字符串进行深度复制以恢复它们,否则源字符串将已经修剪过地传递给下一个方法。出于显而易见的原因,在测量这些方法的性能时,这些时间并未被计算在内。这是用于深度复制的代码,供好奇者参考:

public static List<string> CloneStrings(List<string> source) {
    using (MemoryStream ms = new MemoryStream()) {
        BinaryFormatter bf = new BinaryFormatter();
        bf.Serialize(ms, source);
        ms.Position = 0;
        return (List<string>)bf.Deserialize(ms);
    }
}

我选择进行序列化和反序列化,因为我必须确保没有字符串被 intern(字符串驻留)并保留旧的引用!

基准测试

为了运行基准测试,我选择编写一个非常简单的 WinForms 应用程序,它将创建一个包含各种空白字符的随机字符串列表。最终用户可以选择要处理的字符串数量以及每个字符串的近似长度(随机化方法不会生成具有可预测长度的中间字符串,因此最终长度不精确)。相同的源数据单独传递给所有移除空白方法,并记录和显示计时统计信息给用户。基准测试代码非常简单。所有基准测试方法看起来都非常相似,只是一个带有源字符串循环的委托,将此委托传递给 `execute(...)` 方法,该方法将处理日志记录、计时以及一些垃圾回收器问题,以尝试阻止它干扰正在运行的方法。

void execute(Action method, string name) {
    results = new List<string>(strings.Count);
    GC.Collect(GC.MaxGeneration, GCCollectionMode.Default, true);
    GC.WaitForPendingFinalizers();
    log("START: Method " + name);

    watch.Restart();
    method();
    watch.Stop();

    var elapsed = watch.Elapsed;
    //log("FINISHED: Method " + name);
    log(String.Format("ELAPSED: {0}\r\n----", elapsed));
    timings[name] = elapsed;
}

// methods to execute the benchmarks

private void BenchmarkStringReplace() {
    execute(() => {
        foreach (var s in strings) results.Add(TrimAllWithStringReplace(s));
    }, "STRING.REPLACE");
}
private void BenchmarkSplitAndConcat() {
    execute(() => {
        foreach (var s in strings) results.Add(TrimAllWithSplitAndConcat(s));
    }, "SPLIT AND CONCAT");
}
private void BenchmarkRegex() {
    execute(() => {
        foreach (var s in strings) results.Add(TrimAllWithRegex(s));
    }, "REGEX");
}
private void BenchmarkLinq() {
    execute(() => { foreach (var s in strings) results.Add(TrimAllWithLinq(s));
    }, "LINQ");
}
private void BenchmarkArrayCopy() {
    execute(() => {
        foreach (var s in strings) results.Add(TrimAllWithCharArrayCopy(s));
    }, "ARRAY.COPY");
}
private void BenchmarkInplaceCharArray() {
    execute(() => {
        foreach (var s in strings) results.Add(TrimAllWithInplaceCharArray(s));
    }, "INPLACE CHAR ARRAY");
}
private void BenchmarkStringInplace() {
    // this method changes the "immutable" strings in-place, so we need to deep copy it
    var backup = Helpers.CloneStrings(strings);
    execute(() => {
        foreach (var s in strings) results.Add(TrimAllWithStringInplace(s));
    }, "STRING INPLACE");
    strings = backup;
}
private void BenchmarkStringInplaceV2() {
    // this method changes the "immutable" strings in-place, so we need to deep copy it
    var backup = Helpers.CloneStrings(strings);
    execute(() => {
        foreach (var s in strings) results.Add(TrimAllWithStringInplaceV2(s));
    }, "STRING INPLACE V2");
    strings = backup;
}
private void BenchmarkWithLexerLoop() {
    execute(() => {
        foreach (var s in strings) results.Add(TrimAllWithLexerLoop(s));
    }, "LEXERLOOP SWITCH");
}
private void BenchmarkWithLexerLoopCharWhitespace() {
    execute(() => {
        foreach (var s in strings) results.Add(TrimAllWithLexerLoopCharIsWhitespce(s));
    }, "LEXERLOOP CHAR");
}

基准测试设置

基准测试是在一台 PC 上运行的,操作系统为 Windows 7 Ultimate 64位,CPU 为 Core i7-3770K @ 3.5GHz - 3.90GHz,内存 16GiB。我目前正在收集我各种计算机上的其他基准测试结果,并将更新文章,提供不同框架版本、操作系统和 CPU 的结果。我甚至考虑编写 Mono 的测试,看看它在某些 Linux 版本下的性能如何。

基准测试结果

免责声明:本文档原始版本中的基准测试数据是错误地使用应用程序的 DEBUG 版本而不是 RELEASE 版本测得的。虽然结果出人意料,但它们也是完全错误的!了解代码在优化之前的性能有多好并不重要。对绝大多数场景而言,真正重要的是代码在 JIT 榨干其全部潜力之后的性能!

基准测试结果将通过应用程序本身的截图显示,所有测试都有编号。

由于 `string.Replace()` 方法在功能上不与其它方法等同,因此它不会被考虑成为任何基准测试的获胜者。

测试 1(默认):10,000 个 1,024 个字符的字符串

这是默认基准测试,因为应用程序将在启动时使用这些值:10,000 个大约 1,024 个字符的字符串。

这个结果有些令人惊讶。虽然我预计基于正则表达式的解决方案会花费更长的时间,但我没想到它会比最快的托管方法慢约 10 倍,比获胜者慢高达约 17 倍。我也曾预计 LINQ 实现的速度至少能与拆分并连接方法相匹配。但最让我吃惊的是,我能够通过我自己的原地字符数组方法超越**`string.Replace()`** 的速度,尽管后者的 C++ 代码经过了优化!太棒了!

注意:`string.Replace` 和 `Array Copy` 方法的几乎平局使得在每次默认基准测试运行时,获胜者都是随机的,所以我选择了一张我获胜的截图!但功劳应该归于 **JIT**,因为它能够即时优化代码,使其速度如此之快!

其他手动优化的方法都非常快。词法分析器循环字符一直比词法分析器循环开关慢,这让我相信 JIT 无法像优化基于开关的内联逻辑那样优化它。我没有查看 IL 代码,但我真的相信,即使 `isWhiteSpace()` 方法肯定被内联了,但它围绕着内联代码还有一个额外的 `if` 语句,而在“开关”版本中,`switch` 语句直接用在循环中。此后,我已将其他方法更新为直接使用开关,并完全避免调用 `isWhiteSpace` 方法,以避免任何速度损失。这里的基准测试结果来自此优化后的代码版本。

不安全的方法是最快的。它们都比最快的托管方法快了近 50%,这也在意料之中,因为无需额外的数组分配即可处理字符串。这两种方法本可以更快,甚至可能显著更快,如果我能够成功地进行直接字符串长度操作,但这并未成功,我必须创建一个新字符串才能将“修剪后”的字符串返回给调用者,这在内部会分配一个新字符数组,从而减慢了速度。

总冠军:字符串原地 V2
托管冠军:原地字符数组

测试 2:100,000 个 1,024 个字符的字符串

历史重演,正则表达式慢约 15 倍,LINQ 慢约 5.6 倍于最快的方法。**String.Replace** 现在比 **Array.Copy** 稳定更快,但速度差距不大。

总冠军:字符串原地 V2
托管冠军:原地字符数组

测试 3:10,000 个 100,000 个字符的字符串(已中止)

此测试旨在验证是否有任何解决方案在处理更大的单个字符串时表现更好。人们期望 .NET 中的 Regex 引擎足够智能,可以处理更大的字符串。问题是我的 16GiB 内存不足以正确运行此测试,并且由于在每次非托管基准测试之前都需要深度复制源字符串列表(因为字符串将被就地修改),此测试根本无法在我的机器上运行。所以我中止了它。

中止它的另一个原因是:由于所有方法都是连续运行的,并且此测试中的内存使用量非常大(进程使用的峰值内存接近 15 GiB,可能略高于我的 14.95 GiB 物理内存),因此可能存在先前测试的内存分配会干扰后续测试结果的可能性。我的计算机在此测试期间完全无响应,并在测试完成后减慢速度!关闭应用程序花费了很长时间,并且在很久之后内存使用量才从 93% 下降到 15%!也许测试它的最佳方法是为每个测试设置单独的按钮,并在单独的基准测试应用程序调用中独立运行它们,以便每个测试真正完全隔离地运行。也许我会在文章的下一版本中实现它。

获胜者:不适用

测试 4:10,000 个 10,000 个字符的字符串

此测试应等同于(已中止的)**测试 3**,因为它旨在测试移除空白方法在处理更长字符串时的性能。10,000 次重复将防止任何单次测试运行中的偏差,而 10,000 个长字符串将保证每个测试在处理巨型字符串时都必须表现良好。由于此测试的内存占用不会像**测试 3**中 100,000 个长字符串那样密集,它将更清晰地展示在真实场景中处理长字符串时哪个测试更快。

在此次物理内存肯定不是问题的测试中,结果与默认测试几乎相同:**原地字符数组**仍然是托管方面的获胜者,并且这次 **Array.Copy** 一贯优于 **String.Replace**。对于 **LINQ** 和 **Regex** 没有意外,**词法分析器循环**方法仍然非常快,**拆分并连接**也是如此。

非托管方法仍然位居第一,但**字符串原地**方法超越了**字符串原地 V2**,但差距并未扩大,实际上缩小了,因为**原地字符数组**正在缩小差距!太棒了 2!

总冠军:字符串原地
托管冠军:原地字符数组

结论

首先:此基准测试对大多数实际应用程序并不重要。至少不直接重要,因为移除/修剪长字符串中的空白字符或在长时间运行的紧密循环中进行此操作并不常见。本文提出的场景是人为设计的。但这些结果可以推断到我们通常使用正则表达式进行的其他常见字符串操作。如果操作可以很容易地“手工”完成,那么正则表达式的强大功能可能有点过头,使用它的开销可能会对性能产生负面影响。

然而,我们可以安全地假设,`String.Split()` 和 `String.Concat()` 是非常快速和优化的方法,我们可以在许多用例中使用它们,所以不要像我一开始那样,担心浪费资源拆分字符串然后重新连接它们。最终,**拆分并连接**方法在与 **LINQ** 和 **REGEX** 相比时表现良好,但仍然落后于“手动优化”的方法。

虽然**正则表达式**非常简洁强大,但它的性能未能赶上其他任何方法,即使是处理巨型字符串时也是如此。 所以要谨慎使用它,但不要被欺骗:我热爱正则表达式!我认为它们是任何程序员工具箱中不可或缺的一部分。

**原地字符数组**方法被证明速度非常快。甚至比简单的(且功能不等的)C++ 优化的**string.Replace** 方法还要快!

如果我要选择一种方法,我肯定会选择**原地字符数组**。我没想到它能与 `string.Replace` 相媲美,但它超越了它!

不安全的方法表现非常好,但对于更大的字符串,**原地字符数组**方法几乎能与之媲美,速度仅慢 18%。由于不安全方法会破坏 .NET 字符串不可变的约定,我建议优先选择更快的托管方法。

最终,本文表明我们可以在托管的 .NET 中编写非常高性能的代码,而无需诉诸于不安全或原生实现。我会坚持使用它,除非绝对必要。

© . All rights reserved.