[C#]dynamic+reflection=像public一样调用private类成员

警告:这个方法会有严重的性能下降,请自行斟酌使用

起因

在项目上写UnitTest时,感觉写起来不是很顺手,因为我们使用了Dependency Inject框架,所以代码里面有大量的protected属性,这些是我们在写UT时没有必要测试的依赖项,需要被mock掉,我们的mock框架使用的是Moq。由于是protected的属性,无法直接在UT类里对需要mock的属性赋值,因此我们现在的做法是创建一个testable类继承需要被测试的类,并且暴露出一个带参数的构造,来设置需要被mock的属性值,或者实现一个代理方法,来调用被测试类的privateprotected方法。

被测试类大概长这样:


public class AClass : BaseClass
{
  [Dependency]
  protected object DependencyObject1 { get; set; }
  [Dependency]
  protected object DependencyObject2 { get; set; }
  
  protected override void AMethod()
  {
  ...
      DependencyObject1.Method();
      DependencyObject2.Method();
  ...
  }    
}

Testable类大概长这样:


public class AClassTestable : AClass
{
  public AClassTestable(object do1, object  do2) 
  {
      this.DependencyObject1 = do1;
      this.DependencyObject2 = do2;
  }

  public void CallBaseAMethod()
  {
      AMethod();
  }
}

测试类大概长这样:


[TestFixture]
public class AClassTest
{
  private Mock mockDo1;
  private Mock mockDo2;
  private AClassTestable target;
  [SetUp]
  public void Setup()
  {
      mockDo1 = new Mock();
      mockDo2 = new Mock();
      target = new AClassTestable(mockDo1.Object, mockDo2.Object);
  }

  [Test]
  public void ShouldReturnXX()
  {
      ...
      target.CallBaseAMethod();
      ...
  }
}

由于大量的protect属性与方法存在,因此我们需要建立很多的代理类和代理方法来写UT,这是比较痛苦的,写一个UT的成本很高。

转机

后来我们想到了使用反射,来通过反射设置protectedprivate数据成员,并通过反射直接调用protectedprivate成员方法,这样就不用再写testable方法了,因此我们实现了对object的扩展方法,


public static class ObjectExtension
{
  public static void SetNonPublicDataMember(this object obj, string memberName, object value)
  {...}
  public static void InvokeNonPublicMethod(this object obj, string name, param object[] args)
  {...}
}

所以测试代码看起来像这样:

[TestFixture]
public class AClassTest
{
  private Mock mockDo1;
  private Mock mockDo2;
  private AClass target;
  [SetUp]
  public void Setup()
  {
      mockDo1 = new Mock();
      mockDo2 = new Mock();
      target = new AClass();
      target.SetNonPublicDataMember("DependencyObject1", mockDo1.Object);
      target.SetNonPublicDataMember("DependencyObject2", mockDo2.Object); 
  }

  [Test]
  public void ShouldReturnXX()
  {
      ...
      target.InvokeNonPublicMethod("AMethod", (object[])null);
      ...
  }
}

代码简洁了一些,但是感觉这种调用方式还不是特别的方便,所有的非public成员都需要用反射的方式来设置值和调用。

解决

为了让代码再简洁一些,于是思考有没有一种能够像使用public成员一样点吧点吧就能使用private成员的方法呢?然后我就找到了这个 dynamic + reflection 的方案。

先来看下效果:
dynamic

注意AClass里面都是private的成员变量和方法,但是调用的时候却是使用a.Test的形式,就好像调用public方法一样,是不是很帅气,只要拿这个叫做ObjectWrapper的类包装一下,然后把类型声明为dynamic,就可以愉快的点吧点吧啦。

再来看看ObjectWrapper的实现:
主要原理是用到DynamicObject对象,使被包装对象拥有运行时的动态行为。另外覆盖了查找方法和属性的方法,使其能够绕过public private等访问限定符的限制


public class ObjectWrapper : DynamicObject
{
    object _wrapped; //用于存储被包装的对象

    static BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance
        | BindingFlags.Static | BindingFlags.Public; //查找所有实例或静态的类成员

    public ObjectWrapper(object o)
    {
        _wrapped = o;
    }

    //覆盖原有的调用成员方法,改用反射来查找成员方法
    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
        var types = args.Select(a => a.GetType());

        var method = _wrapped.GetType().GetMethod(binder.Name, flags, null, types.ToArray(), null);

        if (method != null)
        {
            result = method.Invoke(_wrapped, args);
            return true;
        }

        return base.TryInvokeMember(binder, args, out result);
    }

    //覆盖默认的获取成员方法,改用反射来查找成员
    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        //先查找属性
        var prop = _wrapped.GetType().GetProperty(binder.Name, flags);
        if (prop != null)
        {
            result = prop.GetValue(_wrapped);
            return true;
        }

        //如果通过查找属性的方式没有找到,则按照字段来查找
        var fld = _wrapped.GetType().GetField(binder.Name, flags);
        if (fld != null)
        {
            result = fld.GetValue(_wrapped);
            return true;
        }

        return base.TryGetMember(binder, out result);
    }

    //覆盖默认的获取成员方法,改用反射来查找成员
    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        //先查找属性
        var prop = _wrapped.GetType().GetProperty(binder.Name, flags);
        if (prop != null)
        {
            prop.SetValue(_wrapped, value, null);
            return true;
        }
        
        //如果通过查找属性的方式没有找到,则按照字段来查找
        var fld = _wrapped.GetType().GetField(binder.Name, flags);
        if (fld != null)
        {
            fld.SetValue(_wrapped, value);
            return true;
        }

        return base.TrySetMember(binder, value);
    }
}

优点:
使用方便,对源代码修改少,能够像public成员一样的调用形式来调用私有成员

缺点:

  1. 由于使用了dynamic类型声明被测对象,因此无法使用智能提示,所以需要自己注意调用的属性和方法
  2. 没有考虑方法重载的情况,在查找方法的时候没有判断参数类型,因此当有多个重名方法的时候只会调用第一个找到的方法
  3. public成员也走了反射的查找,并且调用了DLR,会有严重的性能问题。

性能测试

创建了4000个文件,每个测试文件3个测试,实现都一样,只是类名不一样,分别对testable和wrapper两种方法进行测试,测试结果如下图:

左边是wrapper的结果,右边是testable的结果,可以看到慢了将近3倍,这个速度实在不可接受,还是谨慎使用,看来方便也是有代价的。今天就到这吧。

测试完整代码:
Github

博主为你专属推荐

参考连接

DynamicObject
C# 4.0 AccessPrivateWrapper – Instantiate and Access Private/Internal Classes and Members via dynamic + reflection

打赏

博主开通了微信公众号,欢迎关注啦

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.