孜孜不倦的程序员

多模式 .NET,第 6 部分:反射元编程

Ted Neward


马拉松继续进行。在前一部分中,我们讨论了自动元编程,现在,我们都应该已经熟悉通用性和可变性的概念了。对于元编程,即用程序编写程序的想法,我已经讲了很多内容。

上个月,我研究了一种比较普通的元编程方法:自动元编程,它还有一个更通用的名称,代码生成。在自动元编程方案中,开发人员编写程序,这些程序描述要生成的“事物”。通常,这是在命令行参数或其他输入(如关系数据库架构、XSD 文件、甚至 Web 服务描述语言 (WSDL) 文档)的帮助下完成的。

因为代码生成实际上“就像”代码由人手工编写一样,可变性可以来自于代码内部的任何位置。数据类型、方法、继承 ...根据需要,所有这些都可能发生变化。当然,它也有两个缺点。首先,过多的可变性可能会使生成的代码(往往还有代码生成所用的模板)难以理解。其次,生成的产物基本上是不可编辑的,除非通过使用分部类将代码生成分离或代码生成不再是必要部分。

幸运的是,C# 和 Visual Basic 除自动元编程之外,还提供了许多的元编程策略,并且从 Microsoft .NET Framework 的最初时期就已开经开始这样做了。

一直存在的问题

面向对象的环境中有一个经常发生的问题,对象关系持久性,也称为对象到 XML 转换,或在更现代的 Web 2.0 时代,称为对象到 JSON 转换。不管开发人员怎样努力,似乎都无法避免对象模型以某种方式躲避 CLR 的需要,要么在网络间移动,要么移到磁盘上再移回来。这就是问题所在:以前的设计模式,过程和面向对象,并没有为这种两难境地提供好的解决方案。

假设我们要在代码中建立一种规范、简化的人员表示形式模型:

class Person {
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public int Age { get; set; }

  public Person(string fn, string ln, int a) {
    FirstName = fn; LastName = ln; Age = a;
  }
}

事实证明,保留此对象的实例并不困难,特别是因为属性(以最简单的形式)与关系表的列一一对应,并且属性是可以公开访问的。 您可以编写一个过程来获取 Person 的实例,提取少量数据,将这些数据注入 SQL 语句,然后将生成的语句发送到数据库:

class DB {
  public static bool Insert(Person p) {
    // Obtain connection (not shown)
    // Construct SQL
    string SQL = "INSERT INTO person VALUES (" +
      "'" + p.FirstName + "', " +
      "'" + p.LastName + "', " +
      p.Age + ")";
    // Send resulting SQL to database (not shown)
    // Return success or fail
    return true;
  }
}

过程方法的缺点会使它丑陋的头部快速膨胀:需要插入的新类型(Pet、Instructor、Student 等)将需要代码几乎相同的新方法。 更糟糕的是,如果公开给公共 API 的属性没有与列或内部字段一一对应,情况就会变得更加复杂,编写 SQL 例程的开发人员需要知道哪些字段需要保留,哪些不需要保留,这明显与封装冲突。

从设计的角度看,对象关系问题要捕获将数据保留为通用性中的 SQL 式部分,所以管理数据库连接和事务在一个位置处理,但仍然允许在保留(或检索)内容的实际结构中存在可变性。

回想一下,根据我们早期的调查,过程方法会捕获算法通用性,继承会捕获结构通用性,同时允许(正)可变性,但它们做的都不是我们需要的。 继承方法,即将通用性放入基类中,需要在派生类中工作的开发人员指定 SQL 字符串和处理大部分输入和输入记录(尤其是将通用性向下推回派生类中)。 过程方法需要过程内部(提取和构建 SQL 以执行)有从过程外部指定的某种可变性,而现实证明这相当难以实现。

进入元编程

对于我们频繁提到的对象关系持久性问题,一种解决方案就是自动元编程:使用数据库架构,创建知道如何向数据库保留和从数据库保留自身的类。

不幸的是,这种方法也具有代码生成的所有传统问题,尤其是在类要将对象表示形式更改为比实际数据库架构指示的表示形式更易于使用的形式时。 例如,如果 VARCHAR(2000) 列是 .NET Framework System.String 而不是 char[2000] 的话,它可能更容易处理。

其他代码生成技术从类定义开始,然后沿着持久的类定义创建一个数据库架构…但是这意味着在一定程度上,现在对象层次结构复制到两个不同的模型中,一个只用于持久性,而另一个用于其他事务。 (注意:只要一有必要将对象转换为 XML,另一个层次结构就会突然变成您必须处理的内容,并且它还要处理 JSON。 很快,这种方法就会变得难以控制。)

幸运的是,反射元编程提供了可能的缓解办法。 自 1.0 版起,System.Reflection 就是 .NET Framework 的一部分,它允许开发人员在运行时检查对象的结构,在这种情况下,就为有持久性意识的基础结构提供了检查正保留的对象的结构并从那里生成所需 SQL 的机会。 对于 System.Reflection 的基本介绍都已经归入 msdn.microsoft.com/library/f7ykdhsy(v=VS.400) 上的 MSDN 文档和 MSDN 杂志文章“使用反射发现和评估 .NET Framework 中最常见的类型”(msdn.microsoft.com/magazine/cc188926) 与“CLR 完全介绍:反射之反思”(msdn.microsoft.com/magazine/cc163408) 中。 我在这里就不再深入讨论了。

反射允许算法的通用性,允许该算法操纵结构的可变性,同时还能保持封装的外观。 因为反射(在适当配置的安全上下文中)能够访问对象的私有成员,不用强制将内部数据成员变为公共成员,即可对其进行操作。 正可变性,即通过添加内容变化的能力,一如既往地易于使用,因为字段的数目多半与大多数基于反射的代码无关。 但是,负可变性,通过减少内容变化的能力,则似乎不太适用。 毕竟没有字段的类实际上不需要保留,是不是? 而循环通过私有字段的基于反射的基础结构根本不会在循环过程中遇到太多问题,这似乎没有意义。

但是,此处的负可变性与只是没有字段有轻微的不同。 在某些情况下,Person 类会有根本不想保留的内部字段。 或者,更直白地说,Person 类将有一些它要以不同数据格式,而不是以其 CLR 承载的表示方法保留的字段。 Person.Birthdate 要存储为字符串,甚至可能跨三个列(日、月、年)存储,而不是存储在一个列中。 换言之,反射元编程中的负可变性不是关于减少字段的,而是对特定类型的实例做一些不同于标准方法的处理(例如,将字符串保存为 VARCHAR 列是标准做法,但对于一个或多个特定字段,会将字符串保留为 BLOB 列)。

.NET Framework 使用自定义属性来传递这种负可变性。 开发人员使用属性来标记类中的元素,以传递自定义处理的愿望,如对象序列化中的 @NotSerialized。 但要记得,该属性自身并不做什么,它只是寻找该属性的代码的一个标记。 该属性自身也不会提供任何负可变性,而只是在负可变性应当发生时使之更易于识别。

属性还可用于传递正可变性。 例如,假定方法缺少某个属性指示完全没有事务相似性时,.NET Framework 使用属性传递事务处理的方式。

魔镜魔镜告诉我

没有属性,反射元编程会建立一种全新的可变性。 现在,名称 可用于引用程序内的元素(而不是通过编译器符号),并且在时间上比编辑器过去允许的时间晚(运行时)。 例如,前期的 NUnit 单元测试框架,和它在 Java 世界里的兄弟 JUnit 一样,使用反射来发现名称以“test”开头的方法,并假定它们是要作为测试套件的一部分执行的测试方法。

基于名称的方法需要开发人员采用过去一直为人眼保留的元素,即事物名称,需要他们遵循严格的约定,如在 NUnit 方法前加“test”前缀。 自定义属性的使用解放了基于命名的约定(但代价是需要在询问的类中有额外的代码构造),这实际上创建了一个接受机制,开发人员必须接受这个机制,才能获得元编程的好处。

属性还提供了用属性标记任意数据的能力,为元编程行为提供了一种更加精细的参数化。 这通常不可能通过自动元编程实现,尤其是在客户端需要对结构类似的构造执行不同的行为时(如前文示例中的字符串到 BLOB 列,而不是 VARCHAR 列)。

但是,由于其绑定到运行时的特点,反射通常会对广泛使用它的代码产生性能影响。 此外,反射不能为上个月的自动元编程方案中提到的问题提供解决方案,例如类的泛滥。 还有另一种元编程解决方案,不过,我们要到下个月再进行讲解。

祝您工作愉快!

Ted Neward 是 Neward & Associates 的负责人,这是一家专门研究 .NET Framework 企业系统和 Java 平台系统的独立公司。他曾写过 100 多篇文章,是 C# 领域最优秀的专家之一;是 INETA 发言人;并且著作或合著过十几本书,包括《Professional F# 2.0》(Wrox,2010 年)。此外,他还定期提供咨询和指导。您可以通过 ted@tedneward.com 向他提问或咨询,也可以访问他的博客 (blogs.tedneward.com)。

衷心感谢以下技术专家对本文的审阅:Anthony Green