探索动态程序集

本文探讨了如何在运行时创建动态程序集,通过反射和IL代码生成实现类型工厂,以满足非程序员客户创建自定义派生类的需求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

探索动态程序集

 

Written by Allen Lee

 

        我相信,当你看到标题中“动态程序集”(Dynamic Assembly)这个字眼时,就冒出了很多想法和问题,然而,在我们深入这个概念之前,先来看看我遇到了什么问题。

 

1. 发生了什么事?

A:我们的客户要处理一组 Shape 对象。
B:怎么处理?
A:计算其中每个对象的面积。 这点需求实在太简单了,不是吗?你只需要建立一个如下图所示的继承体系就可以做到!

Figure #1 

        Shape 是一个抽象类,它包含一个名为 CalculateArea 的抽象方法,顾名思义,这个方法就是用来计算面积的。此外,上图还展示了一个名为 Triangle 的派生类,它包含了两个三角形的属性:Base(底)和 Altitude(高),当然,它重写了 Shape 的 CalculateArea 方法。依样画葫,我们就可以得到 Circle、Square 了。

        现在假设有一个 GetShapeObjects 方法:

None.gif //  Code #01
None.gif

None.gifIEnumerable
< Shape >  GetShapeObjects()
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
// dot.gif
ExpandedBlockEnd.gif
}

        那么只需一个 foreach 就可以处理这组 Shape 对象了:

None.gif //  Code #02
None.gif

None.gif
foreach  (Shape shape  in  GetShapeObjects())
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    Console.WriteLine(shape.CalculateArea());
ExpandedBlockEnd.gif}

A:你的方案很好,事实上,如果我们的客户也是程序员的话,那么你的方案简直可以算得上无懈可击了。不幸的是,我们的客户对程序开发一窍不通,所以你不能指望他们能像你那样在 Visual Studio 里创建 Shape 的派生类,他们希望有一个程序通过一些他们能看懂的提示帮助他们创建这些派生类。
B:……

        现在,你应该搞清楚发生了什么事。简而言之,就是帮助那些不懂程序开发的客户在程序运行期间创建出符合他们要求的派生类。

 

2. 程序员,你如何创建派生类?(C# 版)

        在进入正题之前,请允许我再一次离题。回顾一下我们是如何用 C# 创建 Figure #1 中的 Triangle 类:

        首先,我们声明一个 Triangle 类,并让它继承 Shape 抽象类。

None.gif //  Code #03
None.gif

None.gif
public   class  Triangle : Shape
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
ExpandedBlockEnd.gif}

        接着,我们为它添加类型为 double 的 Base 属性。

None.gif //  Code #04
None.gif

None.gif
private   double  m_Base;
None.gif
public   double  Base
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
ExpandedSubBlockStart.gifContractedSubBlock.gif    
get dot.gifreturn m_Base; }
ExpandedSubBlockStart.gifContractedSubBlock.gif    
set dot.gif{ m_Base = value; }
ExpandedBlockEnd.gif}

        然后,我们为它添加类型为 double 的 Altitude 属性。

None.gif //  Code #05
None.gif

None.gif
private   double  m_Altitude;
None.gif
public   double  Altitude
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
ExpandedSubBlockStart.gifContractedSubBlock.gif    
get dot.gifreturn m_Altitude; }
ExpandedSubBlockStart.gifContractedSubBlock.gif    
set dot.gif{ m_Altitude = value; }
ExpandedBlockEnd.gif}

        最后,我们重写 CalculateArea 方法。

None.gif //  Code #06
None.gif

None.gif
public   override   double  CalculateArea()
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
return Base * Altitude / 2;
ExpandedBlockEnd.gif}

        人们总是倾向于忽略那些司空见惯的事物,因此,为人们所适应的问题通常不被看作问题。我们对上述的派生类创建过程毫无疑问是非常熟悉的,然而,我们的客户却对此一无所知,因此,显式回顾这个过程将有助于我们思考如何实现类型工厂。

 

3. 如果我们提供类型工厂……

B:我们可以提供类型工厂。
A:什么?类型工厂?
B:是的,我们可以模仿程序员创建派生类的过程实现一个类型工厂。
A:有意思,能详细一点吗?
B:当然可以。假设 ShapeTypeFactory 就是我们将要提供给客户的类型工厂,那么程序员创建 Triangle 派生类的过程将可以表达成以下代码:

None.gif //  Code #07
None.gif

None.gifShapeTypeFactory fac 
=   new  ShapeTypeFactory();
None.giffac.CreateShapeType(
" Triangle " );
None.giffac.AddShapeProperty(
" Base " );
None.giffac.AddShapeProperty(
" Altitude " );
None.giffac.ImplementCalculateAreaMethod(
" Base * Altitude / 2 " );
None.giffac.SaveShapeType();

A:这个主意听起来不错,你赶快去试一下。

3.1 创建类型

        说罢了,动态程序集就是在程序运行时通过发射(Emit)IL 代码生成的程序集,而一旦生成,它就和我们平时看到的程序集没什么两样了。由于这里要生成的是程序集,所以这个过程将会和上面用 C# 创建派生类的过程稍稍不同,其中的区别在于这里我们需要显式定义程序集(以及模块)并将之保存,而上面则通过编译器来完成。

        在继续之前,我有必要对生成的程序集做一些约定,拿 Code #07 作为例子,它生成的程序集将会:

  • 放在 Shapes 目录中;
  • 它的名字为 DynamicShape.Shapes.Triangle.dll;
  • 其中 Triangle 类位于 DynamicShape.Shapes 命名空间中。

        由于类型的全限定名字将被多次用到,于是我把作为 CreateShapeType 的参数传进来的类型名字作为 ShapeTypeFactory 的成员变量储存起来,并提供一些辅助属性:

None.gif //  Code #08
None.gif

None.gif
class  ShapeTypeFactory
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
private string m_ShapeName;
InBlock.gif
InBlock.gif    
public string ShapeName
ExpandedSubBlockStart.gifContractedSubBlock.gif    
dot.gif{
ExpandedSubBlockStart.gifContractedSubBlock.gif        
get dot.gifreturn m_ShapeName; }
ExpandedSubBlockEnd.gif    }

InBlock.gif
InBlock.gif    
public string FullQualifiedShapeName
ExpandedSubBlockStart.gifContractedSubBlock.gif    
dot.gif{
ExpandedSubBlockStart.gifContractedSubBlock.gif        
get dot.gifreturn String.Format("DynamicShape.Shapes.{0}", m_ShapeName); }
ExpandedSubBlockEnd.gif    }

InBlock.gif
InBlock.gif    
public string FileName
ExpandedSubBlockStart.gifContractedSubBlock.gif    
dot.gif{
ExpandedSubBlockStart.gifContractedSubBlock.gif        
get dot.gifreturn String.Format("DynamicShape.Shapes.{0}.dll", m_ShapeName); }
ExpandedSubBlockEnd.gif    }

ExpandedBlockEnd.gif}

        在 CreateShapeType 方法里,我们首先把参数储存在成员变量中:

None.gif //  Code #09
None.gif

None.gifm_ShapeName 
=  shapeName;

        接着,我们创建一个 AssemblyName 的对象实例来存放基本的程序集信息:

None.gif //  Code #10
None.gif

None.gifAssemblyName assemblyName 
=   new  AssemblyName();
None.gifassemblyName.Name 
=  FullQualifiedShapeName;
None.gifassemblyName.Version 
=   new  Version( " 1.0.0.0 " );

        然后,我们通过 AppDomain.DefineDynamicAssembly 方法创建 AssemblyBuilder 对象实例,由于这个对象实例在整个动态程序集创建过程结束时还需要用到,于是我们把它储存在成员变量中:

None.gif //  Code #11
None.gif

None.gifm_AssemblyBuilder 
=  AppDomain.CurrentDomain.DefineDynamicAssembly(
None.gif    assemblyName,
None.gif    AssemblyBuilderAccess.Save,
None.gif    
" Shapes "
None.gif);

        由于我们希望把生成的动态程序集存放在 Shapes 目录中,于是我们需要以参数的形式给 DefineDynamicAssembly 方法指出,并且也只有此时才能指定动态程序集的生成目录。

        再来,就是通过 AssemblyBuilder.DefineDynamicModule 方法创建模块,此时我们需要指定模块的名字以及文件名:

None.gif //  Code #12
None.gif

None.gifModuleBuilder moduleBuilder 
=  m_AssemblyBuilder.DefineDynamicModule(
None.gif    FullQualifiedShapeName,
None.gif    FileName
None.gif);

考验脑力区:

单模块程序集和多模块程序集有什么不同?如何创建多模块程序集?

        最后,轮到类型的创建了,通过 ModuleBuilder.DefineType 可以创建 TypeBuilder 对象实例,由于这个对象在后面添加属性以及重写方法时需要用到,于是我把它储存在成员变量中:

None.gif //  Code #13
None.gif

None.gifm_TypeBuilder 
=  moduleBuilder.DefineType(
None.gif    FullQualifiedShapeName,
None.gif    TypeAttributes.Public,
None.gif    
typeof (Shape)
None.gif);

        在整个类型创建过程结束之时,我们需要调用 TypeBuilder.CreateType 方法以便生成类型,并且调用 AssemblyBuilder.Save 方法保存动态程序集,这也是 SaveShapeType 方法的职责:

None.gif //  Code #14
None.gif

None.gif
public   void  SaveShapeType()
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    m_TypeBuilder.CreateType();
InBlock.gif    m_AssemblyBuilder.Save(FileName);
ExpandedBlockEnd.gif}

3.2 添加属性

        在 C# 中为一个类添加一个可读写的属性是一件非常容易的事情(C# 3.0 的自动属性使得这个过程变得更加简洁)。如果你使用 Visual Studio 2005 的话,只需要输入 prop,接着敲击 Tab 键,然后对属性模板稍作修改就可以了!

        然而,这个过程在这里将会异常繁杂,这可以归功于属性的本质。在继续之前,我认为有必要先回顾一下属性这个东西。拿 Code #05 作为例子,与之对应的 IL 代码是:

None.gif //  Code #15
None.gif

None.gif.field 
private  float64 m_Altitude
None.gif
None.gif.method 
public  hidebysig specialname instance float64 
None.gif        get_Altitude() cil managed
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif  
// Code size       12 (0xc)
InBlock.gif
  .maxstack  1
InBlock.gif  .locals init ([
0] float64 CS$1$0000)
InBlock.gif  IL_0000:  nop
InBlock.gif  IL_0001:  ldarg.
0
InBlock.gif  IL_0002:  ldfld      float64 DynamicShape.Shapes.Triangle::m_Altitude
InBlock.gif  IL_0007:  stloc.
0
InBlock.gif  IL_0008:  br.s       IL_000a
InBlock.gif  IL_000a:  ldloc.
0
InBlock.gif  IL_000b:  ret
ExpandedBlockEnd.gif}

None.gif
None.gif.method 
public  hidebysig specialname instance  void  
None.gif        set_Altitude(float64 
' value ' ) cil managed
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif  
// Code size       9 (0x9)
InBlock.gif
  .maxstack  8
InBlock.gif  IL_0000:  nop
InBlock.gif  IL_0001:  ldarg.
0
InBlock.gif  IL_0002:  ldarg.
1
InBlock.gif  IL_0003:  stfld      float64 DynamicShape.Shapes.Triangle::m_Altitude
InBlock.gif  IL_0008:  ret
ExpandedBlockEnd.gif}

None.gif
None.gif.property instance float64 Altitude()
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif  .
get instance float64 DynamicShape.Shapes.Triangle::get_Altitude()
InBlock.gif  .
set instance void DynamicShape.Shapes.Triangle::set_Altitude(float64)
ExpandedBlockEnd.gif}

考验脑力区:

hidebysig 的作用是什么?specialname 又有什么用?

根据上面的代码,我们可以把定义一个属性的步骤归纳为:

  • 定义一个私有字段 m_Altitude;
  • 定义一个名为 get_Altitude 的方法,该方法返回 m_Altitude 的值;
  • 定义一个名为 set_Altitude 的方法,该方法对 m_Altitude 进行设值;
  • 定义一个名为 Altitude 的属性,并以 get_Altitude 方法作为读访问器,set_Altitude 方法作为写访问器。

考验脑力区:

属性的读写访问器可以分别具有不同的访问级别吗?

        搞清楚属性的本质后,我们就可以动手实现 AddShapeProperty 方法了:

None.gif //  Code #16
None.gif

None.gif
public   void  AddShapeProperty( string  propertyName)
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    FieldBuilder fieldBuilder 
= m_TypeBuilder.DefineField(
InBlock.gif        String.Format(
"m_{0}", propertyName),
InBlock.gif        
typeof(double),
InBlock.gif        FieldAttributes.Private
InBlock.gif    );
InBlock.gif
InBlock.gif    PropertyBuilder propertyBuilder 
= m_TypeBuilder.DefineProperty(
InBlock.gif        propertyName,
InBlock.gif        PropertyAttributes.None,
InBlock.gif        
typeof(double),
InBlock.gif        
null
InBlock.gif    );
InBlock.gif
InBlock.gif    MethodBuilder getterBuilder 
= m_TypeBuilder.DefineMethod(
InBlock.gif        String.Format(
"get_{0}", propertyName),
InBlock.gif        MethodAttributes.Public 
| MethodAttributes.SpecialName | MethodAttributes.HideBySig,
InBlock.gif        
typeof(double),
InBlock.gif        Type.EmptyTypes
InBlock.gif    );
InBlock.gif
InBlock.gif    ILGenerator getterILGenerator 
= getterBuilder.GetILGenerator();
InBlock.gif    getterILGenerator.Emit(OpCodes.Ldarg_0);
InBlock.gif    getterILGenerator.Emit(OpCodes.Ldfld, fieldBuilder);
InBlock.gif    getterILGenerator.Emit(OpCodes.Ret);
InBlock.gif
InBlock.gif    propertyBuilder.SetGetMethod(getterBuilder);
InBlock.gif
InBlock.gif    MethodBuilder setterBuilder 
= m_TypeBuilder.DefineMethod(
InBlock.gif        String.Format(
"set_{0}", propertyName),
InBlock.gif        MethodAttributes.Public 
| MethodAttributes.SpecialName | MethodAttributes.HideBySig,
InBlock.gif        
null,
ExpandedSubBlockStart.gifContractedSubBlock.gif        
new Type[] dot.giftypeof(double) }
InBlock.gif    );
InBlock.gif
InBlock.gif    ILGenerator setterILGenerator 
= setterBuilder.GetILGenerator();
InBlock.gif    setterILGenerator.Emit(OpCodes.Ldarg_0);
InBlock.gif    setterILGenerator.Emit(OpCodes.Ldarg_1);
InBlock.gif    setterILGenerator.Emit(OpCodes.Stfld, fieldBuilder);
InBlock.gif    setterILGenerator.Emit(OpCodes.Ret);
InBlock.gif
InBlock.gif    propertyBuilder.SetSetMethod(setterBuilder);
ExpandedBlockEnd.gif}

考验脑力区:

无论是读取还是写入字段的值,我们都要先载入第一个参数(OpCodes.Ldarg_0),为什么要这样?这个参数是什么?

        此时此刻,不知道你有否这样一番感概:原来编译器在后面默默地为我做了这么多事情!

3.3 重写 CalculateArea 方法

        终于到了重写 CalculateArea 方法了,然而,在这个看似简单的环节里却隐藏着巨大的困难。细心观察 Code #07,不难发现我们需要把“Base * Altitude / 2”这样的表达式解析成 IL 代码!我想放弃了,但又不甘心,只好硬着头皮上网找找看……

增值服务区:

《利用堆栈解析算术表达式一:基本过程》

        我们传递给 ImplementCalculateAreaMethod 方法的是“Base * Altitude / 2”,而最终解释后符合 IL 堆栈语义的是“Base Altitude * 2 /”,前者叫做“中缀表达式”,而后者则为“后缀表达式”。我们希望解析器除了支持运算符和常量运算数外,还要支持以单词为单位的变量,因为这些变量最终会被重定向到类型所包含的属性。找了很久都没有发现满足要求的解析器,无奈只好自己硬着头皮写一个。

        由于我真的很懒,并且本文的主题是动态程序集,于是我只在这里实现一个功能异常有限(甚至不能称之为解析器)的解析器。为此,我制定了如下约束:

  • 表达式可包含的符号为常量运算数、算术运算符以及以单词为单位的变量符号;
  • 变量符号必须与对应的类型所包含的属性一致;
  • 表达式中每个符号之间以空格隔开;
  • 表达式(目前)只支持乘法(×)和除法(÷)运算。

        下面我利用堆栈把被我高度约束中缀表达式解析成 IL 符号序列:

None.gif //  Code #17
None.gif

None.gif
public   class  FormulaExpression
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
public static IList Parse(string formulaExpression)
ExpandedSubBlockStart.gifContractedSubBlock.gif    
dot.gif{
InBlock.gif        ArrayList reversePolishNotation 
= new ArrayList();
InBlock.gif        Stack
<OpCode> operatorStack = new Stack<OpCode>();
InBlock.gif
InBlock.gif        
foreach (string token in formulaExpression.Split(' '))
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            OpCode operatorToken;
InBlock.gif
InBlock.gif            
if (TryParse(token, out operatorToken))
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                
if (operatorStack.Count > 0)
ExpandedSubBlockStart.gifContractedSubBlock.gif                
dot.gif{
InBlock.gif                    reversePolishNotation.Add(operatorStack.Pop());
ExpandedSubBlockEnd.gif                }

InBlock.gif
InBlock.gif                operatorStack.Push(operatorToken);
ExpandedSubBlockEnd.gif            }

InBlock.gif            
else
ExpandedSubBlockStart.gifContractedSubBlock.gif            
dot.gif{
InBlock.gif                
double constant;
InBlock.gif
InBlock.gif                
if (Double.TryParse(token, out constant))
ExpandedSubBlockStart.gifContractedSubBlock.gif                
dot.gif{
InBlock.gif                    reversePolishNotation.Add(constant);
ExpandedSubBlockEnd.gif                }

InBlock.gif                
else
ExpandedSubBlockStart.gifContractedSubBlock.gif                
dot.gif{
InBlock.gif                    reversePolishNotation.Add(token);
ExpandedSubBlockEnd.gif                }

ExpandedSubBlockEnd.gif            }

ExpandedSubBlockEnd.gif        }

InBlock.gif
InBlock.gif        reversePolishNotation.Add(operatorStack.Pop());
InBlock.gif
InBlock.gif        
return reversePolishNotation;
ExpandedSubBlockEnd.gif    }

InBlock.gif
InBlock.gif    
private static bool TryParse(string operatorToken, out OpCode result)
ExpandedSubBlockStart.gifContractedSubBlock.gif    
dot.gif{
InBlock.gif        
switch (operatorToken)
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            
case "*":
InBlock.gif                result 
= OpCodes.Mul;
InBlock.gif                
return true;
InBlock.gif            
case "/":
InBlock.gif                result 
= OpCodes.Div;
InBlock.gif                
return true;
InBlock.gif            
default:
InBlock.gif                result 
= new OpCode();
InBlock.gif                
return false;
ExpandedSubBlockEnd.gif        }

ExpandedSubBlockEnd.gif    }

ExpandedBlockEnd.gif}

        在输出的 IL 符号序列里只存在三种类型的符号:类型为 double 的常量运算数、类型为 string 的变量属性名和类型为 OpCode 的 IL 操作码。

        至此,我们可以开始重写 CalculateAreaMethod 了:

None.gif //  Code #18
None.gif

None.gif
public   void  ImplementCalculateAreaMethod( string  formulaExpression)
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    MethodBuilder calculateAreaMethodBuilder 
= m_TypeBuilder.DefineMethod(
InBlock.gif        
"CalculateArea",
InBlock.gif        MethodAttributes.Public 
| MethodAttributes.ReuseSlot | MethodAttributes.Virtual | MethodAttributes.HideBySig,
InBlock.gif        
typeof(double),
InBlock.gif        Type.EmptyTypes
InBlock.gif    );
InBlock.gif
InBlock.gif    ILGenerator calculateAreaMethodILGenerator 
= calculateAreaMethodBuilder.GetILGenerator();
InBlock.gif    
foreach (object token in FormulaExpression.Parse(formulaExpression))
ExpandedSubBlockStart.gifContractedSubBlock.gif    
dot.gif{
InBlock.gif        
if (token.GetType() == typeof(double))
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            calculateAreaMethodILGenerator.Emit(OpCodes.Ldc_R8, (
double)token);
ExpandedSubBlockEnd.gif        }

InBlock.gif        
else if (token.GetType() == typeof(string))
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            calculateAreaMethodILGenerator.Emit(OpCodes.Ldarg_0);
InBlock.gif            calculateAreaMethodILGenerator.Emit(OpCodes.Call, m_Properties[(
string)token].GetGetMethod());
ExpandedSubBlockEnd.gif        }

InBlock.gif        
else
ExpandedSubBlockStart.gifContractedSubBlock.gif        
dot.gif{
InBlock.gif            calculateAreaMethodILGenerator.Emit((OpCode)token);
ExpandedSubBlockEnd.gif        }

ExpandedSubBlockEnd.gif    }

InBlock.gif    calculateAreaMethodILGenerator.Emit(OpCodes.Ret);
ExpandedBlockEnd.gif}

        值得提醒的是,当你重写一个方法时,你只需为其贴上 MethodAttributes.ReuseSlot 和 MethodAttributes.Virtual,剩下的事情 TypeBuilder 会帮你处理的。再者,在获取属性值的时候,我们其实是调用它的读访问器。由于前面通过 AddShapeProperty 方法添加的属性在这里会用到,所以我用了一个 Dictionary 来储存这些属性。这个 Dictionary 在 ShapeTypeFactory 的构造函数里初始化,并在 AddShapeProperty 方法末尾添加条目。

A:你的类型工厂到底是怎么搞的?
B:出了什么事?
A:客户说他们想创建梯形,生成的东西乱七八糟!现在客户很生气,后果很严重!
B:……

 

4. 再谈重写 CalculateArea 方法

        或许客户对梯形情有独钟,无论如何,先来看看梯形的面积公式:(TopBase + BottomBase) * Altitude / 2。这下好了,不但有加号,还有括号,难怪客户说生成的东西乱七八糟。

B:或许,我们可以提供预定义的公式方法,以缓解表达式解析器的不足。
A:真的行吗?
B:应该没问题。
A:请不要再出问题了,你没有收到消息吗,最近高层打算裁员!
B:……

4.1 定义公式

        面积公式的本质就是函数,中学课本喜欢把这些公式表达成“y = f(x[, ...])”。这样,梯形公式就可以表达成这样:

S = f(a, b, h) = (a + b) * h / 2

        实质上,a、b 和 h 相当于方法的输入参数,S 则相当于方法的返回值,于是,我们可以定义这样一个方法来表达梯形的面积公式:

None.gif //  Code #19
None.gif

None.gif
public   static   double  CalculateTrapezoidArea( double  topBase,  double  bottomBase,  double  altitude)
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
return (topBase + bottomBase) * altitude / 2;
ExpandedBlockEnd.gif}

        显然,上面这个方法和我们平时看到的方法没什么两样,为了使之与其它方法区分开来,我们有必要为它打上一个标记——FormulaMethodAttribute。现在,让我们考虑一下 FormulaMethodAttribute 里面应该包含一些什么信息。既然我们的客户不懂程序开发,给他们显示 CalculateTrapezordArea 这个字眼很明显是不友好的,再者,客户很有可能希望查看这个方法所使用的面积公式,所以,FormulaMethodAttribute 里面应该包含 FriendlyName 和 FormulaExpression 两个属性:

None.gif //  Code #20
None.gif

None.gif[AttributeUsage(AttributeTargets.Method, AllowMultiple 
=   false , Inherited  =   false )]
None.gif
public   class  FormulaMethodAttribute : Attribute
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
private string m_FriendlyName;
InBlock.gif
InBlock.gif    
public string FriendlyName
ExpandedSubBlockStart.gifContractedSubBlock.gif    
dot.gif{
ExpandedSubBlockStart.gifContractedSubBlock.gif        
get dot.gifreturn m_FriendlyName; }
ExpandedSubBlockStart.gifContractedSubBlock.gif        
set dot.gif{ m_FriendlyName = value; }
ExpandedSubBlockEnd.gif    }

InBlock.gif
InBlock.gif    
private string m_FormulaExpression;
InBlock.gif
InBlock.gif    
public string FormulaExpression
ExpandedSubBlockStart.gifContractedSubBlock.gif    
dot.gif{
ExpandedSubBlockStart.gifContractedSubBlock.gif        
get dot.gifreturn m_FormulaExpression; }
ExpandedSubBlockStart.gifContractedSubBlock.gif        
set dot.gif{ m_FormulaExpression = value; }
ExpandedSubBlockEnd.gif    }

ExpandedBlockEnd.gif}

        现在,我们把 FormulaMethodAttribute 应用到 CalculateTrapezoidArea 上:

None.gif //  Code #21
None.gif

None.gif[FormulaMethod(FriendlyName 
=   " Trapezoid Area Formula " , FormulaExpression  =   " (TopBase + BottomBase) * Altitude / 2 " )]
None.gif
public   static   double  CalculateTrapezoidArea( double  topBase,  double  bottomBase,  double  altitude)
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
// dot.gif
ExpandedBlockEnd.gif
}

考验脑力区:

特性(attribute)是在什么时候被实例化的?

4.2 供应公式

        我们可以预先定义好很多公式,但如果这些公式仅仅存放在某个角落,就没有达到我们原本的目的了,所以我们需要一个公式士多,以便在需要时给我们供应公式。

        由于公式是以方法级别的粒度存在的,所以我们要对指定目录中每个文件中的每个类型中的每个方法进行判定,并把满足要求的方法加载进来。假设所有公式都统一存放在 Formulas 目录里,并且加载时会存放在类型为 IList<T> 的集合 m_FormulaMethods 中。我们的搜寻行动分为三步:

        首先,搜寻 Formulas 目录里所有 dll 文件的文件名(不带扩展名);

None.gif //  Code #22
None.gif

None.gifvar files 
=  from file  in  Directory.GetFiles( " Formulas " )
None.gif               where String.Equals(Path.GetExtension(file), 
" .dll " , StringComparison.InvariantCultureIgnoreCase)
None.gif               select Path.GetFileNameWithoutExtension(file);
None.gif
None.gif
foreach  (var file  in  files)
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
// dot.gif
ExpandedBlockEnd.gif
}

考验脑力区:

什么时候我们不能使用 var 声明变量?

        接着,搜寻给定程序集文件里所有满足要求方法:

None.gif //  Code #23
None.gif

None.gifvar methods 
=  from type  in  Assembly.Load(assemblyName).GetTypes()
None.gif                      from method 
in  type.GetMethods(BindingFlags.DeclaredOnly  |  BindingFlags.Public  |  BindingFlags.Static)
None.gif                      where IsFormulaMethod(method)
None.gif                      select method;
None.gif
None.gif
foreach  (var method  in  methods)
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
// dot.gif
ExpandedBlockEnd.gif
}

考验脑力区:

IEnumerable<T>.Select 方法和 IEnumerable<T>.SelectMany 方法有什么不同?

        在这次搜寻里,我们需要判断找到的方法是否满足要求,这是通过 IsFormulaMethod 方法做到:

None.gif //  Code #24
None.gif

None.gif
private   bool  IsFormulaMethod(MethodInfo method)
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
object[] customAttributes = method.GetCustomAttributes(typeof(FormulaMethodAttribute), false);
InBlock.gif
InBlock.gif    
return customAttributes.Length == 1;
ExpandedBlockEnd.gif}

        我们的判断标准很简单,只要找到的方法打上了 FormulaMethodAttribute 标记就算满足要求,当然,这个标记只允许打一次,这是在 FormulaMethodAttribute 定义时规定的(AllowMultiple = false)。

        最后,就是把找到的满足要求的方法添加到 m_FormulaMethods 集合里。由于从方法提取元数据的操作比较繁杂,于是我们在这里把数据提取出来并储存在一个名为 FormulaMethod 的类中:

None.gif //  Code #25
None.gif

None.gifFormulaMethodAttribute formulaMethodAttribute 
=  ExtractFormulaMethodAttribute(method);
None.gif
None.gifm_FormulaMethods.Add(
None.gif    
new  FormulaMethod
ExpandedBlockStart.gifContractedBlock.gif    
dot.gif {
InBlock.gif        FriendlyName 
= formulaMethodAttribute.FriendlyName,
InBlock.gif        FormulaExpression 
= formulaMethodAttribute.FormulaExpression,
InBlock.gif        MethodInfo 
= method
ExpandedBlockEnd.gif    }

None.gif);

考验脑力区:

对象初始化器对属性有什么要求?

        元数据的提取是通过 ExtractFormulaMethodAttribute 方法做到的:

None.gif //  Code #26
None.gif

None.gif
private  FormulaMethodAttribute ExtractFormulaMethodAttribute(MethodInfo method)
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
return method.GetCustomAttributes(typeof(FormulaMethodAttribute), false).Cast<FormulaMethodAttribute>().Single<FormulaMethodAttribute>();
ExpandedBlockEnd.gif}

        另外,每当需要时实例化一个公式士多对象,并在指定的目录搜寻和加载所有公式并不是一个好主意,于是,我使用 Singleton 模式来实现公式士多:

None.gif //  Code #27
None.gif

None.gif
private   static  FormulaMethodStore m_Instance  =   new  FormulaMethodStore();
None.gif
public   static  FormulaMethodStore Instance
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
ExpandedSubBlockStart.gifContractedSubBlock.gif    
get dot.gifreturn m_Instance; }
ExpandedBlockEnd.gif}

考验脑力区:

FormulaMethodStore 的对象实例确切是在什么时候被创建的呢?

        而 FormulaMethod 类的实现如下:

None.gif //  Code #28
None.gif

None.gif
public   class  FormulaMethod
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
private string m_FriendlyName;
InBlock.gif
InBlock.gif    
public string FriendlyName
ExpandedSubBlockStart.gifContractedSubBlock.gif    
dot.gif{
ExpandedSubBlockStart.gifContractedSubBlock.gif        
get dot.gifreturn m_FriendlyName; }
ExpandedSubBlockStart.gifContractedSubBlock.gif        
set dot.gif{ m_FriendlyName = value; }
ExpandedSubBlockEnd.gif    }

InBlock.gif
InBlock.gif    
private string m_FormulaExpression;
InBlock.gif
InBlock.gif    
public string FormulaExpression
ExpandedSubBlockStart.gifContractedSubBlock.gif    
dot.gif{
ExpandedSubBlockStart.gifContractedSubBlock.gif        
get dot.gifreturn m_FormulaExpression; }
ExpandedSubBlockStart.gifContractedSubBlock.gif        
set dot.gif{ m_FormulaExpression = value; }
ExpandedSubBlockEnd.gif    }

InBlock.gif
InBlock.gif    
private MethodInfo m_MethodInfo;
InBlock.gif
InBlock.gif    
public MethodInfo MethodInfo
ExpandedSubBlockStart.gifContractedSubBlock.gif    
dot.gif{
ExpandedSubBlockStart.gifContractedSubBlock.gif        
get dot.gifreturn m_MethodInfo; }
ExpandedSubBlockStart.gifContractedSubBlock.gif        
set dot.gif{ m_MethodInfo = value; }
ExpandedSubBlockEnd.gif    }

InBlock.gif
InBlock.gif    
public override string ToString()
ExpandedSubBlockStart.gifContractedSubBlock.gif    
dot.gif{
InBlock.gif        
return m_FriendlyName;
ExpandedSubBlockEnd.gif    }

ExpandedBlockEnd.gif}

4.3 重写 CalculateArea 方法

        正所谓“养兵千日,用在一时”,现在是时候了,让我们用上刚才所准备的一切吧!慢着,好像还差点什么?!对了,还缺类型属性和方法参数之间的映射。现在,我们需要一个数组,它储存了类型的属性名字,这些名字是按照公式的参数顺序来排列的。这样,我们就可以依次载入属性的值,然后调用公式进行计算:

None.gif //  Code #29
None.gif

None.gif
public   void  ImplementCalculateAreaMethod(MethodInfo method,  string [] propertyNames)
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    MethodBuilder calculateAreaMethodBuilder 
= m_TypeBuilder.DefineMethod(
InBlock.gif        
"CalculateArea",
InBlock.gif        MethodAttributes.Public 
| MethodAttributes.ReuseSlot | MethodAttributes.Virtual | MethodAttributes.HideBySig,
InBlock.gif        
typeof(double),
InBlock.gif        Type.EmptyTypes
InBlock.gif    );
InBlock.gif
InBlock.gif    ILGenerator calculateAreaMethodILGenerator 
= calculateAreaMethodBuilder.GetILGenerator();
InBlock.gif    
foreach (string propertyName in propertyNames)
ExpandedSubBlockStart.gifContractedSubBlock.gif    
dot.gif{
InBlock.gif        calculateAreaMethodILGenerator.Emit(OpCodes.Ldarg_0);
InBlock.gif        calculateAreaMethodILGenerator.Emit(OpCodes.Call, m_Properties[propertyName].GetGetMethod());
ExpandedSubBlockEnd.gif    }

InBlock.gif    calculateAreaMethodILGenerator.Emit(OpCodes.Call, method);
InBlock.gif    calculateAreaMethodILGenerator.Emit(OpCodes.Ret);
ExpandedBlockEnd.gif}

B:客户那边有消息了吗?
A:嗯,他们很满意,似乎已经把那个糟糕的表达式解析器忘记了。现在他们有一个小小的要求,就是希望拥有一个类型士多。
B:没问题,只要他们愿意加钱。
A:……

 

5. 类型士多

        类型士多的实现与公式士多的相似,就是对指定目录中每个文件中的每个类型进行判定,并把满足要求的类型加载进来。假设所有类型都统一存放在 Shapes 目录里,并且加载时会存放在类型为 IList<T> 的集合 m_ShapeTypes 中。我们的搜寻行动分为两步:

         首先,搜寻 Shapes 目录里所有 dll 文件的文件名(不带扩展名);

None.gif //  Code #30
None.gif

None.gifvar files 
=  from file  in  Directory.GetFiles(m_Path)
None.gif            where String.Equals(Path.GetExtension(file), 
" .dll " , StringComparison.InvariantCultureIgnoreCase)
None.gif            select Path.GetFileNameWithoutExtension(file);
None.gif
None.gif
foreach  (var file  in  files)
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    
// dot.gif
ExpandedBlockEnd.gif
}

        然后,搜寻给定程序集文件里所有满足要求类型:

None.gif //  Code #31
None.gif

None.gifvar types 
=  from type  in  Assembly.Load(assemblyName).GetTypes()
None.gif            where type.IsSubclassOf(
typeof (Shape))
None.gif            select type;
None.gif
None.gif
foreach  (var type  in  types)
ExpandedBlockStart.gifContractedBlock.gif
dot.gif {
InBlock.gif    m_ShapeTypes.Add(type);
ExpandedBlockEnd.gif}

        需要提醒的是,这里我使用 Type.IsSubclassOf 方法来判断找到的类型是否为 Shape 的派生类。

A:客户说类型士多有点问题。
B:什么问题?
A:新创建的类型并没有及时地反映到类型士多中,而且没有任何更新途径,他们希望类型士多能够自动检测新创建的类型并更新自身的数据。
B:我明白了。

        由于类型士多和公式士多一样,使用了 Singleton 模式来实现,这样,类型的搜寻和加载仅发生在类型士多的对象实例被创建之时,此后即使有新的类型被创建出来,也不会被类型士多发现。为了解决这个问题,我们可以使用 FileSystemWatcher 监视 Shapes 目录,以便当新的类型创建出来时可以把它加载进来。由于 FileSystemWatcher 对象在类型士多对象实例的整个生命周期都存在,我们可以把这个对象声明为一个成员变量 m_Watcher,接着在构造函数里面初始化它:

None.gif //  Code #32
None.gif

None.gifm_Watcher 
=   new  FileSystemWatcher();
None.gifm_Watcher.Path 
=  m_Path;
None.gifm_Watcher.NotifyFilter 
=  NotifyFilters.LastWrite;
None.gifm_Watcher.Filter 
=   " *.dll " ;
None.gifm_Watcher.Created 
+=   new  FileSystemEventHandler(OnShapeTypeAdded);
None.gifm_Watcher.EnableRaisingEvents 
=   true ;

        我们把 OnShapeTypeAdded 方法挂接到 FileSystemWatcher.Created 事件上,当客户创建了新的类型,这个方法将调用 Code #30 的代码把这个类型加载进来。当然,我们也可以对公式士多如法炮制,让它也具备这种检测并自动更新的功能。

B:客户那边又来新的需求了。
A:什么需求?
B:他们想要一个向导程序。
A:他们愿意加钱吗?
B:你是不是想钱想到疯了?
A:……

 

下一集,我将会介绍如何为类型工厂创建一个向导程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值