Effective C# 原則43:請勿濫用反射(譯)

來源:互聯網
上載者:User

Effective C# 原則43:請勿濫用反射
Item 43: Don't Overuse Reflection

建立二進位的組件時,同時也意味著你要使用遲後綁定和反射來尋找你所須要的具有特殊功能代碼。反射是一個很有力的工具,而且它讓你可以寫出可動態配置的軟體。使用反射,一個應用程式可以通過添加新的組件來更新功能,而這些組件是在軟體最開始發布時沒有的。這是有利的。

這一伸縮性也帶來了一些複雜的問題,而且複雜問題的增加又會增加出現其它問題的可能。當你使用反射時,你是圍繞著C#的安全類型。然而,成員調用的參數和傳回值是以System.Object類型存在的。你必須在運行時確保這些類型是正確的。簡單的說,使用反射可以讓建立動態程式變得很容易,但同時也讓程式出現錯誤變得很容易。通常,簡單的思考一下,你就可以通過建立一系列介面集合來最小化或者移除反射,而這些介面集合應該表達你對類型的假設。

反射給了你建立類型執行個體的功能,以及在對象上調用成員方法,以及訪問對象上的成員資料。這聽上去就跟每天的編程任務是一樣的。確實是這樣的,對於反射,並沒有什麼新奇的:它就是動態建立其它的二進位組件。大多數情況下,你並不須要像反射這樣的伸縮功能,因為有其它可選的更易維護的方案。

讓我們從建立一個給定類型的執行個體開始,你可以經常使用一個類廠來完成同樣的任務。考慮下面的代碼,它通過使用反射,調用預設的建構函式建立了一個MyType 的執行個體:

// Usage:Create a new object using reflection:
Type t = typeof( MyType );
MyType obj = NewInstance( t ) as MyType;

// Example factory function, based on Reflection:
object NewInstance( Type t )
{
  // Find the default constructor:
  ConstructorInfo ci = t.GetConstructor( new Type[ 0 ] );
  if ( ci != null )
    // Invoke default constructor, and return
    // the new object.
    return ci.Invoke( null );

  // If it failed, return null.
  return null;
}

代碼通過反射檢測了類型,而且調用了預設的建構函式來建立了一個對象。如果你須要在運行時建立一個預先不知道任何資訊的類型執行個體,這是唯一的選擇。這是一段脆弱的代碼,它依懶於預設的建構函式的存在。而且在你移除了MyType類型的預設建構函式時仍然是可以通過編譯的。你必須在運行時完成檢測,而且捕獲任何可能出現的異常。一個完成同樣功能的類廠函數,在建構函式被移除時是不能通過編譯的:

public MyType NewInstance( )
{
  return new MyType();
}

(譯註:其實VS.Net會給我們添加預設的建構函式,所以上面的兩個方法都是可以編譯,而且可以正確啟動並執行。本人做過測試。但如果給建構函式添加訪問限制,那麼可以讓類廠無法構造對象而產生編譯時間錯誤。)

你應該使用靜態類廠函數來取代依懶於反射的執行個體建立方法。如果你須要執行個體對象使用遲後資料繫結,那麼應該使用類廠函數,而且使用相關的特性來標記它們(參見原則42)。

另一個反射的潛在的用處就是訪問類型的成員。你可以使用成員名和類型在運行時來調用實際的函數:

// Example usage:
Dispatcher.InvokeMethod( AnObject, "MyHelperFunc" );

// Dispatcher Invoke Method:
public void InvokeMethod ( object o, string name )
{
  // Find the member functions with that name.
  MemberInfo[] myMembers = o.GetType( ).GetMember( name );
  foreach( MethodInfo m in myMembers )
  {
    // Make sure the parameter list matches:
    if ( m.GetParameters( ).Length == 0 )
      // Invoke:
     m.Invoke( o, null );
  }
}

在上面的代碼中,進行是錯誤被屏避。如果類型名字打錯了,這個方法就找不到。就沒有方法被調用。

這還只是一個簡單的例子。要建立一個靈活的InvokeMethod版本,須要從GetParameters()方法上返回的參數列表中,檢測所有出現的參數類型。這樣的代碼是很沉長的,而且很糟糕以至於我根本就不想浪費地方來示範。

反射的第三個用處就是訪問資料成員。代碼和訪問成員函數的很類似:

// Example usage:
object field = Dispatcher.RetrieveField ( AnObject, "MyField" );

// elsewhere in the dispatcher class:
public object RetrieveField ( object o, string name )
{
  // Find the field.
  FieldInfo myField = o.GetType( ).GetField( name );
  if ( myField != null )
    return myField.GetValue( o );
  else
    return null;
}

和方法調用一樣,使用反射來取回一個資料成員,要在一個欄位上通過名字來調用類型查詢,看它是否與請求的欄位名相匹配。如果發現一個,就可以使用FieldInfo 結構來傳回值。這個構造在.Net架構裡是很常見的。資料繫結就是利用反射來尋找這些標記了綁定操作的屬性。在這種情況下,資料繫結的動態性質超過了它的開銷。(譯註:也就是說值得使用反射進行動態綁定。)

因此,如果反射是一個如此痛苦的事情,你就須要找一個更好更簡單的可選方案。你有三個選擇:首先就是使用介面。你可以為任何你所期望的類,結構來定義介面(參見原則19)。這可能會使用更清楚的代碼來取代所有的反射代碼:

IMyInterface foo = obj as IMyInterface;
if ( foo != null)
{
  foo.DoWork( );
  foo.Msg = "work is done.";
}

如果你用標記了特性的類廠函數來合并介面,幾乎所有的你所期望於反射的解決方案都變得更簡單:

public class MyType : IMyInterface
{
  [FactoryFunction]
  public static IMyInterface
    CreateInstance( )
  {
    return new MyType( );
  }

  #region IMyInterface
  public string Msg
  {
    get
    {
      return _msg;
    }
    set
    {
      _msg = value;
    }
  }
  public void DoWork( )
  {
    // details elided.
  }
  #endregion
}

把這段代碼與前面的基於反射的方案進行對比。即使這隻是簡單的例子,但還有在某些弱類型上使用所有的反射API時有精彩之處:傳回型別已經是類型化的對象。而在反射上,如果你想取得正確的類型,你須要強制轉換。這一操作可能失敗,而且在繼承上有危險。而在使用介面時,編譯器提供的強型別檢測顯得更清楚而且更易維護。
反射應該只在某些調用目標不能清楚的用介面表示時才使用。.Net的資料繫結是在類型的任何公用屬性上可以工作,把它限制到定義的介面上可能會很大程度上限制它的使用。菜單控制代碼的例子充許任何函數(不管是執行個體的還是靜態)來實現命令控制代碼,使用一個介面同樣會限制這些功能只能是執行個體方法。FxCop 和NUnit (參見原則48)都擴充了反射的使用,它們使用反射,是因為它們遇到的的現實的問題是最好用它來處理的。FxCopy 檢測所有的代碼來評估它們是否與已經的原則矛盾。這須要使用反射。NUnit 必須調用你編譯的測試代碼。它使用反射來斷定哪些你已經寫的代碼要進行單元測試。對於你可能要寫的測試代碼,可能是一個方法集合,但介面是不能表達它們的。NUnit使用特性來發現測試以及測試案例來讓它的工作更簡單(參見原則42)。

當你可以使用介面策划出你所期望調用的方法和屬性時,你就可以擁有一個更清楚,更容易維護的系統。反射是一個在資料以後綁定上功能強大的工具。.Net架構使用它實現對Windows控制項和Web控制項的資料繫結。然而,很多常規情況下很少用,而是使用類廠,委託,以及介面來建立代碼,這可以產生出更容易維護的系統。

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

Item 43: Don't Overuse Reflection
Building binary components sometimes means utilizing late binding and reflection to find the code with the particular functionality you need. Reflection is a powerful tool, and it enables you to write software that is much more dynamic. Using reflection, an application can be upgraded with new capabilities by adding new components that were not available when the application was deployed. That's the upside.

With this flexibility comes increased complexity, and with increased complexity comes increased chance for many problems. When you use reflection, you circumvent C#'s type safety. Instead, the Invoke members use parameters and return values typed as System.Object. You must make sure the proper types are used at runtime. In short, using reflection makes it much easier to build dynamic programs, but it is also much easier to build broken programs. Often, with a little thought, you can minimize or remove the need for reflection by creating a set of interface definitions that express your assumptions about a type.

Reflection gives you the capability to create instances of objects, invoke members on those objects, and access data members in those objects. Those sound like normal everyday programming tasks. They are. There is nothing magic about reflection: It is a means of dynamically interacting with other binary components. In most cases, you don't need the flexibility of reflection because other alternatives are more maintainable.

Let's begin with creating instances of a given type. You can often accomplish the same result using a class factory. Consider this code fragment, which creates an instance of MyType by calling the default constructor using reflection:

// Usage:Create a new object using reflection:
Type t = typeof( MyType );
MyType obj = NewInstance( t ) as MyType;

// Example factory function, based on Reflection:
object NewInstance( Type t )
{
  // Find the default constructor:
  ConstructorInfo ci = t.GetConstructor( new Type[ 0 ] );
  if ( ci != null )
    // Invoke default constructor, and return
    // the new object.
    return ci.Invoke( null );

  // If it failed, return null.
  return null;
}

 

The code examines the type using reflection and invokes the default constructor to create the object. If you need to create a type at runtime without any previous knowledge of the type, this is the only option. This is brittle code that relies on the presence of a default constructor. It still compiles if you remove the default constructor from MyType. You must perform runtime testing to catch any problems that arise. A class factory function that performed the same operations would not compile if the default constructor was removed:

public MyType NewInstance( )
{
  return new MyType();
}

 

You should create static factory functions instead of relying on reflection to instantiate objects. If you need to instantiate objects using late binding, create factory functions and tag them as such with attributes (see Item 42).

Another potential use of reflection is to access members of a type. You can use the member name and the type to call a particular function at runtime:

// Example usage:
Dispatcher.InvokeMethod( AnObject, "MyHelperFunc" );

// Dispatcher Invoke Method:
public void InvokeMethod ( object o, string name )
{
  // Find the member functions with that name.
  MemberInfo[] myMembers = o.GetType( ).GetMember( name );
  foreach( MethodInfo m in myMembers )
  {
    // Make sure the parameter list matches:
    if ( m.GetParameters( ).Length == 0 )
      // Invoke:
     m.Invoke( o, null );
  }
}

 

Runtime errors are lurking in the previous code. If the name is typed wrong, the method won't be found. No method will be called.

It's also a simple example. Creating a more robust version of InvokeMethod would need to check the types of all proposed parameters against the list of all parameters returned by the GetParameters() method. That code is lengthy enough and ugly enough that I did not even want to waste the space to show it to you. It's that bad.

The third use of reflection is accessing data members. The code is similar to accessing member functions:

// Example usage:
object field = Dispatcher.RetrieveField ( AnObject, "MyField" );

// elsewhere in the dispatcher class:
public object RetrieveField ( object o, string name )
{
  // Find the field.
  FieldInfo myField = o.GetType( ).GetField( name );
  if ( myField != null )
    return myField.GetValue( o );
  else
    return null;
}

 

As with the method invocation, using reflection to retrieve a data member involves querying the type for a field with a name that matches the requested field. If one is found, the value can be retrieved using the FieldInfo structure. This construct is rather common in the framework. DataBinding makes use of reflection to find the properties that are the targets of binding operation. In those cases, the dynamic nature of data binding outweighs the possible costs.

So, if reflection is such a painful process, you need to look for better and simpler alternatives. You have three options. The first is interfaces. You can define interfaces for any contract that you expect classes or structs to implement (see Item 19). That would replace all the reflection code with a few far clearer lines of code:

IMyInterface foo = obj as IMyInterface;
if ( foo != null)
{
  foo.DoWork( );
  foo.Msg = "work is done.";
}

 

If you combine interfaces with a factory function tagged with an attribute, almost any system you thought deserved a solution based on reflection gets much more simple:

public class MyType : IMyInterface
{
  [FactoryFunction]
  public static IMyInterface
    CreateInstance( )
  {
    return new MyType( );
  }

  #region IMyInterface
  public string Msg
  {
    get
    {
      return _msg;
    }
    set
    {
      _msg = value;
    }
  }
  public void DoWork( )
  {
    // details elided.
  }
  #endregion
}

 

Contrast this code with the reflection-based solution shown earlier. Even these simple examples have glossed over some of the weakly typed issues common to all the reflection APIs: The return values are all typed as objects. If you want to get the proper type, you need to cast or convert the type. Those operations could fail and are inherently dangerous. The strong type checking that the compiler provides when you create interfaces is much clearer and more maintainable.

Reflection should be used only when the invocation target can't be cleanly expressed using an interface. .NET data binding works with any public property of a type. Limiting it to an interface definition would greatly limit its reach. The menu handler sample allows any function (either instance or static) to implement the command handler. Using an interface would limit that functionality to instance methods only. Both FxCop and NUnit (see Item 48) make extensive use of reflection. They use reflection because the nature of the problems they address are best handled using it. FxCopy examines all your code to evaluate it against a set of known rules. That requires reflection. NUnit must call test code you've written. It uses reflection to determine what code you've written to unit test your code. An interface cannot express the full set of methods used to test any code you might write. NUnit does use attributes to find tests and test cases to make its job easier (see Item 42).

When you can factor out the methods or properties that you intend to invoke using interfaces, you'll have a cleaner, more maintainable system. Reflection is a powerful late-binding mechanism. The .NET Framework uses it to implement data binding for both Windows- and web-based controls. However, in many less general uses, creating code using class factories, delegates, and interfaces will produce more maintainable systems.

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.