Effective C# 原則42:使用特性進行簡單的反射(譯)

來源:互聯網
上載者:User

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.
 
   

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.