65.9K
CodeProject 正在变化。 阅读更多。
Home

DelegatedTypeBuilder - 在运行时创建新类型和适配器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2012年2月28日

CPOL

9分钟阅读

viewsIcon

20142

downloadIcon

246

本文介绍了一种在运行时构建新类型的方法,而无需用户理解 IL 指令。

背景

我喜欢在运行时生成代码。我曾为我的远程处理、ORM、鸭子类型转换和游戏解决方案(至少是第一个版本)这样做过,但每次我都必须直接使用 IL。

即使我经常这样做,我也认为 IL 指令很难理解,并且通过 Reflection.Emit 方法,很容易编写无法编译或存在严重错误的 IL 代码。

我一直想要的是能够轻松地在运行时创建新的方法、属性、事件,尤其是字段,而无需直接访问 IL。

之前的解决方案

我之前的所有解决方案都基于接口或抽象类。最通用的一个只是实现所有方法来调用一个单一的方法,告知实际调用了哪个方法并将所有参数作为数组传递。

该解决方案存在许多问题。最重要的问题是

  • 它一直在创建数组
  • 它将所有值类型参数装箱
  • 程序员很容易创建一个带有大量 switch 的巨大方法,因此难以维护
  • 如果需要未包含在接口中的新属性或方法,则无法添加它们

还有一种鸭子类型/结构化类型的解决方案。它避免了额外的数组,但需要一个具有兼容方法、属性和事件的现有类型。事实上,只有当您无法访问原始类使其实现给定接口并希望在运行时生成包装器时,它才有用。我甚至尝试添加自动转换,但如果类型不兼容,它也无法解决问题,因此很少找到可以使用的场景。

DelegatedTypeBuilder

我的新解决方案是 `DelegatedTypeBuilder` 类,它基于委托(顾名思义),并允许您创建任意数量的方法、属性和事件。

它不直接绑定到接口,但我仍然推荐它们。毕竟,如果您不知道要调用哪些方法或属性(并且可能从列表中加载它们),您大概可以使用字典。但如果您知道,那么请在代码中使用接口(它比使用 dynamic 关键字或反射运行得更快,允许您使用代码补全,并在拼写错误时在编译时显示错误)。

`DelegatedTypeBuilder` 实际上允许我们做什么?

  • 添加方法,这些方法将调用给定的委托。这样的委托可以是通用的(也就是说,它将接收数组中的所有参数),也可以是针对给定数量的参数的(避免数组分配)。
  • 添加属性,这些属性可以有一个字段,也可以没有。无论哪种情况,都可以为 get 和 set 访问器提供委托(并将字段值或引用作为参数传递给 get/set 访问器),或者如果它不使用不同的字段类型,则可以自动实现。
  • 添加事件,这些事件需要调用一个 action 来进行添加和移除。
  • 所有委托都可以接收一个额外的用户实例参数,因此您可以在其中存储所需的所有信息。
  • 允许在创建方法和属性之前指定数据类型转换,因此您可以创建一个由接收 `string` 的 action 实现的整数属性。
  • 并允许方法直接重定向到用户实例中的另一个方法(或通过另一个委托访问子实例),使用所有可用的数据类型转换,从而非常容易地创建适配器。
  • 允许转换为同一类型,作为创建简单验证或值更正的方式(例如,修剪所有 `string` 以去除开头或结尾的额外空格)。

DelegatedTypeBuilder 的工作原理

`DelegatedTypeBuilder` 的概念很容易理解。难点在于 IL 生成。

`DelegatedTypeBuilder` 构建一个类型,该类型将接收一个用户实例并具有一个静态委托列表。

每个添加的方法都必须接收一个委托来执行实际工作。这样的委托会被添加到列表中(如果它还不在列表中),并且 IL 会被构建来访问该委托并传递所有参数。

如果需要转换或类型转换,它们也会被执行。类型转换是直接完成的,而转换也将委托注册到委托列表中,并通过调用该委托来完成转换。

并且,在构建类型时,委托列表被转换为数组,因为数组访问速度更快(并且更容易从 IL 指令中使用)。

AdapterGenerator

`DelegatedTypeBuilder` 是非常通用的。它已经允许用户在不知道如何创建 IL 指令的情况下定义新类型,但它仍然是一个“低级”资源。

它强制用户手动添加所有内容,如果新对象必须实现接口,则由用户负责创建所有必需的方法和属性,即使它们的工作是抛出 `NotSupportedException`。

考虑到数据类型转换能力,我认为 `AdapterGenerator` 会非常有用,所以我创建了这个类。

它的作用是实现一个接口并将其重定向到一个实际对象,转换所有不匹配的数据类型。

作为一项额外功能,如果接口依赖于 `INotifyPropertyChanged`,它已经用对 `PropertyChanged` 事件的调用实现了所有属性设置。

LazyLoaderGenerator

模式之所以存在,是因为某些问题太普遍,但又无法完全解决而不编写重复的代码,因此模式被创建用来告诉如何正确地做事。

`INotifyPropertyChanged` 是其中一种情况,但它已经被 AdaptorGenerator 解决了。另一种常见情况是 Lazy 模式。

.NET 类库现在有 Lazy 类来帮助避免部分重复代码。我已经介绍了两个 Lazy 解决方案(Lazy Alternatives - LazyAndWeak and BackgroundLoader),因为我认为 Lazy 类不完整。

但即使在我的解决方案中,也无法避免重复。

我们需要

  • 为每个属性创建一个私有字段并初始化它
  • 实现属性 getter 以返回 `privateField.Value`
  • 通常,我们会给一个委托来初始化 lazy 对象,可能是像 _Load("PropertyNameHere") 这样的东西

那么问题就不局限于代码量,还在于它有多容易出错,因为用户通常会复制粘贴代码并更改名称,并且可能会

  • 忘记更改属性名称(这不太糟糕,因为编译器会显示错误)
  • 忘记更改字段名称(这样就会加载错误的值)
  • 忘记更改或拼错 `string` 中的名称(在这种情况下,错误只能在运行时看到)

那么,为什么不避免这一点呢?

这就是 `LazyLoaderGenerator` 的目的。

它简单地实现了一个由只读属性组成的接口,使用了正确的 Lazy 模式。

它还支持用户实例,如果您需要为 Lazy 加载器获取额外信息,并允许您为每个属性类型注册 Lazy 加载器(因此 `string` 的加载方式将与 Image 不同)。

为了确保同一类型的不同属性获得正确的值,属性名称会被传递给加载器操作,这样它就可以找到一个适当的文件(例如)与该名称匹配。

而且,因为我不喜欢默认的 Lazy 类,您可以将其配置为 LazyAndWeak(这样未使用的值就可以被收集)、Lazy(.Net 的那个)或 BackgroundLoader,以便在后台预加载值。

例如,想象一下您想将所有按钮的图像作为“类型化资源”放到应用程序中,但只想在需要时加载它们。

您可以创建这样的接口

public interface IApplicationResources
{
  ImageSource CloseApplicationImage { get; }
  ImageSource OpenFileImage { get; }
  ImageSource SaveFileImage { get; }
}

您只会实现加载一次。如果文件使用属性名称 + ".png"(例如),实现起来会非常容易。

现在,正如我所说的用户实例。想象一下您有大图标、中图标和小图标。

您将创建 IApplicationResources 接口的三个实例,一个带有“Large”参数,另一个带有“Medium”,还有一个带有“Small”。因此,您注册的委托可以由这样的方法实现

public static readonly BitmapSource _LoadImage(string kind, string name)
{
  string fullName = string.Concat("images\\", kind, "\\", name, ".png");
  using (var stream = File.OpenRead(fullName))
  {
    var decoder = new PngBitmapDecoder(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
    return decoder.Frames[0];
  }
}

示例

示例代码离完整的应用程序还有很长的路要走。它只是一个关于如何使用 `AdapterGenerator` 和 `LazyLoaderGenerator` 的示例。

示例本身不直接使用 `DelegatedTypeBuilder`,但 `AdapterGenerator` 和 `LazyLoaderGenerator` 都使用它。

在示例中,Model 有一个 `string` (UserName) 和一个用作布尔值的 char,它保存 0 或 1。

`IModel` 是一个 `INotifyPropertyChanged`,将由 `AdapterGenerator` 实现,它也有一个 UserName,但添加了 `string` 到 `string` 的转换以修剪多余的空格,并且添加了 bool 到 char 和 char 到 bool 的转换以将 true 转换为 '1',false 转换为 '0',反之亦然。

在 Window 中,有一个 `TextBox` 和 `CheckBox` 的副本,用于显示绑定何时真正起作用。

然后在底部是惰性加载器测试。事实上,我没有什么可以加载的,所以我让 Lazy 代码等待 3 秒(使用 `Thread.Sleep`)来模拟缓慢加载,然后返回一个简单的 `string`。

我希望这已经足够展示代码的潜力了,也许将来我会添加更好的示例。

垃圾回收

直到 .NET 3.5,动态生成的代码都无法被收集。因此,即使我们丢失了对生成代码的所有引用,它也会一直存在直到 AppDomain 结束。

.NET 4.0 允许我们创建可收集的程序集,而 DelegateTypeBuilder 会在被请求时使用它。

为什么是“被请求时”,而不是“总是”?

例如,如果您在加载时为所有类创建适配器并希望它们一直保留,那么就没有必要让程序集可收集。这可以加快速度,因为所有必需的适配器类型将一直存在。

但事实是 AppDomain 中动态程序集的数量。默认情况下,我只为所有生成的类型生成一个不可收集的动态程序集。当可收集时,每个 DelegatedTypeBuilder 实例都会创建自己的动态程序集。

请查看 `DelegatedTypeBuilder` 的构造函数以及 `AdapterGenerator` 和 `LazyLoaderGenerator` 类中的 `MustBeCollectible` 属性。

运行时创建的代码与预生成

一些编辑器或编译时工具可能会为您生成代码。那么,哪个更好?

这取决于许多因素。如果您拥有预生成代码所需的一切,通常会更好,因为代码将在您运行时可用,并且您在不受信任的环境(如 Silverlight)中运行不会有问题。

预生成的代码还可以使用直接引用(而不是接口)的优势,可以直接扩展,并且在利用部分类时,未使用的部分方法会被简单地从编译中删除。

但然后我应该说,运行时生成的代码减少了开发人员可见的文件数量,减少了应用程序的大小,因为代码只在运行时生成,并且完全避免了过时文件或对无效文件的更改的风险,因为用户永远看不到它们。

此外,如果您确实需要在运行时构建类型(例如,从配置文件),那么预生成的代码就不是一个选择。

© . All rights reserved.