Динамика в .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<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]);
}
}
}
Итак, поскольку это мой первый выстрел в динамике (как в С#, так и в другом месте на самом деле...), я просто хотел спросить, правильно ли я делаю это. Очевидно, что код работает по назначению, но является ли это правильным способом? Есть ли очевидные оптимизации или ловушки, которые я пропускаю и т.д.?
Ответы
Ответ 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 только для чтения, а не что-то, что может быть изменено напрямую, если это было то, что было необходимо в вашей реализации.
Помимо этого соображения, я считаю, что использование динамики прекрасное, поскольку вы используете их, но вы жертвуете большим количеством механизма безопасности, который обычно предоставляет С#. Поэтому я бы порекомендовал, что если вы будете использовать этот метод, я бы порекомендовал его писать в хорошо укомплектованном и проверенном классе, который не подвергает динамический тип более крупному телу клиентского кода.