Wu.Country@侠缘

勤学似春起之苗,不见其增,日有所长; 辍学如磨刀之石,不见其损,日所有亏!

导航

Effective C# 原则43:请勿滥用反射(译)

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.

posted on 2007-04-02 19:42  Wu.Country@侠缘  阅读(1378)  评论(3编辑  收藏  举报