В настоящее время я изменяю библиотеку Blazor , и исходный код текущего состояния доступен на gitlab .
Моя ситуация выглядит следующим образом:
У меня есть LineChartData
объект, который должен хранить несколько наборов данных для LineCharts.
Эти стажеры наборов данных имеют список данных. Вместо того, чтобы просто работать с List<object>
, я хотел иметь возможность иметь List<TData>
.
Поскольку существует смешанная диаграмма, которая может принимать как LineChartDatasets, так и BarChartDatasets, существует интерфейс с именем IMixableDataset
.
Я начал с того, что сделал этот интерфейс универсальным, чтобы он теперь выглядел так (упрощенно):
public interface IMixableDataset<TData>
{
List<TData> Data { get; }
}
Затем я также сделал свой реализующий класс (LineChartDataset
) универсальным, и теперь он выглядит так (упрощенно):
public class LineChartDataset<TData> : IMixableDataset<TData>
{
public List<TData> Data { get; }
}
Следующим было LineChartData
. Сначала я также сделал этот шаблон и продолжал его до тех пор, пока не достиг верхнего уровня (см. Текущее состояние моей основной ветки). Однако позже я захотел изменить это, потому что хотел поддерживать несколько наборов данных с разными типами значений. По этой причине я вернул общие вещи во всех классах «над» наборами данных, и LineChartData
теперь выглядит так (упрощенно):
public class LineChartData
{
// HashSet to avoid duplicates
public HashSet<LineChartDataset<object>> Datasets { get; }
}
Я решил пойти с LineChartDataset<object>
, потому что: поскольку все можно преобразовать в объект, (на мой взгляд) XYZ<Whatever>
также следует преобразовать в XYZ<object>
, но, как я узнал, это не так.
Ключевое слово where тоже не помогло, так как я не хочу, чтобы TData
содержал отношения, отличные от object
- это может быть int
, string
или что-то совершенно другое. Единственное отношение, которое должны иметь эти LineDataset
, это то, что они LineDataset
s, а не какой тип они содержат.
Затем я узнал о ковариантности и контравариантности (вне и в ключевых словах). Я попытался сделать TData
в IMixableDataset
ковариантным, но так как List
и IList
/ ICollection
все инвариантны, я не смог их убедить.
Я также читал о IReadOnlyCollection<>
, который является ковариантным, но я не могу использовать это, потому что я должен быть в состоянии изменить список после создания.
Я также пытался использовать неявные / явные операторы для преобразования LineChartDataset<whatever>
в LineChartDataset<object>
, но у этого есть несколько проблем:
- Поскольку я создал новый экземпляр, мне нужно было хранить и использовать новый экземпляр вместо исходного для добавления элементов, полностью разрушая безопасность типов, которую я имел с исходным.
- Так как в * 1058 есть еще много свойств, мне бы тоже пришлось все их клонировать.
Если есть способ преобразовать более конкретное значение в другое при сохранении экземпляра и без необходимости писать код для каждого свойства , это может быть решением.
Полный пример, который воспроизводит полученную ошибку и показывает проблему:
// Provides access to some Data of a certain Type for multiple Charts
public interface IMixableDataset<TData>
{
List<TData> Data { get; }
}
// Contains Data of a certain Type (and more) for a Line-Chart
public class LineChartDataset<TData> : IMixableDataset<TData>
{
public List<TData> Data { get; } = new List<TData>();
}
// Contains Datasets (and more) for a Line-Chart
// This class should not be generic since I don't want to restrict what values the Datasets have.
// I only want to ensure that each Dataset intern only has one type of data.
public class LineChartData
{
// HashSet to avoid duplicates and Public because it has to be serialized by JSON.Net
public HashSet<LineChartDataset<object>> Datasets { get; } = new HashSet<LineChartDataset<object>>();
}
// Contains the ChartData (with all the Datasets) and more
public class LineChartConfig
{
public LineChartData ChartData { get; } = new LineChartData();
}
public class Demo
{
public void DesiredUseCase()
{
LineChartConfig config = new LineChartConfig();
LineChartDataset<int> intDataset = new LineChartDataset<int>();
intDataset.Data.AddRange(new[] { 1, 2, 3, 4, 5 });
config.ChartData.Datasets.Add(intDataset);
// the above line yields following compiler error:
// cannot convert from 'Demo.LineChartDataset<int>' to 'Demo.LineChartDataset<object>'
// the config will then get serialized to json and used to invoke some javascript
}
public void WorkingButBadUseCase()
{
LineChartConfig config = new LineChartConfig();
LineChartDataset<object> intDataset = new LineChartDataset<object>();
// this allows mixed data which is exactly what I'm trying to prevent
intDataset.Data.AddRange(new object[] { 1, 2.9, 3, 4, 5, "oops there's a string" });
config.ChartData.Datasets.Add(intDataset); // <-- No compiler error
// the config will then get serialized to json and used to invoke some javascript
}
}
Причина, по которой у всех есть только геттеры, заключается в моей первоначальной попытке использовать out
. Даже если подумать, что это не сработает, я узнал, что обычно вы не выставляете Setters для свойств Collection. Это не исправить, а также не очень важно для вопроса, но я думаю, стоит упомянуть.
Второй полный пример. Здесь я использую out
и IReadOnlyCollection
. Я удалил описания класса (уже видимые в предыдущем примере), чтобы сделать его короче.
public interface IMixableDataset<out TData>
{
IReadOnlyCollection<TData> Data { get; }
}
public class LineChartDataset<TData> : IMixableDataset<TData>
{
public IReadOnlyCollection<TData> Data { get; } = new List<TData>();
}
public class LineChartData
{
public HashSet<IMixableDataset<object>> Datasets { get; } = new HashSet<IMixableDataset<object>>();
}
public class LineChartConfig
{
public LineChartData ChartData { get; } = new LineChartData();
}
public class Demo
{
public void DesiredUseCase()
{
LineChartConfig config = new LineChartConfig();
IMixableDataset<int> intDataset = new LineChartDataset<int>();
// since it's ReadOnly, I of course can't add anything so this yields a compiler error.
// For my use case, I do need to be able to add items to the list.
intDataset.Data.AddRange(new[] { 1, 2, 3, 4, 5 });
config.ChartData.Datasets.Add(intDataset);
// the above line yields following compiler error (which fairly surprised me because I thought I correctly used out):
// cannot convert from 'Demo.IMixableDataset<int>' to 'Demo.IMixableDataset<object>'
}
}
Итак, вопрос:
Есть ли в любом случае изменчивая и ковариантная коллекция?
Если нет, есть ли обходной путь или что-то, что я могу сделать для достижения этой функциональности?
Дополнительные материалы:
- Я использую новейшую версию всего (ядро .net, VS, Blazor, C #). Поскольку библиотека является .NET Standard, я все еще на C # 7.3.
- В репо под WebCore / Pages / FetchData вы можете прекрасно увидеть, чего я хочу достичь (см. Комментарии в конце файла).