Создать и использовать ковариантный и изменяемый список (или потенциальный обходной путь) - PullRequest
2 голосов
/ 23 июня 2019

В настоящее время я изменяю библиотеку 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 вы можете прекрасно увидеть, чего я хочу достичь (см. Комментарии в конце файла).

1 Ответ

2 голосов
/ 24 июня 2019

При более внимательном рассмотрении вашего примера я вижу одну серьезную проблему: вы пытаетесь задействовать типы значений (например, int) в дисперсии типов. Что бы там ни было, дисперсия типа C # применяется только к ссылочным типам.

Так что, нет, извините, но совершенно невозможно сделать именно то, что вы просите. Вы должны будете представлять все коллекции на основе типов значений как object, а не как их конкретные типы значений.

Теперь, что касается коллекций ссылочных типов, ваш пример будет работать нормально, с одним незначительным изменением. Вот модифицированная версия вашего второго примера, демонстрирующая его работу, с одним небольшим изменением:

public interface IMixableDataset<out TData>
{
    IReadOnlyCollection<TData> Data { get; }
}

public class LineChartDataset<TData> : IMixableDataset<TData>
{
    private readonly List<TData> _list = new List<TData>();

    public IReadOnlyCollection<TData> Data => _list;

    public void AddRange(IEnumerable<TData> collection) => _list.AddRange(collection);
}

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();

        // Must use reference types to take advantage of type variance in C#
        LineChartDataset<string> intDataset = new LineChartDataset<string>();

        // Using the non-interface method to add the range, you can still mutate the object
        intDataset.AddRange(new[] { "1", "2", "3", "4", "5" });

        // Your original code works fine when reference types are used
        config.ChartData.Datasets.Add(intDataset);
    }
}

В частности, обратите внимание, что я добавил метод AddRange() в ваш класс LineChartDataset<TData>. Это обеспечивает безопасный для типа способ изменить коллекцию. Обратите внимание, что код, который хочет изменить коллекцию, должен знать правильный тип, минуя ограничения на отклонения.

Вариант интерфейса IMixableDataset<TData> сам по себе, конечно, не может включать в себя способ добавления вещей, потому что это не было бы безопасно для типов. Вы сможете обработать свой LineChartDataset<string> как IMixableDataset<object>, а затем, если вы сможете добавлять вещи через этот интерфейс, вы сможете добавить какой-либо другой тип объекта, даже не относительный тип, такой как в штучной упаковке * Значение 1019 * для вашей коллекции, которая должна содержать только string объектов.

Но так же, как инвариант List<T> может реализовать ковариант IReadOnlyCollection<T>, ваш конкретный класс LineChartDataset<TData> может реализовать IMixableDataset<TData>, в то же время предоставляя механизм добавления элементов. Это работает, потому что, хотя конкретный тип определяет, что объект может на самом деле делать, интерфейсы просто определяют контракт, который должны соблюдать пользователи ссылки, позволяя компилятору обеспечивать безопасность типов при использовании интерфейса, даже при использовании по-разному. , (Инвариантный конкретный тип также обеспечивает безопасность типа, но только потому, что тип должен соответствовать точно , что, конечно, более ограничительно / менее гибко.)

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


Кроме того: эта дисперсия типа в C # ограничена ссылочными типами, основана на прагматическом требовании, чтобы дисперсия типа не влияла на код времени выполнения. Это просто преобразование типов во время компиляции. Это означает, что вы должны иметь возможность просто копировать ссылки вокруг. Для поддержки типов значений потребуется добавить новую упаковочную логику и логику распаковки, где в противном случае ее не было бы. Это также не так полезно, потому что типы значений не обладают той же богатой степенью наследования типов, которую могут иметь ссылочные типы (типы значений могут только когда-либо наследовать object, поэтому вариантные сценарии гораздо менее полезны и интересны, в общем) .

...