【问题标题】:Why not use `dynamic` instead of reflection when the property is known?当属性已知时,为什么不使用“动态”而不是反射?
【发布时间】:2018-01-05 07:30:25
【问题描述】:

这个问题类似于this one,但假设我们在编译时知道成员名称。


假设我们有一个类

public class MyClass
{
    public string TheProperty { get; set; }
}

在另一种方法中,我们想设置该类实例的TheProperty 成员,但在编译时我们不知道实例的类型,我们只知道编译时的属性名称。 所以,在我看来,现在有两种方法可以做到这一点:

object o = new MyClass(); // For simplicity.

o.GetType().GetProperty("TheProperty").SetValue(o, "bar"); // (1)    
((dynamic) o).TheProperty = "bar"; // (2)

我使用 System.Diagnostics.Stopwatch 类测量了这个测试用例,发现 反射花费了 475 个滴答声,而使用 dynamic 的方式花费了 0 个滴答声,因此大约与直接致电new MyClass().TheProperty = "bar" 一样快。


由于我几乎从未见过第二种方式,所以我有点困惑,现在我的问题是:

  • 是不是走错了路?
  • 第二种方式应该优于第一种方式还是相反?我不认为使用第二种方式有任何缺点。如果找不到该属性,(1) 和 (2) 都会抛出异常,不是吗?
  • 为什么第二种方式看似更快,却很少使用?

【问题讨论】:

  • dynamic 的主要目的是用于 LINQ 中的 匿名类型,或者作为一种更简单的方式与 COM 对话,而无需在超空间中处理所有乏味的事情,嗯,我的意思是类型图书馆之类的。所以,否则是的,你可能不会使用太多。另外,dynamic 应该小心使用,因为即使它可以编译,如果找不到该成员(很像反射),您可能会收到运行时错误。当然除了匿名类型的情况
  • 是的,你是对的,我也考虑过运行时错误,但我认为这两种情况(正如你已经说过的)都会导致运行时错误,所以我不认为这是在这种情况下似乎不鼓励使用 dynamic 的唯一原因。
  • 一般是的。 LINQ 中的匿名类型非常棒,就像匿名方法和 lambda 表达式一样
  • 没有办法在你使用它之前检查“TheProperty”是否真的存在,但你可以通过反射。我猜这取决于你的用例。
  • 帮自己一个忙,不要手动滚动微基准;要使您的基准以这种方式具有代表性,您必须考虑很多事情。利用别人的努力:benchmarkdotnet.org

标签: c# dynamic reflection


【解决方案1】:

(...)reflection 耗时 475 tick,而使用 dynamic 的方式耗时 0 tick(...)

这完全是错误的。问题是您不了解dynamic 的工作原理。我将假设您正确设置了基准:

  1. 在发布模式下运行,优化已开启且未使用调试器。
  2. 您在实际测量时间之前对方法进行了测试。

这是你可能没有做的关键部分:

  1. Jit 动态测试不实际执行动态运行时绑定

为什么 3 很重要?因为运行时会缓存动态调用并重用它!因此,在一个简单的基准测试实现中,如果您做对了事情,您将承担初始动态调用 jitting 方法的成本,因此您不会对其进行测量。

运行以下基准测试:

public static void Main(string[] args)
{
    var repetitions = 1;
    var isWarmup = true;
    var foo = new Foo();

    //warmup
    SetPropertyWithDynamic(foo, isWarmup); //JIT method without caching the dynamic call
    SetPropertyWithReflection(foo); //JIT method
    var s = ((dynamic)"Hello").Substring(0, 2); //Start up the runtime compiler

    for (var test = 0; test < 10; test++)
    {
        Console.WriteLine($"Test #{test}");
        var watch = Stopwatch.StartNew();

        for (var i = 0; i < repetitions; i++)
        {
            SetPropertyWithDynamic(foo);
        }

        watch.Stop();
        Console.WriteLine($"Dynamic benchmark: {watch.ElapsedTicks}");

        watch = Stopwatch.StartNew();

        for (var i = 0; i < repetitions; i++)
        {
            SetPropertyWithReflection(foo);
        }

        watch.Stop();
        Console.WriteLine($"Reflection benchmark: {watch.ElapsedTicks}");
    }

    Console.WriteLine(foo);
    Console.ReadLine();
}

static void SetPropertyWithDynamic(object o, bool isWarmup = false)
{
    if (isWarmup)
        return;

    ((dynamic)o).TheProperty = 1;
}

static void SetPropertyWithReflection(object o)
{
    o.GetType().GetProperty("TheProperty").SetValue(o, 1);
}

public class Foo
{
    public int TheProperty { get; set; }
    public override string ToString() => $"Foo: {TheProperty}";
}

发现第一次运行和后续运行之间的区别?

【讨论】:

  • 感谢您的努力。我按照你的方式尝试过,我必须说我很震惊dynamic 与反射相比有多慢 - 它回答了这个问题并解释了为什么我只见过反射而不是dynamic。非常感谢。
  • @ThomasFlinkow 欢迎您。一篇小评论;我已经更新了答案以消除在运行时启动编译器的成本。它在一定程度上改进了动态调用,但它您将在第一次动态调用时产生的成本(您不会在所有后续调用中,无论是否缓存)。
  • @ThomasFlinkow 好吧,不要对dynamic 太苛刻。不要忘记反射比直接做myClass.TheProperty = "Up..up..and away!" 要慢得多。直接调用和说委托或通过接口之间甚至存在差异(尽管很小)。 msdn.microsoft.com/en-us/library/ms973852.aspx :)
  • @InBetween 感谢您指出所有 JIT / 编译器成本,并感谢代码和 cmets。
  • @MickyD 我对dynamic 并没有太苛刻,我只是很惊讶地看到我的“基准”如此错误并显示出完全荒谬的结果......:D
猜你喜欢
  • 1970-01-01
  • 2013-01-16
  • 2013-01-29
  • 2013-01-23
  • 2011-01-11
  • 2011-09-30
  • 2011-03-29
  • 2020-02-07
  • 2012-03-15
相关资源
最近更新 更多