.NET 4.0 中的动态:我做得对吗?
本文关键字:动态 NET | 更新日期: 2023-09-27 17:56:13
昨天,我在 .NET 4.0 中使用新的dynamic
类型编写了第一行代码。我发现这有用的场景如下:
我有一个包含多个值列表的类。这可以是List<string>
、List<bool>
、List<int>
或任何类型的列表。使用这些列表的方式是,我向一个或多个列表添加一个值。然后我"同步"它们,以便它们都以相同的长度结束(那些太短的长度用默认值填充)。然后我继续添加更多值,再次同步等。目标是使其中一个列表中任何索引处的项与另一个列表中同一索引处的项相关。(是的,通过将所有这些包装在另一个类中可以更好地解决这个问题,但这不是这种情况的重点。
我在几个类中都有这种结构,所以我想使列表的这种同步尽可能通用。但由于列表的内部类型可能会有所不同,因此这并不像我最初想象的那么简单。但是,进入当天的英雄:动态:)
我编写了以下帮助程序类,该类可以获取列表(任何类型的)集合以及每个列表的默认值:
using System;
using System.Collections.Generic;
using System.Linq;
namespace Foo.utils
{
public class ListCollectionHelper
{
/// <summary>
/// Takes a collection of lists and synchronizes them so that all of the lists are the same length (matching
/// the length of the longest list present in the parameter).
///
/// It is assumed that the dynamic type in the enumerable is of the type Tuple<ICollection<T>, T>, i.e. a
/// list of tuples where Item1 is the list itself, and Item2 is the default value (to fill the list with). In
/// each tuple, the type T must be the same for the list and the default value, but between the tuples the type
/// might vary.
/// </summary>
/// <param name="listCollection">A collection of tuples with a List<T> and a default value T</param>
/// <returns>The length of the lists after the sync (length of the longest list before the sync)</returns>
public static int SyncListLength(IEnumerable<dynamic> listCollection)
{
int maxNumberOfItems = LengthOfLongestList(listCollection);
PadListsWithDefaultValue(listCollection, maxNumberOfItems);
return maxNumberOfItems;
}
private static int LengthOfLongestList(IEnumerable<dynamic> listCollection)
{
return listCollection.Aggregate(0, (current, tuple) => Math.Max(current, tuple.Item1.Count));
}
private static void PadListsWithDefaultValue(IEnumerable<dynamic> listCollection, int maxNumberOfItems)
{
foreach (dynamic tuple in listCollection)
{
FillList(tuple.Item1, tuple.Item2, maxNumberOfItems);
}
}
private static void FillList<T>(ICollection<T> list, T fillValue, int maxNumberOfItems)
{
int itemsToAdd = maxNumberOfItems - list.Count;
for (int i = 0; i < itemsToAdd; i++)
{
list.Add(fillValue);
}
}
}
}
下面是我用来验证我最终是否达到预期行为的一组简短单元测试:
using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Foo.utils;
namespace Foo.UnitTests
{
[TestClass]
public class DynamicListSync
{
private readonly List<string> stringList = new List<string>();
private readonly List<bool> boolList = new List<bool>();
private readonly List<string> stringListWithCustomDefault = new List<string>();
private readonly List<int> intList = new List<int>();
private readonly List<dynamic> listCollection = new List<dynamic>();
private const string FOO = "bar";
[TestInitialize]
public void InitTest()
{
listCollection.Add(Tuple.Create(stringList, default(String)));
listCollection.Add(Tuple.Create(boolList, default(Boolean)));
listCollection.Add(Tuple.Create(stringListWithCustomDefault, FOO));
listCollection.Add(Tuple.Create(intList, default(int)));
}
[TestMethod]
public void SyncEmptyLists()
{
Assert.AreEqual(0, ListCollectionHelper.SyncListLength(listCollection));
}
[TestMethod]
public void SyncWithOneListHavingOneItem()
{
stringList.Add("one");
Assert.AreEqual(1, ListCollectionHelper.SyncListLength(listCollection));
Assert.AreEqual("one", stringList[0]);
Assert.AreEqual(default(Boolean), boolList[0]);
Assert.AreEqual(FOO, stringListWithCustomDefault[0]);
Assert.AreEqual(default(int), intList[0]);
}
[TestMethod]
public void SyncWithAllListsHavingSomeItems()
{
stringList.Add("one");
stringList.Add("two");
stringList.Add("three");
boolList.Add(false);
boolList.Add(true);
stringListWithCustomDefault.Add("one");
Assert.AreEqual(3, ListCollectionHelper.SyncListLength(listCollection));
Assert.AreEqual("one", stringList[0]);
Assert.AreEqual("two", stringList[1]);
Assert.AreEqual("three", stringList[2]);
Assert.AreEqual(false, boolList[0]);
Assert.AreEqual(true, boolList[1]);
Assert.AreEqual(default(Boolean), boolList[2]);
Assert.AreEqual("one", stringListWithCustomDefault[0]);
Assert.AreEqual(FOO, stringListWithCustomDefault[1]);
Assert.AreEqual(FOO, stringListWithCustomDefault[2]);
Assert.AreEqual(default(int), intList[0]);
Assert.AreEqual(default(int), intList[1]);
Assert.AreEqual(default(int), intList[2]);
}
}
}
所以,由于这是我第一次尝试动态(无论是在 C# 中还是在其他任何地方......),我只想问一下我做得是否正确。显然,代码按预期工作,但这是正确的方法吗?我是否缺少任何明显的优化或陷阱等?
C# 中对动态进行了相当多的黑客攻击,我最初认为它们会非常整洁,因为我是 Ruby/Javascript 完成的动态类型的忠实粉丝,但遗憾的是在实现中感到失望。因此,我对"我做得对吗"的看法归结为"这个问题是否适合动态" - 这是我对此的看法。
- 加载
- 时的性能影响和 JIT 所有与动态相关的程序集都可能非常严重。
- C-Sharp 运行时绑定程序在第一次动态解析方法时在内部引发并捕获异常。这是每个调用站点发生的(即,如果你在一个动态对象上有 10 行代码调用方法,你会得到 10 个异常)。如果您将调试器设置为"第一次机会异常时中断",这真的很烦人,并且它还会用第一次机会异常消息填充调试输出窗口。你可以抑制这些,但Visual Studio这样做很烦人。
- 这两件事加起来 - 在冷启动时,您的应用程序可能需要更长的时间来加载。在带有 SSD 的核心 i7 上,我发现当我的 WPF 应用程序首次加载所有动态内容时,它会在加载程序集、JIT 和抛出捕获异常时停滞大约 1-2 秒。(有趣的是,IronRuby没有这些问题,它的DLR实现比C#的要好得多)
- 加载后,性能非常好。
- 动态扼杀了智能感知和Visual Studio的其他不错的功能。虽然我个人并不介意这一点,因为我有做很多 ruby 代码的背景,但我组织中的其他几个开发人员很生气。
- 动态会使调试变得更加困难。像ruby/javascript这样的语言提供了一个REPL(交互式提示)来帮助它们,但C#还没有。如果你只是使用动态来解析方法,它不会那么糟糕,但如果你尝试使用它来动态实现数据结构(ExpandoObject等),那么调试在C#中就会成为一个真正的痛苦。当我的同事不得不使用ExpandoObject调试一些代码时,他们对我更加恼火。
总的来说:
- 如果你可以在不需要动态的情况下做某事,请不要使用它们。C# 实现它们的方式太尴尬了,你的同事会生你的气。
- 如果只需要非常小的动态要素,请使用反射。使用反射经常被引用的"性能问题"通常不是什么大问题。
- 特别是尽量避免客户端应用中由于加载/启动性能损失而导致的动态。
我对这种特定情况的建议:
- 看起来您可能可以通过将东西作为
Object
传递来避免这里的动态。我建议你这样做。 - 您必须停止使用
Tuple
来传递数据对,并创建一些自定义类,但这也可能改进您的代码,因为这样您就可以将有意义的名称附加到数据,而不仅仅是Item1
和Item2
添加动态关键字主要是为了使Microsoft Office互操作更容易,以前你必须编写相当复杂的代码(在C#中)才能使用Microsoft Office API,Office界面代码现在可以更干净。
这样做的原因是,Office API最初是为Visual Basic 6(或VB脚本)编写的;.NET 4.0添加了几种语言功能,使这更容易(以及动态的,您还可以获得命名和可选参数)。
使用 dynamic 关键字时,它将丢失编译时检查,因为使用 dynamic 关键字的对象是在运行时解析的。由于必须加载提供动态支持的程序集,因此存在一些内存开销。 此外,还会有一些性能开销,类似于使用反射。
我不认为这是动态的解决方案。当您需要有条件地使用一堆不同的类型时,动态很有用。如果这是一个字符串,做点什么,如果它是一个 int,做点别的,如果它是一个 Puppy 类实例,调用 bark()。动态使您不必像这样使用大量类型转换或丑陋的泛型乱扔代码。动态和其他高级语言功能的用途适用于代码生成器、解释器等...
这是一个很酷的功能,但除非您与动态语言或COM互操作交谈,否则它仅适用于您遇到令人讨厌的高级问题。
您可以使用 IList
来完成同样的事情,而不是在这里使用动态。(非通用) 两者都消除了编译时类型检查,但由于泛型列表也实现了IList
,您仍然可以使用 IList
进行运行时类型检查。
另外,附带问题,为什么使用.Aggregate()
而不是.Max()
来找到最大长度?
我还没有仔细研究过它,但是在声明集合时使用动态关键字真的有必要吗?在 .NET 4.0 中,还有支持协变和逆变的新机制,这意味着您还应该能够使用以下代码。
var listCollection = new List<IEnumerable<object>>();
listCollection.Add(new List<int>());
这里的缺点是,您的列表包含只读的 IEnumerable 实例,而不是可以直接修改的内容(如果这是实现中需要的内容)。
除了这个考虑之外,我认为动态的使用很好,因为你正在使用它们,但你确实牺牲了很多 C# 通常提供的安全机制。因此,我建议的是,如果您使用此技术,我建议您将其编写在一个包含良好且经过测试的类中,该类不会向任何更大的客户端代码主体公开动态类型。