Динамика в .NET 4.0: я делаю это правильно? - PullRequest
10 голосов
/ 10 мая 2011

Вчера я написал свои первые строки кода, используя новый тип 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]);
        }
    }
}

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

Ответы [ 5 ]

4 голосов
/ 10 ноября 2011

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

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

В целом:

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

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

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

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

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

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

1 голос
/ 18 мая 2011

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

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

1 голос
/ 17 мая 2011

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

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

0 голосов
/ 18 мая 2011

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

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

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

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

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

...