在C#字符串中替换多个字符

227

有没有更好的方法来替换字符串?

我很惊讶Replace方法不支持字符数组或者字符串数组。我猜我可以自己写一个扩展方法,但是我想知道是否有更好的内置方法来完成以下操作?注意最后一个Replace方法使用的是字符串而不是字符。

myString.Replace(';', '\n').Replace(',', '\n').Replace('\r', '\n').Replace('\t', '\n').Replace(' ', '\n').Replace("\n\n", "\n");

1
信不信由你,这实际上是最快的解决方案之一,因为替换使用了矢量化(CPU SIMD),很难被超越。但从分配的角度来看,它还可以改进,但仅当您的函数经常进行替换时才能改进。如果您的代码更像是“安全过滤器”,那就保持原样。 - Olivier Giniaux
16个回答

261
你可以使用替换正则表达式。
s/[;,\t\r ]|[\n]{2}/\n/g
  • s/开头表示搜索
  • []之间的字符是要搜索的字符(任意顺序)
  • 第二个/分隔搜索文本和替换文本

用英语表达如下:

"搜索;,\t\r、空格或正好两个连续的\n,并将其替换为\n"

在C#中,您可以执行以下操作:(导入System.Text.RegularExpressions后)

Regex pattern = new Regex("[;,\t\r ]|[\n]{2}");
pattern.Replace(myString, "\n");

2
\t\r被包含在\s中。因此,你的正则表达式等同于[;,\s] - NullUserException
3
"\s"实际上等同于"[ \f\n\r\t\v]",因此您包含了一些原始问题中没有的内容。另外,原问题要求使用Replace("\n\n", "\n"),而您的正则表达式无法处理该操作。 - NullUserException
17
请注意,在用户无法配置的简单替换操作中,使用正则表达式并不是最优选择,因为与常规字符串操作相比,它非常缓慢。根据我在搜索“C#正则表达式性能替换”时找到的第一篇基准测试文章,它大约要慢13倍。 - too
1
啊,正则表达式,权力的象形文字!我唯一能看到的问题是正则表达式的人类可读性;许多人拒绝理解它们。最近我添加了一个解决方案,供那些寻找不太复杂的替代方案的人使用。 - sɐunıɔןɐqɐp
那么,如果我们想用多个字符替换多个字符,该如何编写呢? - Habip Oğuz

148
如果您感到特别聪明,不想使用正则表达式:
char[] separators = new char[]{' ',';',',','\r','\t','\n'};

string s = "this;is,\ra\t\n\n\ntest";
string[] temp = s.Split(separators, StringSplitOptions.RemoveEmptyEntries);
s = String.Join("\n", temp);

你也可以轻松地将此内容包装在扩展方法中。
编辑:或者只需等待2分钟,我最终会写出来 :)
public static class ExtensionMethods
{
   public static string Replace(this string s, char[] separators, string newVal)
   {
       string[] temp;

       temp = s.Split(separators, StringSplitOptions.RemoveEmptyEntries);
       return String.Join( newVal, temp );
   }
}

然后,就完成了...

char[] separators = new char[]{' ',';',',','\r','\t','\n'};
string s = "this;is,\ra\t\n\n\ntest";

s = s.Replace(separators, "\n");

3
非常浪费内存,尤其是对于较大的字符串。 - MarcinJuraszek
2
@MarcinJuraszek 哈哈...这可能是我第一次听到有人声称内置字符串方法比正则表达式的内存效率低。 - Paul Walls
16
你说得对。在我发布内容之前,我应该先进行测量。我进行了基准测试,发现 Regex.Replace 比多次连续使用 string.Replace 更慢 8 倍,并且比 Split + Join 慢 4 倍。请参见:https://gist.github.com/MarcinJuraszek/c1437d925548561ba210a1c6ed144452。 - MarcinJuraszek
7
好的解决方案!只需要小小的补充说明。不幸的是,如果你想要替换字符串中的第一个字符(或前几个字符),这种方法就不可行了。比如说,如果你想要替换示例字符串中的字符 't',Split 方法会将第一个单词 "this" 中的 't' 去掉,因为它是一个 "EmptyEntry"。如果你使用 "StringSplitOptions.None" 而非 "RemoveEmptyEntries" 参数,Split 方法会保留这个 Entry,然后 Join 方法会添加分隔符号。希望这能帮到你。 - Pierre

76
您可以使用Linq的Aggregate函数:
string s = "the\nquick\tbrown\rdog,jumped;over the lazy fox.";
char[] chars = new char[] { ' ', ';', ',', '\r', '\t', '\n' };
string snew = chars.Aggregate(s, (c1, c2) => c1.Replace(c2, '\n'));

这里是扩展方法:

public static string ReplaceAll(this string seed, char[] chars, char replacementCharacter)
{
    return chars.Aggregate(seed, (str, cItem) => str.Replace(cItem, replacementCharacter));
}

扩展方法使用示例:

string snew = s.ReplaceAll(chars, '\n');

26
这是最简短的方法:
myString = Regex.Replace(myString, @"[;,\t\r ]|[\n]{2}", "\n");

2
这个一行代码也可以在初始化程序中使用。 - Guney Ozsan

12

哦,这场表演真是太可怕了!答案略有过时,但仍然...

public static class StringUtils
{
    #region Private members

    [ThreadStatic]
    private static StringBuilder m_ReplaceSB;

    private static StringBuilder GetReplaceSB(int capacity)
    {
        var result = m_ReplaceSB;

        if (null == result)
        {
            result = new StringBuilder(capacity);
            m_ReplaceSB = result;
        }
        else
        {
            result.Clear();
            result.EnsureCapacity(capacity);
        }

        return result;
    }


    public static string ReplaceAny(this string s, char replaceWith, params char[] chars)
    {
        if (null == chars)
            return s;

        if (null == s)
            return null;

        StringBuilder sb = null;

        for (int i = 0, count = s.Length; i < count; i++)
        {
            var temp = s[i];
            var replace = false;

            for (int j = 0, cc = chars.Length; j < cc; j++)
                if (temp == chars[j])
                {
                    if (null == sb)
                    {
                        sb = GetReplaceSB(count);
                        if (i > 0)
                            sb.Append(s, 0, i);
                    }

                    replace = true;
                    break;
                }

            if (replace)
                sb.Append(replaceWith);
            else
                if (null != sb)
                    sb.Append(temp);
        }

        return null == sb ? s : sb.ToString();
    }
}

12

字符串只是不可变的字符数组

你只需要让它可变:

  • 使用 StringBuilder
  • 进入 unsafe 的世界并使用指针进行操作 (虽然很危险)

并尽量减少字符数组迭代的次数。注意这里使用了 HashSet,因为它避免了在循环内部遍历字符序列。如果需要更快的查找,可以使用基于 array[256] 的优化查找来替换 HashSet

使用 StringBuilder 的示例

public static void MultiReplace(this StringBuilder builder, 
    char[] toReplace, 
    char replacement)
{
    HashSet<char> set = new HashSet<char>(toReplace);
    for (int i = 0; i < builder.Length; ++i)
    {
        var currentCharacter = builder[i];
        if (set.Contains(currentCharacter))
        {
            builder[i] = replacement;
        }
    }
}

编辑 - 优化版本(仅适用于ASCII)

public static void MultiReplace(this StringBuilder builder, 
    char[] toReplace,
    char replacement)
{
    var set = new bool[256];
    foreach (var charToReplace in toReplace)
    {
        set[charToReplace] = true;
    }
    for (int i = 0; i < builder.Length; ++i)
    {
        var currentCharacter = builder[i];
        if (set[currentCharacter])
        {
            builder[i] = replacement;
        }
    }
}

那么你只需要像这样使用它:

var builder = new StringBuilder("my bad,url&slugs");
builder.MultiReplace(new []{' ', '&', ','}, '-');
var result = builder.ToString();

6
请记住,在.NET中,字符串是wchar_t类型的。您只替换了所有可能字符的子集(并且您需要使用65536个布尔值来优化...) - gog
更准确地说,char 是一个 UTF-16 代码单元,而 string 则“表示文本作为一系列 UTF-16 代码单元。”如果你想用表情符号替换 'x' 呢?;-) - Pablo H

7
我知道这个问题已经很老了,但我想提供两个更有效的选项:
首先,Paul Walls发布的扩展方法很好,但可以通过使用StringBuilder类使其更加高效。 StringBuilder类类似于字符串数据类型,但专门用于需要多次更改字符串值的情况。这是我使用StringBuilder制作的扩展方法版本:
public static string ReplaceChars(this string s, char[] separators, char newVal)
{
    StringBuilder sb = new StringBuilder(s);
    foreach (var c in separators) { sb.Replace(c, newVal); }
    return sb.ToString();
}

我已经运行了这个操作100,000次,使用StringBuilder只需要73毫秒,而使用string则需要81毫秒。因此,除非您正在运行许多操作或使用巨大的字符串,否则差异通常是可以忽略的。
其次,这里是一个可以使用的一行循环代码:
foreach (char c in separators) { s = s.Replace(c, '\n'); }

我个人认为这是最好的选择。它非常高效,不需要编写扩展方法。在我的测试中,它仅用了63ms就运行了100,000次迭代,使其成为最有效的选项。 以下是一个示例:

string s = "this;is,\ra\t\n\n\ntest";
char[] separators = new char[] { ' ', ';', ',', '\r', '\t', '\n' };
foreach (char c in separators) { s = s.Replace(c, '\n'); }

感谢Paul Walls提供本示例的前两行。


6

您还可以直接编写这些字符串扩展方法,然后将它们放在您的解决方案中的任何位置:

using System.Text;

public static class StringExtensions
{
    public static string ReplaceAll(this string original, string toBeReplaced, string newValue)
    {
        if (string.IsNullOrEmpty(original) || string.IsNullOrEmpty(toBeReplaced)) return original;
        if (newValue == null) newValue = string.Empty;
        StringBuilder sb = new StringBuilder();
        foreach (char ch in original)
        {
            if (toBeReplaced.IndexOf(ch) < 0) sb.Append(ch);
            else sb.Append(newValue);
        }
        return sb.ToString();
    }

    public static string ReplaceAll(this string original, string[] toBeReplaced, string newValue)
    {
        if (string.IsNullOrEmpty(original) || toBeReplaced == null || toBeReplaced.Length <= 0) return original;
        if (newValue == null) newValue = string.Empty;
        foreach (string str in toBeReplaced)
            if (!string.IsNullOrEmpty(str))
                original = original.Replace(str, newValue);
        return original;
    }
}

这样调用它们:


"ABCDE".ReplaceAll("ACE", "xy");

xyBxyDxy


And this:

"ABCDEF".ReplaceAll(new string[] { "AB", "DE", "EF" }, "xy");

xyCxyF


5
一种.NET Core版本,用于将一组定义的字符串字符替换为特定字符。它利用了最近引入的Span类型和string.Create方法。
这个想法是准备一个替换数组,因此不需要对每个字符串字符进行实际比较操作。因此,替换过程类似于状态机的工作方式。为了避免初始化替换数组的所有项目,让我们在那里存储oldChar ^ newChar(XOR)值,这样可以获得以下好处:
  • 如果字符没有更改:ch ^ ch = 0 - 不需要初始化非更改项
  • 通过异或找到最终字符:ch ^ repl[ch]
    • ch ^ 0 = ch - 没有更改的字符情况
    • ch ^ (ch ^ newChar) = newChar - 替换的字符
因此,唯一的要求是确保替换数组在初始化时被清零。我们将使用ArrayPool<char>来避免每次调用ReplaceAll方法时进行分配。为了确保数组被清零而不需要昂贵的调用Array.Clear方法,我们将维护一个专门用于ReplaceAll方法的池。我们将在将替换数组(仅确切的项目)返回到池之前清除它。
public static class StringExtensions
{
    private static readonly ArrayPool<char> _replacementPool = ArrayPool<char>.Create();

    public static string ReplaceAll(this string str, char newChar, params char[] oldChars)
    {
        // If nothing to do, return the original string.
        if (string.IsNullOrEmpty(str) ||
            oldChars is null ||
            oldChars.Length == 0)
        {
            return str;
        }

        // If only one character needs to be replaced,
        // use the more efficient `string.Replace`.
        if (oldChars.Length == 1)
        {
            return str.Replace(oldChars[0], newChar);
        }

        // Get a replacement array from the pool.
        var replacements = _replacementPool.Rent(char.MaxValue + 1);

        try
        {
            // Intialize the replacement array in the way that
            // all elements represent `oldChar ^ newChar`.
            foreach (var oldCh in oldChars)
            {
                replacements[oldCh] = (char)(newChar ^ oldCh);
            }

            // Create a string with replaced characters.
            return string.Create(str.Length, (str, replacements), (dst, args) =>
            {
                var repl = args.replacements;

                foreach (var ch in args.str)
                {
                    dst[0] = (char)(repl[ch] ^ ch);
                    dst = dst.Slice(1);
                }
            });
        }
        finally
        {
            // Clear the replacement array.
            foreach (var oldCh in oldChars)
            {
                replacements[oldCh] = char.MinValue;
            }

            // Return the replacement array back to the pool.
            _replacementPool.Return(replacements);
        }
    }
}

4
使用RegEx.Replace,类似这样:
  string input = "This is   text with   far  too   much   " + 
                 "whitespace.";
  string pattern = "[;,]";
  string replacement = "\n";
  Regex rgx = new Regex(pattern);
  string result = rgx.Replace(input, replacement);

这里有更多关于 RegEx.Replace 的 MSDN 文档


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接