Effective C# 原则42:使用特性进行简单的反射

JerryXia 发表于 , 阅读 (1,425)

当你创建了一个与反射相关的系统时,你应该为你自己的类型,方法,以及属性定义一些自己的特性,这样可以让它们更容易的被访问。自定义的特性标示了你想让这些方法在运行时如何被使用。特性可以测试一些目标对象上的属性。测试这些属性可以最小化因为反射时可能而产生的类型错误。

假设你须要创建一个机制,用于在运行时的软件上添加一个菜单条目到一个命令句柄上。这个须要很简单:放一个程序集到目录里,然后程序可以自己发现关于它的一些新菜单条目以及新的菜单命令。这是利用反射可以完成的最好的工作之一:你的主程序须要与一些还没有编写的程序集进行交互。这个新的插件同样不用描述某个集合的功能,因为这可以很好的用接口来完成编码。

让我们为创建一个框架的插件来开始动手写代码吧。你须要通过Assembly.LoadFrom() 函数来加载一个程序,而且要找到这个可能提供菜单句柄的类型。然后须要创建这个类型的一个实例对象。接着还要找到这个实例对象上可以与菜单命令事件句柄的申明相匹配的方法。完成这些任务之后,你还须要计算在菜单的什么地方添加文字,以及什么文字。

特性让所有的这些任务变得很简单。通过用自己定义的特性来标记不同的类以及事件句柄,你可以很简单的完成这些任务:发现并安装这些潜在的命令句柄。你可以使用特性与反射来协作,最小化一些在原则43中描述的危险事情。

第一个任务就是写代码,发现以及加载插件程序集。假设这个插件在主执行程序所在目录的子目录中。查找和加载这个程序集的代码很简单:

// Find all the assemblies in the Add-ins directory:
string AddInsDir = string.Format( "{0}/Addins",
  Application.StartupPath );
string[] assemblies = Directory.GetFiles( AddInsDir, "*.dll" );
foreach ( string assemblyFile in assemblies )
{
  Assembly asm = Assembly.LoadFrom( assemblyFile );
  // Find and install command handlers from the assembly.
}

接下来,你须要把上面最后一行的注释替换成代码,这些代码要查找那些实现了命令句柄的类并且要安装这些句柄。加载完全程序集之后,你就可以使用反射来查找程序集上所有暴露出来的类型,使用特性来标识出哪些暴露出来的类型包含命令句柄,以及哪些是命令句柄的方法。下面是一个添加了特性的类,即标记了命令句柄类型:

// Define the Command Handler Custom Attribute:
[AttributeUsage( AttributeTargets.Class )]
public class CommandHandlerAttribute : Attribute
{
  public CommandHandlerAttribute( )
  {
  }
}

这个特性就是你须要为每个命令标记的所有代码。总是用AttributeUsage 特性标记一个特性类,这就是告诉其它程序以及编译器,在哪些地方这个特性可以使用。前面这个例子表示CommandHandlerAttribute只能在类上使用,它不能应用在其它语言的元素上。

你可以调用GetCustomAttributes来断定某个类是否具有CommandHandlerAttribute特性。只有具有该特性的类型才是插件的候选类型 :

// Find all the assemblies in the Add-ins directory:
string AddInsDir = string.Format( "{0}/Addins", Application.StartupPath);
string[] assemblies = Directory.GetFiles( AddInsDir, "*.dll" );
foreach ( string assemblyFile in assemblies )
{
  Assembly asm = Assembly.LoadFrom( assemblyFile );
  // Find and install command handlers from the assembly.
  foreach( System.Type t in asm.GetExportedTypes( ))
  {
    if (t.GetCustomAttributes(
      typeof( CommandHandlerAttribute ), false ).Length > 0 )
    {
      // Found the command handler attribute on this type.
      // This type implements a command handler.
      // configure and add it.
    }
    // Else, not a command handler. Skip it.
  }
}

现在,让我们添加另一个新的特性来查找命令句柄。一个类型应该可以很简单的实现好几个命令句柄,所以你可以定义新的特性,让插件的作者可以把它添加到命令句柄上。这个特性会包含一参数,这些参数用于定义新的菜单命令应该放在什么地方。每一个事件句柄处理一个特殊的命令,而这个命令应该在菜单的某个特殊地方。为了标记一个命令句柄,你要定义一个特性,用于标记一个属性,让它成为一个命令句柄,并且申明菜单上的文字以及父菜单文字。DynamicCommand特性要用两个参数来构造:菜单命令文字以及父菜单的文字。这个特性类还包含一个构造函数,这个构造函数用于为菜单初始化两个字符串。这些内容同样可以使用可读可写的属性:

[AttributeUsage( AttributeTargets.Property ) ]
public class DynamicMenuAttribute : System.Attribute
{
  private string _menuText;
  private string _parentText;

  public DynamicMenuAttribute( string CommandText,
    string ParentText )
  {
    _menuText = CommandText;
    _parentText = ParentText;
  }

  public string MenuText
  {
    get { return _menuText; }
    set { _menuText = value; }
  }

  public string ParentText
  {
    get { return _parentText; }
    set { _parentText = value; }
  }
}

这个特性类已经做了标记,这样它只能被应用到属性上。而命令句柄必须在类中以属性暴露出来,用于提供给命令句柄来访问。使用这一技术,可以让程序在启动的时候查找和添加命令句柄的代码变得很简单。

现在你创建了这一类型的一个对象:查找命令句柄,以及添加它们到新的菜单项中。你可以把特性和反射组合起来使用,用于查找和使用命令句柄属性,对对象进行推测:

// Expanded from the first code sample:
// Find the types in the assembly
foreach( Type t in asm.GetExportedTypes( ) )
{
  if (t.GetCustomAttributes(
    typeof( CommandHandlerAttribute ), false).Length > 0 )
  {
    // Found a command handler type:
    ConstructorInfo ci =
      t.GetConstructor( new Type[0] );
    if ( ci == null ) // No default ctor
      continue;
    object obj = ci.Invoke( null );
    PropertyInfo [] pi = t.GetProperties( );

    // Find the properties that are command
    // handlers
    foreach( PropertyInfo p in pi )
    {
      string menuTxt = "";
      string parentTxt = "";
      object [] attrs = p.GetCustomAttributes(
        typeof ( DynamicMenuAttribute ), false );
      foreach ( Attribute at in attrs )
      {
        DynamicMenuAttribute dym = at as 
          DynamicMenuAttribute;
        if ( dym != null )
        {
          // This is a command handler.
          menuTxt = dym.MenuText;
          parentTxt = dym.ParentText;
          MethodInfo mi = p.GetGetMethod();
          EventHandler h = mi.Invoke( obj, null )
            as EventHandler;
          UpdateMenu( parentTxt, menuTxt, h );
        }
      }
    }
  }
}

private void UpdateMenu( string parentTxt, string txt,
  EventHandler cmdHandler )
{
  MenuItem menuItemDynamic = new MenuItem();
  menuItemDynamic.Index = 0;
  menuItemDynamic.Text = txt;
  menuItemDynamic.Click += cmdHandler;

  //Find the parent menu item.
  foreach ( MenuItem parent in mainMenu.MenuItems )
  {
    if ( parent.Text == parentTxt )
    {
      parent.MenuItems.Add( menuItemDynamic );
      return;
    }
  }
  // Existing parent not found:
  MenuItem newDropDown = new MenuItem();
  newDropDown.Text = parentTxt;
  mainMenu.MenuItems.Add( newDropDown );
  newDropDown.MenuItems.Add( menuItemDynamic );
}

现在你将要创建一个命令句柄的示例。首先,你要用CommandHandler 特性标记类型,正如你所看到的,我们习惯性的在附加特性到项目上时,在名字上省略Attribute:

Now you'll build a sample command handler. First, you tag the type with the CommandHandler attribute. As you see here, it is customary to omit Attribute from the name when attaching an attribute to an item:

[CommandHandler]
public class CmdHandler
{
  // Implementation coming soon.
}

在CmdHandler 类里面,你要添加一个属性来取回命令句柄。这个属性应该用DynamicMenu 特性来标记:

[DynamicMenu( "Test Command", "Parent Menu" )]
public EventHandler CmdFunc
{
  get
  {
    if ( theCmdHandler == null )
      theCmdHandler = new System.EventHandler
        (this.DynamicCommandHandler);
    return theCmdHandler;
  }
}

private void DynamicCommandHandler(
  object sender, EventArgs args )
{
  // Contents elided.
}

就是这了。这个例子演示了你应该如何使用特性来简化使用反射的程序设计习惯。你可以用一个特性来标记每个类型,让它提供一个动态的命令句柄。当你动态的载入这个程序集时,可以更简单的发现这个菜单命令句柄。通过应用AttributeTargets (另一个特性),你可以限制动态命令句柄应用在什么地方。这让从一个动态加载的程序集上查找类型的困难任务变得很简单:你确定从很大程度上减少了使用错误类型的可能。这还不是简单的代码,但比起不用特性,还算是不错的。

特性可以申明运行的意图。通过使用特性来标记一个元素,可以在运行时指示它的用处以及简化查找这个元素的工作。如何没有特性,你须要定义一些命名转化,用于在运行时来查找类型以及元素。任何命名转化都会是发生错误的起源。通过使用特性来标记你的意图,就把大量的责任从开发者身上移到了编译器身上。特性可以是只能放置在某一特定语言元素上的,特性同样也是可以加载语法和语义信息的。

你可以使用反射来创建动态的代码,这些代码可以在实际运行中进行配置。设计和实现特性类,可以强制开发者为申明一些类型,方法,以及属性,这些都是可以被动态使用的,而且减少潜在的运行时错误。也就是说,让你增加了创建让用户满足的应用程序的机会。

添加新评论