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

117

有没有更好的方法来做这件事...

MyString.Trim().Replace("&", "and").Replace(",", "").Replace("  ", " ")
         .Replace(" ", "-").Replace("'", "").Replace("/", "").ToLower();

我已经扩展了字符串类,以使其只保持一个功能,但是否有更快的方法?

public static class StringExtension
{
    public static string clean(this string s)
    {
        return s.Replace("&", "and").Replace(",", "").Replace("  ", " ")
                .Replace(" ", "-").Replace("'", "").Replace(".", "")
                .Replace("eacute;", "é").ToLower();
    }
}

为了好玩(也为了停止评论区中的争论),我上传了一个 gist 以对下面各个示例进行基准测试。

https://gist.github.com/ChrisMcKee/5937656

使用正则表达式选项的效果非常差;使用字典选项速度最快;冗长的字符串构建器替换版本略快于简写的版本。


1
根据您的基准测试结果,似乎字典版本没有执行所有替换操作,这可能是使其比StringBuilder解决方案更快的原因。 - toad
1
@toad 你好,来自2009年;我在四月份下面添加了一条评论,指出了一个明显的错误。虽然我跳过了D,但主旨已经更新了。字典版本仍然更快。 - Chris McKee
可能是重复问题:替代String.Replace多次的方法? - Tot Zam
1
@TotZam 至少在标记之前检查日期;这是来自2009年的,而不是2012年的。 - Chris McKee
由于许多答案似乎关注性能,我认为应该指出Andrej Adamanko的答案可能是许多替换中最快的; 尤其是在大型输入字符串上,肯定比链接.Replace()更快,正如他在答案中所述。 - person27
请注意,这个问题和许多答案可能不适用于任意替换字符串。"abc".Replace("a", "c").Replace("c", "d")将产生"dbd",而不是预期的"cbd"。 - Rei Miyasaka
10个回答

164

更快 - 不是。更有效 - 是的,如果您使用 StringBuilder 类。在您的实现中,每个操作都会生成一个字符串的副本,这可能会影响性能。字符串是不可变对象,因此每个操作只返回一个修改后的副本。

如果您希望该方法在多个长度显著的 Strings 上被积极调用,最好将其实现迁移到 StringBuilder 类上。使用它可以直接在该实例上执行任何修改,因此可以节省不必要的复制操作。

public static class StringExtention
{
    public static string clean(this string s)
    {
        StringBuilder sb = new StringBuilder (s);

        sb.Replace("&", "and");
        sb.Replace(",", "");
        sb.Replace("  ", " ");
        sb.Replace(" ", "-");
        sb.Replace("'", "");
        sb.Replace(".", "");
        sb.Replace("eacute;", "é");

        return sb.ToString().ToLower();
    }
}

2
为了清晰起见,字典答案是最快的。https://dev59.com/I3M_5IYBdhLWcg3wlEFH#1321366 - Chris McKee
6
在您的 https://gist.github.com/ChrisMcKee/5937656 的基准测试中,字典测试并不完整:它没有进行所有替换,“ ”只替换“ ”而不是“ ”。可能没有进行所有替换是其在基准测试中最快的原因。正则表达式替换也不完整。但最重要的是您的字符串TestData非常短。就像接受的答案所述,字符串必须具有显着长度,StringBuilder才能发挥优势。能否请您使用10kB、100kB和1MB的字符串重新进行基准测试? - Leif
这是一个很好的观点;目前它被用于URL清理,因此测试100kb-1mb将是不现实的。我会更新基准测试,使其使用整个内容,这是一个错误。 - Chris McKee
为了获得最佳性能,请循环遍历字符并自行替换。但是,如果您有多个字符的字符串(查找它们会强制您一次比较多个字符,而替换它们则需要分配更多内存并移动其余部分的字符串),这可能会很繁琐。 - Chayim Friedman
当输入字符串中没有任何要替换的字符或字符串时,这将是一个非常糟糕的解决方案。在这种情况下,String.Replace只会返回原始引用,并且与StringBuilder解决方案相比非常便宜。 - Michel van Engelen
在结尾处孤立的 ToLower 调用将强制重新分配整个字符串。有什么建议可以防止这种内存浪费,同时看起来仍然可读? - julealgon

32

如果你只是想要一个美观的解决方案,并且不需要节省几个纳秒,那么考虑一下 LINQ sugar 呢?

var input = "test1test2test3";
var replacements = new Dictionary<string, string> { { "1", "*" }, { "2", "_" }, { "3", "&" } };

var output = replacements.Aggregate(input, (current, replacement) => current.Replace(replacement.Key, replacement.Value));

类似于Gist中的示例C(如果您在其上方查看,则较丑的linq语句在注释中) - Chris McKee
4
有趣的是,你将一个功能性语句定义为比过程式语句更“丑陋”。 - TimS
不会争论这个问题,这只是个人偏好而已。就像你所说的,LINQ 只是一种语法糖;而我已经在代码上方放置了等效的内容 :) - Chris McKee

18

这会更加高效:

public static class StringExtension
{
    public static string clean(this string s)
    {
        return new StringBuilder(s)
              .Replace("&", "and")
              .Replace(",", "")
              .Replace("  ", " ")
              .Replace(" ", "-")
              .Replace("'", "")
              .Replace(".", "")
              .Replace("eacute;", "é")
              .ToString()
              .ToLower();
    }
}

真的很难读懂。我相信你知道它是做什么的,但初级开发人员可能会对实际发生的事情感到困惑。我同意-我也总是寻找最简短的写作方式-但这只是为了满足自己的需求。其他人则对这堆混乱感到恐慌。 - Piotr Kula
4
实际上这个速度比较慢。 基准开销... 13毫秒 StringClean-user151323... 2843毫秒 StringClean-TheVillageIdiot... 2921毫秒每次重新运行的结果都不同,但答案是正确的。 - Chris McKee

11

或许更容易理解一些?

    public static class StringExtension {

        private static Dictionary<string, string> _replacements = new Dictionary<string, string>();

        static StringExtension() {
            _replacements["&"] = "and";
            _replacements[","] = "";
            _replacements["  "] = " ";
            // etc...
        }

        public static string clean(this string s) {
            foreach (string to_replace in _replacements.Keys) {
                s = s.Replace(to_replace, _replacements[to_replace]);
            }
            return s;
        }
    }

此外,还要添加“新来的城镇”对于StringBuilder的建议...


6
这样写更易读:private static Dictionary<string, string> _replacements = new Dictionary<string, string>() { {"&", "and"}, {",", ""}, {" ", " "} /* 等等 */ }; - ANeves
2
私有静态只读字典<string,string> Replacements = new Dictionary<string,string>() { {“&”,“和”},{“,”,“”},{“ ”,“”} // };public static string Clean(this string s) { return Replacements.Keys.Aggregate(s, (current, toReplace) => current.Replace(toReplace, Replacements[toReplace])); } - Chris McKee
3
在这里使用词典没有任何意义,只需使用“List<Tuple<string,string>>”。这也改变了替换的顺序,并且不如例如s.Replace("a").Replace("b").Replace("c")快。不要使用这种方法! - Thomas

10
有一件事情在建议的解决方案中可能需要优化。多次调用Replace()会使代码对同一个字符串进行多次遍历。对于非常长的字符串,由于CPU缓存容量不足,解决方案可能会变慢。也许应该考虑在单次遍历中替换多个字符串
从链接中提取的关键内容:
static string MultipleReplace(string text, Dictionary<string, string> replacements) {
    return Regex.Replace(text,
                 "(" + String.Join("|", replacements.Keys) + ")",
                 delegate(Match m) { return replacements[m.Value]; });
}

    // somewhere else in code
    string temp = "Jonathan Smith is a developer";

    var adict = new Dictionary<string, string>();
    adict.Add("Jonathan", "David");
    adict.Add("Smith", "Seruyange");

    string rep = MultipleReplace(temp, adict);

2
很多答案似乎关注性能,如果是这样,那么这是最好的选择。而且它很简单,因为它只是一个已记录的重载,其中你根据匹配返回预期值,在这个例子中,使用字典将它们匹配起来。应该很容易理解。 - person27
2
添加了来自链接页面的代码,以防链接页面失效导致此答案变得无用。 - Caius Jard
请修改你的答案,使代码真正可用。在你的 MultipleReplace 方法中,没有名为 'adict' 的变量,请将其改为 'replacements' 并删除 .ToArray()。正确缩进代码以使其更易读。感谢您提供的解决方案。 - Bijan Negari

4
使用LINQ的另一个选项是:
[TestMethod]
public void Test()
{
  var input = "it's worth a lot of money, if you can find a buyer.";
  var expected = "its worth a lot of money if you can find a buyer";
  var removeList = new string[] { ".", ",", "'" };
  var result = input;

  removeList.ToList().ForEach(o => result = result.Replace(o, string.Empty));

  Assert.AreEqual(expected, result);
}

1
你可以声明 var removeList = new List<string> { /*...*/ };,然后只需调用 removeList.ForEach( /*...*/ ); 简化代码。还要注意,这并没有完全回答问题,因为 所有 找到的字符串都将被替换为 String.Empty - Tok'
2
Linq 究竟在哪里使用?这样做会将 removeList 浪费地转换为 List,而这个目标是不必要的,只是为了让它成为一行。但是 Lamdas 和 Linq 并不是同义词。 - Devin Burke
请注意,List.ForEach不是LINQ的东西,它是一个List的功能。 - Caius Jard

0

这本质上是Paolo Tedesco的答案,但我想让它可重复使用。

    public class StringMultipleReplaceHelper
    {
        private readonly Dictionary<string, string> _replacements;

        public StringMultipleReplaceHelper(Dictionary<string, string> replacements)
        {
            _replacements = replacements;
        }

        public string clean(string s)
        {
            foreach (string to_replace in _replacements.Keys)
            {
                s = s.Replace(to_replace, _replacements[to_replace]);
            }
            return s;
        }
    }

需要注意的一点是,我不得不停止它作为扩展,删除static修饰符,并从clean(this string s)中删除this。我很乐意听取有关如何更好地实现这一点的建议。


0

可以使用带有MatchEvaluator的正则表达式:

    var pattern = new Regex(@"These|words|are|placed|in|parentheses");
    var input = "The matching words in this text are being placed inside parentheses.";
    var result = pattern.Replace(input , match=> $"({match.Value})");

注意:

  • 显然可以使用不同的表达式(例如:\b(\w*test\w*)\b)来匹配单词。
  • 我希望它能更加优化,以便在表达式中查找模式并进行替换。
  • 优点是在进行替换时能够处理匹配元素。

这个答案可以更好地使用match委托,而不仅仅是提供与匹配的相同值;它是一个无操作的操作。 - Caius Jard

0

我正在做类似的事情,但在我的情况下,我正在进行序列化/反序列化,因此需要能够双向操作。我发现使用string[][]几乎与字典相同,包括初始化,但您也可以反向操作,将替代项返回其原始值,这是字典无法实现的。

编辑:您可以使用Dictionary<Key,List<Values>>以获得与string[][]相同的结果


这似乎没有提供答案给问题。 - Caius Jard

-1
string input = "it's worth a lot of money, if you can find a buyer.";
for (dynamic i = 0, repl = new string[,] { { "'", "''" }, { "money", "$" }, { "find", "locate" } }; i < repl.Length / 2; i++) {
    input = input.Replace(repl[i, 0], repl[i, 1]);
}

3
你应该考虑在回答中添加上下文信息,比如简要地解释一下代码的用途,以及如果有必要的话,为什么你会按照这种方式编写代码。 - Neil

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