欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

创建您自己的.NET DynamicObject 为什么、何时和如何

程序员文章站 2022-05-28 12:48:22
...

目录

扰流板

什么是动态的

让我们创建自己的DynamicObject

包装起来


扰流板

GitHub上提供了该项目的完整源代码repo包含卡片图像的完整生成器,我将其命名为Card Artist。棋盘游戏设计师可以使用它来创建自己的纸牌以进行游戏测试或专业印刷。GitHub存储库还包含该应用程序的二进制文件以及有关如何创建自己的卡片模板的文档。

什么是动态的

Microsoft2010年发布了动态语言运行时(DLR),作为.NET Framework版本4的一部分。DLR允许在.NET CLR之上支持动态语言,例如Python。那时,动态类型也被添加到C#中,从而允许与动态语言进行互操作。

动态类型的变量类似于对象类型的变量:

  • 它不包含有关其引用的对象类型的任何信息。
  • 它可以保存对任何类型的对象的引用。

与对象不同,动态类型的变量可用于调用方法并访问其类型的属性和字段,而无需强制转换:

class Class1
{
  public int ReturnValue(int a, int b) => a + b;
  public int A = 5;
}

var typedReference = new Class1();
var a2 = typedReference.A;
var b2 = typedReference.ReturnValue(1, 2);

//Now with object
object objectReference = new Class1();
var a2 = ((Class1)objectReference).A;
var b2 = ((Class1)objectReference).ReturnValue(1, 2);

//Now with dynamic
dynamic dynamicReference = new Class1();
int a3 = dynamicReference.A;
int b3 = dynamicReference.ReturnValue(1, 2);

//This compiles successfully and fails at runtime
dynamicReference.ThisFunctionDoesntExist();

请注意,我们需要指定类型a3b3,因为编译器没有关于该字段的类型的信息A和方法ReturnValue的返回类型。编译器甚至不知道是否存在这样的字段和方法,正如它不会抱怨我们调用的事实所证明的ThisFunctionDoesntExist()。如果我们没有为a3b3指定类型(而是使用var),它们也将是动态类型的变量。在任何时候,我们都可以将动态类型的变量强制转换为其实际类型。

我本人满怀激情地讨厌动态语言:为什么您会放弃让编译器为您查找错误的优势,而只是避免编写更多的代码行?因此,很少在C#中看到动态的用法。

尽管在编译时无法访问类型,但是使用动态更有意义。通常,您将使用反射

var type = Assembly.GetExecutingAssembly().GetType("MyNamespace.Class1");
var referenceToUnknowType = type
  .GetConstructor(Array.Empty<Type>())
  .Invoke(Array.Empty<object>());
var a4 = (int)t
  .GetField("A")
  .GetValue(c);
var b4 = (int)t
  .GetMethod("ReturnValue", new Type[] { typeof(int), typeof(int) })
  .Invoke(c, new object[] { 1, 2 });

这不漂亮!

如果您想知道为什么会需要这样的东西,那么在编写支持插件的应用程序时通常会使用反射。由于插件是与应用程序分开编写的,因此在编译应用程序本身时不能使用插件的类型。

另一个用例是访问private对象的方法和成员(ugh!)。

动态允许以更易读的方式编写相同的代码:

var type = Assembly.GetExecutingAssembly().GetType("MyNamespace.Class1");
var dynamicReferenceToUnknownType = type
  .GetConstructor(Array.Empty<Type>())
  .Invoke(Array.Empty<object>());
int a4 = dynamicReferenceToUnknownType.A;
int b4 = dynamicReferenceToUnknownType.ReturnValue(1, 2);

这与使用反射一样安全,因为反射也不提供任何编译时验证,但是可读性更高。

值得注意的是,反射代码比普通代码(上面使用的typedReference代码)慢数百倍。动态也因其速度较慢而臭名昭著:在这种情况下,它比使用反射要快一点,但仍比静态类型的替代要慢数百倍。

动态类型还可以用于轻松访问其他非静态类型的数据,例如COM对象(此处有关此信息)或JSON文件

让我们创建自己的DynamicObject

如前所述,动态对象的语法非常简单干净,但是使用它们的速度却很慢且容易出错。之所以缓慢,是因为DLR每次都必须使用其名称搜索对象的成员,危险来自可能找不到成员。

因此,动态对象非常适合暴露具有相同缓慢而危险特征的操作。解析XML文件就是这样的操作之一:

  • 这很慢,因为必须解析XML语言,并且元素和属性名称必须与我们的查询匹配,
  • 这是有风险的,因为C#编译器不能保证XML文件遵守预期的架构(该文件甚至不是应用程序的一部分,所以编译器可以做什么?)。

此外,动态对象不提供任何智能感知支持。但是,无论我们使用哪种技术,我们都永远不会获得遍历XML数据结构的任何智能感知支持,因此在此也不会丢失任何信息。

通常,为了使用C#访问XML数据,我们需要编写如下代码:

var xml = @"<Characters>
  <Batman Age=""81"">
    <Equipment>
      <Item>Batarangs</Item>
      <Item>Shark repellent</Item>
    </Equipment>
  </Batman>
  <Robin Age=""37"">
    <Equipment>
      <Item>Red hood</Item>
    </Equipment>
  </Robin>
</Characters>";

var characters = XDocument.Parse(xml).Root;
var batmanAge = int.Parse(characters.Element("Batman").Attribute("Age").Value);
var batarangs = characters.Element("Batman").Element("Equipment")
  .Elements().First().Value;

通过将XML数据公开为动态对象,我们可以实现更具可读性的语法。这可以通过创建一个新类并扩展DynamicObject来完成。

class XmlDynamicElement : DynamicObject
{
  private readonly XElement Element;

  public XmlDynamicElement(XElement element)
  {
    Element = element;
  }
  ...
}

dynamic characters = new XmlDynamicElement(XDocument.Parse(xml).Root);
int batmanAge = characters["Batman", 0].Age;
string batarangs = characters["Batman", 0]["Equipment", 0][0];

 

创建您自己的.NET DynamicObject 为什么、何时和如何

Dynamic Duo

XMLDynamicObject

首先,我们的新XmlDynamicElement类应支持使用[]运算符访问子元素。Element[int]将返回第n个孩子。Element[string, int]将返回具有指定名称的第n个元素。这实际上很容易实现:

class XmlDynamicElement : DynamicObject
{
  public override bool TryGetIndex(GetIndexBinder binder, object[] indexes,
                                   out object result)
  {
    result = null;
    XElement childElement;
    if (indexes.Length == 1 &&
        indexes[0] is int index)
    {
      childElement = Element.Elements().ElementAtOrDefault(index);
    }
    else if (indexes.Length == 1 &&
             indexes[0] is string name)
    {
      result = Element.Elements(name).Select(e => new XmlDynamicElement(e))
        .ToArray();
      return true;
    }
    else if (indexes.Length == 2 &&
             indexes[0] is string name2 &&
             indexes[1] is int index2)
    {
      childElement = Element.Elements(name2).ElementAtOrDefault(index2);
    }
    else
      throw new ArgumentException("Invalid index type");

    if (childElement == null)
      throw new IndexOutOfRangeException();
    result = new XmlDynamicElement(childElement);
    return true;
  }
...

在上面的代码中,我还添加了对Element[string]的支持,返回包含具有指定名称的元素的数组。我没有返回IEnumerable,因为扩展方法不能很好地适应动态环境。我真的不需要处理异常,这些异常只会在用户使用无效索引时浮出水面。

接下来,我们希望将一个XmlDynamicElement自动转换为一个string或数字。如果我们想取回XElement,那就更好了。

class XmlDynamicElement : DynamicObject
{
  public override bool TryConvert(ConvertBinder binder, out object result)
  {
    if (binder.Type == typeof(String))
      result = ToString();
    else if (binder.Type == typeof(XElement))
      result = Element;
    else
      result = Convert.ChangeType(ToString(), binder.Type,
        CultureInfo.InvariantCulture);
    return true;
  }

  public override string ToString() =>
    Element.Nodes().Aggregate(new StringBuilder(),
      (sb, n) => sb.Append(n.ToString())).ToString();
...

为了保持连贯性,我将定义到string的转换,以返回与提供元素连接内容的.ToString()方法相同的值。我还利用Convert.ChangeType轻松地支持默认转换为多种类型。

XmlDynamicElement实现的最后一点是允许访问属性。因为XML元素不能具有两个具有相同名称的属性,所以我们可以将特性表示为属性:

class XmlDynamicElement : DynamicObject
{
  public override bool TryGetMember(GetMemberBinder binder, out object result)
  {
    var attribute = Element.Attribute(binder.Name);
    result = attribute != null ? new XmlDynamicAttribute(attribute) : null;
    return attribute != null;
  }
  
  public override IEnumerable<string> GetDynamicMemberNames() =>
    Element.Attributes().Select(a => a.Name.ToString());
...

然后,用非常相似的DynamicObject表示属性:

class XmlDynamicAttribute : DynamicObject
{
  private readonly XAttribute Attribute;
  
  public XmlDynamicAttribute(XAttribute attribute)
  {
    Attribute = attribute;
  }
  
  public override bool TryConvert(ConvertBinder binder, out object result)
  {
    if (binder.Type == typeof(String))
      result = ToString();
    else if (binder.Type == typeof(XAttribute))
      result = Attribute;
    else    
      result = Convert.ChangeType(ToString(), binder.Type,
        CultureInfo.InvariantCulture);
    return true;
  }
  
  public override string ToString() =>
    return Attribute.Value;
}

我们可能想在类中添加一些额外的功能作为方法,例如,访问元素名称或其所有子元素可能很有用。

class XmlDynamicElement : DynamicObject
{
  public string Xml() =>
    Element.ToString();
  
  public XmlDynamicElement[] Elements() =>
    Element.Elements().Select(e => new XmlDynamicElement(e))
      .ToArray();
  
  public XmlDynamicAttribute[] Attributes() =>
    Element.Attributes().Select(a => new XmlDynamicAttribute(a))
      .ToArray();
...

不幸的是,当XmlDynamicElement对象位于动态引用之后时,这些方法将无法访问。为了解决这个问题,我们需要实现TryInvokeMember

class XmlDynamicElement : DynamicObject
{
  public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args,
                                       out object result)
  {
    if (args == null || args.Length == 0)
    {
      switch (binder.Name)
      {
        case "Xml":
          result = Xml();
          return true;
        case "Elements":
          result = Elements();
          return true;
        case "Attributes":
          result = Attributes();
          return true;
      }
    }
    result = null;
    return false;
  }
...

包装起来

这种方法会对性能产生巨大影响。基于一些粗浅的测试,我可以看到,这个实现比直接使用XElementXAttribute慢约20倍。简化语法要付出巨大的代价!

但有时可用性是最重要的。在几篇文章中,我们将使用XmlDynamicElement允许用户从模板化的XAML文档引用XML数据,这正是这种情况。因为我们会要求用户使用c#XAML的奇怪组合来编写XML查询(如果等不及就去查Razor),所以我们真的希望尽可能降低增加的复杂性。