.NET 4.0中的动态特性:我做得对吗?

10
昨天我使用.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&lt;ICollection&lt;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&lt;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#还是其他任何地方...),我只想问一下我是否做得对。显然,代码按预期工作,但这是正确的方式吗?我是否忽略了任何明显的优化或陷阱等?


使用 DataTable 工作会不会更容易些呢? - Oliver
2
@Oliver:正如我在问题中简要提到的那样:是的,可能有更好的方法来解决这个特定的问题而不使用动态类型。但由于这是我第一次发现动态类型甚至稍微有用,所以我更感兴趣的是这个问题的这个方面。 - Julian
请访问 http://codereview.stackexchange.com/。 - liori
1
@liori:谢谢,我不知道那个SE网站。但是我觉得这个问题在SO上同样适用,所以我至少现在会把它留在这里... - Julian
5个回答

4
我在C#中使用动态特性进行了一些研究,最初认为它们会非常好用,因为我是Ruby/Javascript动态类型的粉丝,但是实现让我感到失望。因此,我的看法是“我是否正确地使用了它”,这取决于“这个问题是否适合使用动态特性”-以下是我的想法。
  • 加载和JIT所有与动态相关的程序集会对性能产生严重影响。
  • C#运行时绑定器在动态解析方法时内部抛出并捕获异常。这发生在每个调用点上(即,如果您有10行代码调用动态对象上的方法,则会有10个异常)。如果您的调试器设置为“在第一次机会异常时中断”,这真的很烦人,而且它还会在调试输出窗口中填充第一次机会异常消息。您可以抑制这些异常,但Visual Studio使其变得很麻烦。
  • 这两个问题加起来,您的应用程序在冷启动时可能需要更长时间才能加载。在一台配有Core i7和SSD的计算机上,我发现当我的WPF应用程序首次加载所有动态内容时,它会停顿1-2秒钟来加载程序集、JIT和抛出异常。(有趣的是,IronRuby没有这些问题,它的DLR实现比C#更好)
  • 一旦所有内容都加载完成,性能非常好。
  • 动态特性会破坏Intellisense和Visual Studio的其他好用功能。虽然我个人不介意这一点,因为我有大量的Ruby代码背景,但是我们组织中的其他开发人员感到很烦恼。
  • 动态特性可能会使调试变得更加困难。像Ruby/Javascript这样的语言提供了一个REPL(交互式提示符),这有助于它们,但C#还没有。如果您只是使用动态特性来解析方法,那么情况就不会太糟糕,但如果您尝试使用它来动态实现数据结构(ExpandoObject等),那么在C#中调试就会变得非常麻烦。当我的同事们不得不调试一些使用ExpandoObject的代码时,他们更加烦恼了。

总的来说:

  • 如果您可以不使用动态特性完成某些事情,请不要使用它们。C#的实现方式太笨拙了,您的同事会对您感到生气。
  • 如果您只需要非常小的动态特性,请使用反射。使用反射所引用的“性能问题”通常不是大问题。
  • 特别是由于加载/启动性能损失,尽量避免在客户端应用程序中使用动态特性。

对于这种特定情况,我的建议是:

  • 看起来你可以通过将事物作为Object传递来避免使用动态类型。我建议你这样做。
  • 你需要更改使用Tuple来传递数据对,然后创建一些自定义类,但这可能会改善你的代码,因为你可以给数据附加有意义的名称而不仅是Item1Item2

+1 对于一个非常详尽和出色的回答。同时,我也感谢对我的具体解决方案的评论。在我的情况下,这段代码只在项目内部的几个地方使用,所以我不会费心做任何重大改变。但是我肯定会将您的建议带给我,以备将来参考 :) - Julian

3
我认为动态关键字的主要目的是使Microsoft Office互操作更加容易,在此之前,您必须编写相当复杂的代码(使用C#)才能使用Microsoft Office API,现在Office接口代码可以更加简洁。
这是因为Office API最初是为Visual Basic 6(或VB脚本)编写的。 .NET 4.0添加了几个语言功能,以使这更容易(除了动态外,还有命名和可选参数)。
当您使用动态关键字时,它会失去编译时检查,因为使用动态关键字的对象在运行时解析。加载提供动态支持的程序集会带来一些内存开销。还会有一些性能开销,类似于使用反射。

谢谢RickL :) 我知道你失去了编译时检查的概念(在运行时解析类型是动态性的主要概念,对吧?),而且会有一些性能方面的开销。我更关心我写的代码是否有特别糟糕的地方(不过不要太专注于这可能不是完美示例的问题),是否有任何“隐藏的陷阱”我已经踩进去之类的。 - Julian
3
我感觉通常情况下,除非用于与COM或脚本语言(例如IronPython)交互,否则使用动态关键字是不好的编程实践。在我看来,最好尽可能使用标准的面向对象编程概念。 - RickL
我会在某种程度上同意你的看法。动态编程可能是一种不好的编程实践,至少如果你过度使用它们的话(但我想大多数事情都是这样)。现在我只是简单地开始研究它们,但我的印象是有一些有效的情况下,动态编程将使您的生活更加轻松(而不会完全搞乱您的代码)。当然,如果我问题中的代码是动态使用的特别糟糕的例子,我也很乐意听取意见 :) - Julian

1

我认为这不是动态解决方案。当您需要有条件地使用许多不同类型时,动态非常有用。如果这是一个字符串,做某事,如果它是一个整数,做另一件事,如果它是Puppy类的实例,则调用bark()。动态使您无需在代码中添加大量类型转换或丑陋的泛型。动态和其他高级语言功能的用途是为代码生成器、解释器等而设计的...

这是一个很酷的功能,但除非您正在与动态语言或COM互操作交互,否则它只适用于当您遇到棘手的高级问题时。


1

我认为你可以使用 IList 来完成同样的事情,而不是在这里使用动态。两者都消除了编译时类型检查,但由于泛型列表也实现了 IList,因此仍然可以使用 IList 进行运行时类型检查。

另外,顺便问一下,你为什么要使用 .Aggregate() 而不是 .Max() 来找到最大长度?


我同意,至少使用IList,您将能够调用类似Count这样的方法,而无需了解或转换到底层类型。此外,动态关键字的使用可以保留,直到您实际需要处理每个元素而不是在整个代码中都将列表本身作为动态处理。 - jpierson
@recursive:我可能会错过一些显而易见的东西,但如果我使用非泛型IList,那么在某个时候我不得不进行一些强制类型转换吗?那么这难道不就和使用dynamic一样好(或者糟糕)了吗? - Julian
如果你将类型转换为已知类型,而不是使用动态类型,那么从那时起,你就可以获得编译时类型安全和智能感知。对我来说,这似乎比使用动态类型作为通用参数更具优势。 - recursive
@recursive: 关于´.Aggregate()´和´.Max()´的问题:这只是我对LINQ扩展方法不够熟悉,过于盲目地相信ReSharper在将for循环转换为LINQ表达式时的建议。谢谢你给我启发 :) - Julian
@recursive:我仍然不确定我完全理解你的意思。如果您查看我的代码,"真正的逻辑"在通用函数´FillList<T>(ICollection<T> list, T fillValue, int maxNumberOfItems)´中。我不明白如果我只有一个非泛型IList,我将如何能够将该列表中的对象转换为´Tuple<List<String>, String>´、´Tuple<List<int>, int>´或´Tuple<List<Boolean>, Boolean>´等,而不添加很多类型检查......(顺便说一句,我现在要离开工作了,所以可能明天之前不能跟进评论)。 - Julian
我不确定这是否适用于您的用例,但您可以从该方法中消除通用参数,并改用object而不是T。我可能错了,但我认为在使用动态语言时,泛型并没有为您提供任何类型安全性。 - recursive

0

我还没有仔细研究过,但在声明集合时使用动态关键字是否真的必要?在.NET 4.0中,还有新机制来支持协变和逆变,这意味着您也应该能够使用下面的代码。

    var listCollection = new List<IEnumerable<object>>();

    listCollection.Add(new List<int>());

这里的缺点是,您的列表仅包含只读的IEnumerable实例,而不是如果在您的实现中需要修改直接修改的内容。

除此之外,我认为使用动态类型是可以的,但您会牺牲C#通常提供的许多安全机制。因此,我建议如果您使用此技术,最好将其编写在一个经过充分测试的类中,并且不要将动态类型暴露给任何更大的客户端代码。


.NET 4确实支持协变性,但是List<T>不是协变的,因为它在泛型类型中具有输入和输出。 - recursive
@recursive - 啊,好的观点。为了让其他人知道你在引用什么,我找到了这个很好的SO参考资料分享给大家,我会相应地更新我的答案。https://dev59.com/aW025IYBdhLWcg3wvIgo - jpierson
新的协变和逆变机制对我来说是新的,但正如你所指出的,最终我会得到一系列只读IEnumberable实例(而且由于我的代码的整个重点是向其中一些实例添加项目,这对我没有太大帮助)。不过还是感谢你提供的其他建议。:9 - Julian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接