大家都经常使用.NET反射,用内置的反射API就可以进行灵活而强大的操作。然而,有的时候我们需要某些反射的操作,但是却有不能加载包含所需类型的程序集。例如,我们要操作的类型是.NET Compact Framework或者Silverlight的类型,而我们的程序运行在桌面版本的.NET Framework上等等。实际上,我们有时仅仅需要非常简单的操作,例如从一个类型获得它的数组类型;将一个泛型类型的类型参数转变一下;改变一个类型的程序集版本或以上操作的逆向操作等。实际上,.NET类型的完全限定类型名称(Fully qualified type name)或称作Assembly qualified type name就包含以上操作所需的所有信息。只要解析这个字符串,就能进行以上简易的“离线反射”动作。而且这个字符串还能够被Type.GetType静态方法解析,所以一旦回到“在线”状态,马上就可以用这个字符串找到真正的类型。我们来看看这个字符串长得什么样子:

Type tString = typeof(String); //简单类型
Type tPointer = typeof(int*); //指针类型
Type tArray = typeof(float[]); //一维数组
Type tArray2D = typeof(float[,]); //二维数组
Type tGenericDef = typeof(List<>); //泛型类型定义
Type tGenericType = typeof(List<string>); //泛型构造类型
Type tComplex = typeof(IDictionary<string, List<int[]>[,]>); //....

要输出他们的完全限定名称,只需要打印一下Type的AssemblyQualifiedName属性就行了。以上类型的AssemblyQualifiedName属性分别是:

String
System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
int*
System.Int32*, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
float[]
System.Single[], mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
float[,]
System.Single[,], mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
List<>
System.Collections.Generic.List`1, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
List<string>
System.Collections.Generic.List`1[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
IDictionary<string, List<int[]>[,]>
System.Collections.Generic.IDictionary`2[[System.String, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Collections.Generic.List`1[[System.Int32[], mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]][,], mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]], mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

我们可以看到,简单类型的完全限定名称就是该类型的带命名空间全名,加上程序集的名字,再加上一些程序集属性构成。在这个基础上,可以加星号,方括号等修饰使之变成原始类型的指针类型或数组类型。实际上,按引用传递的参数也是一种特殊的类型,可以在源类型名称后加&号构成。加上泛型以后,情况就更加复杂了。首先需要通过反引号`跟一个数字表示类型参数的个数。如果是构造泛型类型,那么具体的类型参数在后面用双层方括号包围组成。具体泛型参数类型也是使用完全限定名称表示的。这样一来这个完全限定名称可能会很长,而且有递归定义的成分。使用正则表达式是没法正确解析的。为了一劳永逸地支持该名称在未来版本.NET的语法,我们写一个递归下降的Parser来解析之。不用担心,并不难写。

首先我们要写出完全限定类型名的语法定义。我们这里使用BNF范式来表达,并不用特别严格的方式,只求较好理解。凭空写出这一文法貌似有些难度,我们发现MSDN里竟然就有这个文法的定义!先抄来看看:

TypeSpec

:= ReferenceTypeSpec

 

| SimpleTypeSpec

ReferenceTypeSpec

:= SimpleTypeSpec '&'

SimpleTypeSpec

:= PointerTypeSpec

 

| ArrayTypeSpec

 

| TypeName

PointerTypeSpec

:= SimpleTypeSpec '*'

ArrayTypeSpec

:= SimpleTypeSpec '[ReflectionDimension]'

 

| SimpleTypeSpec '[ReflectionEmitDimension]'

ReflectionDimension

:= '*'

 

| ReflectionDimension ',' ReflectionDimension

 

| NOTOKEN

ReflectionEmitDimension

:= '*'

 

| Number '..' Number

 

| Number '…'

 

| ReflectionDimension ',' ReflectionDimension

 

| NOTOKEN

Number

:= [0-9]+

TypeName

:= NamespaceTypeName

 

| NamespaceTypeName ',' AssemblyNameSpec

NamespaceTypeName

:= NestedTypeName

 

| NamespaceSpec '.' NestedTypeName

NestedTypeName

:= IDENTIFIER

 

| NestedTypeName '+' IDENTIFIER

NamespaceSpec

:= IDENTIFIER

 

| NamespaceSpec '.' IDENTIFIER

AssemblyNameSpec

:= IDENTIFIER

 

| IDENTIFIER ',' AssemblyProperties

AssemblyProperties

:= AssemblyProperty

 

| AssemblyProperties ',' AssemblyProperty

AssemblyProperty

:= AssemblyPropertyName '=' AssemblyPropertyValue

这里面提供了很多信息,但是如果仔细看一看的话就会发现他是错的。最主要的问题就是AssemblyNameSpec的位置不对。对于数组和指针等类型,AssemblyNameSpce应该出现在*或[]的右侧,而按照这个文法则会出现在左侧。此外他并不支持泛型类型的名称。虽然微软声称这是.NET 3.5版的文法,但很明显他们根本就没好好更新它……所以我们还是要自立更生。重写新的文法时,我们主要做了几处改动:

  1. 修正了AssemblyNameSpec的位置
  2. 增加了泛型定义和泛型构造类型的名称
  3. 去掉了数组维度中仅用在Reflection.Emit的文法。因为我们的使用目标不包含Emit
  4. 将左递归尽量改写成右递归。实际上,这里出现的大部分左递归都是尾左递归。实现的时候可以不用递归的。还有一些不太明显的,会影响我们编写左递归等一下再进行消除。

于是得到了这样一个草稿:

 

QualifiedTypeName

:= TypeSpec

 

| TypeSpec ',' AssemblyNameSpec

TypeSpec

:= ReferenceTypeSpec

 

| SimpleTypeSpec

ReferenceTypeSpec

:= SimpleTypeSpec '&'

SimpleTypeSpec

:= PointerTypeSpec

 

| ArrayTypeSpec

 

| NamespaceTypeName

PointerTypeSpec

:= SimpleTypeSpec '*'

ArrayTypeSpec

:= SimpleTypeSpec '[ReflectionDimension]'

ReflectionDimension

:= ZeroLowerBoundDimension

 

| UnknownLowerBoundDimension

ZeroLowerBoundDimension

:= NOTOKEN

 

| ',' ZeroLowerBoundDimension

UnknownLowerBoundDimension

:= '*'

 

| '*' ',' UnknownLowerBoundDimension

Number

:= [0-9]+

NamespaceTypeName

:= TypeName

 

| NamespaceSpec '.' TypeName

TypeName

:= NestedTypeName

 

| ConstructedTypeName

ConstructedTypeName

:= GenericTypeName


| GenericTypeName '[TypeArgumentsSpec]'

GenericTypeName

:= NestedTypeName '`' Number


| NestedTypeName

TypeArgumentsSpec

:= TypeArgumentSpec |


TypeArgumentSpec ',' TypeArgumentsSpec

TypeArgumentSpec

:= '[QualifiedTypeName]'

NestedTypeName

:= IDENTIFIER

 

| GenericTypeName '+' IDENTIFIER

NamespaceSpec

:= IDENTIFIER

 

| NamespaceSpec '.' IDENTIFIER

AssemblyNameSpec

:= IDENTIFIER

 

| IDENTIFIER ',' AssemblyProperties

AssemblyProperties

:= AssemblyProperty

 

| AssemblyProperty ',' AssemblyProperties

AssemblyProperty

:= AssemblyPropertyName '=' AssemblyPropertyValue

蓝色部分展示了某些重要的新增文法。文法的正确性是很难保证的,可能需要多次修改才能写出准确的文法。这主要靠经验和尝试了,好在本次使用的文法还是比较简单的。下面我们就开始着手编写这个文法的Parser.

 

词法分析

首先是词法分析部分。完全限定名称的词法非常简单。最主要的单词是“标识符”、“数字”和一些标点符号。标识符就是类名称、命名空间名称、程序集名称以及各种属性值的单词。一共有两种类型的标识符,一种是用作类名称的,一种是用作属性值的。它们允许的符号是不一样的。而且类名称中还可以出现转义的特殊符号。我们列举出所有要扫描的单词:

单词 包含的符号 备注
标识符 所有非控制字符除了空白符和标点符号 以下标点符合可以转义:,+&*[].\
属性用标识符 和普通标识符相同,不包含等号 不能转义
数字 [0-9]+  
标点符号 ,+&*[].`= 每种都是一个独立单词

我们采用一个手写的基于自动机的Scanner类来进行词法分析。因为解析过程主要由后面的Parser驱动,所以我们的Scanner其实主要实现Peek的功能。同时还增加了识别字符串末尾和跳过空白符的功能。

image

我们特别增加了Seek功能。因为我们的递归下降Parser有时需要超前查看两个以上的符号才能完成解析。那种情况之下我们需要Seek回到超前查看之前的地方。

定义完全限定名称的AST

下面我们定义解析时使用的AST(抽象语法树,Abstract Syntax Tree)。原则上说,每个非终结符都需要一个AST节点,这样就可以自动生成AST。不过我们是实用主义者,我们定义的AST最后是为了方便使用而设计的。所以这里我使用了“标注(Annotation)”式的语法树。它简化了所需要的节点数量,而将基本节点加上某些属性的标注作为特殊节点存在。比如,命名空间我们就不额外设计一个节点,而是给类名称节点增加一个Namespace属性。当解析到命名空间的时候,我们就设置类名称节点的Namespace属性。

image

实现解析器

现在到了最后一步,要编写解析器了。实际上我们已经是万事俱备,只欠东风了。因为文法都已经写好了,写Parser是水到渠成的。例如,这样一个文法:

QualifiedTypeName

:= TypeSpec

 

| TypeSpec ',' AssemblyNameSpec

我们写出来的递归下降Parser就是这样的:

private FullyQualifiedTypeName ParseQualifiedTypeName()
{
//QualifiedTypeName := TypeSpec
// | TypeSpec ',' AssemblyNameSpec

var typeSpec = ParseTypeSpec();
FullyQualifiedTypeName result = typeSpec;

if (m_scanner.ScanComma())
{
m_scanner.EatWhiteSpaces();
result = ParseAssemblyNameSpec(typeSpec);
}

return result;
}

总的来说将文法转化成递归下降Parser的步骤是:

  1. 每个产生式对应一个方法
  2. 计算FIRST集合和FOLLOW集合,由此进行分支预测的依据。其实,大多数时候用眼睛一看就知道怎么分支预测了。不过少数情况下会有问题。
  3. 调用分支的产生式继续进行分析,或者直接解析单词(这样Scanner的位置就向前移动了)。

这样我们在写Parser的时候一次之需要关注一个产生式。未写完的解析方法可以保留成空的。下面我们着重介绍几个需要特别对待的产生式。首先是有尾递归形式的产生式。例如ZeroLowerBoundDimension := NOTOKEN |  ',' ZeroLowerBoundDimension。我们在解析的时候可以直接用循环语句解析逗号分隔的AssemblyProperty,而不用进行递归。说这里是“尾递归”结构,就是因为写成递归下降的Parser后会有尾递归出现。现在用一个循环即可:

//ZeroLowerBoundDimension
while (!m_scanner.ScanRightBracket())
{
if (m_scanner.ScanComma())
{
rank += 1;
}
else
{
if (m_scanner.ScanRightBracket())
{
break;
}
else
{
throw new ParserException(m_scanner.CurrentLocation, "Expected ','");
}
}
}

第二种情况是出现左递归的产生式。所谓左递归就是某非终结符的产生式最左边就是该非终结符本身。比如这样的:

SimpleTypeSpec

:= PointerTypeSpec

 

| ArrayTypeSpec

 

| NamespaceTypeName

PointerTypeSpec

:= SimpleTypeSpec '*'

ArrayTypeSpec

:= SimpleTypeSpec '[ReflectionDimension]'

我们可以看到SimpleTypeSpec可以产生PointerTypeSpec,而PointerTypeSpce的产生式最左边又是SimpleTypeSpec。这样如果使用递归下降的写法就会直接陷入死循环。所以我们要改写这个文法。首先从非递归的那一个产生式开始:SimpleTypeSpec := NamespaceTypeName这一个就是非递归的选项。那两个会递归的产生式最后一定是要生成这个非递归项的(因为实际世界没有死循环)。于是我们将非递归项提取出来变成公因式,然后将剩下的部分作为一个新的产生式:

SimpleTypeSpec

:= NamespaceTypeName TypeSufix

   
TypeSufix := NOTOKEN
 

| '*' TypeSufix

 

| '[ReflectionDimension]' TypeSufix

这样左递归就变成右递归了。这样的文法在完全限定名中还有一些,都可以如法炮制。

最后一种需要考虑的情形就是有不定长度的左公因式。在我们的文法里,有关Namespace的文法就是这样:

NamespaceTypeName

:= TypeName

 

| NamespaceSpec '.' TypeName

由于TypeName和NamespaceSpec都会先产生IDENTIFIER单词,于是IDENTIFIER就成了两者的左公因式。而且NamespaceSpec是一个递归结构的文法,所以我们无法简单地进行分支预测。这里我们用了一个小小的回溯逻辑:首先观察文法,NamespaceSpec一定以圆点结束。所以一点我们扫描不到圆点,而是遇到了其他单词,那么NamespaceSpec一定就结束了。这时回溯到最后一个圆点的位置开始继续Parse即可:

private TypeNameSpec ParseNamespaceSpec()
{
int beforeParse = m_scanner.CurrentLocation;
int afterLastDot = beforeParse;

string ns = string.Empty;

while (!m_scanner.IsEndOfString())
{
string identifier = m_scanner.ScanIdentifier();
if (identifier == null)
{
throw new ParserException(m_scanner.CurrentLocation, "Expected an identifier");
}

if (m_scanner.ScanDot())
{
afterLastDot = m_scanner.CurrentLocation;
//continue with namespace parsing
ns = ns + identifier + '.';
}
else
{
//not a dot! this is not a namespace
//back to last dot
m_scanner.Seek(afterLastDot);
break;
}
}

//get the namespace
TypeNameSpec result = new TypeNameSpec();
result.Namespace = ns;

return result;
}

这个逻辑不会对性能造成太大的影响,因为Namespace后面很快就会遇到非圆点非标识符的单词。

现在我们已经克服了所有的障碍,可以将整个Parser写出来了。具体的代码可以到这里下载: FullyQualifiedNameParser.zip

最后我们要测试一下写出的Parser是否正确。我准备了两个相当复杂的完全限定类型名称,诸位可以去尝试解析一下:

System.Collections.Generic.Dictionary`2+Enumerator[[System.Int32[,]],[System.Collections.Generic.List`1[[System.String]]]][]
System.Windows.PresentationFrameworkCollection`1[[System.Windows.Controls.ColumnDefinition, System.Windows, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Windows, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e

第一个类型是Dictionary<int[,], List<string>>.Enumerator类的完全限定名称,第二个类型是Silverlight类型PresentationFrameworkCollection<ColumnDefinition>的类型,在一般的桌面项目中肯定没有,正好发挥我们Parser纯文本的优势。

应用举例

这是一个非常基本的组件,所以用途是非常多的。我们这里只介绍几个基本用法。首先我们可以从已知类型生成他们的数组类型:

FullyQualifiedTypeNameParser typeNameParser = new FullyQualifiedTypeNameParser(typeOfT.AssemblyQualifiedName);
var typeName = typeNameParser.Parse();
var arrayTypeName = typeName.MakeArrayType(1, true);

这个可以确保正确生成的是正确数组类型的名字,不管原类型是不是泛型或者已经是数组。在typeOfT本地不存在,不能使用Type.GetType进行反射操作时,这个尤为有用。因为单纯字符串操作来生成数组类型很容易出错。

除了数组,我们还可以生成类型的指针类型,获取泛型类型的类型参数,更改类型参数,更改程序集的版本,切换签名和未签名程序集等等。在不能进行反射的时候,用一个简单的Parser就可以进行相当多有用的反射操作,确实可以助人一臂之力。

 posted on 2009-08-27 14:12  装配脑袋  阅读(6653)  评论(26编辑  收藏  举报