Reputation: 5009
How can I restructure my code to get rid of the runtime error happening at the point indicated?
DataSeries<SimpleDataPoint>
needs to be able to cast back to IDataSeries<IDataPoint>
somehow
I have tried using inheritance of two interfaces, like this:
public class DataSeries<TDataPoint> : IDataSeries<TDataPoint>, IDataSeries<IDataPoint>
but received compiler error:
'DataSeries<TDataPoint>'
cannot implement both
'IDataSeries<TDataPoint>'
and
'IDataSeries<IDataPoint>'
because they may unify for some type parameter substitutions
Using covariance doesn't seem to be an option because I can't make the interfaces covariant or contravariant.
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();
}
}
}
Upvotes: 4
Views: 234
Reputation: 6924
This minimal outline of your original code demonstrates that the issue can be fixed by making TDataPoint
covariant in the IDataSeries
interface declaration:
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 { }
}
Upvotes: -1
Reputation: 5009
So my question got me thinking about code smells, and things like "What am I really trying to achieve?"
Well, here's what I figured out I want to achieve: I want to convert DataSeries<TDataPoint>
to IReadOnlyDataSeries<IDataPoint>
only when I am passing it as an input to a class that processes read-only data from a IReadonlyDataSeries<IDataPoint>
object.
Here's the important change made:
// 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);
}
Here's the full version of the revised code:
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();
}
}
}
Upvotes: 2
Reputation: 34293
The problem is that new DataSeries<SimpleDataPoint>
is not IDataSeries<IDataPoint>
, because calling IDataSeries<IDataPoint>.Value = new AnotherDataPoint()
and IDataPoint value = IDataSeries<IDataPointBase>.Value
can fail. That is, runtime can't guarantee that what you're doing is type-safe, so it throws an exception to tell you that. Runtime can only guarantee one of these operations to be safe only if your interface is marked as covariant or contravariant. It's marked as neither, so it isn't type-safe, so it can't be done.
If you intend to bypass type safety, you can create an unsafe proxy:
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);
}
}
You can use this proxy like this:
IDataSeries<IDataPoint> ds = new DataSeries<SimpleDataPoint>(source).GetUnsafeProxy();
Note that the last two methods use type casting, so calling them is not safe, they can throw in case of incompatible types. If want to cast DataSeries
not only to base types, but to other types too, you'll have to add even more type casts to the unsafe proxy and lose even more type safety. The choice is yours.
Upvotes: 2