使用 Reflection.Emit 创建动态类型简介:第 2 部分
动态类型创建简介的第 2 部分。本文展示了如何在动态类型中实际生成方法以及如何调用它们。
引言
在我之前的文章中,我介绍了动态类型,它们的一些可能用途,然后高级地概述了如何使用动态类型实现解决方案。那一切都很好,但现在,让我们来看一个实现和调用动态类型的实际代码示例。
但首先,对于那些没有阅读或不想阅读第一篇文章的人,这里是动态类型的一个简要概述。
动态类型是程序运行时生成的类型或类。当应用程序启动时,您将至少运行一个 AppDomain。为了将动态类型添加到您的 AppDomain,您需要首先创建并将动态程序集添加到您的 AppDomain。动态程序集是在运行时创建并添加到 AppDomain 的程序集。它通常不保存到文件,而是仅存在于内存中。一旦完成此操作,并且在我将介绍的几个步骤之后,您就可以使用 Reflection.Emit
类来创建动态类型。
那么,动态类型有什么用呢?首先,它们就是很酷(我们是开发人员,我们不需要更好的理由)。我的意思是,来吧?在运行时将 IL 发射到内存中以创建您自己的自定义类!这简直太棒了。但严肃地说,动态类型的有用之处在于,您可以让程序评估在运行时可能不知道的数据状态,以便创建针对当前情况优化的类。
动态类型的挑战在于,您不能简单地将 C# 代码转储到动态程序集中,然后让 C# 编译器将其编译为 IL。那会太容易了,Microsoft 的 Reflection.Emit 团队希望您为动态类型付出努力。您必须使用 Reflection.Emit
中的类来定义和生成类型、方法、构造函数和属性定义,然后将 IL 操作码插入或“发射”到这些定义中。听起来很有趣吗?
问题陈述
我偶尔会遇到一个常见问题,即从其他开发人员或开发团队那里继承应用程序。应用程序使用 DataSet
从某个数据库检索数据,但开发人员使用整数序数从 DataRow
中提取数据,而不是字符串序数。
//Using integer ordinals:
foreach (DataRow row in dataTable.Rows)
{
Customer c = new Customer();
c.Address = row[0].ToString();
c.City = row[1].ToString();
c.CompanyName = row[2].ToString();
c.ContactName = row[3].ToString();
c.ContactTitle = row[4].ToString();
c.Country = row[5].ToString();
c.CustomerId = row[6].ToString();
customers.Add(c);
}
//Using string ordinals:
foreach (DataRow row in dataTable.Rows)
{
Customer c = new Customer();
c.Address = row["Address"].ToString();
c.City = row["City"].ToString();
c.CompanyName = row["CompanyName"].ToString();
c.ContactName = row["ContactName"].ToString();
c.ContactTitle = row["ContactTitle"].ToString();
c.Country = row["Country"].ToString();
c.CustomerId = row["CustomerID"].ToString();
customers.Add(c);
}
任何注重性能的开发人员都会很快抓住这一点并指出使用整数序数比使用字符串序数更快,我完全同意这一点。为了演示两者之间的性能差异,我使用 Nick Wienholt 的性能测量框架运行了一个快速性能测量测试(*有关性能测量框架的说明请参见文章末尾)。字符串序数测试的归一化测试持续时间(从现在开始称为 NTD)为 4.87,而整数序数测试的 NTD 为 1,这意味着它的执行时间几乎是使用整数序数的 3 倍。在构建具有高用户负载的性能关键型应用程序时,这种微小的时间差异可能是不可接受的,尤其是在使用整数序数可以轻松提高性能的情况下。
但是,整数序数存在一个维护问题,我已经遇到过太多次了。如果您的 DBA 决定重新设计表结构并添加一个新列,但不是在表的末尾,而是在中间的某个位置,会发生什么?如果表完全重组,列的顺序也完全改变了呢?而且,如果您作为开发人员,没有被告知这些情况,会发生什么?很可能您的应用程序会崩溃,因为它正在尝试将 SQL 数据类型转换为不匹配的 .NET 数据类型。甚至更糟的是,您的应用程序不会崩溃,但会继续运行,但现在数据已损坏。
信不信由你,这种情况偶尔会发生(至少对我来说是这样)。由于越来越多地使用由第三方供应商或其他开发团队维护的 Web 服务,应用程序如今更容易受到影响。这种情况最近促使我编写了一个实用程序类,它能提供整数序数的速度,同时具有字符串序数的维护性。
DataRowAdapter 的版本 1
为了解决这个简单的问题,我提出了以下类
public class DataRowAdapter
{
private static bool isInitialized = false;
private static int[] rows = null;
public static void Initialize(DataSet ds)
{
if (isInitialized) return;
rows = new int[ds.Tables[0].Columns.Count];
rows[0] = ds.Tables[0].Columns["Address"].Ordinal;
rows[1] = ds.Tables[0].Columns["City"].Ordinal;
rows[2] = ds.Tables[0].Columns["CompanyName"].Ordinal;
rows[3] = ds.Tables[0].Columns["ContactName"].Ordinal;
.
.//pull the rest of the ordinal values by column name
.
isInitialized = true;
}
//static properties for returning integer ordinal
public static int Address { get {return rows[0];} }
public static int City { get {return rows[1];} }
public static int CompanyName { get {return rows[2];} }
public static int ContactName { get {return rows[3];} }
}
这个类的目的相当明显。您将 DataSet
传递到静态 Initialize()
方法中;它会遍历 DataTable
中的每一列,并将整数序数存储到整数数组中。然后,我定义了一个静态属性,用于返回该列的整数序数。下面显示了使用此类从 DataRow
检索数据的代码。
DataRowAdapter.Initialize(dataSet);
foreach (DataRow row in dataSet.Tables[0].Rows)
{
Customer c = new Customer();
c.Address = row[DataRowAdapter.Address].ToString();
c.City = row[DataRowAdapter.City].ToString();
c.CompanyName = row[DataRowAdapter.CompanyName].ToString();
c.ContactName = row[DataRowAdapter.ContactName].ToString();
.
.
customers.Add(c);
}
这一切都相当简单。DataRowAdapter
充当整数序数检索工具,因此现在,您的代码可以以伪字符串序数的方式从 DataRow
中拉取数据,但在幕后,它仍然基于列的整数索引进行访问。而且,如果您的 DBA 决定更改列的顺序,您将无需更新数据访问代码。
为了查看 DataRowAdapter
是否确实有助于性能,我运行了一个性能测试,将此方法与直接使用整数序数进行比较。使用 DataRowAdapter
访问 DataRow
中的数据得出的 NTD 为 1.04,仅慢 4%。与使用字符串序数相比,这还不错,字符串序数慢了将近 300%!
解决这个问题的更好方法
这效果很好,持续时间很长。但是,当我开始将这种类设计用于越来越多的 DataTable
时,我意识到维护起来变得很痛苦。我必须为每个 DataTable
列签名创建具有硬编码静态属性的新类。在创建了大约 15 个不同的类之后,它开始让人烦恼。
进入 Reflection.Emit
命名空间。Reflection.Emit
命名空间有一堆类,它们的主要工作是在运行时(即应用程序运行时)动态创建程序集和类型。这为什么很重要?因为有了 Reflection.Emit
,您现在可以在运行时为每个 DataTable
动态生成一个 DataRowAdapter
类,而不是硬编码一堆非常专业的静态类。理论上,您应该将 DataSet
或 DataTable
传递给工厂类,工厂类应该根据 DataTable
的列结构生成一个新的 DataRowAdapter
类。而且,一旦工厂生成了一个新的 DataRowAdapter
,它就不必再次生成它,因为它已经加载到 AppDomain 中了。很方便,是吧?
使用 Reflection.Emit
的缺点(总有缺点,对吧?)是您不能简单地将一个字符串变量塞满 C# 代码,然后即时编译它(实际上,使用 System.CodeDom
命名空间和 CSharpCodeProvider
类,您可以这样做,但这必须经过 C# 编译器和 JIT 编译器,这会慢得多)。使用 Reflection.Emit
,您可以在内存中创建一个新程序集,然后直接将 IL 操作码发射到该程序集中。优点是您不必运行 C# 编译器,因为您发射的代码是 IL。缺点是您必须理解 IL。但是,有一些方法可以使其更容易,我稍后会介绍。
定义通用接口
使用 Reflection.Emit
还有另一个问题。您没有可编程的 API。想想看,您将在运行时生成的类在设计时不存在。那么,如何调用它呢?啊,这就是接口的力量。
所以,第一步是弄清楚动态类型的公共接口是什么。这是一个相当简单的示例,所以接口也应该相当简单。因此,经过长时间的深思熟虑,我得出了以下接口
public interface IDataRowAdapter
{
int GetOrdinal(string colName);
}
由于我不知道需要的列名,接口就不能很好地具有硬编码的静态属性,对吗?相反,我决定使用一个名为 GetOrdinal()
的方法,该方法接受一个列名的字符串值并返回该列的整数序数。
工厂类生成的所有动态类型都将继承此接口,并且此接口也将是工厂类的返回类型。您的程序将调用工厂类,传入一个 DataTable
,并返回一个 IDataRowAdapter
。然后,它可以调用 IDataRowAdapter.GetOrdinal()
来获取列名的整数序数。
还有另一种方法。您可以不定义所有动态类型都可以继承的公共接口,而是使用后期绑定并通过反射访问动态类型的方法和属性。但是,出于几个原因,这应该被认为是“不良形式”。首先,接口是与类型的契约。它保证方法将存在并且可以被调用。如果您通过反射使用后期绑定方法调用,则无法保证该方法对该类型存在。您可能会拼错方法名,并且编译器不会发出警告。直到应用程序运行并尝试调用该方法时,您才会知道有问题,届时将抛出反射异常。
后期绑定方法调用的第二个问题是反射速度很慢。您通过使用动态类型获得的任何性能优势很可能会因此而丢失。
我该怎么做?
在 C# 中使用接口 IDataRowAdapter
原型化新的 DataRowAdapter
时,我尝试了几种不同的方法来确定字符串列名对应的整数序数值。由于我试图找到一种快速、动态地从 DataRow
获取数据的方法,我为每种方法创建了一个性能测试并测量了结果。以下是不同方法及其与使用整数序数相比的结果列表。
- 使用基于传入的字符串列名的
switch
语句。 - 为列值创建枚举,并使用枚举上的
switch
块(认为它会比对字符串进行切换更快)。使用Enum.Parse(columnName)
创建枚举实例。 - 使用多个“
if
”块来检查列名。 - 使用
Dictionary<string, int>
存储列名/序数映射。
结果有点出人意料。最慢的是枚举切换。这是因为 Enum.Parse()
使用反射来创建列枚举的实例。此方法的 NTD 为 10.74,而整数序数为 1。
接下来最慢的是字符串 switch 语句,其 NTD 为 3.71,而整数序数为 1。并没有比直接使用字符串序数快多少。
接下来是使用通用 Dictionary
,其 NTD 为 3.4,而整数序数为 1。仍然不是很好。
而获胜者是多个“if
”语句,其 NTD 为 2.6,而整数序数为 1。现在,它仍然比直接整数序数查找慢得多,但比字符串查找快得多,而且您仍然可以获得列名安全性。
我决定采用的实际实现如下所示的 C# 代码。当我使用 Reflection.Emit
生成类型时,我将以此为基础生成 IL。
public class DataRowAdapter : IDataRowAdapter
{
public int GetOrdinal(string colName)
{
if (colName == "Address")
return 0;
if (colName == "City")
return 1;
if (colName == "CompanyName")
return 2;
if (colName == "ContactName")
return 3;
.
.
throw new ApplicationException("Column not found");
}
}
现在,您能看到动态类型的好处了吗?您永远不会在设计时硬编码这样的东西,因为您不确定“城市”列是否真的在序数位置 1。但是使用 Reflection.Emit
,您可以确定,因为您是根据运行时确定的证据来生成类的。
解决方案设计
接下来要做的是为生成动态类型并将其返回给调用者的类设计。对于动态类型生成器,我决定采用工厂模式。这非常适合此解决方案的需求,因为调用者无法显式调用动态类型的构造函数。此外,我想向调用者隐藏动态类型的实现细节。因此,工厂类的公共 API 将是这样的
public class DataRowAdapterFactory
{
public static IDataRowAdapter CreateDataRowAdapter(DataSet ds, string tableName)
{
//method implementation
}
public static IDataRowAdapter CreateDataRowAdapter(DataTable dt)
{
//method implementation
}
//private factory methods
}
因为每个动态类型都将针对特定的列列表进行硬编码,所以每个传入工厂的具有不同 TableName
值的 DataTable
都将导致工厂生成一个新类型。如果 DataTable
第二次传入工厂,该 DataTable
的动态类型已经生成,工厂只需返回已生成类型的实例。
设置动态类型
(注意,一些 Reflection.Emit
函数的描述可能会重复我上一篇文章中的内容,但我想让读者即使没有阅读第 1 部分也能理解。)
在开始编写 GetOrdinal()
方法的功能之前,我想介绍一下如何设置一个程序集来保存新类型。由于 Reflection.Emit
无法向现有程序集添加新类型,因此您必须在内存中生成一个全新的程序集。为此,您可以使用 AssemblyBuilder
类。
private static AssemblyBuilder asmBuilder = null;
private static ModuleBuilder modBuilder = null;
private static void GenerateAssemblyAndModule()
{
if (asmBuilder == null)
{
AssemblyName assemblyName = new AssemblyName();
assemblyName.Name = "DynamicDataRowAdapter";
AppDomain thisDomain = Thread.GetDomain();
asmBuilder = thisDomain.DefineDynamicAssembly(
assemblyName, AssemblyBuilderAccess.Run);
modBuilder = assBuilder.DefineDynamicModule(
asmBuilder.GetName().Name, false);
}
}
要创建新的 AssemblyBuilder
实例,您需要首先使用 AssemblyName
实例。创建一个新实例,并将其分配给您要调用的程序集名称。然后,获取静态 Thread.GetDomain()
方法的 AppDomain
。此 AppDomain
实例将允许您使用 DefineDynamicAssembly()
方法创建新的动态程序集。只需传入 AssemblyName
类和 AssemblyBuilderAccess
的枚举值。在此实例中,我不想将此程序集保存到文件,但如果我想保存,我可以使用 AssemblyBuilderAccess.Save
或 AssemblyBuilderAccess.RunAndSave
。
幸运的是,一旦创建了 AssemblyBuilder
,就可以一遍又一遍地使用相同的实例来创建所有新的动态类型,因此它只需要创建一次。
一旦创建了 AssemblyBuilder
,还需要创建一个 ModuleBuilder
实例,该实例稍后将用于创建新的动态类型。使用 AssemblyBuilder.DefineDynamicModule()
方法创建一个新实例。如果您愿意,可以为您的动态程序集创建任意数量的模块,但在本例中,只需要一个。
现在,开始创建动态类型
private static TypeBuilder CreateType(ModuleBuilder modBuilder, string typeName)
{
TypeBuilder typeBuilder = modBuilder.DefineType(typeName,
TypeAttributes.Public |
TypeAttributes.Class |
TypeAttributes.AutoClass |
TypeAttributes.AnsiClass |
TypeAttributes.BeforeFieldInit |
TypeAttributes.AutoLayout,
typeof(object),
new Type[] {typeof(IDataRowAdapter)});
return typeBuilder;
}
动态类型是通过 TypeBuilder
类创建的。您通过调用 ModuleBuilder.DefineType()
方法创建一个 TypeBuilder
类的实例,将类名作为第一个参数,将 TypeAttributes
的枚举值作为第二个参数,该枚举值定义了动态类型的所有特征。第三个参数是动态类型继承自的类的 Type
实例;在此例中,是 System.Object
。第四个值是动态类型将继承的接口数组。这在此解决方案中非常重要,因此我传入 IDataRowAdapter
的值。
这里有一点要指出。您是否注意到在创建这些 Reflection.Emit
类实例时存在一种模式?AppDomain
用于创建 AssemblyBuilder
,AssemblyBuilder
用于创建 ModuleBuilder
,ModuleBuilder
用于创建 TypeBuilder
?这是工厂模式的另一个示例,它是 Reflection.Emit
命名空间中的一个常见主题。您能猜到如何创建 MethodBuilder
、ConstructorBuilder
、FieldBuilder
或 PropertyBuilder
类吗?当然是通过 TypeBuilder
!
因使用动态类型而产生的设计变更
我想停下来一分钟,谈谈我的设计。DataRowAdapter
的最终原型使用字符串列名通过多个 if
语句来确定要返回的序数。但现在类型是在运行时创建的,有一种更快的方法可用。比较两个整数比比较两个字符串快得多。那么,如何从字符串获取整数值呢?当然是 string.GetHashCode()
!现在,在您开始尖叫哈希码不能保证对所有可能的字符串都是唯一的之前,让我解释一下。虽然我不能说每个字符串都会输出唯一的哈希码值,但我可以说在一个小字符串列表(例如 DataTable
的列名列表)中,它很可能是唯一的。
因此,我创建了一个方法来检查 DataTable
的所有哈希码是否唯一。如果它发现列名是唯一的,那么动态类型工厂将输出一个 switch
语句来检查整数值。如果它发现它们不唯一,那么动态类型工厂将输出多个 if
语句来检查字符串相等性。
我想看看使用列名的哈希码与使用字符串比较的差异有多大,以证明类型工厂增加的复杂性是合理的。当我运行性能测试时,我发现使用哈希码给我的 NTD 为 1.35,而直接使用整数序数。现在,原始静态 DataRowAdapter
的 NTD 为 1.04,但我也必须为每个 DataTable
维护一个类,如果应用程序很大,这会变得非常麻烦。使用此解决方案中使用的动态类型,无需维护。而且,通常,维护效益将胜过性能效益,尤其是当性能下降不是很严重时。
接下来,我运行了一个测试,以检查如果我使用字符串比较,DataRowAdapter
的运行速度如何。这里的结果并不是很好。我的 NTD 达到了 1.9,比直接使用整数序数慢两倍,但仍然比直接使用字符串序数快得多。但是,我希望在设计中保留这一点,以防列名列表的哈希码值不唯一。
因此,通过这种设计,大多数情况下,您将获得整数相等性检查的性能优势,并且偶尔,类型会回退到字符串相等性检查。无论哪种方式,都比直接使用字符串序数更快。
使用 Reflection.Emit 编写 GetOrdinal 方法
接下来是动态类型工厂的核心,创建 GetOrdinal()
方法。到目前为止,我没有展示太多关于 IL 的工作,但现在,我们将深入了解 Reflection.Emit
。
下面是 GetOrdinal
的代码
private static void CreateGetOrdinal(TypeBuilder typeBuilder, DataTable dt)
{
int colIndex = 0;
//create the needed type arrays
Type[] oneStringArg = new Type[1] {typeof(string)};
Type[] twoStringArg = new Type[2] {typeof(string), typeof(string)};
Type[] threeStringArg =
new Type[3] {typeof(string), typeof(string), typeof(string)};
//create needed method and contructor info objects
ConstructorInfo appExceptionCtor =
typeof(ApplicationException).GetConstructor(oneStringArg);
MethodInfo getHashCode = typeof(string).GetMethod("GetHashCode");
MethodInfo stringConcat = typeof(string).GetMethod("Concat", threeStringArg);
MethodInfo stringEquals = typeof(string).GetMethod("op_Equality", twoStringArg);
//defind the method builder
MethodBuilder method = typeBuilder.DefineMethod("GetOrdinal",
MethodAttributes.Public | MethodAttributes.HideBySig |
MethodAttributes.NewSlot | MethodAttributes.Virtual |
MethodAttributes.Final, typeof(Int32), oneStringArg);
//create IL Generator
ILGenerator il = method.GetILGenerator();
//define return jump label
System.Reflection.Emit.Label outLabel = il.DefineLabel();
//define return jump table used for the many if statements
System.Reflection.Emit.Label[] jumpTable =
new System.Reflection.Emit.Label[dt.Columns.Count];
if (AllUniqueHashValues(dt))
{
//create the return int index value, and hash value
LocalBuilder colRetIndex = il.DeclareLocal(typeof(Int32));
LocalBuilder parmHashValue = il.DeclareLocal(typeof(Int32));
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Callvirt, getHashCode);
il.Emit(OpCodes.Stloc_1);
foreach (DataColumn col in dt.Columns)
{
//define label
jumpTable[colIndex] = il.DefineLabel();
//compare the two hash codes
il.Emit(OpCodes.Ldloc_1);
il.Emit(OpCodes.Ldc_I4, col.ColumnName.GetHashCode());
il.Emit(OpCodes.Bne_Un, jumpTable[colIndex]);
//if equal, load the ordianal into loc0 and return
il.Emit(OpCodes.Ldc_I4, col.Ordinal);
il.Emit(OpCodes.Stloc_0);
il.Emit(OpCodes.Br, outLabel);
il.MarkLabel(jumpTable[colIndex]);
colIndex++;
}
}
else
{
//create the return int index value, and hash value
LocalBuilder colRetIndex = il.DeclareLocal(typeof(Int32));
foreach (DataColumn col in dt.Columns)
{
//define label
jumpTable[colIndex] = il.DefineLabel();
//compare the two strings
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldstr, col.ColumnName);
il.Emit(OpCodes.Call, stringEquals);
il.Emit(OpCodes.Brfalse, jumpTable[colIndex]);
//if equal, load the ordianal into loc0 and return
il.Emit(OpCodes.Ldc_I4, col.Ordinal);
il.Emit(OpCodes.Stloc_0);
il.Emit(OpCodes.Br, outLabel);
il.MarkLabel(jumpTable[colIndex]);
colIndex++;
}
}
//error handler if cant find column name
il.Emit(OpCodes.Ldstr, "Column '");
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldstr, "' not found");
il.Emit(OpCodes.Callvirt, stringConcat);
il.Emit(OpCodes.Newobj, appExceptionCtor);
il.Emit(OpCodes.Throw);
//label for if user found column name
il.MarkLabel(outLabel);
//return ordinal for column
il.Emit(OpCodes.Ldloc_0);
il.Emit(OpCodes.Ret);
}
我们首先要做的是设置一些项目,以便它们可以在方法中稍后使用。我们将要发出的 GetOrdinal()
方法在其生命周期中需要调用四个方法。它们是 String.GetHashCode()
、String.Concat()
、String.op_Equality()
和 ApplicationException
的构造函数。我们需要创建 MethodInfo
和 ConstructorInfo
对象,以便为这些方法发出“call
”或“callvirt
”操作码。
下一步是根据 TypeBuilder
创建一个 MethodBuilder
实例。同样,我只是查看了我在 ILDASM 中用 C# 编写的原型,以确定在定义 MethodBuilder
时要使用哪些 MethodAttribute
。TypeBuilder.DefineMethod()
与 DefineConstructor
(在第一篇文章中描述)非常相似,除了一点。DefineMethod()
有一个额外的参数供您定义返回类型。如果您定义的方法没有返回值,则只需为该参数传入 null
。
接下来,需要一个 ILGenerator
实例,它的创建方式与 MethodBuilder
几乎完全相同。
现在,我需要定义几个 Label
。在 Reflection.Emit
中,Label
用于告诉 CLR 跳到或分支到哪里。分支是 IL 中 if 语句、switch 语句和循环的编写方式,与 goto 语句非常相似。我定义的第一个标签将在找到正确的序数并且过程应该跳到方法的末尾以便返回时使用。然后创建一个 Label
数组,每个列名一个 Label
,这将用于定义字符串比较的“if
”语句块或用于比较列名哈希值的“switch
”语句。
要使用标签,您必须首先使用 ILGenerator.DefineLabel()
定义它。接下来,您将其作为第二个参数传递给 ILGenerator.Emit()
方法,其中第一个参数是众多分支操作码中的任何一个(Brfalse
、Brtrue
、Be
、Bge
、Ble
、Br
等)。这基本上表示“如果 x 为(真、假、==、>=、<=)则转到此标签”。最后,您必须使用 ILGenerator.MarkLabel()
方法,将 Label
实例作为唯一参数传入。它的作用是告诉进程在遇到分支操作码时跳到 Label
标记的位置。对于“if
”语句,您将在定义分支操作码的下方标记您的 Label
。对于“loop”语句,您很可能会在定义分支操作码的上方标记您的 Label
(因此,循环)。
那么,我怎么知道 GetOrdinal
要发出什么 IL 呢?与我知道如何定义 MethodBuilder
的方式相同。用 C# 编写它,编译它,然后查看 ILDasm.exe 中生成的 IL。一旦从 ILDASM 获取了 IL,剩下的就是简单但繁琐的任务,即使用 ILGenerator.Emit(Opcodes.*)
语句复制 IL。
我不会详细介绍 GetOrdinal()
中的每一行代码,因为如果您查看 C# 版本 GetOrdinal()
生成的 IL,它应该相当明显。
创建和使用动态类型的实例
现在,创建动态类型的所有工具都已就位,我还有最后一个领域需要介绍:工厂类如何创建新的动态类型并向调用者返回新实例,以及如何使用动态类型。下面显示了工厂类的基本结构。
public static IDataRowAdapter CreateDataRowAdapter(DataTable dt)
{
return CreateDataRowAdapter(dt, true);
}
TypeBuilder typeBuilder = null;
private static IDataRowAdapter CreateDataRowAdapter(DataTable dt,
bool returnAdapter)
{
//return no adapter if no columns or no table name
if (dt.Columns.Count == 0 || dt.TableName.Length == 0)
return null;
//check to see if type instance is already created
if (adapters.ContainsKey(dt.TableName))
return (IDataRowAdapter)adapters[dt.TableName];
//Create assembly and module
GenerateAssemblyAndModule();
//create new type for table name
TypeBuilder typeBuilder = CreateType(modBuilder, "DataRowAdapter_" +
dt.TableName.Replace(" ", ""));
//create get ordinal
CreateGetOrdinal(typeBuilder, dt);
IDataRowAdapter dra = null;
Type draType = typeBuilder.CreateType();
//assBuilder.Save(assBuilder.GetName().Name + ".dll");
//Create an instance of the DataRowAdapter
IDataRowAdapter dra = (IDataRowAdapter) =
Activator.CreateInstance(draType, true);
//cache adapter instance
adapters.Add(dt.TableName, dra);
//if just initializing adapter, dont return instance
if (!returnAdapter)
return null;
return dra;
}
工厂做的第一件事是检查 TypeBuilder
类是否已经创建。如果还没有,工厂会调用我之前展示的私有方法,这些方法会创建 DynamicAssembly
、DynamicModule
、TypeBuilder
和动态类型的 GetOrdinal()
方法。一旦这些步骤完成,它会使用 TypeBuilder.CreateType()
方法返回 DataRowAdapter
的 Type
实例。然后我使用 Activator.CreateInstance
,使用生成的类型来实际创建动态类型的工作实例。太棒了!
这是关键时刻。创建一个新类型并调用该类型的构造函数将调用之前构建的构造函数。如果 IL 发射错误,CLR 将抛出异常。如果一切正常,您将拥有 DataRowAdapter
的工作副本。在工厂拥有 DataRowAdapter
的实例后,它会将其向下转换并返回 IDataRowAdapter
。
使用动态类型相当简单。要记住的主要事情是您必须针对接口进行编码,因为在设计时,该类不存在。下面是一个调用 DataRowAdapterFactory
类并请求 DataRowAdapter
的示例代码。工厂返回一个 IDataRowAdapter
实例,然后您可以调用 GetOrdinal()
并传入所需的列名。DataRowAdapter
将找出请求的整数序数并返回它。这在下面显示。
IDataRowAdapter dra = DataRowAdapterFactory.CreateDataRowAdapter(dataTable);
foreach (DataRow row in data.Rows)
{
Customer c = new Customer();
c.Address = row[dra.GetOrdinal("Address")].ToString();
c.City = row[dra.GetOrdinal("City")].ToString();
c.CompanyName = row[dra.GetOrdinal("CompanyName")].ToString();
c.ContactName = row[dra.GetOrdinal("ContactName")].ToString();
.
.//pull the rest of the values
customers.Add(c);
}
下面是类图和序列图,它们说明了如何使用工厂创建 IDataRowAdapter
,以及使用者如何使用 IDataRowAdapter
。
我怎么知道我的 IL 是正确的?
(本节是我第一篇文章的重复,但我将其包含在内以作教学用途。)
好的,现在我已经完成了动态类型工厂,我在 Visual Studio 中编译了解决方案,没有错误。如果我运行它,它会工作,对吧?也许吧。Reflection.Emit
的缺点是您可以发出几乎任何您想要的 IL 组合。但是,没有设计时编译器检查您编写的 IL 是否有效。有时,当您使用 TypeBuilder.CreateType()
“烘焙”类型时,如果出现问题,它会抛出异常,但仅针对某些问题。有时,直到您第一次实际尝试调用方法时才会出现错误。还记得 JIT 编译器吗?JIT 编译器直到第一次调用方法时才会尝试编译和验证您的 IL。因此,很有可能,实际上很可能,您直到实际运行应用程序、生成类型并第一次调用动态类型时才会发现您的 IL 无效。但是,CLR 会给出有用的错误消息,对吧?不太可能。通常,我会收到非常有用的“公共语言运行时检测到无效程序”异常。
好的,那么如何判断您的动态类型是否包含有效的 IL?PEVerify.exe 来救援!PEVerify 是 .NET 附带的一个工具,可以检查程序集中的有效 IL 代码、结构和元数据。但是,为了使用 PEVerify,您必须将动态程序集保存到物理文件(请记住,到目前为止,动态程序集只存在于内存中)。要为动态程序集创建实际文件,您需要对工厂代码进行一些更改。首先,将 AppDomain.DefineDynamicAssembly()
方法中的最后一个参数从 AssemblyBuilderAccess.Run
更改为 AssemblyBuilderAccess.RunAndSave
。其次,更改 AssemblyBuilder.DefineDynamicModule()
以将其第二个参数作为程序集文件名传入。最后,添加一行新代码将程序集保存到文件。我将其放在调用 TypeBuilder.CreateType()
之后,如下所示
Type draType = typeBuilder.CreateType();
assBuilder.Save(assBuilder.GetName().Name + ".dll");
现在已经就位,运行应用程序并创建动态类型。一旦运行,您应该在解决方案的 Debug 文件夹中有一个名为“DynamicDataRowAdapter.dll”的新 DLL(假设您使用的是 Debug 构建)。打开 .NET 命令提示符窗口,输入“PEVerify <路径到程序集>\DynamicDataRowAdapter.dll”,然后按 Enter。PEVerify 将验证程序集,并告诉您一切正常或有什么问题。PEVerify 的优点是它提供了关于错误原因和问题位置的详细信息。另请注意,仅仅因为 PEVerify 出现错误并不意味着程序集无法运行。例如,当我第一次编写 Factory 类时,在调用静态 String.Equals()
方法时使用了“callvirt
”操作码。这导致 PEVerify 输出错误,但它仍然运行。这是一个简单的修复,改为使用“call
”操作码调用静态方法,下次我运行 PEVerify 时,没有发现错误。
(static
和 sealed
方法调用使用“call
”操作码而不是“callvirt
”操作码。这是因为“callvirt
”操作码会使 CLR 检查类型的继承链以确定实际调用哪个函数。由于 static
和 sealed
方法不受继承影响,CLR 不需要进行此检查,可以直接使用“call
”操作码调用方法。)
最后一点,如果您将代码更改为输出物理文件,请务必将其改回原来的样子。这是因为一旦动态程序集保存到文件,它就会被锁定,因此在第一个动态类型生成并保存到文件后,您将无法添加任何新的动态类型。在这种情况下,这将是一个问题,因为每次将新的 DataTable
传入工厂时,它都会尝试创建一个新的动态类型。但是,在第一个类型生成之后,如果您将程序集保存到文件,则会抛出异常。
另一种双重检查您的 IL 的方法是使用免费工具 Reflector(只需谷歌搜索)将 IL 反编译回 C#,并查看您发出的内容。
谈到过度工程!
所以,这似乎为了获得一点性能提升而做了大量复杂的工作。真的值得吗?也许不值得……很可能不值得。这取决于您正在编写的应用程序。我编写的一些应用程序是服务器应用程序,它们受到大量访问,需要获得每一点性能提升。
本文的目的不是展示一个新的实用程序类,而是让您领略一下 Reflection.Emit
可以实现什么。可能性太多了。例如,我希望在本系列中撰写的下一篇文章将介绍一个完全由动态类型和 Reflection.Emit
完成的简单面向方面编程 (AOP) 框架。
调试动态类型
动态类型有一个阴暗面。它们可能难以维护和调试。这里有一个我喜欢在开发中提出的问题。我称之为“被公交车撞”问题。IL 相当复杂。没有多少人了解它,甚至不想了解它。那么,如果您的开发团队中只有您一个人了解 IL,而您被公交车撞了呢?谁来维护解决方案?我并不是说如果动态类型有必要,您就应该放弃它们的优点,但产品中至少有两个人了解复杂性总是好的。
第二个问题是调试。由于您正在将 IL 代码转储到内存中,因此在 Visual Studio 中创建断点并逐步调试您的 IL 并不容易,如果您需要调试动态类型。存在替代方案和解决方案,我将在未来的动态类型文章中讨论它们。
关于整数序数的性能说明
在本文中,我针对使用字符串和整数序数访问 DataRow
运行了比较性能测量。仅仅因为整数序数比字符串序数快四倍,并不意味着您的网页加载速度也会快四倍。远非如此!从 DataReader
中提取数据只是页面可能做的一小部分。如果您的页面使用任何类型的数据绑定,情况尤其如此。数据绑定非常昂贵,其性能成本很可能会使您的整数序数增益不明显。因此,不要期望在一次页面加载中看到明显的性能增益。您可能看到一些好处的唯一方法是,如果将您的网页置于负载测试工具之下,并在高并发用户访问时测量每秒的请求数。
性能测量框架说明
本文中引用的所有性能测试均使用 Nick Wienholt 的性能测量框架完成。您可以从此处下载源代码和一篇描述如何使用它的文章。所有测试结果均通过执行问题代码块 5,000 次,并测量此持续时间的时间来计算。然后重复 10 次,得到 10 个时间测量值。然后将这 10 个测量值一起计算,得出各种统计数据,例如归一化值、中位数、平均值、最小值和最大值以及标准差。性能测量框架为您完成所有这些工作。您只需编写问题代码块和一些框架插件设置,然后运行测试。
Reflection.Emit 的其他选项
除了使用 Reflection.Emit
之外,还有其他几种选择。您始终可以构建表示要生成代码的 System.CodeDom
对象图,并通过 CodeDomProvider
运行它以在内存中创建程序集,然后将程序集加载到您的 AppDomain 中。或者,您也可以使用 CodeDom 类将 C# 代码字符串转储到 CodeSnippetExpression
中,然后通过 CodeDomProvider
运行它。这两种方法都适合您,但创建动态类型的重点是为了提高效率。这两种方法都必须经过 C# 编译器,然后程序集必须加载到您的 AppDomain 中才能被您的应用程序调用。这些是您在使用 Reflection.Emit
创建动态类型时可以跳过的额外步骤。