.NET高级话题

04-13Ctrl+D 收藏本站

关灯 直达底部

Advanced.NET

下面的内容涉及某些尚未介绍过的特性:通过正则装配件(regex assemblies)构建正则表达式库,使用.NET专属的特性匹配嵌套结构,以及对Capture对象的讲解。

正则表达式装配件

Regex Assemblies

.NET能够把Regex对象封装到装配件(assembly)中,在构建正则表达式库时这很有用。下一页的补充内容提供了示例。

运行补充内容中的例子,能够在当前工程的bin代码目录下创建JfriedlsRegexLibrary.DLL。然后我们可以通过Visual Studio.NET的Project>Add Reference将其加入,在其他工程中使用这个装配件。

要使用装配件中的类,首先必须导入:

Imports jfriedl

然后就可以像其他任何类一样引用它们,例如:

在这个例子中,我仅仅从 jfriedl namespace 导入,但也可以很简单地从 jfiedl.CSV namespace导入,然后这样创建Regex对象:

Dim FieldRegex as GetField=New GetField\'生成新的regex对象

这是两种风格。

也可以不进行任何导入,而是直接使用:

Dim FieldRegex as jfriedl.CSV.GetField=New jfriedl.CSV.GetField

这有点麻烦,但是清楚地说明了对象的出处。同样,这只是风格的问题。

匹配嵌套结构

Matching Nested Constructs

微软提供了一种创新的功能,专门用于匹配对称的结构(长期以来,正则表达式对此无能为力)。它理解起来并不容易——本节篇幅不长,但请注意,其中的内容分量不少。

最简单的办法就是用一个例子来说明:

它匹配第一组正确配对的嵌套括号,例如中用下画线标注的部分。第一个开括号不会匹配,因为它没有对应的闭括号。

这里简要说明了程序的工作原理:

1.每匹配一个(超过正则表达式开头「(」的)‘(’,「(?<DEPTH>)」会把正则表达式保存的当

前括号嵌套深度值加1。

2.每匹配一个‘)’,「(?<-DEPTH>)」会把深度减1。

3.「(?(DEPTH)(?!))」确保最后的「)」匹配时,深度应该为0。

因为引擎的回溯堆栈保存了当前匹配成功分组的信息,这个办法没有问题。「(?<DEPTH>)」只是使用了命名捕获的「()」,它总是能成功匹配。因为它紧跟在「(」之后,它的成功匹配(此信息会保存在堆栈中,直到出栈为止)用于标记开括号的数目。

因此,当前已经成功匹配的‘DEPTH’分组总数就保存在回溯堆栈中。我们希望在找到闭括号之后减去它们。.NET独有的「(?<-DEPTH>)」结构,会从堆栈中去掉最近的“successful DEPTH”标记。如果不存在这样的标记,「(?<-DEPTH>)」就会报告失败,整个正则表达式的匹配宣告失败。

最后的「(?(DEPTH)(?!))」是一个普通的条件判断,如果‘DEPTH’分组匹配成功它会应用「(?!)」。如果在程序运行到此处时选择应用此分支,就表示还存在未匹配的开括号。果真如此的话,我们就需要退出匹配(我们不希望匹配不对称的序列)所以我们用否定型顺序环视「(?!)」来做检查,确保匹配失败。

看到了吗?这就是.NET正则表达式匹配嵌套结构的原理。

Capture对象

Capture Objects

.NET的对象模型中还包括Capture对象,之前一直没有介绍过。依视角的不同,它可能为匹配结果增加了新的观察角度,也可能是增加把结果弄得更糟。

Capture对象几乎等价于 Group对象,因为它表示一组捕获型括号匹配的文本。与 Group对象一样,它提供了 Value(匹配的文本)、Length(匹配文本的长度),以及 Index(匹配文本在目标字符串中的偏移值,编号从0开始)。

Group对象和Capture对象的主要差别是,每个Group对象都包含了一组Captures,分别对应到匹配过程中各分组的未确定匹配(intermediary match),以及该分组最终匹配的文本。

看下面这个例子:

Dim M as Match=Regex.Match("abcdefghijk","^(..)+")

正则表达式匹配了5 组「(..)」,包括了字符串中的绝大多数字符’:因为加号在括号外面,加号控制的每次迭代都会重新捕获,这个捕获型括号最后保存的是‘ij’(也就是说,M.Groups(1).Value等于‘ij’)。相反,M.Groups(1)同样包含一组Capture,它们对应到「(..)」的匹配过程:

你也许会注意到,最后匹配的‘ij’等同于最终全局匹配中的M.Groups(1).Value。看起来,Group的Value就是本分组最终匹配文本的简记法。M.Groups(1).Value是:

M.Groups (1).Captures(M.Groups(1).Captures.Count-1).Value

关于Capture,还要讲几点:

●M.Groups(1).Capture 是一个 CaptureCollection,与普通的集合类(collection)一样,它包含了Items和Count属性。不过,通常大家都不会使用这两个属性,而是通过索引值直接访问,例如 M.Groups(1).Captures(3)(在 C#中是 M.Groups[1].Captures[3])。

●Capture对象没有Success方法,如果需要,请测试Group的Success。

●到现在,我们已经看到,Capture 对象在 Group 对象内部可用。Match 对象也有Captures 属性,尽管涌出并不大。M.Captures 可以直接访问编号为 0 的分组的Captures属性(也就是说 M.Captures等价于 M.Groups(0).Captures)。因为编号为0的分组表示整个匹配,所以不会有“遍历”匹配的迭代,所以编号为0的捕获集合类只有一个Capture。因为它们包含与编号为0的匹配同样的信息,M.Capters和M.Groups(0).Captures并不是很有用。

.NET的Capture对象是一种创新,但是因为与对象模型“集成过度(overly integrated)”,使用起来反而更复杂,而且令人迷惑。在仔细参阅了.NET的文档,并真正理解了这些对象之后,我感觉这种做法有利也有弊。一方面,我乐于看到这种创新。虽然它的用法并不会马上显现出来,但这或许是因为一直以来我都习惯于用传统的正则表达式特性来思考问题。

另一方面,在匹配过程中的额外的分组,匹配完成之后把它们封装到一个对象中,似乎降低了效率,我并不希望降低效率,除非要得到额外的信息。增加的 Capture分组在大多数匹配中不会用到,但是照目前的情况来看,生成 Match 对象时会构建所有的 Group 和Capture对象(以及它们相关的GroupCollection和CaptureCollection对象)。所以无论是否需要,它们都在那里,如果你能够发现Capture对象的使用价值,就不要放过。