在一个字符串中替换多个字符,最快的方法是什么?

29

我正在将旧数据库中的多个 string 字段的记录导入到新数据库中。这个过程非常缓慢,我怀疑是因为我使用了以下方法:

foreach (var oldObj in oldDB)
{
    NewObject newObj = new NewObject();
    newObj.Name = oldObj.Name.Trim().Replace('^', 'Č').Replace('@', 'Ž').Replace('[', 'Š')
        .Replace(']', 'Ć').Replace('`', 'ž').Replace('}', 'ć')
        .Replace('~', 'č').Replace('{', 'š').Replace('\\', 'Đ');
    newObj.Surname = oldObj.Surname.Trim().Replace('^', 'Č').Replace('@', 'Ž').Replace('[', 'Š')
        .Replace(']', 'Ć').Replace('`', 'ž').Replace('}', 'ć')
        .Replace('~', 'č').Replace('{', 'š').Replace('\\', 'Đ');
    newObj.Address = oldObj.Address.Trim().Replace('^', 'Č').Replace('@', 'Ž').Replace('[', 'Š')
        .Replace(']', 'Ć').Replace('`', 'ž').Replace('}', 'ć')
        .Replace('~', 'č').Replace('{', 'š').Replace('\\', 'Đ');
    newObj.Note = oldObj.Note.Trim().Replace('^', 'Č').Replace('@', 'Ž').Replace('[', 'Š')
        .Replace(']', 'Ć').Replace('`', 'ž').Replace('}', 'ć')
        .Replace('~', 'č').Replace('{', 'š').Replace('\\', 'Đ');
    /*
    ... some processing ...
    */
}

现在,我已经通过网络阅读了一些帖子和文章,看到了许多不同的想法。有些人说最好使用MatchEvaluator进行正则表达式,有些人则认为最好保持原样。
虽然对于我来说,自己做一个基准测试可能更容易,但我决定在这里提出一个问题,以防其他人也有同样的问题,或者有人事先知道。
那么,在C#中最快的方法是什么?
编辑
我在这里发布了基准测试。乍一看,Richard的方法似乎是最快的。然而,他的方式和Marc的方式都不会有任何作用,因为正则表达式模式是错误的。在纠正模式之后,情况就变了。
@"\^@\[\]`\}~\{\\" 

to

@"\^|@|\[|\]|`|\}|~|\{|\\" 

看起来旧的方法使用串联的.Replace()调用是最快的


6
你觉得那就是原因吗?你应该知道才对。为了找到瓶颈,你需要对应用程序进行分析,不要猜测。请进行性能分析来确定。 - Oded
1
我曾经询问过这个问题,并接受了这个答案,但我不确定那是否符合你的需求。 - Şafak Gür
1
@Oded,我是否怀疑或知道并不是问题所在,问题是如何在替换字符串中的多个字符时获得更好的性能。无关紧要,您应该假设示例代码中的/* ... some processing ... */部分肯定不是瓶颈,因为我以这种形式提出了问题。感谢建设性的评论。 - Dejan Janjušević
1
我的观点是,如果你没有数据,你可能会进行一些微小的优化,而存在宏观优化的机会。怀疑某段代码是问题并不意味着它就是问题所在 - 你可能只是把精力集中在了错误的问题上。 - Oded
很抱歉您在我的第一条评论中读出了某种语气 - 我尽量保持中立和客观的语气。我也喜欢让我的评论简短明了。 - Oded
显示剩余2条评论
8个回答

30
感谢大家的输入。我编写了一个快速而简单的基准测试来测试你们的输入。我已经测试了解析4个字符串,进行了500,000次迭代,并完成了4次传递。结果如下:
*** Pass 1 使用旧的(链式字符串替换)方法完成时间为814毫秒 logicnp (ToCharArray)方法完成时间为916毫秒 oleksii (StringBuilder)方法完成时间为943毫秒 André Christoffer Andersen (Lambda w/ Aggregate)方法完成时间为2551毫秒 Richard (Regex w/ MatchEvaluator)方法完成时间为215毫秒 Marc Gravell (Static Regex)方法完成时间为1008毫秒
*** Pass 2 使用旧的(链式字符串替换)方法完成时间为786毫秒 logicnp (ToCharArray)方法完成时间为920毫秒 oleksii (StringBuilder)方法完成时间为905毫秒 André Christoffer Andersen (Lambda w/ Aggregate)方法完成时间为2515毫秒 Richard (Regex w/ MatchEvaluator)方法完成时间为217毫秒 Marc Gravell (Static Regex)方法完成时间为1025毫秒
*** Pass 3 使用旧的(链式字符串替换)方法完成时间为775毫秒 logicnp (ToCharArray)方法完成时间为903毫秒 oleksii (StringBuilder)方法完成时间为931毫秒 André Christoffer Andersen (Lambda w/ Aggregate)方法完成时间为2529毫秒 Richard (Regex w/ MatchEvaluator)方法完成时间为214毫秒 Marc Gravell (Static Regex)方法完成时间为1022毫秒
*** Pass 4 使用旧的(链式字符串替换)方法完成时间为799毫秒 logicnp (ToCharArray)方法完成时间为908毫秒 oleksii (StringBuilder)方法完成时间为938毫秒 André Christoffer Andersen (Lambda w/ Aggregate)方法完成时间为2592毫秒 Richard (Regex w/ MatchEvaluator)方法完成时间为225毫秒 Marc Gravell (Static Regex)方法完成时间为1050毫秒
以下是此基准测试的代码。请查看代码并确认@Richard拥有最快的方法。请注意,我没有检查输出是否正确,我认为它们是正确的。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Text.RegularExpressions;

namespace StringReplaceTest
{
    class Program
    {
        static string test1 = "A^@[BCD";
        static string test2 = "E]FGH\\";
        static string test3 = "ijk`l}m";
        static string test4 = "nopq~{r";

        static readonly Dictionary<char, string> repl =
            new Dictionary<char, string> 
            { 
                {'^', "Č"}, {'@', "Ž"}, {'[', "Š"}, {']', "Ć"}, {'`', "ž"}, {'}', "ć"}, {'~', "č"}, {'{', "š"}, {'\\', "Đ"} 
            };

        static readonly Regex replaceRegex;

        static Program() // static initializer 
        {
            StringBuilder pattern = new StringBuilder().Append('[');
            foreach (var key in repl.Keys)
                pattern.Append(Regex.Escape(key.ToString()));
            pattern.Append(']');
            replaceRegex = new Regex(pattern.ToString(), RegexOptions.Compiled);
        }

        public static string Sanitize(string input)
        {
            return replaceRegex.Replace(input, match =>
            {
                return repl[match.Value[0]];
            });
        } 

        static string DoGeneralReplace(string input) 
        { 
            var sb = new StringBuilder(input);
            return sb.Replace('^', 'Č').Replace('@', 'Ž').Replace('[', 'Š').Replace(']', 'Ć').Replace('`', 'ž').Replace('}', 'ć').Replace('~', 'č').Replace('{', 'š').Replace('\\', 'Đ').ToString(); 
        }

        //Method for replacing chars with a mapping 
        static string Replace(string input, IDictionary<char, char> replacementMap)
        {
            return replacementMap.Keys
                .Aggregate(input, (current, oldChar)
                    => current.Replace(oldChar, replacementMap[oldChar]));
        } 

        static void Main(string[] args)
        {
            for (int i = 1; i < 5; i++)
                DoIt(i);
        }

        static void DoIt(int n)
        {
            Stopwatch sw = new Stopwatch();
            int idx = 0;

            Console.WriteLine("*** Pass " + n.ToString());
            // old way
            sw.Start();
            for (idx = 0; idx < 500000; idx++)
            {
                string result1 = test1.Replace('^', 'Č').Replace('@', 'Ž').Replace('[', 'Š').Replace(']', 'Ć').Replace('`', 'ž').Replace('}', 'ć').Replace('~', 'č').Replace('{', 'š').Replace('\\', 'Đ');
                string result2 = test2.Replace('^', 'Č').Replace('@', 'Ž').Replace('[', 'Š').Replace(']', 'Ć').Replace('`', 'ž').Replace('}', 'ć').Replace('~', 'č').Replace('{', 'š').Replace('\\', 'Đ');
                string result3 = test3.Replace('^', 'Č').Replace('@', 'Ž').Replace('[', 'Š').Replace(']', 'Ć').Replace('`', 'ž').Replace('}', 'ć').Replace('~', 'č').Replace('{', 'š').Replace('\\', 'Đ');
                string result4 = test4.Replace('^', 'Č').Replace('@', 'Ž').Replace('[', 'Š').Replace(']', 'Ć').Replace('`', 'ž').Replace('}', 'ć').Replace('~', 'č').Replace('{', 'š').Replace('\\', 'Đ');
            }
            sw.Stop();
            Console.WriteLine("Old (Chained String.Replace()) way completed in " + sw.ElapsedMilliseconds.ToString() + " ms");

            Dictionary<char, char> replacements = new Dictionary<char, char>();
            replacements.Add('^', 'Č');
            replacements.Add('@', 'Ž');
            replacements.Add('[', 'Š');
            replacements.Add(']', 'Ć');
            replacements.Add('`', 'ž');
            replacements.Add('}', 'ć');
            replacements.Add('~', 'č');
            replacements.Add('{', 'š');
            replacements.Add('\\', 'Đ');

            // logicnp way
            sw.Reset();
            sw.Start();
            for (idx = 0; idx < 500000; idx++)
            {
                char[] charArray1 = test1.ToCharArray();
                for (int i = 0; i < charArray1.Length; i++)
                {
                    char newChar;
                    if (replacements.TryGetValue(test1[i], out newChar))
                        charArray1[i] = newChar;
                }
                string result1 = new string(charArray1);

                char[] charArray2 = test2.ToCharArray();
                for (int i = 0; i < charArray2.Length; i++)
                {
                    char newChar;
                    if (replacements.TryGetValue(test2[i], out newChar))
                        charArray2[i] = newChar;
                }
                string result2 = new string(charArray2);

                char[] charArray3 = test3.ToCharArray();
                for (int i = 0; i < charArray3.Length; i++)
                {
                    char newChar;
                    if (replacements.TryGetValue(test3[i], out newChar))
                        charArray3[i] = newChar;
                }
                string result3 = new string(charArray3);

                char[] charArray4 = test4.ToCharArray();
                for (int i = 0; i < charArray4.Length; i++)
                {
                    char newChar;
                    if (replacements.TryGetValue(test4[i], out newChar))
                        charArray4[i] = newChar;
                }
                string result4 = new string(charArray4);
            }
            sw.Stop();
            Console.WriteLine("logicnp (ToCharArray) way completed in " + sw.ElapsedMilliseconds.ToString() + " ms");

            // oleksii way
            sw.Reset();
            sw.Start();
            for (idx = 0; idx < 500000; idx++)
            {
                string result1 = DoGeneralReplace(test1);
                string result2 = DoGeneralReplace(test2);
                string result3 = DoGeneralReplace(test3);
                string result4 = DoGeneralReplace(test4);
            }
            sw.Stop();
            Console.WriteLine("oleksii (StringBuilder) way completed in " + sw.ElapsedMilliseconds.ToString() + " ms");

            // André Christoffer Andersen way
            sw.Reset();
            sw.Start();
            for (idx = 0; idx < 500000; idx++)
            {
                string result1 = Replace(test1, replacements);
                string result2 = Replace(test2, replacements);
                string result3 = Replace(test3, replacements);
                string result4 = Replace(test4, replacements);
            }
            sw.Stop();
            Console.WriteLine("André Christoffer Andersen (Lambda w/ Aggregate) way completed in " + sw.ElapsedMilliseconds.ToString() + " ms");

            // Richard way
            sw.Reset();
            sw.Start();
            Regex reg = new Regex(@"\^|@|\[|\]|`|\}|~|\{|\\");
            MatchEvaluator eval = match =>
            {
                switch (match.Value)
                {
                    case "^": return "Č";
                    case "@": return "Ž";
                    case "[": return "Š";
                    case "]": return "Ć";
                    case "`": return "ž";
                    case "}": return "ć";
                    case "~": return "č";
                    case "{": return "š";
                    case "\\": return "Đ";
                    default: throw new Exception("Unexpected match!");
                }
            };
            for (idx = 0; idx < 500000; idx++)
            {
                string result1 = reg.Replace(test1, eval);
                string result2 = reg.Replace(test2, eval);
                string result3 = reg.Replace(test3, eval);
                string result4 = reg.Replace(test4, eval);
            }
            sw.Stop();
            Console.WriteLine("Richard (Regex w/ MatchEvaluator) way completed in " + sw.ElapsedMilliseconds.ToString() + " ms");

            // Marc Gravell way
            sw.Reset();
            sw.Start();
            for (idx = 0; idx < 500000; idx++)
            {
                string result1 = Sanitize(test1);
                string result2 = Sanitize(test2);
                string result3 = Sanitize(test3);
                string result4 = Sanitize(test4);
            }
            sw.Stop();
            Console.WriteLine("Marc Gravell (Static Regex) way completed in " + sw.ElapsedMilliseconds.ToString() + " ms\n");
        }
    }
}

2020年6月更新
由于这个问题仍然受到关注,我想添加来自user1664043的额外输入,使用StringBuilder w/ IndexOfAny编译了.NET Core 3.1,以下是结果:

*** 第一次测试
旧方法(串联String.Replace())完成时间为199毫秒
logicnp(ToCharArray)方式完成时间为296毫秒
oleksii(StringBuilder)方式完成时间为416毫秒
André Christoffer Andersen(Lambda w/ Aggregate)方式完成时间为870毫秒
Richard(Regex w/ MatchEvaluator)方式完成时间为1722毫秒
Marc Gravell(Static Regex)方式完成时间为395毫秒
user1664043(StringBuilder w/ IndexOfAny)方式完成时间为459毫秒
*** 第二次测试 旧方法(串联String.Replace())完成时间为215毫秒 logicnp(ToCharArray)方式完成时间为239毫秒 oleksii(StringBuilder)方式完成时间为341毫秒 André Christoffer Andersen(Lambda w/ Aggregate)方式完成时间为758毫秒 Richard(Regex w/ MatchEvaluator)方式完成时间为1591毫秒 Marc Gravell(Static Regex)方式完成时间为354毫秒 user1664043(StringBuilder w/ IndexOfAny)方式完成时间为426毫秒
*** 第三次测试 旧方法(串联String.Replace())完成时间为199毫秒 logicnp(ToCharArray)方式完成时间为265毫秒 oleksii(StringBuilder)方式完成时间为337毫秒 André Christoffer Andersen(Lambda w/ Aggregate)方式完成时间为817毫秒 Richard(Regex w/ MatchEvaluator)方式完成时间为1666毫秒 Marc Gravell(Static Regex)方式完成时间为373毫秒 user1664043(StringBuilder w/ IndexOfAny)方式完成时间为412毫秒
*** 第四次测试 旧方法(串联String.Replace())完成时间为199毫秒 logicnp(ToCharArray)方式完成时间为230毫秒 oleksii(StringBuilder)方式完成时间为324毫秒 André Christoffer Andersen(Lambda w/ Aggregate)方式完成时间为791毫秒 Richard(Regex w/ MatchEvaluator)方式完成时间为1699毫秒 Marc Gravell(Static Regex)方式完成时间为359毫秒 user1664043(StringBuilder w/ IndexOfAny)方式完成时间为413毫秒

更新后的代码:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace Test.StringReplace
{
    class Program
    {
        static string test1 = "A^@[BCD";
        static string test2 = "E]FGH\\";
        static string test3 = "ijk`l}m";
        static string test4 = "nopq~{r";

        static readonly Dictionary<char, string> repl =
            new Dictionary<char, string>
            {
                {'^', "Č"}, {'@', "Ž"}, {'[', "Š"}, {']', "Ć"}, {'`', "ž"}, {'}', "ć"}, {'~', "č"}, {'{', "š"}, {'\\', "Đ"}
            };

        static readonly Regex replaceRegex;

        static readonly char[] badChars = new char[] { '^', '@', '[', ']', '`', '}', '~', '{', '\\' };

        static readonly char[] replacementChars = new char[] { 'Č', 'Ž', 'Š', 'Ć', 'ž', 'ć', 'č', 'š', 'Đ' };

        static Program() // static initializer 
        {
            StringBuilder pattern = new StringBuilder().Append('[');
            foreach (var key in repl.Keys)
                pattern.Append(Regex.Escape(key.ToString()));
            pattern.Append(']');
            replaceRegex = new Regex(pattern.ToString(), RegexOptions.Compiled);
        }

        public static string Sanitize(string input)
        {
            return replaceRegex.Replace(input, match =>
            {
                return repl[match.Value[0]];
            });
        }

        static string DoGeneralReplace(string input)
        {
            var sb = new StringBuilder(input);
            return sb.Replace('^', 'Č').Replace('@', 'Ž').Replace('[', 'Š').Replace(']', 'Ć').Replace('`', 'ž').Replace('}', 'ć').Replace('~', 'č').Replace('{', 'š').Replace('\\', 'Đ').ToString();
        }

        //Method for replacing chars with a mapping 
        static string Replace(string input, IDictionary<char, char> replacementMap)
        {
            return replacementMap.Keys
                .Aggregate(input, (current, oldChar)
                    => current.Replace(oldChar, replacementMap[oldChar]));
        }

        static string ReplaceCharsWithIndexOfAny(string sIn)
        {
            int replChar = sIn.IndexOfAny(badChars);
            if (replChar < 0)
                return sIn;

            // Don't even bother making a copy unless you know you have something to swap
            StringBuilder sb = new StringBuilder(sIn, 0, replChar, sIn.Length + 10);
            while (replChar >= 0 && replChar < sIn.Length)
            {
                var c = replacementChars[replChar];
                sb.Append(c);

                ////// This approach lets you swap a char for a string or to remove some
                ////// If you had a straight char for char swap, you could just have your repl chars in an array with the same ordinals and do it all in 2 lines matching the ordinals.
                ////c = c switch
                ////{
                ////    ////case "^":
                ////    ////    c = "Č";
                ////    ////    ...
                ////    '\ufeff' => null,
                ////    _ => replacementChars[replChar],
                ////};

                ////if (c != null)
                ////{
                ////    sb.Append(c);
                ////}

                replChar++; // skip over what we just replaced
                if (replChar < sIn.Length)
                {
                    int nextRepChar = sIn.IndexOfAny(badChars, replChar);
                    sb.Append(sIn, replChar, (nextRepChar > 0 ? nextRepChar : sIn.Length) - replChar);
                    replChar = nextRepChar;
                }
            }

            return sb.ToString();
        }

        static void Main(string[] args)
        {
            for (int i = 1; i < 5; i++)
                DoIt(i);
        }

        static void DoIt(int n)
        {
            Stopwatch sw = new Stopwatch();
            int idx = 0;

            Console.WriteLine("*** Pass " + n.ToString());
            // old way
            sw.Start();
            for (idx = 0; idx < 500000; idx++)
            {
                string result1 = test1.Replace('^', 'Č').Replace('@', 'Ž').Replace('[', 'Š').Replace(']', 'Ć').Replace('`', 'ž').Replace('}', 'ć').Replace('~', 'č').Replace('{', 'š').Replace('\\', 'Đ');
                string result2 = test2.Replace('^', 'Č').Replace('@', 'Ž').Replace('[', 'Š').Replace(']', 'Ć').Replace('`', 'ž').Replace('}', 'ć').Replace('~', 'č').Replace('{', 'š').Replace('\\', 'Đ');
                string result3 = test3.Replace('^', 'Č').Replace('@', 'Ž').Replace('[', 'Š').Replace(']', 'Ć').Replace('`', 'ž').Replace('}', 'ć').Replace('~', 'č').Replace('{', 'š').Replace('\\', 'Đ');
                string result4 = test4.Replace('^', 'Č').Replace('@', 'Ž').Replace('[', 'Š').Replace(']', 'Ć').Replace('`', 'ž').Replace('}', 'ć').Replace('~', 'č').Replace('{', 'š').Replace('\\', 'Đ');
            }

            sw.Stop();
            Console.WriteLine("Old (Chained String.Replace()) way completed in " + sw.ElapsedMilliseconds.ToString() + " ms");

            Dictionary<char, char> replacements = new Dictionary<char, char>();
            replacements.Add('^', 'Č');
            replacements.Add('@', 'Ž');
            replacements.Add('[', 'Š');
            replacements.Add(']', 'Ć');
            replacements.Add('`', 'ž');
            replacements.Add('}', 'ć');
            replacements.Add('~', 'č');
            replacements.Add('{', 'š');
            replacements.Add('\\', 'Đ');

            // logicnp way
            sw.Reset();
            sw.Start();
            for (idx = 0; idx < 500000; idx++)
            {
                char[] charArray1 = test1.ToCharArray();
                for (int i = 0; i < charArray1.Length; i++)
                {
                    char newChar;
                    if (replacements.TryGetValue(test1[i], out newChar))
                        charArray1[i] = newChar;
                }

                string result1 = new string(charArray1);

                char[] charArray2 = test2.ToCharArray();
                for (int i = 0; i < charArray2.Length; i++)
                {
                    char newChar;
                    if (replacements.TryGetValue(test2[i], out newChar))
                        charArray2[i] = newChar;
                }

                string result2 = new string(charArray2);

                char[] charArray3 = test3.ToCharArray();
                for (int i = 0; i < charArray3.Length; i++)
                {
                    char newChar;
                    if (replacements.TryGetValue(test3[i], out newChar))
                        charArray3[i] = newChar;
                }

                string result3 = new string(charArray3);

                char[] charArray4 = test4.ToCharArray();
                for (int i = 0; i < charArray4.Length; i++)
                {
                    char newChar;
                    if (replacements.TryGetValue(test4[i], out newChar))
                        charArray4[i] = newChar;
                }

                string result4 = new string(charArray4);
            }

            sw.Stop();
            Console.WriteLine("logicnp (ToCharArray) way completed in " + sw.ElapsedMilliseconds.ToString() + " ms");

            // oleksii way
            sw.Reset();
            sw.Start();
            for (idx = 0; idx < 500000; idx++)
            {
                string result1 = DoGeneralReplace(test1);
                string result2 = DoGeneralReplace(test2);
                string result3 = DoGeneralReplace(test3);
                string result4 = DoGeneralReplace(test4);
            }

            sw.Stop();
            Console.WriteLine("oleksii (StringBuilder) way completed in " + sw.ElapsedMilliseconds.ToString() + " ms");

            // André Christoffer Andersen way
            sw.Reset();
            sw.Start();
            for (idx = 0; idx < 500000; idx++)
            {
                string result1 = Replace(test1, replacements);
                string result2 = Replace(test2, replacements);
                string result3 = Replace(test3, replacements);
                string result4 = Replace(test4, replacements);
            }

            sw.Stop();
            Console.WriteLine("André Christoffer Andersen (Lambda w/ Aggregate) way completed in " + sw.ElapsedMilliseconds.ToString() + " ms");

            // Richard way
            sw.Reset();
            sw.Start();
            Regex reg = new Regex(@"\^|@|\[|\]|`|\}|~|\{|\\");
            MatchEvaluator eval = match =>
            {
                switch (match.Value)
                {
                    case "^": return "Č";
                    case "@": return "Ž";
                    case "[": return "Š";
                    case "]": return "Ć";
                    case "`": return "ž";
                    case "}": return "ć";
                    case "~": return "č";
                    case "{": return "š";
                    case "\\": return "Đ";
                    default: throw new Exception("Unexpected match!");
                }
            };
            for (idx = 0; idx < 500000; idx++)
            {
                string result1 = reg.Replace(test1, eval);
                string result2 = reg.Replace(test2, eval);
                string result3 = reg.Replace(test3, eval);
                string result4 = reg.Replace(test4, eval);
            }

            sw.Stop();
            Console.WriteLine("Richard (Regex w/ MatchEvaluator) way completed in " + sw.ElapsedMilliseconds.ToString() + " ms");

            // Marc Gravell way
            sw.Reset();
            sw.Start();
            for (idx = 0; idx < 500000; idx++)
            {
                string result1 = Sanitize(test1);
                string result2 = Sanitize(test2);
                string result3 = Sanitize(test3);
                string result4 = Sanitize(test4);
            }

            sw.Stop();
            Console.WriteLine("Marc Gravell (Static Regex) way completed in " + sw.ElapsedMilliseconds.ToString() + " ms");

            // user1664043 way
            sw.Reset();
            sw.Start();
            for (idx = 0; idx < 500000; idx++)
            {
                string result1 = ReplaceCharsWithIndexOfAny(test1);
                string result2 = ReplaceCharsWithIndexOfAny(test2);
                string result3 = ReplaceCharsWithIndexOfAny(test3);
                string result4 = ReplaceCharsWithIndexOfAny(test4);
            }

            sw.Stop();
            Console.WriteLine("user1664043 (StringBuilder w/ IndexOfAny) way completed in " + sw.ElapsedMilliseconds.ToString() + " ms\n");
        }
    }
}

2
“Regex”更快并不令人惊讶。它被构建为以荒谬的效率搜索字符串。永远记住,工具法则是不好的 - 利用为您正在尝试完成的任务而构建的技术,因此不要害怕使用Regex。C#并不是因为有API就擅长所有事情。很好的问题和良好的基准测试@Dejan。 - Mike Perrenoud
2
我还想补充一点 - 你的测试字符串非常短。虽然这可能适用于您的真实数据(在这种情况下,您的基准测试非常好且准确),但它会扭曲长字符串的结果,其中包含不同数量的字符需要进行替换等。我怀疑这是造成string.Replace相对良好性能的原因 - 它确实会反复创建字符串(只有在发生更改时才会这样做),但循环和字符串结果都非常小,因此代价不大。在长字符串上,差异将更加明显。 - Luaan
2
我认为正则表达式方法是最快的,因为字符串开头缺少'[',结尾缺少']'。根据所示示例,没有进行替换,因为我们没有匹配项!我认为两种正则表达式方法之间的巨大时间差异可以简单地解释为这个原因。 - Serge Weinstock
2
我在三个版本(不同长度和替换)中运行了这个测试,而logicnp始终是最好的,其次是oleksii。他甚至在原始测试中对我来说也是最好的?.NET 4.5,发布版,Win7,i7 - watbywbarif
2
总结一下:原始数字似乎没有反映出正则表达式已经损坏的事实。我重新运行了测试(添加了我的建议方法),并验证了结果是否正确。然后我取消了验证并重新运行了它们。我还使用相同的静态正则表达式、在测试运行中创建的正则表达式以及使用Regex.Replace(input,pattern,eval)运行了MatchEvaluator方法。MatchEvaluator方法始终是最慢的。根据测试数据,链式StringBuilder.Replace始终是最快的,然后是ToCharArray、string.Replace链、我的方法、Marc的方法,然后是Richard的方法。 - user1664043
显示剩余6条评论

15

最快的方法是自己进行性能比较。像问题中所示,尝试使用 StringBuilderRegex.Replace

但微基准测试并不考虑整个系统的范围。如果该方法只是整个系统的一小部分,则其性能可能对整个应用程序的性能无关紧要。

一些注释:

  1. 像上面使用 String (我假设)会创建大量中间字符串:需要更多的 GC 工作。但它很简单。
  2. 使用 StringBuilder 允许修改相同的基础数据,并且每次替换都会产生更少的垃圾。这样做会比使用 String 稍微复杂一些。
  3. 使用regex最为复杂(因为您需要编写代码来确定替换),但允许单个表达式。除非替换列表非常大并且在输入字符串中替换很少出现(即大多数替换方法调用仅代表搜索字符串的成本,而不会替换任何内容),否则我认为这将比其他方法更慢。

由于减少了 GC 负载,我预计 #2 在重复使用(数千次)时会略微更快。

对于正则表达式方法,您需要像下面这样:

newObj.Name = Regex.Replace(oldObj.Name.Trim(), @"[@^\[\]`}~{\\]", match => {
  switch (match.Value) {
    case "^": return "Č";
    case "@": return "Ž";
    case "[": return "Š";
    case "]": return "Ć";
    case "`": return "ž";
    case "}": return "ć";
    case "~": return "č";
    case "{": return "š";
    case "\\": return "Đ";
    default: throw new Exception("Unexpected match!");
  }
});

可以通过使用参数化的方式,使用一个Dictionary<char,char>来保存替换内容,并使用可重用的MatchEvaluator


感谢您的回答。请看我发布的基准测试作为另一个答案。 - Dejan Janjušević
@DejanJanjušević 对于正则表达式的笔误我很抱歉...我知道我需要一个字符类(正在更正)。 - Richard
然而,当我修复了打字错误后,结果很差...甚至比Marc的静态正则表达式还要慢。 - Dejan Janjušević
值得注意的是,正则表达式引擎会缓存最近传递给静态方法的几个(15个左右)正则表达式,因此在这个测试中,我不希望看到明确创建Regex实例的任何差异(只有第一次使用静态方法时编译速度会较慢)。 - Richard

10

试试这个:

Dictionary<char, char> replacements = new Dictionary<char, char>();
// populate replacements

string str = "mystring";
char []charArray = str.ToCharArray();

for (int i = 0; i < charArray.Length; i++)
{
    char newChar;
    if (replacements.TryGetValue(str[i], out newChar))
    charArray[i] = newChar;
}

string newStr = new string(charArray);

+1 我只会尝试添加一个IndexOfAny,以避免在不需要循环字符串时进行循环。 - Steve
2
@Steve - IndexOfAny 也会内部使用循环。没有办法避免单个循环。 - logicnp
感谢您的回答。请看我发布的基准测试作为另一个答案。 - Dejan Janjušević
1
@logicnp 是的,但是IndexOfAny非常快 - 如果字符串通常没有任何需要替换的内容,那么这可能意味着显著的节省(包括完全删除创建新char[]的过程 - 这不是很大的成本,但在事物的范围内仍然很重要)。请注意,ToCharArray和后面的new string(charArray)都会复制字符串的字符数据并分配所需的内存。 - Luaan
大部分时间都花在字典查找上。可以通过使用字符数组,将键字符作为索引,替换字符作为值来避免这种情况。然后,您可以检查 '\0' 来查看是否存在替换字符。 - TOS

7

一个可能的解决方案是使用StringBuilder类。

你可以先将代码重构为单一方法。

public string DoGeneralReplace(string input)
{
    var sb = new StringBuilder(input);
    sb.Replace("^", "Č")
      .Replace("@", "Ž") ...;
}


//usage
foreach (var oldObj in oldDB)
{
    NewObject newObj = new NewObject();
    newObj.Name = DoGeneralReplace(oldObj.Name);
    ...
}

感谢您的回答。请看我发布的基准测试作为另一个答案。 - Dejan Janjušević

3
你可以使用lambda表达式,在字符映射上使用Aggregate来实现这一点:
  //Method for replacing chars with a mapping
  static string Replace(string input, IDictionary<char, char> replacementMap) {
      return replacementMap.Keys
          .Aggregate(input, (current, oldChar) 
              => current.Replace(oldChar, replacementMap[oldChar]));
  }

你可以按照以下方式运行:
  private static void Main(string[] args) {
      //Char to char map using <oldChar, newChar>
      var charMap = new Dictionary<char, char>();
      charMap.Add('-', 'D'); charMap.Add('|', 'P'); charMap.Add('@', 'A');

      //Your input string
      string myString = "asgjk--@dfsg||jshd--f@jgsld-kj|rhgunfh-@-nsdflngs";

      //Your own replacement method
      myString = Replace(myString, charMap);

      //out: myString = "asgjkDDAdfsgPPjshdDDfAjgsldDkjPrhgunfhDADnsdflngs"
  }

谢谢您的回答。请看一下我发表的基准测试,作为另一个答案。 - Dejan Janjušević

2

好的,我会尝试做一些像这样的事情:

    static readonly Dictionary<char, string> replacements =
       new Dictionary<char, string>
    {
        {']',"Ć"}, {'~', "č"} // etc
    };
    static readonly Regex replaceRegex;
    static YourUtilityType() // static initializer
    {
        StringBuilder pattern = new StringBuilder().Append('[');
        foreach(var key in replacements.Keys)
            pattern.Append(Regex.Escape(key.ToString()));
        pattern.Append(']');
        replaceRegex = new Regex(pattern.ToString(), RegexOptions.Compiled);
    }
    public static string Sanitize(string input)
    {
        return replaceRegex.Replace(input, match =>
        {
            return replacements[match.Value[0]];
        });
    }

这个技术只需要在顶部维护一个位置,并构建一个预编译的 Regex 来处理替换。所有开销都仅在一次完成(因此是static)。

谢谢你的回答。请看我发布的基准测试作为另一个答案。 - Dejan Janjušević

1

使用IndexOfAny的混合StringBuilder方法:

protected String ReplaceChars(String sIn)
{
    int replChar = sIn.IndexOfAny(badChars);
    if (replChar < 0)
        return sIn;

    // Don't even bother making a copy unless you know you have something to swap
    StringBuilder sb = new StringBuilder(sIn, 0, replChar, sIn.Length + 10);
    while (replChar >= 0 && replChar < sIn.Length)
    {
        char? c = sIn[replChar];
        string s = null;
        // This approach lets you swap a char for a string or to remove some
        // If you had a straight char for char swap, you could just have your repl chars in an array with the same ordinals and do it all in 2 lines matching the ordinals.
        switch (c)
        {
            case "^": c = "Č";
            ...
            case '\ufeff': c = null; break;
        }
        if (s != null) sb.Append(s);
        else if (c != null) sb.Append(c);

        replChar++; // skip over what we just replaced
        if (replChar < sIn.Length)
        {
            int nextRepChar = sIn.IndexOfAny(badChars, replChar);
            sb.Append(sIn, replChar, (nextRepChar > 0 ? nextRepChar : sIn.Length) - replChar);
            replChar = nextRepChar;
        }
    }
    return sb.ToString();
}

嗨,我对此很好奇,已将您的输入添加到基准测试中。请查看编辑后的被接受答案以获取结果。 - Dejan Janjušević

1
自从.NET Core 2.1以来,使用带有switch语句的string.Create可能是最快的方法。
public static string ReplaceChars(s) {
    return string.Create(s.Length, s, (cc, s) =>
    {
        s.AsSpan().CopyTo(cc); // or just s.CopyTo(cc) in .NET 6+ 
        for (int i = 0; i < cc.Length; i++)
        {
            switch (cc[i])
            {
                case '^': cc[i] = 'Č'; break;
                case '@': cc[i] = 'Ž'; break;
                case '[': cc[i] = 'Š'; break;
                case ']': cc[i] = 'Ć'; break;
                case '`': cc[i] = 'ž'; break;
                case '}': cc[i] = 'ć'; break;
                case '~': cc[i] = 'č'; break;
                case '{': cc[i] = 'š'; break;
                case '\\': cc[i] = 'Đ'; break;
            }
        }
    });
}

从我的基准测试来看,与使用临时的char[]RegExStringBuilder.Replace相比,速度最快,提高了4到10倍。上述方法的主要优势是:
  1. 通过使用string.Create,不需要临时缓冲区。
  2. Span.CopyTo将由框架进行优化。
  3. 只需循环一次并直接访问Span
  4. 没有额外的GC分配。

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