不区分大小写的字符串替换

298

我有一个字符串叫做"hello world"

我需要将单词"world"替换为"csharp"

我使用以下代码实现:

string.Replace("World", "csharp");

但是结果是,我没有得到被替换的字符串。原因是大小写敏感。原始字符串包含了 "world",而我尝试替换的是 "World"。

有没有办法避免 string.Replace 方法的大小写敏感性?


5
这里你可以找到类似的问题:有没有不区分大小写的string.Replace替代方案? - Michał Kuliński
微软没有将这样基本的功能加入框架中,真是遗憾! - Elmue
3
对于.NET 5和最近的.NET Core版本,只需使用string.Replace的重载,该重载采用StringComparison,如https://dev59.com/rW015IYBdhLWcg3w_Q0_#64677285/中所述。 - Sepia
20个回答

394

你可以使用正则表达式进行不区分大小写的替换:

class Program
{
    static void Main()
    {
        string input = "hello WoRlD";
        string result = 
           Regex.Replace(input, "world", "csharp", RegexOptions.IgnoreCase);
        Console.WriteLine(result); // prints "hello csharp"
    }
}

25
不适用于正则表达式语言元素,因此它不是通用方法。Steve B的答案是正确的。 - AsValeO
1
所以最好不要写hello. world?或任何其他包含正则表达式运算符的内容。 - Sebastian Mach
13
以防万一有人不想继续阅读,这是2011年的接受答案,并获得了大量投票。如果您只需要替换字母数字,则可以正常工作。但是,如果您需要替换任何标点符号字符,则可能会遇到大麻烦。Oleg Zarevennyi的答案更好,但由于是在2017年发布的,所以只获得了很少的投票。 - Tony Pulokas
替换使用$$无效。如果您的替换内容可能包含$$,请使用Oleg的解决方案。 - Steven Spyrka
正则表达式太复杂了。它真的是最糟糕的东西之一。学习曲线非常陡峭,仅仅入门就需要花费很高的代价,要真正掌握更是难上加难。因此,大多数人只能去互联网上复制/粘贴经过充分测试的正则表达式。即使这样,它们通常也不完美。试着寻找一个完美的用于电子邮件的正则表达式,你就会明白我的意思了。它确实有其用处,特别是在声明性上下文中,例如 IIS 重写规则,在那里你不能使用过程逻辑。但是,在 c# 中,我绝对不需要它,我可以编写完全易读且易于维护的代码。 - BVernon

147
var search = "world";
var replacement = "csharp";
string result = Regex.Replace(
    stringToLookInto,
    Regex.Escape(search), 
    replacement.Replace("$","$$"), 
    RegexOptions.IgnoreCase
);

Regex.Escape方法非常有用,如果你使用了用户输入,其中可能包含正则表达式语言元素

更新

根据评论,实际上您不必对替换字符串进行转义。

这是一个测试代码的小fiddle:

using System;
using System.Text.RegularExpressions;           
public class Program
{
    public static void Main()
    {

        var tests = new[] {
            new { Input="abcdef", Search="abc", Replacement="xyz", Expected="xyzdef" },
            new { Input="ABCdef", Search="abc", Replacement="xyz", Expected="xyzdef" },
            new { Input="A*BCdef", Search="a*bc", Replacement="xyz", Expected="xyzdef" },
            new { Input="abcdef", Search="abc", Replacement="x*yz", Expected="x*yzdef" },       
            new { Input="abcdef", Search="abc", Replacement="$", Expected="$def" },
        };


        foreach(var test in tests){
            var result = ReplaceCaseInsensitive(test.Input, test.Search, test.Replacement);

            Console.WriteLine(
                "Success: {0}, Actual: {1}, {2}",
                result == test.Expected,
                result,
                test
            );

        }


    }

    private static string ReplaceCaseInsensitive(string input, string search, string replacement){
        string result = Regex.Replace(
            input,
            Regex.Escape(search), 
            replacement.Replace("$","$$"), 
            RegexOptions.IgnoreCase
        );
        return result;
    }
}

它的输出结果是:

Success: True, Actual: xyzdef, { Input = abcdef, Search = abc, Replacement = xyz, Expected = xyzdef } 
Success: True, Actual: xyzdef, { Input = ABCdef, Search = abc, Replacement = xyz, Expected = xyzdef }
Success: True, Actual: xyzdef, { Input = A*BCdef, Search = a*bc, Replacement = xyz, Expected = xyzdef } 
Success: True, Actual: x*yzdef, { Input = abcdef, Search = abc, Replacement = x*yz, Expected = x*yzdef} 
Success: True, Actual: $def, { Input = abcdef, Search = abc, Replacement = $, Expected = $def }

2
如果替换字符串为“!@#$%^&*()”,则此方法会失败。你将得到“!@#$%^&*()”作为替换结果。 - Kcoder
2
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Danny Tuppeny
2
@dannyTuppeny:你说得对...我已经相应更新了答案。 - Steve B

108

比其他正则表达式方法快2.5倍,并且最有效

/// <summary>
/// Returns a new string in which all occurrences of a specified string in the current instance are replaced with another 
/// specified string according the type of search to use for the specified string.
/// </summary>
/// <param name="str">The string performing the replace method.</param>
/// <param name="oldValue">The string to be replaced.</param>
/// <param name="newValue">The string replace all occurrences of <paramref name="oldValue"/>. 
/// If value is equal to <c>null</c>, than all occurrences of <paramref name="oldValue"/> will be removed from the <paramref name="str"/>.</param>
/// <param name="comparisonType">One of the enumeration values that specifies the rules for the search.</param>
/// <returns>A string that is equivalent to the current string except that all instances of <paramref name="oldValue"/> are replaced with <paramref name="newValue"/>. 
/// If <paramref name="oldValue"/> is not found in the current instance, the method returns the current instance unchanged.</returns>
[DebuggerStepThrough]
public static string Replace(this string str,
    string oldValue, string newValue,
    StringComparison comparisonType)
{

    // Check inputs.
    if (str == null)
    {
        // Same as original .NET C# string.Replace behavior.
        throw new ArgumentNullException(nameof(str));
    }
    if (oldValue == null)
    {
        // Same as original .NET C# string.Replace behavior.
        throw new ArgumentNullException(nameof(oldValue));
    }
    if (oldValue.Length == 0)
    {
        // Same as original .NET C# string.Replace behavior.
        throw new ArgumentException("String cannot be of zero length.");
    }
    if (str.Length == 0)
    {
        // Same as original .NET C# string.Replace behavior.
        return str;
    }
    

    //if (oldValue.Equals(newValue, comparisonType))
    //{
    //This condition has no sense
    //It will prevent method from replacesing: "Example", "ExAmPlE", "EXAMPLE" to "example"
    //return str;
    //}



    // Prepare string builder for storing the processed string.
    // Note: StringBuilder has a better performance than String by 30-40%.
    StringBuilder resultStringBuilder = new StringBuilder(str.Length);



    // Analyze the replacement: replace or remove.
    bool isReplacementNullOrEmpty = string.IsNullOrEmpty(newValue);



    // Replace all values.
    const int valueNotFound = -1;
    int foundAt;
    int startSearchFromIndex = 0;
    while ((foundAt = str.IndexOf(oldValue, startSearchFromIndex, comparisonType)) != valueNotFound)
    {
        
        // Append all characters until the found replacement.
        int charsUntilReplacment = foundAt - startSearchFromIndex;
        bool isNothingToAppend = charsUntilReplacment == 0;
        if (!isNothingToAppend)
        {
            resultStringBuilder.Append(str, startSearchFromIndex, charsUntilReplacment);
        }
        


        // Process the replacement.
        if (!isReplacementNullOrEmpty)
        {
            resultStringBuilder.Append(newValue);
        }
        

        // Prepare start index for the next search.
        // This needed to prevent infinite loop, otherwise method always start search 
        // from the start of the string. For example: if an oldValue == "EXAMPLE", newValue == "example"
        // and comparisonType == "any ignore case" will conquer to replacing:
        // "EXAMPLE" to "example" to "example" to "example" … infinite loop.
        startSearchFromIndex = foundAt + oldValue.Length;
        if (startSearchFromIndex == str.Length)
        {
            // It is end of the input string: no more space for the next search.
            // The input string ends with a value that has already been replaced. 
            // Therefore, the string builder with the result is complete and no further action is required.
            return resultStringBuilder.ToString();
        }
    }
    

    // Append the last part to the result.
    int charsUntilStringEnd = str.Length - startSearchFromIndex;
    resultStringBuilder.Append(str, startSearchFromIndex, charsUntilStringEnd);


    return resultStringBuilder.ToString();

}

注意:StringComparison comparisonType参数中使用StringComparison.OrdinalIgnoreCase,忽略大小写。这是最快的、不区分大小写的替换所有值的方法。


这种方法的优点:

  • 高CPU和内存效率;
  • 它是最快的解决方案,比其他正则表达式方法快2.5倍(证明在结尾处);
  • 适用于从输入字符串中删除部分(将newValue设置为null),针对此进行了优化;
  • 与原始的.NET C#string.Replace行为相同,同样有异常;
  • 注释良好,易于理解;
  • 更简单-没有正则表达式。正则表达式总是较慢,因为它们的多功能性(即使编译也是如此);
  • 这种方法经过充分测试,没有像其他高评级解决方案中的无限循环等隐藏缺陷:

@AsValeO:不适用于Regex语言元素,因此不是通用方法

@Mike Stillion:这段代码存在问题。如果新文本是旧文本的超集,这可能会产生无限循环。


基准测试证明:这个解决方案比@Steve B.的正则表达式快2.59倍,代码如下:

// Results:
// 1/2. Regular expression solution: 4486 milliseconds
// 2/2. Current solution: 1727 milliseconds — 2.59X times FASTER! than regex!

// Notes: the test was started 5 times, the result is an average; release build.

const int benchmarkIterations = 1000000;
const string sourceString = "aaaaddsdsdsdsdsd";
const string oldValue = "D";
const string newValue = "Fod";
long totalLenght = 0;

Stopwatch regexStopwatch = Stopwatch.StartNew();
string tempString1;
for (int i = 0; i < benchmarkIterations; i++)
{
    tempString1 = sourceString;
    tempString1 = ReplaceCaseInsensitive(tempString1, oldValue, newValue);

    totalLenght = totalLenght + tempString1.Length;
}
regexStopwatch.Stop();



Stopwatch currentSolutionStopwatch = Stopwatch.StartNew();
string tempString2;
for (int i = 0; i < benchmarkIterations; i++)
{
    tempString2 = sourceString;
    tempString2 = tempString2.Replace(oldValue, newValue,
        StringComparison.OrdinalIgnoreCase);

    totalLenght = totalLenght + tempString2.Length;
}
currentSolutionStopwatch.Stop();

原始想法 - @Darky711;感谢@MinerR提供StringBuilder


6
我敢打赌,如果使用 StringBuilder 而不是 string,你可以让这个过程更快。 - MineR
2
@MineR 你说得对,我最初只是更新了 @Darky711 的解决方案,避免了无限循环,所以我使用了 String。然而,StringBuilderString 快 **30-40%**。我已经更新了解决方案。谢谢 ;) - Oleg Zarevennyi
2
有趣的方法。在性能要求较高时,可能是更好的选择(比我的更好:))。通常是将方法添加到共享代码库中的一种方式。 - Steve B
2
使用 'nameof' 表达式只适用于 C# 6.0 及以上版本。如果您使用的是 VS2013,则可以通过简单地删除异常中的操作数来使用它。 - LanchPad
1
调用replace("abc","abc\u00AD","def",StringComparison.CurrentCulture)时出现了错误,期望的结果是"def"(\u00AD是软连字符——测试用例取自于.net core字符串替换测试用例,网址为https://github.com/dotnet/runtime/blob/1bfdab43807670a7fba468d09b9afd1a72d1f8e3/src/libraries/System.Runtime/tests/System/StringTests.cs)。修复方法是将"if (startSearchFromIndex == str.Length)"更改为"if (startSearchFromIndex >= str.Length)"。 - Dani Avni
显示剩余2条评论

37

有很多建议使用正则表达式。那么不用它的扩展方法如何:

public static string Replace(this string str, string old, string @new, StringComparison comparison)
{
    @new = @new ?? "";
    if (string.IsNullOrEmpty(str) || string.IsNullOrEmpty(old) || old.Equals(@new, comparison))
        return str;
    int foundAt = 0;
    while ((foundAt = str.IndexOf(old, foundAt, comparison)) != -1)
    {
        str = str.Remove(foundAt, old.Length).Insert(foundAt, @new);
        foundAt += @new.Length;
    }
    return str;
}

请注意,比较参数并未用于执行实际的替换操作(它始终不区分大小写)。 - Bolo
3
这段代码存在问题。如果new中的文本是old中文本的超集,那么它可能会产生一个无限循环。一旦在FoundAt处插入了new,则需要将FoundAt的值增加new的长度,才能避免这种情况。 - Mike Stillion
@Bolo 我已经编辑过了,使用了比较参数(可能需要一点时间进行同行评审)。 - bradlis7
2
我还会将返回新字符串的条件分开:if(old.Equals(@new, comparison)) return @new;,因为新字符串可能在大小写方面有所不同。 - sɐunıɔןɐqɐp
老实说,@new 看起来让我眼睛疼。 - Selman Genç
显示剩余2条评论

34

扩展程序使我们的生活更轻松:

static public class StringExtensions
{
    static public string ReplaceInsensitive(this string str, string from, string to)
    {
        str = Regex.Replace(str, from, to, RegexOptions.IgnoreCase);
        return str;
    }
}

16
逃避使我们的生活更少出错 :-) 使用Regex.Escape(search)将input中的search替换为replacement.Replace("$", "$$"),并忽略大小写。 - Vman

21

.Net Core内置了这个方法:Replace(String, String, StringComparison)文档。现在我们可以简单地写: "...".Replace("oldValue", "newValue", StringComparison.OrdinalIgnoreCase)


19
您可以使用 Microsoft.VisualBasic 命名空间来查找此辅助函数:
Replace(sourceString, "replacethis", "withthis", , , CompareMethod.Text)

2
我曾经为我的答案感到自豪,直到我看到这个更好的答案,因为它是内置的。例如:Strings.Replace("TeStInG123", "t", "z", 1, -1, CompareMethod.Text) 返回 "zeSzInG123"。 - Bolo
2
警告:如果被搜索的字符串为空字符串,Strings.Replace将返回null。 - Mafu Josh
2
在 .Net 4.7.2 中,您需要添加对 Microsoft.VisualBasic 的引用才能使其正常工作。在 .Net Core 中,Microsoft.VisualBasic.Strings 类(无论是在版本 10.3.0 中)似乎没有实现 Replace 函数。如果您首先添加 -AssemblyName Microsoft.VisualBasic,则此方法也适用于 Powershell。 - Prof Von Lemongargle
我不喜欢这个解决方案,因为即使没有替换任何内容,它也会复制整个字符串,所以可能会非常庞大。我之前重新编写了它,以避免这种情况发生,因为字符串是不可变的。 - Brain2000

8

修改了 @Darky711 的答案,使用传递进来的比较类型,并尽可能与框架中的替换命名和 xml 注释匹配。

/// <summary>
/// Returns a new string in which all occurrences of a specified string in the current instance are replaced with another specified string.
/// </summary>
/// <param name="str">The string performing the replace method.</param>
/// <param name="oldValue">The string to be replaced.</param>
/// <param name="newValue">The string replace all occurrances of oldValue.</param>
/// <param name="comparisonType">Type of the comparison.</param>
/// <returns></returns>
public static string Replace(this string str, string oldValue, string @newValue, StringComparison comparisonType)
{
    @newValue = @newValue ?? string.Empty;
    if (string.IsNullOrEmpty(str) || string.IsNullOrEmpty(oldValue) || oldValue.Equals(@newValue, comparisonType))
    {
        return str;
    }
    int foundAt;
    while ((foundAt = str.IndexOf(oldValue, 0, comparisonType)) != -1)
    {
        str = str.Remove(foundAt, oldValue.Length).Insert(foundAt, @newValue);
    }
    return str;
}

6

(编辑:不知道“裸露的链接”问题,对此感到抱歉)

源自这里

string myString = "find Me and replace ME";
string strReplace = "me";
myString = Regex.Replace(myString, "me", strReplace, RegexOptions.IgnoreCase);

看起来你不是第一个抱怨缺乏不区分大小写的字符串替换功能的人。


3
这样不行吗:我无法想象还有什么比这更快更容易的事情。
public static class ExtensionMethodsString
{
    public static string Replace(this String thisString, string oldValue, string newValue, StringComparison stringComparison)
    {
        string working = thisString;
        int index = working.IndexOf(oldValue, stringComparison);
        while (index != -1)
        {
            working = working.Remove(index, oldValue.Length);
            working = working.Insert(index, newValue);
            index = index + newValue.Length;
            index = working.IndexOf(oldValue, index, stringComparison);
        }
        return working;
    }
}

我不知道它是否更快,但它很简洁,不使用正则表达式开销和潜在问题,并使用内置的StringComparison。 - fvlinden

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