首页 > 解决方案 > 创建和使用协变和可变列表(或潜在的解决方法)

问题描述

我目前正在修改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- 它可能是intstring或完全不同的东西。这些LineDatasets 应该具有的唯一关系是它们是LineDatasets,而不是它们包含的类型。

然后我了解了协方差和逆变(out 和 in-keyword)。我试着做协变TDataIMixableDataset但因为ListIList/ICollection都是不变的,我无法说服。
我还阅读了IReadOnlyCollection<>哪些是协变的,但我不能使用它,因为我必须能够在创建后修改列表。

我也尝试过使用隐式/显式运算符进行转换LineChartDataset<whatever>LineChartDataset<object>但这有一些问题:

如果有一种方法可以将更具体的一个转换为另一个,同时保留实例并且不必为每个属性编写代码,这可能是一个解决方案。

重现我得到的错误并显示问题的完整示例:

// 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. 即使认为这没有成功,我也了解到您通常不会为 Collection-properties 公开 Setter。这不是解决方法,对于这个问题也不是很重要,但我认为值得一提。

第二个完整的例子。我在这里使用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>'
    }
}

所以问题是:
无论如何有一个可变和协变的集合?
如果没有,是否有解决方法或我可以做些什么来实现此功能?

额外的东西:

标签: c#genericscovariance

解决方案


更仔细地查看您的示例,我发现了一个主要问题:您正试图将值类型(例如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>。这提供了一种类型安全的方法来改变集合。请注意,想要改变集合的代码必须知道正确的类型,绕过方差限制。

当然, variant 接口IMixableDataset<TData>本身不能包含添加内容的方法,因为这不是类型安全的。您可以将您LineChartDataset<string>视为 a IMixableDataset<object>,然后如果您可以通过该接口添加东西,您就可以将一些其他类型的对象,甚至是像装箱值这样的非引用类型添加int到您的集合中只包含string对象。

但是,正如不变量List<T>可以实现协变一样IReadOnlyCollection<T>,您的具体LineChartDataset<TData>类可以实现IMixableDataset<TData>同时仍然提供添加项目的机制。这是因为虽然具体类型决定了对象实际上可以做什么,但接口只是定义了引用的用户必须遵守的契约,允许编译器在使用接口时确保类型安全,即使以变体方式使用也是如此. (不变的具体类型也确保了类型安全,但只是因为类型必须完全匹配,这当然更具限制性/不太灵活。)

如果您不介意object为基于值类型的集合使用任何特定的值类型,那么上述方法将起作用。这有点笨拙,因为任何时候您实际上想要获取值类型值,您都需要检索它们object,然后根据需要进行转换以实际使用它们。但至少更广泛的变体方法会成功,并且不需要对任何引用类型进行特殊处理。


另外:C# 中的类型变化仅限于引用类型是基于类型变化不会影响运行时代码的实用要求。这只是一个编译时类型转换。这意味着您必须能够复制引用。要支持值类型,需要在原本不存在的地方添加新的装箱和拆箱逻辑。它也不是很有用,因为值类型没有引用类型可以拥有的丰富程度的类型继承(值类型只能继承object,所以变体场景通常没那么有用和有趣)。


推荐阅读