Effective C# 利用特性簡化反射

當你創建了一個與反射相關的系統時,你應該爲你自己的類型,方法,以及屬性定義一些自己的特性,這樣可以讓它們更容易的被訪問。自定義的特性標示了你想讓這些方法在運行時如何被使用。特性可以測試一些目標對象上的屬性。測試這些屬性可以最小化因爲反射時可能而產生的類型錯誤。

        假設你須要創建一個機制,用於在運行時的軟件上添加一個菜單條目到一個命令句柄上。這個須要很簡單:放一個程序集到目錄裏,然後程序可以自己發現關於它的一些新菜單條目以及新的菜單命令。這是利用反射可以完成的最好的工作之一:你的主程序須要與一些還沒有編寫的程序集進行交互。這個新的插件同樣不用描述某個集合的功能,因爲這可以很好的用接口來完成編碼。

        讓我們爲創建一個框架的插件來開始動手寫代碼吧。你須要通過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 (另一個特性),你可以限制動態命令句柄應用在什麼地方。這讓從一個動態加載的程序集上查找類型的困難任務變得很簡單:你確定從很大程度上減少了使用錯誤類型的可能。這還不是簡單的代碼,但比起不用特性,還算是不錯的。

        特性可以申明運行的意圖。通過使用特性來標記一個元素,可以在運行時指示它的用處以及簡化查找這個元素的工作。如何沒有特性,你須要定義一些命名轉化,用於在運行時來查找類型以及元素。任何命名轉化都會是發生錯誤的起源。通過使用特性來標記你的意圖,就把大量的責任從開發者身上移到了編譯器身上。特性可以是隻能放置在某一特定語言元素上的,特性同樣也是可以加載語法和語義信息的。

        我們通常使用反射來創建可以被重新配置的動態代碼。通過設計和實現特性類,可以強制開發者用它們來生命申明一些可以被動態使用的類型,方法,以及屬性,可以減少應用程序的運行時錯誤提高軟件的用戶滿意度。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章