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

JerryXia 发表于 , 阅读 (811)

创建二进制的组件时,同时也意味着你要使用迟后绑定和反射来查找你所须要的具有特殊功能代码。反射是一个很有力的工具,而且它让你可以写出可动态配置的软件。使用反射,一个应用程序可以通过添加新的组件来更新功能,而这些组件是在软件最开始发布时没有的。这是有利的。

这一伸缩性也带来了一些复杂的问题,而且复杂问题的增加又会增加出现其它问题的可能。当你使用反射时,你是围绕着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控件的数据绑定。然而,很多常规情况下很少用,而是使用类厂,委托,以及接口来创建代码,这可以产生出更容易维护的系统。

添加新评论