探索动态程序集
Written by Allen Lee
我相信,当你看到标题中“动态程序集”(Dynamic Assembly)这个字眼时,就冒出了很多想法和问题,然而,在我们深入这个概念之前,先来看看我遇到了什么问题。
1. 发生了什么事?
A:我们的客户要处理一组 Shape 对象。
B:怎么处理?
A:计算其中每个对象的面积。 这点需求实在太简单了,不是吗?你只需要建立一个如下图所示的继承体系就可以做到!

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

{
//
}
那么只需一个 foreach 就可以处理这组 Shape 对象了:
//
Code#02
foreach
(Shapeshape
in
GetShapeObjects())

{
Console.WriteLine(shape.CalculateArea());
}
A:你的方案很好,事实上,如果我们的客户也是程序员的话,那么你的方案简直可以算得上无懈可击了。不幸的是,我们的客户对程序开发一窍不通,所以你不能指望他们能像你那样在 Visual Studio 里创建 Shape 的派生类,他们希望有一个程序通过一些他们能看懂的提示帮助他们创建这些派生类。
B:……
现在,你应该搞清楚发生了什么事。简而言之,就是帮助那些不懂程序开发的客户在程序运行期间创建出符合他们要求的派生类。
2. 程序员,你如何创建派生类?(C# 版)
在进入正题之前,请允许我再一次离题。回顾一下我们是如何用 C# 创建 Figure #1 中的 Triangle 类:
首先,我们声明一个 Triangle 类,并让它继承 Shape 抽象类。
//
Code#03
public
class
Triangle:Shape

{

}
接着,我们为它添加类型为 double 的 Base 属性。
//
Code#04
private
double
m_Base;
public
double
Base

{

get
{returnm_Base;}

set
{m_Base=value;}
}
然后,我们为它添加类型为 double 的 Altitude 属性。
//
Code#05
private
double
m_Altitude;
public
double
Altitude

{

get
{returnm_Altitude;}

set
{m_Altitude=value;}
}
最后,我们重写 CalculateArea 方法。
//
Code#06
public
override
double
CalculateArea()

{
returnBase*Altitude/2;
}
人们总是倾向于忽略那些司空见惯的事物,因此,为人们所适应的问题通常不被看作问题。我们对上述的派生类创建过程毫无疑问是非常熟悉的,然而,我们的客户却对此一无所知,因此,显式回顾这个过程将有助于我们思考如何实现类型工厂。
3. 如果我们提供类型工厂……
B:我们可以提供类型工厂。
A:什么?类型工厂?
B:是的,我们可以模仿程序员创建派生类的过程实现一个类型工厂。
A:有意思,能详细一点吗?
B:当然可以。假设 ShapeTypeFactory 就是我们将要提供给客户的类型工厂,那么程序员创建 Triangle 派生类的过程将可以表达成以下代码:
//
Code#07
ShapeTypeFactoryfac
=
new
ShapeTypeFactory();
fac.CreateShapeType(
"
Triangle
"
);
fac.AddShapeProperty(
"
Base
"
);
fac.AddShapeProperty(
"
Altitude
"
);
fac.ImplementCalculateAreaMethod(
"
Base*Altitude/2
"
);
fac.SaveShapeType();
A:这个主意听起来不错,你赶快去试一下。
3.1 创建类型
说罢了,动态程序集就是在程序运行时通过发射(Emit)IL 代码生成的程序集,而一旦生成,它就和我们平时看到的程序集没什么两样了。由于这里要生成的是程序集,所以这个过程将会和上面用 C# 创建派生类的过程稍稍不同,其中的区别在于这里我们需要显式定义程序集(以及模块)并将之保存,而上面则通过编译器来完成。
在继续之前,我有必要对生成的程序集做一些约定,拿 Code #07 作为例子,它生成的程序集将会:
- 放在 Shapes 目录中;
- 它的名字为 DynamicShape.Shapes.Triangle.dll;
- 其中 Triangle 类位于 DynamicShape.Shapes 命名空间中。
由于类型的全限定名字将被多次用到,于是我把作为 CreateShapeType 的参数传进来的类型名字作为 ShapeTypeFactory 的成员变量储存起来,并提供一些辅助属性:
//
Code#08
class
ShapeTypeFactory

{
privatestringm_ShapeName;

publicstringShapeName


{

get
{returnm_ShapeName;}
}

publicstringFullQualifiedShapeName


{

get
{returnString.Format("DynamicShape.Shapes.{0}",m_ShapeName);}
}

publicstringFileName


{

get
{returnString.Format("DynamicShape.Shapes.{0}.dll",m_ShapeName);}
}
}
在 CreateShapeType 方法里,我们首先把参数储存在成员变量中:
//
Code#09
m_ShapeName
=
shapeName;
接着,我们创建一个 AssemblyName 的对象实例来存放基本的程序集信息:
//
Code#10
AssemblyNameassemblyName
=
new
AssemblyName();
assemblyName.Name
=
FullQualifiedShapeName;
assemblyName.Version
=
new
Version(
"
1.0.0.0
"
);
然后,我们通过 AppDomain.DefineDynamicAssembly 方法创建 AssemblyBuilder 对象实例,由于这个对象实例在整个动态程序集创建过程结束时还需要用到,于是我们把它储存在成员变量中:
//
Code#11
m_AssemblyBuilder
=
AppDomain.CurrentDomain.DefineDynamicAssembly(
assemblyName,
AssemblyBuilderAccess.Save,
"
Shapes
"
);
由于我们希望把生成的动态程序集存放在 Shapes 目录中,于是我们需要以参数的形式给 DefineDynamicAssembly 方法指出,并且也只有此时才能指定动态程序集的生成目录。
再来,就是通过 AssemblyBuilder.DefineDynamicModule 方法创建模块,此时我们需要指定模块的名字以及文件名:
//
Code#12
ModuleBuildermoduleBuilder
=
m_AssemblyBuilder.DefineDynamicModule(
FullQualifiedShapeName,
FileName
);
考验脑力区:
单模块程序集和多模块程序集有什么不同?如何创建多模块程序集?
最后,轮到类型的创建了,通过 ModuleBuilder.DefineType 可以创建 TypeBuilder 对象实例,由于这个对象在后面添加属性以及重写方法时需要用到,于是我把它储存在成员变量中:
//
Code#13
m_TypeBuilder
=
moduleBuilder.DefineType(
FullQualifiedShapeName,
TypeAttributes.Public,
typeof
(Shape)
);
在整个类型创建过程结束之时,我们需要调用 TypeBuilder.CreateType 方法以便生成类型,并且调用 AssemblyBuilder.Save 方法保存动态程序集,这也是 SaveShapeType 方法的职责:
//
Code#14
public
void
SaveShapeType()

{
m_TypeBuilder.CreateType();
m_AssemblyBuilder.Save(FileName);
}
3.2 添加属性
在 C# 中为一个类添加一个可读写的属性是一件非常容易的事情(C# 3.0 的自动属性使得这个过程变得更加简洁)。如果你使用 Visual Studio 2005 的话,只需要输入 prop,接着敲击 Tab 键,然后对属性模板稍作修改就可以了!
然而,这个过程在这里将会异常繁杂,这可以归功于属性的本质。在继续之前,我认为有必要先回顾一下属性这个东西。拿 Code #05 作为例子,与之对应的 IL 代码是:
//
Code#15
.field
private
float64m_Altitude

.method
public
hidebysigspecialnameinstancefloat64
get_Altitude()cilmanaged

{
//Codesize12(0xc)
.maxstack1
.localsinit([0]float64CS$1$0000)
IL_0000:nop
IL_0001:ldarg.0
IL_0002:ldfldfloat64DynamicShape.Shapes.Triangle::m_Altitude
IL_0007:stloc.0
IL_0008:br.sIL_000a
IL_000a:ldloc.0
IL_000b:ret
}

.method
public
hidebysigspecialnameinstance
void
set_Altitude(float64
'
value
'
)cilmanaged

{
//Codesize9(0x9)
.maxstack8
IL_0000:nop
IL_0001:ldarg.0
IL_0002:ldarg.1
IL_0003:stfldfloat64DynamicShape.Shapes.Triangle::m_Altitude
IL_0008:ret
}

.propertyinstancefloat64Altitude()

{
.getinstancefloat64DynamicShape.Shapes.Triangle::get_Altitude()
.setinstancevoidDynamicShape.Shapes.Triangle::set_Altitude(float64)
}
考验脑力区:
hidebysig 的作用是什么?specialname 又有什么用?
根据上面的代码,我们可以把定义一个属性的步骤归纳为:
- 定义一个私有字段 m_Altitude;
- 定义一个名为 get_Altitude 的方法,该方法返回 m_Altitude 的值;
- 定义一个名为 set_Altitude 的方法,该方法对 m_Altitude 进行设值;
- 定义一个名为 Altitude 的属性,并以 get_Altitude 方法作为读访问器,set_Altitude 方法作为写访问器。
考验脑力区:
属性的读写访问器可以分别具有不同的访问级别吗?
搞清楚属性的本质后,我们就可以动手实现 AddShapeProperty 方法了:
//
Code#16
public
void
AddShapeProperty(
string
propertyName)

{
FieldBuilderfieldBuilder=m_TypeBuilder.DefineField(
String.Format("m_{0}",propertyName),
typeof(double),
FieldAttributes.Private
);

PropertyBuilderpropertyBuilder=m_TypeBuilder.DefineProperty(
propertyName,
PropertyAttributes.None,
typeof(double),
null
);

MethodBuildergetterBuilder=m_TypeBuilder.DefineMethod(
String.Format("get_{0}",propertyName),
MethodAttributes.Public|MethodAttributes.SpecialName|MethodAttributes.HideBySig,
typeof(double),
Type.EmptyTypes
);

ILGeneratorgetterILGenerator=getterBuilder.GetILGenerator();
getterILGenerator.Emit(OpCodes.Ldarg_0);
getterILGenerator.Emit(OpCodes.Ldfld,fieldBuilder);
getterILGenerator.Emit(OpCodes.Ret);

propertyBuilder.SetGetMethod(getterBuilder);

MethodBuildersetterBuilder=m_TypeBuilder.DefineMethod(
String.Format("set_{0}",propertyName),
MethodAttributes.Public|MethodAttributes.SpecialName|MethodAttributes.HideBySig,
null,

newType[]
{typeof(double)}
);

ILGeneratorsetterILGenerator=setterBuilder.GetILGenerator();
setterILGenerator.Emit(OpCodes.Ldarg_0);
setterILGenerator.Emit(OpCodes.Ldarg_1);
setterILGenerator.Emit(OpCodes.Stfld,fieldBuilder);
setterILGenerator.Emit(OpCodes.Ret);

propertyBuilder.SetSetMethod(setterBuilder);
}
考验脑力区:
无论是读取还是写入字段的值,我们都要先载入第一个参数(OpCodes.Ldarg_0),为什么要这样?这个参数是什么?
此时此刻,不知道你有否这样一番感概:原来编译器在后面默默地为我做了这么多事情!
3.3 重写 CalculateArea 方法
终于到了重写 CalculateArea 方法了,然而,在这个看似简单的环节里却隐藏着巨大的困难。细心观察 Code #07,不难发现我们需要把“Base * Altitude / 2”这样的表达式解析成 IL 代码!我想放弃了,但又不甘心,只好硬着头皮上网找找看……
增值服务区:
《利用堆栈解析算术表达式一:基本过程》
我们传递给 ImplementCalculateAreaMethod 方法的是“Base * Altitude / 2”,而最终解释后符合 IL 堆栈语义的是“Base Altitude * 2 /”,前者叫做“中缀表达式”,而后者则为“后缀表达式”。我们希望解析器除了支持运算符和常量运算数外,还要支持以单词为单位的变量,因为这些变量最终会被重定向到类型所包含的属性。找了很久都没有发现满足要求的解析器,无奈只好自己硬着头皮写一个。
由于我真的很懒,并且本文的主题是动态程序集,于是我只在这里实现一个功能异常有限(甚至不能称之为解析器)的解析器。为此,我制定了如下约束:
- 表达式可包含的符号为常量运算数、算术运算符以及以单词为单位的变量符号;
- 变量符号必须与对应的类型所包含的属性一致;
- 表达式中每个符号之间以空格隔开;
- 表达式(目前)只支持乘法(×)和除法(÷)运算。
下面我利用堆栈把被我高度约束中缀表达式解析成 IL 符号序列:
//
Code#17
public
class
FormulaExpression

{
publicstaticIListParse(stringformulaExpression)


{
ArrayListreversePolishNotation=newArrayList();
Stack<OpCode>operatorStack=newStack<OpCode>();

foreach(stringtokeninformulaExpression.Split(''))


{
OpCodeoperatorToken;

if(TryParse(token,outoperatorToken))


{
if(operatorStack.Count>0)


{
reversePolishNotation.Add(operatorStack.Pop());
}

operatorStack.Push(operatorToken);
}
else


{
doubleconstant;

if(Double.TryParse(token,outconstant))


{
reversePolishNotation.Add(constant);
}
else