替换字符串中的反向引用语法(为什么使用美元符号?)

50
在Java以及其他几种编程语言中,模式中的回溯引用前面需要加上反斜杠(例如\1、\2、\3等),但在替换字符串中则需要加上美元符号(例如$1、$2、$3以及$0)。下面是一个代码片段来说明这一点:
System.out.println(
    "left-right".replaceAll("(.*)-(.*)", "\\2-\\1") // WRONG!!!
); // prints "2-1"

System.out.println(
    "left-right".replaceAll("(.*)-(.*)", "$2-$1")   // CORRECT!
); // prints "right-left"

System.out.println(
    "You want million dollar?!?".replaceAll("(\\w*) dollar", "US\\$ $1")
); // prints "You want US$ million?!?"

System.out.println(
    "You want million dollar?!?".replaceAll("(\\w*) dollar", "US$ \\1")
); // throws IllegalArgumentException: Illegal group reference

问题:

  • 在替换字符串中使用$表示反向引用,这种方式只有Java使用吗?如果不是,哪种语言开始使用它?哪些模式使用了它而哪些没有?
  • 为什么这是一个好主意?为什么不坚持相同的模式语法呢?这难道不会导致语言更加协调和容易学习吗?
    • 如果上述语句中的1和4是“正确”的语句而不是2和3,那么语法是否会更加简洁?

1
\1\2是八进制转义序列,分别描述了八进制数字1和2所代表的字符(请参见http://java.sun.com/docs/books/jls/second_edition/html/lexical.doc.html#101089)。这就是为什么您需要不同的引用语法。 - Gumbo
请参见https://dev59.com/kXA65IYBdhLWcg3w7TWZ。 - polygenelubricants
2个回答

36
在Java中,使用$来表示替换字符串中的反向引用,这种方式并不唯一。Perl也使用了这种方式,而且Perl显然比Java的Pattern类更早。Java的正则表达式支持是明确以Perl正则表达式为基础描述的。
例如: http://perldoc.perl.org/perlrequick.html#Search-and-replace
那么,为什么这是一个好主意呢?显然你并不认为这是个好主意!但是,这样做的一个原因是使Java的搜索/替换支持与Perl更加兼容。
还有另一个可能的原因是,$可能被认为比\更好的选择。因为在Java字符串中,\必须写成\\。
但这都是纯粹的猜测。我们都不在设计决策时的会议室内。最终,为什么要以这种方式设计替换字符串语法并不重要。决策已经确定,并被确定下来,任何进一步讨论都只是学术性质的...除非你恰好正在为Java设计新的语言或新的正则表达式库。

10
+1 赞同…现在很多正则表达式引擎之所以采用这种方式,是因为Perl以这种方式实现。因此,要真正理解它,你需要理解Perl背后的原因。(警告:不要在家里尝试) - David Z
5
Perl的正则表达式非常强大。现在你随处可见它被应用于JavaScript、XML、Java、PHP等等编程语言中。 - amphetamachine
3
"Perl pwns at regex" 可以翻译为 "Perl 在正则表达式方面很强",意思是 Perl 编程语言在处理正则表达式方面非常出色。 - Stephen C

20
经过一些研究,我现在了解这个问题:Perl必须使用不同的符号来表示模式反向引用和替换反向引用,而java.util.regex.*之所以选择这样做,不是因为技术原因,而是出于传统原因。

关于Perl

请注意,此时我对Perl的了解仅来自于阅读维基百科文章,如有错误,请随意纠正。
Perl之所以必须按此方式处理的原因如下:
  • Perl使用$作为标记(即与变量名相关的符号)。
  • Perl字符串文本是可变插值的。
  • Perl正则表达式实际上将组作为变量$1、$2等捕获。
因此,由于Perl的解释方式及其正则表达式引擎的工作方式,模式中的反向引用(例如\1)必须使用前导斜杆,因为如果使用标记$(例如$1),它会导致意外地将变量插入到模式中。
由于在Perl中替换字符串的工作方式,它会在每个匹配中进行求值。对Perl来说,最自然的方法是在此处使用变量插值,因此正则表达式引擎将组捕获到变量$1、$2等中,以使其与语言的其他部分无缝协作。

参考文献


关于Java

Java和Perl是两种非常不同的语言,但最重要的是Java中没有变量插值。此外,replaceAll是一种方法调用,在Java中与所有方法调用一样,参数在方法调用之前只会被计算一次。

因此,仅有变量插值特性是不够的,因为实质上替换字符串必须在每次匹配时重新评估,这并不是Java中方法调用的语义。在replaceAll被调用之前就被评估的插值替换字符串实际上是无用的;插值需要在每次匹配期间发生。

由于这不是Java语言的语义,replaceAll必须手动进行“即时”插值。因此,在替换字符串中使用$作为反向引用的转义符号绝对没有技术原因。它完全可以是\。相反,在模式中的反向引用也可以使用$而不是\进行转义,从技术上讲仍然可以正常工作。

Java采用这种方式处理正则表达式的原因纯粹是传统的:它只是遵循Perl设定的先例。


5
在正则表达式中,“$”已经被用作锚定符号。如果将其用作反向引用符号,这将非常混乱,甚至不可能实现。在替换字符串中,反斜杠用于消除歧义;如果“$10”可能指的是第十个组,但你想让它表示第一个组后面加上零,你需要写成“$1\0”。当然,你还可以用它来转义字面上的“$”。这与它在正则表达式和Java字符串字面值中的使用方式一致,因此这并不是完全随意的选择。 - Alan Moore

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