Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamics in .NET 4.0: am I doing it right?

Yesterday I wrote my first lines of code using the new dynamic type in .NET 4.0. The scenario where I found this useful is as follows:

I have a class holding several lists of values. This can be List<string>, List<bool>, List<int> or really any kind of list. The way these are used, is that I add a value to one or more of these lists. Then I "synchronize" them, so that they all end up the same length (those too short are filled with a default value). And then I continue to add more values, sync again etc. The goal is that the item at any index in one of the lists are related to an item at the same index in another list. (Yes, this could probably be better solved by wrapping all this in another class, but that's not the point in this case.)

I have this construct in a couple of classes, so I wanted to make this synchronizing of the lists as generic as possible. But since the inner type of the lists might vary, this wasn't as straight forward as I first had thought. But, enter the hero of the day: dynamics :)

I wrote the following helper class that can take a collection of lists (of any type) together with a default value for each list:

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);
            }
        }
    }
}

And below is a short set of unit tests I used to verify that I ended up with the desired behaviour:

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]);
        }
    }
}

So, since this is my first shot at dynamics (both in C# and anywhere else really...), I just wanted to ask if I'm doing this right. Obviously the code works as intended, but is this the right way of doing it? Are there any obvious optimizations or pitfalls I'm missing etc?

like image 248
Julian Avatar asked May 10 '11 10:05

Julian


3 Answers

I've done quite a bit of hacking around with dynamics in C#, I initially thought they would be really neat as I'm a big fan of the dynamic typing done by Ruby/Javascript, but was sadly dissapointed in the implementation. As such, my opinion for "Am I doing it right" comes down to "is this problem a good fit for dynamics" - here are my thoughts on that.

  • the performance hit on load to pull in and JIT all the dynamic-related assemblies can be quite severe.
  • The C-Sharp runtime binder internally throws and catches an exception the first time a method is resolved dynamically. This happens per call-site (i.e. if you have 10 lines of code calling methods on a dynamic object you get 10 exceptions). This is really annoying if you have your debugger set to "break on first chance exceptions", and it also fills up the debug output window with first chance exception messages. You can suppress these, but visual studio makes it annoying to do so.
  • These two things add up - on a cold start your app can take noticeably longer to load. On a core i7 with an SSD, I found that when my WPF app first loaded all the dynamic stuff, it would stall for about 1-2 seconds loading assemblies, JITing, and throw-catching exceptions. (Interestingly IronRuby doesn't have these problems, it's implementation of the DLR is much better than C#'s)
  • Once things are loaded the performance is very good.
  • Dynamics kill intellisense and other nice features of visual studio. While I personally didn't mind this as I have a background of doing lots of ruby code, several other developers in my organization got annoyed.
  • Dynamics can make debugging a LOT harder. Languages like ruby/javascript provide a REPL (interactive prompt) which helps them, but C# doesn't yet have one. If you're just using dynamic to resolve methods, it won't be so bad, but if you try and use it to dynamically implement data structures (ExpandoObject, etc) then debugging becomes a real pain in C#. My co-workers were even more annoyed at me when they had to debug some code using an ExpandoObject.

On the whole:

  • If you can do something without needing dynamics, don't use them. The way C# implements them is just too awkward and your co-workers will be angry with you.
  • If you only need a very small dynamic feature, use reflection. The oft-quoted "performance issues" from using reflection are often not a big deal.
  • Especially try and avoid dynamics in a client app due to the load/start performance penalties.

My advice for this specific situation:

  • It looks like you can probably avoid dynamics here by just passing things around as Object. I would suggest that you do this.
  • You would have to switch away from using Tuple to pass your pairs of data around, and make some custom class(es), but this would probably also improve your code as then you can attach meaningful names to the data instead of just Item1 and Item2
like image 163
Orion Edwards Avatar answered Sep 28 '22 08:09

Orion Edwards


I believe that the dynamic keyword was primarily added to make Microsoft Office interop easier, where previously you had to write quite convoluted code (in C#) to be able to use the Microsoft Office API, Office interface code can be much cleaner now.

The reson for this is that the Office API was originally written to be used by Visual Basic 6 (or VB script); .NET 4.0 adds several language features to make this easier (as well as dynamic, also you get named and optional parameters).

When you use the dynamic keyword, it loses compile-time checking, as the objects using the dynamic keyword are resolved at run-time. There is some memory overhead as the assembly that provides dynamic support has to be loaded in. Also there will be some performance overhead, similar to using Reflection.

like image 28
RickL Avatar answered Sep 28 '22 08:09

RickL


I don't think this is a solution for dynamic. Dynamic is useful when you need to work with a bunch of different types conditionally. If this is a string, do something, if it's an int, do something else, if its a Puppy class instance, call bark(). dynamic frees you from having to litter code like this with tons of type casting or ugly generics. Uses for dynamic and other advanced language features are meant for code generators, interpreters etc...

It's a cool feature, but unless your talking to a dynamic language or COM interop, it's only meant for when you have a nasty advanced problem.

like image 35
Tjaart Avatar answered Sep 28 '22 10:09

Tjaart