【问题标题】:Inheritance and Casting error (generic interfaces)继承和转换错误(通用接口)
【发布时间】:2015-05-21 19:09:50
【问题描述】:

如何重组我的代码以消除在指示点发生的运行时错误?

DataSeries<SimpleDataPoint> 需要能够以某种方式转换回 IDataSeries<IDataPoint>

我尝试过使用两个接口的继承,如下所示:

public class DataSeries<TDataPoint> : IDataSeries<TDataPoint>, IDataSeries<IDataPoint> 但收到编译器错误:

'DataSeries<TDataPoint>' 不能同时实现

'IDataSeries<TDataPoint>'

'IDataSeries<IDataPoint>' 因为它们可能会针对某些类型参数替换统一

使用协变似乎不是一种选择,因为我无法使接口协变或逆变。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1 {

class Program {

    [STAThread]
    static void Main(string[] args) {

        var source = new object();

        // compiles fine, but ...
        // runtime error here - cannot cast
        var ds = (IDataSeries<IDataPoint>)new DataSeries<SimpleDataPoint>(source);

        Console.ReadKey();
    }
}

public interface IDataPoint {
    int Index { get; set; }
    double Value { get; set; }
    DateTime TimeStampLocal { get; set; }
    IDataPoint Clone();
}

public sealed class SimpleDataPoint : IDataPoint {
    public int Index { get; set; }
    public double Value { get; set; }
    public DateTime TimeStampLocal { get; set; }
    public IDataPoint Clone() {
        return new SimpleDataPoint {
            Index = Index,
            Value = Value,
            TimeStampLocal = TimeStampLocal,
        };
    }
}

public interface IDataSeries<TDataPoint> where TDataPoint : class, IDataPoint {
    object Source { get; }
    int Count { get; }
    double GetValue(int index);
    DateTime GetTimeStampLocal(int index);
    TDataPoint GetDataPoint(int index);
    TDataPoint GetLastDataPoint();
    void Add(TDataPoint dataPoint);
    IDataSeries<TDataPoint> Branch(object source);
}

public class DataSeries<TDataPoint> : IDataSeries<TDataPoint> where TDataPoint : class, IDataPoint {

    readonly List<TDataPoint> _data = new List<TDataPoint>();

    public object Source {
        get;
        private set;
    }

    public DataSeries(object source) {
        Source = source;
    }

    public int Count {
        get { return _data.Count; }
    }
    public TDataPoint GetDataPoint(int index) {
        return _data[index];
    }
    public TDataPoint GetLastDataPoint() {
        return _data[_data.Count - 1];
    }
    public DateTime GetTimeStampLocal(int index) {
        return _data[index].TimeStampLocal;
    }
    public double GetValue(int index) {
        return _data[index].Value;
    }
    public void Add(TDataPoint dataPoint) {
        _data.Add(dataPoint);
    }

    public IDataSeries<TDataPoint> Branch(object source) {
        throw new NotImplementedException();
    }
}
}

【问题讨论】:

  • 您发布的是编译时错误,而不是强制转换异常。
  • @Yuval,代码编译。我正在寻找运行时转换错误的解决方案。提到的编译错误是一个错误,阻止了我尝试过的一种可能的解决方案。
  • 实际上,我认为存在代码异味,我不仅要修复运行时错误,还要构建没有异味的代码 :)
  • @YuvalItzchakov,感谢您的编辑
  • 您可以创建两个额外的接口。一个协变:IReadOnlyDataSeries&lt;out TDataPoint&gt;,另一个在 TDataPoint 上逆变:IWriteOnlyDataSeries&lt;in TDataPoint&gt;

标签: c# generics casting covariance


【解决方案1】:

问题是new DataSeries&lt;SimpleDataPoint&gt; 不是 IDataSeries&lt;IDataPoint&gt;,因为调用IDataSeries&lt;IDataPoint&gt;.Value = new AnotherDataPoint()IDataPoint value = IDataSeries&lt;IDataPointBase&gt;.Value 可能会失败。也就是说,运行时不能保证你正在做的事情是类型安全的,所以它会抛出一个异常来告诉你。只有当您的接口被标记为协变或逆变时,运行时才能保证这些操作中的一个是安全的。它被标记为两者都不是,因此它不是类型安全的,因此无法完成。

如果你打算绕过类型安全,你可以创建一个不安全的代理:

public class DataSeries<TDataPoint> : IDataSeries<TDataPoint>
    where TDataPoint : class, IDataPoint
{
    // ...

    public IDataSeries<IDataPoint> GetUnsafeProxy ()
    {
        return new UnsafeProxy(this);
    }

    private class UnsafeProxy : IDataSeries<IDataPoint>
    {
        private readonly DataSeries<TDataPoint> _owner;

        public UnsafeProxy (DataSeries<TDataPoint> owner)
        {
            _owner = owner;
        }

        public object Source
        {
            get { return _owner.Source; }
        }

        public int Count
        {
            get { return _owner.Count; }
        }

        public double GetValue (int index)
        {
            return _owner.GetValue(index);
        }

        public DateTime GetTimeStampLocal (int index)
        {
            return _owner.GetTimeStampLocal(index);
        }

        public IDataPoint GetDataPoint (int index)
        {
            return _owner.GetDataPoint(index);
        }

        public IDataPoint GetLastDataPoint ()
        {
            return _owner.GetLastDataPoint();
        }

        public void Add (IDataPoint dataPoint)
        {
            _owner.Add((TDataPoint)dataPoint);
        }

        public IDataSeries<IDataPoint> Branch (object source)
        {
            return (IDataSeries<IDataPoint>)_owner.Branch(source);
        }
    }

你可以像这样使用这个代理:

IDataSeries<IDataPoint> ds = new DataSeries<SimpleDataPoint>(source).GetUnsafeProxy();

请注意,最后两个方法使用类型转换,因此调用它们是不安全的,它们可以在类型不兼容的情况下抛出。如果不仅要将DataSeries 转换为基本类型,还想转换为其他类型,则必须向不安全代理添加更多类型转换并失去更多类型安全性。选择权在你。

【讨论】:

  • 是的,很棒的答案@Discord,我也在下面粘贴我自己的答案......这是一个很棒的思考练习!
  • 将您的答案标记为“答案”,因为它为所提出的问题提供了精确的解决方案(尽管使用起来很危险)。但是,在我的代码中使用它会很疯狂,因为为未来的开发人员创建维护陷阱会很愚蠢。 (代码味道)
  • 我在下面的答案(创建“只读”数据序列)是我将在我的解决方案中使用的......事实上,将输入序列声明为“只读”真的很酷,因为它使代码的意图超清晰
【解决方案2】:

所以我的问题让我想到了代码异味,以及诸如“我真正想要实现的目标是什么?”

好吧,这就是我想实现的目标:我只想将DataSeries&lt;TDataPoint&gt; 转换为IReadOnlyDataSeries&lt;IDataPoint&gt;,仅当我将它作为输入传递给处理来自IReadonlyDataSeries&lt;IDataPoint&gt; 对象的只读数据的类时.

以下是所做的重要更改:

// here's the covariant, read-only part of the interface declaration
public interface IReadOnlyDataSeries<out TDataPoint> where TDataPoint : class, IDataPoint {
    object Source { get; }
    int Count { get; }
    double GetValue(int index);
    DateTime GetTimeStampLocal(int index);
    TDataPoint GetDataPoint(int index);
    TDataPoint GetLastDataPoint();
}

// add a few bits to the read-write fully-typed interface, breaking covariance,
// but being able to implicitly cast to the covariant readonly version when needed
public interface IDataSeries<TDataPoint> : IReadOnlyDataSeries<TDataPoint> where TDataPoint : class, IDataPoint {
    void Add(TDataPoint dataPoint);
    IDataSeries<TDataPoint> Branch(object source);
}

这是修改后的代码的完整版本:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1 {

class Program {

    [STAThread]
    static void Main(string[] args) {

        var source = new object();

        // implicit conversion works great!!
        // therefore I can achieve the goal of passing the fully-typed read-write dataseries
        // into objects that just want simple read-only data
        var inputSeries = new DataSeries<SimpleDataPoint>(source);

        // passing inputSeries into the constructor involves an implicit
        // conversion to IReadOnlyDataSeries<IDataPoint>
        var processor = new DataProcessor(inputSeries);

        Console.ReadKey();
    }

    public class DataProcessor {

        IReadOnlyDataSeries<IDataPoint> InputSeries;
        DataSeries<SimpleDataPoint> OutputSeries;

        public DataProcessor(IReadOnlyDataSeries<IDataPoint> inputSeries) {
            InputSeries = inputSeries;
            OutputSeries = new DataSeries<SimpleDataPoint>(this);
        }
    }
}

public interface IDataPoint {
    int Index { get; set; }
    double Value { get; set; }
    DateTime TimeStampLocal { get; set; }
    IDataPoint Clone();
}

public sealed class SimpleDataPoint : IDataPoint {
    public int Index { get; set; }
    public double Value { get; set; }
    public DateTime TimeStampLocal { get; set; }
    public IDataPoint Clone() {
        return new SimpleDataPoint {
            Index = Index,
            Value = Value,
            TimeStampLocal = TimeStampLocal,
        };
    }
}

// here's the covariant, read-only part of the interface declaration
public interface IReadOnlyDataSeries<out TDataPoint> where TDataPoint : class, IDataPoint {
    object Source { get; }
    int Count { get; }
    double GetValue(int index);
    DateTime GetTimeStampLocal(int index);
    TDataPoint GetDataPoint(int index);
    TDataPoint GetLastDataPoint();
}

// add a few bits to the read-write fully-typed interface, breaking covariance,
// but being able to implicitly cast to the covariant readonly version when needed
public interface IDataSeries<TDataPoint> : IReadOnlyDataSeries<TDataPoint> where TDataPoint : class, IDataPoint {
    void Add(TDataPoint dataPoint);
    IDataSeries<TDataPoint> Branch(object source);
}

public class DataSeries<TDataPoint> : IDataSeries<TDataPoint> where TDataPoint : class, IDataPoint {

    readonly List<TDataPoint> _data = new List<TDataPoint>();

    public object Source {
        get;
        private set;
    }

    public DataSeries(object source) {
        Source = source;
    }

    public int Count {
        get { return _data.Count; }
    }
    public TDataPoint GetDataPoint(int index) {
        return _data[index];
    }
    public TDataPoint GetLastDataPoint() {
        return _data[_data.Count - 1];
    }
    public DateTime GetTimeStampLocal(int index) {
        return _data[index].TimeStampLocal;
    }
    public double GetValue(int index) {
        return _data[index].Value;
    }
    public void Add(TDataPoint dataPoint) {
        _data.Add(dataPoint);
    }

    public IDataSeries<TDataPoint> Branch(object source) {
        throw new NotImplementedException();
    }

}
}

【讨论】:

    【解决方案3】:

    原始代码的这个最小轮廓表明可以通过在IDataSeries 接口声明中使TDataPoint 协变来解决该问题:

    using System;
    
    namespace ConsoleApplication1
    {
        class Program
        {
            [STAThread]
            static void Main(string[] args)
            {
                var ds = (IDataSeries<IDataPoint>)new DataSeries<SimpleDataPoint>();
    
                Console.ReadKey();
            }
        }
    
        public interface IDataPoint { }
    
        public sealed class SimpleDataPoint : IDataPoint { }
    
        public interface IDataSeries<out TDataPoint> where TDataPoint : class, IDataPoint { }
    
        public class DataSeries<TDataPoint> : IDataSeries<TDataPoint> where TDataPoint : class, IDataPoint { }
    }
    

    【讨论】:

    • 感谢@Reg 编辑,但是,如果您阅读 IDataSeries 接口的主体,您会发现由于 Add(TDataPoint dataPoint) 方法,它不能是协变的。如果您阅读了我在上面发布的答案,您会看到我提取了数据序列的“只读”部分并使该协变。再次感谢!!非常感谢所有的帮助
    • @bboyle1234--我知道。但是,Add() 方法的问题是稍后才出现的一个细节(或者您当然会首先问一个特定的问题)。 Add() 冲突本身可以作为一个问题以多种方式解决——正如您现在所做的那样,在此过程中接收到一些非常有创意的输入。不过,在原始问题的背景下,以及它对 SO 具有的任何持久价值,我认为表明您首次提出的类、接口和泛型类型参数的排列没有根本问题仍然很有用。
    猜你喜欢
    • 1970-01-01
    • 2012-09-04
    • 1970-01-01
    • 2011-01-05
    • 1970-01-01
    • 1970-01-01
    • 2022-01-25
    • 1970-01-01
    • 2014-10-18
    相关资源
    最近更新 更多