.NET中的Regex.Replace存在Bug吗?

9

The following code...

using System;
using System.Text.RegularExpressions;

public class Program
{
    public static void Main()
    {
        var r = new Regex("(.*)");
        var c = "XYZ";
        var uc = r.Replace(c, "A $1 B");

        Console.WriteLine(uc);
    }
}

.Net Fiddle链接

生成以下输出...

A XYZ BA B

你认为这正确吗?

输出不应该是...

A XYZ B

我觉得我在这里做了一些愚蠢的事情。 我会感激您提供任何帮助,帮助我理解这个问题。


下面是一些有趣的东西...

using System;
using System.Text.RegularExpressions;

public class Program
{
    public static void Main()
    {
        var r = new Regex("(.*)");
        var c = "XYZ";
        var uc = r.Replace(c, "$1");

        Console.WriteLine(uc);
    }
}

.Net Fiddle

输出结果...

XYZ


1
你的正则表达式有两个匹配项,并且 Replace 将替换它们两个。第一个是 "XYZ",第二个是空字符串。我不确定的是为什么它首先有两个匹配项。你可以使用 ^(.*)$ 来强制它考虑字符串的开头和结尾来解决这个问题。 - Matt Burland
1
如果您的模式与空字符串匹配且没有其他限制,则在字符串末尾始终会获得额外的匹配。 - Damien_The_Unbeliever
5
编程的第一条规则:问题总是出在你身上。 - asawyer
1
请参见此处 http://stackoverflow.com/questions/16103346/why-empty-regex-and-empty-capturing-group-regex-return-string-length-plus-one-re 以获取关于字符串末尾额外匹配的解释。 - PashaPash
2
@w0lf:可能因正则表达式引擎而异。Ruby的引擎似乎以相同方式解释它(请参见匹配项:http://rubular.com/r/cRaG0rPowZ)。 - ohaal
显示剩余4条评论
5个回答

5
关于为什么引擎返回了2个匹配项,这是由.NET(也包括Perl和Java)处理全局匹配的方式决定的,即在输入字符串中查找给定模式的所有匹配项。该过程可以描述如下(当前索引通常在搜索开始时设置为0,除非指定):
1. 从当前索引开始进行搜索。 2. 如果没有匹配项: a. 如果当前索引已经指向字符串的末尾(当前索引>=字符串长度),则返回到目前为止的结果。 b. 将当前索引增加1,转到步骤1。 3. 如果主匹配项($0)非空(至少消耗了一个字符),则添加结果并将当前索引设置为主匹配项($0)的末尾。然后转到步骤1。 4. 如果主匹配项($0)为空: a. 如果先前的匹配项非空,则添加结果并转到步骤1。 b. 如果先前的匹配项为空,则回溯并继续搜索。 c. 如果回溯尝试找到一个非空匹配项,则添加结果,将当前索引设置为匹配项的末尾并转到步骤1。 d. 否则,将当前索引增加1。 转到步骤1。
引擎需要检查空匹配项;否则,它将陷入无限循环。 设计人员认识到空匹配项的用法(例如将字符串分割为字符),因此必须设计引擎以避免在某个位置上永久停留。
这个过程解释了为什么末尾有一个空匹配项:因为在(. *)匹配了abc之后,在字符串的末尾(索引3)进行搜索,并且(. *)可以匹配一个空字符串,因此找到了一个空匹配项。 引擎不会产生无数个空匹配项,因为已经在末尾找到了一个空匹配项。
 a b c
^ ^ ^ ^
0 1 2 3

首先匹配:

 a b c
^     ^
0-----3

第二个匹配项:

 a b c
      ^
      3

使用以上全局匹配算法,同一索引位置只能有最多2个匹配项,且仅当第一个匹配为空时才会出现这种情况。
请注意,如果主匹配项为空,则JavaScript仅将当前索引增加1,因此每个索引位置最多匹配1次。然而,在此情况下(.*),如果您使用全局标志g进行全局匹配,则会出现相同的结果:
(以下结果来自Firefox,请注意g标志)。
> "XYZ".replace(/(.*)/g, "A $1 B")
"A XYZ BA  B"

1
我理解你的意思。在结尾匹配空字符串是这个实现方式的一个副作用。但我认为这是一个错误而不是一个特性。.net 实现应该像 JavaScript 正则表达式引擎一样工作。为你的努力点赞。 - Sandeep Datta
@SandeepDatta:我想这是理解和熟悉语言特性与怪异之处的问题。 - nhahtdh

4

我需要思考一下为什么会发生这种情况。我确定你漏掉了某些东西。尝试将正则表达式固定在一个锚点上,可能会解决问题。

var r = new Regex("^(.*)$");

这里是.NetFiddle演示


1
我更喜欢这个正则表达式,因为它也适用于空输入字符串。 - Sandeep Datta

3
你的正则表达式有两个匹配项,Replace 将替换它们。第一个是 "XYZ",第二个是空字符串。我不确定它为什么会有两个匹配项。你可以使用 ^(.*)$ 来强制其考虑字符串的开头和结尾来解决它。
或者使用 + 而不是 * 来强制它匹配至少一个字符。
.* 匹配空字符串,因为它没有字符。
.+ 不匹配空字符串,因为它要求至少一个字符。
有趣的是,在 Javascript(在 Chrome 中):
var r = /(.*)/;
var s = "XYZ";
console.log(s.replace(r,"A $1 B");

将会输出预期的A XYZ B,而不会有多余的匹配。

编辑(感谢@nhahtdh):但是在Javascript正则表达式中添加g标志,将会得到与.NET相同的结果:

var r = /(.*)/g;
var s = "XYZ";
console.log(s.replace(r,"A $1 B");

1
因为你是第一个回答的人,所以已经被接受了。我已经在你的回答中添加了评论。 - Sandeep Datta
@SriramSakthivel:没错,我同意你的观点。我在Javascript中尝试了相同的操作(请参见我的编辑),但它没有匹配和替换空字符串。 - Matt Burland
1
@MattBurland 好奇地尝试了 RegexOptions.EcmaScript 但失败了 :( - Sriram Sakthivel
@SriramSakthivel:下面的Aaron Palmer的回答让我想知道是否这是一个以空值结尾的字符串问题。据我所知,C#字符串(至少在内部)是以空值结尾的,而JavaScript字符串则不是。 - Matt Burland
1
你忘记了加上 g 标志:"XYZ".replace(/(.*)/g, "A $1 B")。这里没有理由 JS 返回不同的结果。如果你只匹配一次(没有 g 标志),那么就什么有趣的事情都不会发生。 - nhahtdh
显示剩余2条评论

3
*量词匹配0或多个。这会导致有2个匹配项。XYZ和空白。请尝试使用+量词,它匹配1个或多个。简单的解释是将字符串看作 XYZ<nothing>。对于每个匹配项:
  1. 我们有匹配项XYZ<nothing>
  2. 对于每个匹配
    • 匹配1:用A $ 1 B($1在此处是XYZ)替换XYZ,结果:A XYZ B
    • 匹配2:用A $ 1 B($1在此处是<nothing>)替换<nothing>,结果:A B
最终结果:A XYZ BA B。为什么<nothing>本身就是一个匹配项值得思考,这是我没有想过的事情。(为什么没有无限的<nothing>匹配项?)

非常有道理!这也解释了第二个片段。 - Sandeep Datta
1
* 匹配0个或多个字符,但默认情况下它是贪婪的正则表达式,对吧?所以它应该匹配所有字符!如果我错了,请纠正我。 - Sriram Sakthivel
我认为这是一个定义问题。贪心地说,全无和全有都是匹配的。如果什么都加在一起,那么无就不再是无了,因此它们必须是两个单独的匹配……? - ohaal
为什么 <nothing> 本身是一个匹配项,这很有趣,而且我没有思考过。 (为什么不存在无限的 <nothing> 匹配项?)所以你想在搜索所有匹配项时在引擎中创建一个无限循环? - nhahtdh
@nhahtdh 请继续! - Sandeep Datta
显示剩余5条评论

1
正则表达式是一种特殊的语言。您必须准确理解(.*)将匹配哪些内容。您还需要了解贪婪性。
*将贪婪地匹配0个或多个字符。因此,在字符串"XYZ"中,它将使用第一个匹配项匹配整个字符串并将其放置在$1位置,从而为您提供以下结果:
A XYZ B
然后它将继续尝试匹配,并在字符串末尾匹配null,将$1设置为null,从而为您提供以下结果:
A B
导致您看到的字符串如下:
A XYZ BA B
如果您想限制贪婪度并逐个匹配每个字符,则可以使用以下表达式:
(.*?)
这将分别匹配每个字符X、Y和Z,以及末尾的null,并得到以下结果:
A BXA BYA BZA B
如果您不希望您的正则表达式超出给定字符串的范围,则请使用^和$标识符限制您的正则表达式。
为了让你更好地理解正在发生的事情,请考虑这个测试以及相应的匹配组。
    [TestMethod()]
    public void TestMethod3()
    {
        var myText = "XYZ";
        var regex = new Regex("(.*)");
        var m = regex.Match(myText);
        var matchCount = 0;
        while (m.Success)
        {
            Console.WriteLine("Match" + (++matchCount));
            for (int i = 1; i <= 2; i++)
            {
                Group g = m.Groups[i];
                Console.WriteLine("Group" + i + "='" + g + "'");
                CaptureCollection cc = g.Captures;
                for (int j = 0; j < cc.Count; j++)
                {
                    Capture c = cc[j];
                    Console.WriteLine("Capture" + j + "='" + c + "', Position=" + c.Index);
                }
            }
            m = m.NextMatch();
        }

输出:

Match1
Group1='XYZ'
Capture0='XYZ', Position=0
Group2=''
Match2
Group1=''
Capture0='', Position=3
Group2=''

请注意,有两个匹配的组。第一个是整个XYZ组,第二个是一个空组。尽管如此,仍然有两个匹配的组。因此,在第一种情况下,$1被替换为XYZ,在第二种情况下被替换为null
另外,请注意,前斜杠/只是在.NET正则表达式引擎中考虑的另一个字符,并没有特殊含义。JavaScript解析器处理/不同,因为它存在于HTML解析器的框架中,其中</是一个特殊考虑因素。
最后,要得到您真正需要的内容,请考虑以下测试:
    [TestMethod]
    public void TestMethod1()
    {
        var r = new Regex(@"^(.*)$");
        var c = "XYZ";
        var uc = r.Replace(c, "A $1 B");

        Assert.AreEqual("A XYZ B", uc);
    }

那么...你是在暗示C#和Javascript行为之间的差异是因为C#字符串是以空字符结尾的(至少在内部是这样),而Javascript字符串不是(据我所知)? - Matt Burland
考虑到C#字符串不是以空字符结尾,这确实是一个有趣的问题。但是,.net正则表达式引擎似乎会尝试匹配超出给定字符串范围的内容,如果没有使用^和$进行限制的话。 - Aaron Palmer
1
尽管从API的角度来看,字符串并不是以null结尾的,但字符数组是以null结尾的,这意味着它可以直接传递给未托管的函数而无需涉及任何复制,假设Interop指定该字符串应作为Unicode进行编组。因此,我的想法是内部的正则表达式引擎必须直接使用字符数组,并看到null终止字符。但我确实不知道。 - Matt Burland
1
@AaronPalmer:我认为它没有超出边界。引擎只是将长度为3的字符串“abc”视为具有4个索引(0、1、2、3)。搜索在索引3处进行,这解释了空字符串结果。当然,访问字符串的索引3根本行不通,但我们应该将索引视为字符之间的空格。 - nhahtdh
@MattBurland,非常可能,并且这也与nhahtdh的解释很好地契合。 - Aaron Palmer

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