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

Effective C# 原则42:使用特性进行简单的反射
Item 42: Utilize Attributes to Simplify Reflection

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

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

让我们为创建一个框架的插件来开始动手写代码吧。你须要通过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 (另一个特性),你可以限制动态命令句柄应用在什么地方。这让从一个动态加载的程序集上查找类型的困难任务变得很简单:你确定从很大程度上减少了使用错误类型的可能。这还不是简单的代码,但比起不用特性,还算是不错的。

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

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

============================
    

Item 42: Utilize Attributes to Simplify Reflection
When you build systems that rely on reflection, you should define custom attributes for the types, methods, and properties you intend to use to make them easier to access. The custom attributes indicate how you intended the method to be used at runtime. Attributes can test some of the properties of the target. Testing these properties minimizes the likelihood of mistyping that can happen with reflection.

Suppose you need to build a mechanism to add menu items and command handlers to a running software system. The requirements are simple: Drop an assembly into a directory, and the program will find out about it and add new menu items for the new command. This is one of those jobs that is best handled with reflection: Your main program needs to interact with assemblies that have not yet been written. The new add-ins also don't represent a set of functionality that can be easily encoded in an interface.

Let's begin with the code you need to create the add-in framework. You need to load an assembly using the Assembly.LoadFrom() function. You need to find the types that might provide menu handlers. You need to create an object of the proper type. Type.GetConstructor() and ConstructorInfo.Invoke() are the tools for that. You need to find a method that matches the menu command event handler signature. After all those tasks, you need to figure out where on the menu to add the new text, and what the text should be.

Attributes make many of these tasks easier. By tagging different classes and event handlers with custom attributes, you greatly simplify your task of finding and installing those potential command handlers. You use attributes in conjunction with reflection to minimize the risks described in Item 43.

The first task is to write the code that finds and loads the add-in assemblies. Assume that the add-ins are in a subdirectory under the main executable directory. The code to find and load the assemblies is simple:

// 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.
}

Next, you need to replace that last comment with the code that finds the classes that implement command handlers and installs the handlers. After you load an assembly, you can use reflection to find all the exported types in an assembly. Use attributes to figure out which exported types contain command handlers and which methods are the command handlers. An attribute class marks the types that have command handlers:

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

This attribute is all the code you need to write to mark each command. Always mark an attribute class with the AttributeUsage attribute; it tells other programmers and the compiler where your attribute can be used. The previous example states that the CommandHandlerAttribute can be applied only to classes; it cannot be applied on any other language element.

You call GetCustomAttributes to determine whether a type has the CommandHandlerAttribute. Only those types are candidates for add-ins:

// 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.
  }
}

Now let's add another new attribute to find command handlers. A type might easily implement several command handlers, so you define a new attribute that add-in authors will attach to each command handler. This attribute will include parameters that define where to place menu items for new commands. Each event handler handles one specific command, which is located in a specific spot on the menu. To tag a command handler, you define an attribute that marks a property as a command handler and declares the text for the menu item and the text for the parent menu item. The DynamicCommand attribute is constructed with two parameters: the command text and the text of the parent menu. The attribute class contains a constructor that initializes the two strings for the menu item. Those strings are also available as read/write properties:

[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; }
  }
}

This attribute class is tagged so that it can be applied only to properties. The command handler must be exposed as a property in the class that provides access to the command handler. Using this technique simplifies finding the command handler code and attaching it to the program at startup.

Now you create an object of that type, find the command handlers, and attach them to new menu items. You guessed ityou use a combination of attributes and reflection to find and use the command handler properties:

// 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 );
}

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.
}

Inside the CmdHandler class, you add a property to retrieve the command handler. That property should be tagged with the DynamicMenu attribute:

[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.
}

That's it. This example shows you how you can utilize attributes to simplify programming idioms that use reflection. You tagged each type that provided a dynamic command handler with an attribute. That made it easier to find the command handlers when you dynamically loaded the assembly. By applying AttributeTargets (another attribute), you limit where the dynamic command attribute can be applied. This simplifies the difficult task of finding the sought types in a dynamically loaded assembly: You greatly decrease the chance of using the wrong types. It's still not simple code, but it is a little more palatable than without attributes.

Attributes declare your runtime intent. Tagging an element with an attribute indicates its use and simplifies the task of finding that element at runtime. Without attributes, you need to define some naming convention to find the types and the elements that will be used at runtime. Any naming convention is a source of human error. Tagging your intent with attributes shifts more responsibilities from the developer to the compiler. The attributes can be placed only on a certain kind of language element. The attributes also carry syntactic and semantic information.

You use reflection to create dynamic code that can be reconfigured in the field. Designing and implementing attribute classes to force developers to declare the types, methods, and properties that can be used dynamically decreases the potential for runtime errors. That increases your chances of creating applications that will satisfy your users.
 
   

原文地址:https://www.cnblogs.com/WuCountry/p/696356.html