У меня есть классы, которые должны генерировать диаграммы. У меня есть три класса, которые используют разные типы данных для серии диаграмм. Я дам только свойства этих классов, чтобы избежать путаницы :)
public class PointBase
{
public decimal X { get; set; }
}
public class Point : PointBase
{
public decimal Y { get; set; }
}
public class Bubble : Point
{
public decimal Size { get; set; }
}
Эти три класса используются в качестве основной части данных в серии диаграмм. Ряд диаграмм представлен как объекты Словаря в других трех классах (графические). Я не буду предоставлять код классов диаграмм, чтобы сосредоточиться на конкретной проблеме. Я просто укажу имена классов:
У меня есть следующий метод, который выполняет точно такой же алгоритм для всех моих классов диаграмм. Вот почему я решил использовать метод с использованием дженериков.
public Dictionary<string, T[]> GenerateSeriesDataDictionary<T>(string[] series, string[] categories) where T : PointBase, new()
{
// Create a new Dictionary.
Dictionary<string, T[]> seriesData = new Dictionary<string, T[]>();
// Gets the number of categories and assigns the categoriesNumber variable with the returned value.
int categoriesNumber = categories.Length;
foreach (string serie in series)
{
Dictionary<string, T> categoriesData = (Dictionary<string, T>)GetCategoriesData(serie);
// Create a new array of Ts that will store the data for the current serie.
T[] dataArray = new T[categories.Length];
for (int i = 0; i < categoriesNumber; i++)
{
// If the serie contains a value for the current category, store it in the array.
// Otherwise, set the value of the current element of the array to the default one.
dataArray[i] = categoriesData.ContainsKey(categories[i]) ? categoriesData[categories[i]] : new T();
}
// Add the dataArray to the seriesData Dictionary.
seriesData.Add(serie, dataArray);
}
return seriesData;
}
Таким образом, вышеупомянутый метод получает два массива строк. Первый представляет названия серии, например "Токио" и "Лондон". Второй - имена категорий для диаграммы - например, имена месяцев - "январь", "февраль" и т.д.
Предполагается, что этот метод создает объект Dictionary, который использует имя сери в качестве ключа и соответствующий массив объектов, представляющий данные для каждой категории (в моем случае - каждый месяц). Например, одна ключевая пара значений в словаре должна выглядеть примерно так: Ключ: "Лондон"; Значение: 17, 16, 20, 25 (скажем, данные представляют максимальную температуру за каждый месяц).
Я в основном выполняю тот же алгоритм, когда я создаю линию, разброс или диаграмму пузырьков (представлены классами CoordinateChart, ScatterChart и BubbleChart). Единственное, что отличается, это тип словаря, который я генерирую.
Я заменил символы '<' и '>' на '(' and ')', потому что они, кажется, исчезают :)
Поэтому я решил поставить логику, которая должна генерировать объект Dictionary в отдельном методе, а затем переопределить этот метод в каждом из этих классов.
Мой метод выглядит так:
public virtual Dictionary<string, PointBase> GetCategoriesData(string serie)
{
Dictionary<string, PointBase> categoriesData = sqlResultDataTable.AsEnumerable()
.Where(row => row["City"].ToString() == serie)
.Select(row => new KeyValuePair<string, PointBase>(row["Month"].ToString(), new PointBase(decimal.Parse(row["Temperature"].ToString()))))
.ToDictionary(item => item.Key, item => item.Value);
return categoriesData;
}
Этот метод в основном получает все пары ключевого значения, представляющие значение "Месяц" и "Температура" для серии (например, "Лондон").
Итак, я делаю все это, чтобы иметь возможность повторного использования кода. В противном случае я мог бы сделать то же самое во всех трех классах, и я мог бы обеспечить индивидуальную реализацию метода GenerateSeriesDataDictionary в каждом из них, но я не думаю, что это хороший подход.
Все в порядке, но я получаю сообщение об ошибке в этой строке:
Dictionary<string, T> categoriesData = (Dictionary<string, T>)GetCategoriesData(serie);
В сообщении об ошибке говорится: "Невозможно преобразовать тип Словарь (строка, PointBase) в словарь (строка, T)".
Я заменил символы '<' и '>' на '(' and ')', потому что они, кажется, исчезают :)
Что я могу сделать?
Создайте базовый класс/интерфейс с параметром общего типа.
public abstract class Chart<T> where T : PointBase, new()
{
public abstract IDictionary<string, T> GetCategoriesData(string serie);
public IDictionary<string, T[]> GenerateSeriesDataDictionary<T>(string[] series, string[] categories)
{
// Call GetCategoriesData, etc.
}
}
Затем создайте производные классы с определенными типами точек:
public class ScatterChart : Chart<Point>
{
public override IDictionary<string, Point> GetCategoriesData(string serie)
{
// Create new Point and set its properties.
// We know it is a Point because we specified it in the class generic type.
}
// ...
}
public class BubbleChart : Chart<Point>
{
public override IDictionary<string, Bubble> GetCategoriesData(string serie)
{
// Create new Bubble and set its properties.
// We know it is a Bubble because we specified it in the class generic type.
}
// ...
}
Вы не можете использовать Dictionary<string, PointBase>
например, Dictionary<string, Bubble>
, поэтому ваш код не будет компилироваться. Это не одно и то же.
Вам либо нужно объявить Dictionary<string, PointBase>
как Dictionary<string, PointBase>
, либо сделать метод GetCategoriesData()
общим.
Это проблема дисперсии типа, ковариация более конкретна.
.Net позволяет вам делать безопасную ковариацию, иначе это будет означать
Dictionary<string, Bubble> bubbles = GetBubbles();
Dictionary<string, PointBase> points = bubbles;
points["London"] = new Point(); //crash, not a Bubble
Рассмотрим использование IEnumerable
который является интерфейсом только для чтения с ковариантным родовым типом.