Динамика в .NET 4.0: я делаю это правильно?

Вчера я написал свои первые строки кода, используя новый тип dynamic в .NET 4.0. Сценарий, в котором я нашел это полезным, выглядит следующим образом:

У меня есть класс, содержащий несколько списков значений. Это может быть 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]);
        }
    }
}

Итак, поскольку это мой первый выстрел в динамике (как в С#, так и в другом месте на самом деле...), я просто хотел спросить, правильно ли я делаю это. Очевидно, что код работает по назначению, но является ли это правильным способом? Есть ли очевидные оптимизации или ловушки, которые я пропускаю и т.д.?

Ответ 1

Я сделал довольно много взлома с динамикой на С#, изначально я думал, что они будут очень аккуратными, поскольку я большой поклонник динамического ввода, сделанного Ruby/Javascript, но был грустно разочарован в реализации, Таким образом, мое мнение о том, "Я делаю это правильно", сводится к тому, что "эта проблема хорошо подходит для динамики" - вот мои мысли об этом.

  • производительность при загрузке, и JIT все связанные с динамикой сборки могут быть довольно серьезными.
  • Связующее средство C-Sharp внутренне выбрасывает и вызывает исключение при первом разрешении метода динамически. Это происходит на каждом сайте-вызове (т.е. Если у вас есть 10 строк методов вызова кода на динамическом объекте, вы получаете 10 исключений). Это действительно раздражает, если у вас есть отладчик, который настроен на "break on first chance exceptions", а также заполняет окно вывода отладки с помощью сообщений об исключительных случайных исключениях. Вы можете подавить их, но визуальная студия делает это раздражающим.
  • Эти две вещи складываются - при холодном запуске ваше приложение может значительно увеличить нагрузку. На ядре i7 с SSD я обнаружил, что когда мое приложение WPF сначала загрузило все динамические материалы, оно остановилось бы примерно на 1-2 секунды, загружая сборки, JITing и исключающие бросание исключения. (Интересно, IronRuby не имеет этих проблем, реализация DLR намного лучше, чем С#)
  • Как только вещи загружаются, производительность очень хорошая.
  • Динамика убивает intellisense и другие приятные черты визуальной студии. Хотя я лично не возражал против этого, поскольку у меня есть опыт выполнения большого количества рубинового кода, некоторые другие разработчики в моей организации раздражены.
  • Динамика может сделать отладку более сложной. Языки, такие как ruby ​​/javascript, предоставляют REPL (интерактивное приглашение), которое им помогает, но С# еще не имеет этого. Если вы просто используете динамический метод для решения, это будет не так уж плохо, но если вы попытаетесь использовать его для динамического внедрения структур данных (ExpandoObject и т.д.), То отладка становится реальной болью на С#. Мои коллеги были еще более раздражены, когда им пришлось отлаживать некоторый код с помощью ExpandoObject.

В целом:

  • Если вы можете что-то сделать без динамики, не используйте их. Способ, которым С# реализует их, слишком неудобен, и ваши сотрудники будут сердиться на вас.
  • Если вам нужна только небольшая динамическая функция, используйте отражение. Часто цитируемые "проблемы производительности" от использования рефлексии часто не имеют большого значения.
  • Особенно старайтесь избегать динамики в клиентском приложении из-за штрафов за загрузку/запуск.

Мой совет для этой конкретной ситуации:

  • Похоже, вы можете избежать динамики здесь, просто передавая вещи как Object. Я бы предположил, что вы это сделаете.
  • Вам нужно будет отказаться от использования Tuple для передачи ваших пар данных и сделать некоторые пользовательские классы, но это, вероятно, также улучшит ваш код, а затем вы сможете прикрепить значимые имена к данным только Item1 и Item2

Ответ 2

Я считаю, что ключевое слово dynamic было добавлено в первую очередь для облегчения взаимодействия между Microsoft Office, когда ранее вам приходилось писать довольно сложный код (на С#), чтобы иметь возможность использовать Microsoft Office API, код интерфейса Office теперь может быть намного чище.

Резонансом для этого является то, что API Office был первоначально написан для использования Visual Basic 6 (или VB script);.NET 4.0 добавляет несколько языковых функций, чтобы сделать это проще (а также динамический, а также получить именованные и необязательные параметры).

Когда вы используете динамическое ключевое слово, оно теряет проверку времени компиляции, так как объекты, использующие динамическое ключевое слово, разрешаются во время выполнения. Накладные расходы на память возникают из-за того, что загружается сборка, которая обеспечивает динамическую поддержку. Также будут некоторые издержки производительности, похожие на использование Reflection.

Ответ 3

Я не думаю, что это решение для динамического. Динамика полезна, когда вам нужно работать с кучей разных типов условно. Если это строка, сделайте что-нибудь, если это int, сделайте что-то еще, если это экземпляр класса Puppy, вызовите bark(). динамика освобождает вас от необходимости подбирать код, подобный этому, с помощью тонны литья или уродливых дженериков. Использование для динамических и других расширенных функций языка предназначено для генераторов кода, интерпретаторов и т.д.

Это классная функция, но если вы не разговариваете с динамическим языком или COM-взаимодействием, это означает только тогда, когда у вас есть неприятная передовая проблема.

Ответ 4

Вместо использования динамики здесь я думаю, что вы можете сделать то же самое, используя IList. (не общие) Оба исключают проверку типа времени компиляции, но поскольку общие списки также реализуют IList, вы все равно можете получить проверку типа времени выполнения с помощью IList.

Кроме того, вопрос о том, почему вы использовали .Aggregate() вместо .Max(), чтобы найти максимальную длину?

Ответ 5

Я еще не посмотрел на него, но это использование динамического ключевого слова, действительно необходимого для вашего использования при объявлении коллекции? В .NET 4.0 есть также новые механизмы для поддержки ковариации и контравариантности, что означает, что вы также должны использовать код ниже.

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

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

Недостатком здесь является то, что ваш список содержит только экземпляры IEnumerable только для чтения, а не что-то, что может быть изменено напрямую, если это было то, что было необходимо в вашей реализации.

Помимо этого соображения, я считаю, что использование динамики прекрасное, поскольку вы используете их, но вы жертвуете большим количеством механизма безопасности, который обычно предоставляет С#. Поэтому я бы порекомендовал, что если вы будете использовать этот метод, я бы порекомендовал его писать в хорошо укомплектованном и проверенном классе, который не подвергает динамический тип более крупному телу клиентского кода.