创建和使用协变和可变列表(或可能的解决方法)

时间:2019-06-23 14:32:40

标签: c# generics covariance

我当前正在修改Blazor library,并且当前状态的源代码可用on 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。我首先也使这个通用,然后继续进行直到达到最高级别(请参阅我的master分支的当前状态)。但是,我后来想更改此设置,因为我想支持具有不同类型值的多个数据集。因此,我恢复了数据集“之上”所有类中的通用内容,现在LineChartData看起来像这样(简化):

public class LineChartData
{
    // HashSet to avoid duplicates
    public HashSet<LineChartDataset<object>> Datasets { get; }
}

我决定使用LineChartDataset<object>,因为:由于一切都可以铸成对象,(在我看来)XYZ<Whatever>也应该铸成XYZ<object>,但是据我了解,这并不是案子。

where关键字无济于事,因为我不想强制TData来使关系与object分开-它可能是intstring或完全不同的东西。这些LineDataset应该具有的唯一关系是它们是LineDataset,而不是它们包含的类型。

然后,我了解了协方差和逆方差(关键字外和关键字内)。我尝试将TData中的IMixableDataset设为协变,但由于ListIList / ICollection都是不变的,所以我无法说服。
我还读到了IReadOnlyCollection<>的协变信息,但是我不能使用它,因为创建后我必须能够修改列表。

我还尝试使用隐式/显式运算符将LineChartDataset<whatever>转换为LineChartDataset<object>,但这有一些问题:

  • 自创建新实例以来,我需要存储和使用新实例而不是原始实例来添加项目,从而完全破坏了原始实例的类型安全性。
  • 由于LineChartDataset中还有更多属性,因此我也必须克隆所有这些属性。

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

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

// 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 for Collection-properties。这不是固定的,对这个问题也不是很重要,但我认为值得一提。

第二个完整示例。在这里,我正在使用outIReadOnlyCollection。我删除了该类的描述(在上一个示例中已经可见)以使其更短。

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 个答案:

答案 0 :(得分:2)

更仔细地查看您的示例,我发现一个主要问题:您试图将值类型(例如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>,然后,如果可以通过该接口添加内容,则可以添加其他类型的对象,甚至是非引用类型就像一个盒装的int值,应该应该只包含string个对象。

但是,就像不变式List<T>可以实现协变IReadOnlyCollection<T>一样,具体的LineChartDataset<TData>类可以实现IMixableDataset<TData>,同时仍然提供添加项的机制。之所以行之有效,是因为尽管具体类型决定了对象可以实际执行的操作,但是接口仅定义了引用用户必须遵守的协定,从而允许编译器确保使用接口的类型安全,即使使用变体方式也是如此。 。 (不变的具体类型也确保类型安全,但这仅是因为类型必须完全匹配 ,这当然更加严格/较不灵活。)

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


另外:C#中的类型差异仅限于引用类型是基于务实的要求,即类型差异不会影响运行时代码。这只是一个编译时类型转换。这意味着您只能复制参考。要支持值类型,将需要在不存在的情况下添加新的装箱和拆箱逻辑。它也不是那么有用,因为值类型没有引用类型可以拥有的丰富程度的类型继承(值类型只能继承object,因此变体场景在这里的用处和趣味大大降低了)一般)。