• 手工解析.NET完全限定类型名称


    大家都经常使用.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就可以进行相当多有用的反射操作,确实可以助人一臂之力。

  • 相关阅读:
    [转帖]J2ME程序开发全方位基础讲解汇总
    IWAM账号密码不一致引起IIS无法处理ASP文件
    [存档]J2ME中随机数字处理全攻略
    利用计划任务和VBS脚本实现自动WEB共享文件夹里的文件
    完美解决Java程序在 MOTO E680i 中声音文件播放
    J2ME中使用pauseApp控制手机临时退出JAVA程序
    Web页中使用MediaPlayer
    严重注意MSSQL视图跨数据库复制的问题
    Java下数字类型的转换
    项目开发:电话留言软件(20050717)
  • 原文地址:https://www.cnblogs.com/Ninputer/p/typename_parser.html
Copyright © 2020-2023  润新知