使用动态是不是被认为是一种不好的做法?

Posted

技术标签:

【中文标题】使用动态是不是被认为是一种不好的做法?【英文标题】:Is the use of dynamic considered a bad practice?使用动态是否被认为是一种不好的做法? 【发布时间】:2015-10-29 19:03:09 【问题描述】:

在 C# 中,有人可以这样做:

MyClass myInstance        = new MyClass();
dynamic mydynamicInstance = myInstance;

然后,调用一个方法,例如:

//This method takes a MyClass argument and does something.
Caller.InvokeMethod(myDynamicInstance);

现在,这将导致在运行时确定 myInstance 类型, 如果有效,则Caller.InvokeMethod会被正常调用。

现在,我的问题是使用dynamic 是否被认为是一种不好的做法,尤其是在以下情况下:

1) InvokeMethod 实例化另一个 myDynamicInstance 类型的实例, 在里面使用反射。

2) 有一个抽象基类MyBaseClass 和它的许多子类,包括MyBaseClass。如果我们对所有这些派生类都有许多InvokeMethod 的重载方法,我们是否可以使用它来允许在运行时确定类型,然后通过方法重载(或方法调用的后期绑定)进行正确调用那个班的)?:

public abstract class MyBaseClass         /*...*/
public class MyClass        : MyBaseClass /*...*/
public class MyAnotherClass : MyBaseClass /*...*/

MyBaseClass myBaseClassRef = new MyClass();
dynamic myDynamicInstance  = myBaseClassRef;

Caller.InvokeMethod(myDynamicInstance);

【问题讨论】:

只有在替代方案是反射时才使用动态。 对我来说主观: - 这是我无法应对类型系统及其局限性的症状。 即便如此,有时反射也是可以的——你会得到一些类型保证等等。 【参考方案1】:

简短的回答是肯定的,使用动态是一种不好的做法。

为什么?

dynamic 关键字是指类型后期绑定,这意味着系统只会在执行过程中检查类型,而不是 在编译期间。这将意味着用户,而不是程序员要发现潜在的错误。错误可能是 MissingMethodException,但它也可能是对具有不良行为的现有方法的无意调用。 想象一下调用一个以计算错误价格或计算错误氧气水平而告终的方法。

一般来说,类型检查有助于获得确定性计算,因此,您应该尽可能使用它。这是shortcomings of dynamic的问题。

但是,动态可能很有用...

Interop with COM like with Office 与动态类型是语言一部分的语言交互(IronPython、IronRuby),因为引入了动态以帮助将它们移植到 .Net。 可以用简洁、优雅的代码替换反射复杂代码(但是,根据具体情况,您仍然应该对这两种方法进行分析,以检查哪一种在性能和编译时检查方面最合适)。

代码库在整个应用程序生命周期中不断发展,即使现在动态看起来还不错, 它开创了一个先例,这可能意味着您的团队会增加动态关键字的使用。它可能导致增加 维护成本(如果上述签名发生变化,您可能会为时已晚)。当然,你可以 依靠单元测试、非回归人类测试等等。但是当你必须在与人类学科相关的 质量并由计算机自动检查相关质量,选择后者。它更不容易出错。

在你的情况下......

在您的情况下,您似乎可以使用通用继承方案(下面的第一个和您在问题中提到的那个), 因为 dynamic 不会给您带来任何额外的好处(它只会消耗您更多的处理能力并让您承担 未来潜在错误的风险)。

这取决于您是否可以更改MyClass 层次结构和/或Caller.InvokeMethod 的代码。

让我们列举 动态的不同可能替代方案...

经过编译的类型检查替代动态关键字方法调用:

最常见的是使用 interface virtual call 像这样的 instance.InvokeMethod() 继承调用正确的实现。

public interface IInvoker :  void InvokeMethod(); 
public abstract class MyBaseClass : IInvoker  public abstract void InvokeMethod(); 
public class MyAnotherClass : MyBaseClass  public override void InvokeMethod()  /* Do something */  
public class MyClass : MyBaseClass  public override void InvokeMethod()  /* Do something */  

另一个性能稍差的方法是使用扩展方法

public static class InvokerEx:

    public static void Invoke(this MyAnotherClass c)  /* Do something */  
    public static void Invoke(this MyClass c)  /* Do something */  

如果 MyBaseClass 层次结构中有多个“访问者”,则可以使用 访问者模式

public interface IVisitor 

    void Visit(this MyAnotherClass c);
    void Visit(this MyClass c);


public abstract class MyBaseClass : IInvoker  public abstract void Accept(IVisitor visitor); 
public class MyAnotherClass : MyBaseClass  public override void Accept(IVisitor visitor)  visitor.Visit(this);  
public class MyClass : MyBaseClass  public override void Accept(IVisitor visitor)  visitor.Visit(this);  

其他变体虽然在这里不是很有用(通用方法)但对性能比较很有趣:

public void InvokeMethod<T>(T instance) where T : IInvoker  return instance.InvokeMethod(); 
动态关键字方法调用的动态替代:

如果您需要在编译时调用未知的方法,我在下面添加了您可以使用的不同技术并更新了性能结果:

MethodInfo.CreateDelegate

        _method = typeof (T).GetMethod("InvokeMethod");
        _func = (Func<T, int>)_method.CreateDelegate(typeof(Func<T, int>));

注意:需要强制转换为 Func 以避免调用 DynamicInvoke(因为它通常较慢)。

DynamicMethod 和 ILGenerator.Emit

它实际上是从头开始构建完整的调用,它是最灵活的,但您必须有一些汇编程序背景才能完全理解它。

        _dynamicMethod = new DynamicMethod("InvokeMethod", typeof (int), new []typeof(T), GetType().Module);
        ILGenerator il = _dynamicMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Call, _method);
        il.Emit(OpCodes.Ret);
        _func = (Func<T, int>) _dynamicMethod.CreateDelegate(typeof (Func<T, int>));

Linq 表达式

它类似于 DynamicMethod,但是您不控制生成的 IL。不过,它确实更具可读性。

        _method = typeof (T).GetMethod("InvokeMethod");
        var instanceParameter = Expression.Parameter(typeof (T), "instance");
        var call = Expression.Call(instanceParameter, _method);
        _delegate = Expression.Lambda<Func<T, int>>(call, instanceParameter).Compile();
        _func = (Func<T, int>) _delegate;

MethodInfo.Invoke

最后但并非最不重要的标准已知反射调用。 但是,即使它很容易弄乱,也不要使用它,因为它的性能确实很差(查看基准测试结果)。更喜欢 CreateDelegate,它真的更快。

        _method = typeof (T).GetMethod("InvokeMethod");
        return (int)_method.Invoke(instance, _emptyParameters);

基准测试的代码可以在GitHub找到。

不同方法的基准测试以获得数量级(对于 1000 万次调用)(.NET Framework 4.5)

For Class standard call:
Elapsed: 00:00:00.0532945
Call/ms: 188679
For MethodInfo.CreateDelegate call:
Elapsed: 00:00:00.1131495
Call/ms: 88495
For Keyword dynamic call:
Elapsed: 00:00:00.3805229
Call/ms: 26315
For DynamicMethod.Emit call:
Elapsed: 00:00:00.1152792
Call/ms: 86956
For Linq Expression call:
Elapsed: 00:00:00.3158967
Call/ms: 31746
For Extension Method call:
Elapsed: 00:00:00.0637817
Call/ms: 158730
For Generic Method call:
Elapsed: 00:00:00.0772658
Call/ms: 129870
For Interface virtual call:
Elapsed: 00:00:00.0778103
Call/ms: 129870
For MethodInfo Invoke call:
Elapsed: 00:00:05.3104416
Call/ms: 1883
For Visitor Accept/Visit call:
Elapsed: 00:00:00.1384779
Call/ms: 72463
 == SUMMARY ==
Class standard call: 1
Extension Method call : 1,19
Generic Method call : 1,45
Interface virtual call : 1,45
MethodInfo.CreateDelegate call : 2,13
DynamicMethod.Emit call : 2,17
Visitor Accept/Visit call : 2,60
Linq Expression call : 5,94
Keyword dynamic call : 7,17
MethodInfo Invoke call : 100,19

编辑:

因此,与访问者模式相比,动态调度仅慢 3 倍。某些应用程序可以接受它,因为它可以删除繁琐的代码。选择始终由您决定。 请记住所有缺点。


编辑:(作为对多次调度好处的回答)

使用像'multiple dispatch'这样的流行模式名称,只是说它更干净,因为它使用更少的代码,恕我直言,它并没有带来额外的好处。 如果您想编写时髦的代码或不关心类型安全和生产稳定性,那么已经有很多语言提供全功能动态类型。我认为 C# 中的 dynamic 关键字介绍是一种缩小强类型语言家族与其他非强类型语言之间差距的方法。这并不意味着您应该改变开发方式并将类型检查丢弃。

更新:2016/11/08 (.NET Framework 4.6.1)

数量级保持不变(即使其中一些有所改进):

Class standard call: 1
Extension Method call : 1,19
Interface virtual call : 1,46
Generic Method call : 1,54
DynamicMethod.Emit call : 2,07
MethodInfo.CreateDelegate call : 2,13
Visitor Accept/Visit call : 2,64
Linq Expression call : 5,55
Keyword dynamic call : 6,70
MethodInfo Invoke call : 102,96

【讨论】:

接受动态的另一种情况:在编译时从未知来源接收数据,例如只有在运行时才知道列时的 SQL 行(或 JSON)。替代实现是 Dictionary。两者都对“未知列”敏感。完全同意,在少数情况下是不好的做法 @JacekCz 您总是期望 SQL(或 Json)有一些行为,因此可以通过对它们进行原型设计来管理它。我是 EntityFramework/Nhibernate 框架的忠实粉丝,但我知道它可能是杀死蜜蜂的坦克…… @FabienBondi 您建议对层次结构调用进行方法虚拟调用,但这实际上意味着方法调用也将在运行时确定,通过后期绑定 - 在内部使用 vtables 等。在这种情况下似乎没有什么好处,因为两者都将在运行时确定,只是想知道..... @NickL。方法虚拟调用绑定是在运行时完成的,但类型检查是在编译时完成的。您不能通过标准虚拟调用调用不存在的方法,但是使用动态(无类型检查),您可以。因此,使用动态会增加引发异常的风险。 C# 中的扩展方法是静态方法,是 IL“内联”编译器技巧的一部分。调用,当然取决于所讨论的功能,不会比这更快。但是,它确实需要在调用堆栈上分配一个额外的变量,而不是调用成员方法。 @Fab:顺便说一句,答案很好!【参考方案2】:

我不完全同意 Fabien 的观点,即它不会给您带来额外的好处。 他使用访问者模式解决的问题称为多次调度,动态也可以为此提供干净的解决方案。当然你必须知道 Fabien 提到的性能、静态类型检查等含义......

public abstract class MyBaseClass



public class MyClass : MyBaseClass


public class MyAnotherClass : MyBaseClass



public class ClassThatIsUsingBaseClass

    public static void PrintName(MyBaseClass baseClass)
    
        Console.WriteLine("MyBaseClass");
    

    public static void PrintName(MyClass baseClass)
    
        Console.WriteLine("MyClass");
    

    public static void PrintName(MyAnotherClass baseClass)
    
        Console.WriteLine("MyAnotherClass");
    

    public static void PrintNameMultiDispatch(MyBaseClass baseClass)
    
        ClassThatIsUsingBaseClass.PrintName((dynamic)baseClass);
    

而用法是

static void Main(string[] args)

    MyBaseClass myClass = new MyClass();
    MyBaseClass myAnotherClass = new MyAnotherClass();
    ClassThatIsUsingBaseClass.PrintName(myClass);
    ClassThatIsUsingBaseClass.PrintName(myAnotherClass);
    ClassThatIsUsingBaseClass.PrintNameMultiDispatch(myClass);
    ClassThatIsUsingBaseClass.PrintNameMultiDispatch(myAnotherClass);

    Console.ReadLine();

输出是

MyBaseClass
MyBaseClass
MyClass
MyAnotherClass

搜索“Multiple dispatch”和“C# multiple dispatch”了解更多信息。

【讨论】:

【参考方案3】:

这在 2015 年得到了回答,今天在 2019 年,我们在 javascript 和 Typescript 中看到的模式确实有充分的理由使用动态;但是,这需要开发者小心。

    private (Boolean Valid, dynamic Result) ValidateID(int ID)
    
        var person = _store.Persons.FirstOrDefault(person => person.ID == ID);
        if (person == null)
        
            string message = $"The person id ID does not exist, please try again.";
            return (false, message);
        
        return (true, person);

    

使用上面的代码:

   var operation = ValidatePersonID(personID);
   if (operation.Valid == false)
     
        //BadRequest takes a string and C# uses co-variance to make it work.
        return BadRequest(operation.Result);
     
   //otherwise just cast the type, but requires the cast to always work.
   var pe = (Person)operation.Result;
   ...

有效性的返回是 True 还是 False 决定了返回的类型。我们仍然使用强制类型转换和/或 BadRequest 上所需的输入类型来选择强类型。我们还选择了编译时类型检查,如果两个返回的类型之一更改为字符串和/或人以外的其他内容,它将无法编译。

我认为这是依赖注入的一种形式,我们可以根据注入的内容实现不同的行为。 C# 纯粹主义者不喜欢这个想法,但 Typescript 人一直都在这样做。

【讨论】:

以上是关于使用动态是不是被认为是一种不好的做法?的主要内容,如果未能解决你的问题,请参考以下文章

为啥单身人士被认为是一种不好的做法? [复制]

Java为参数分配新值,这被认为是一种不好的做法吗?

打开和关闭 PHP 是不是被认为是不好的做法? [关闭]

JavaScript:动态扩展原型是一种不好的做法吗?

在 Jade 中使用 HTML 是不是被认为是不好的做法?

使用仅在“小型设备”上显示的隐藏元素是不是被认为是不好的做法?