0

I'm using a C# data visualization library which makes heavy use of generics. Our application needs to build these charts programtically, including changing the series of the chart at runtime. The code looks like this:

builder.Series(series =>
{
    if (seriesDefinition.Type == ChartType.Bar) {
        series.Bar(data).Name(seriesDefinition.Name);
    }
    else if (seriesDefinition.Type == ChartType.Area) {
        series.Area(data).Name(seriesDefinition.Name);
    }
})

Where the Bar() and Area() calls are library methods, and the seriesDefinition object is our own class which contains configuration information about the series we want to create.

Now, our series definition object is going to have many properties besides just the name, and all of them will need to be applied to the series in the builder. To avoid a massive amount of code duplication, we want to store the result of the Bar() or Area() etc. call, so that we can apply the rest of the configurations once outside of the if-else blocks. To do that, we need to declare its specific type. Working backward from the derived return types of those library methods, we get to this parent type which has the methods we need:

public abstract class ChartSeriesBuilderBase<TSeries, TSeriesBuilder> : IHideObjectMembers
    where TSeries : IChartSeries
    where TSeriesBuilder : ChartSeriesBuilderBase<TSeries, TSeriesBuilder>
{
}

Now I need to declare it with appropriate type arguments:

builder.Series(series =>
{
    ChartSeriesBuilderBase<IChartSeries, ???> seriesBuilder = null; // <--
    if (seriesDefinition.Type == ChartType.Bar) {
        seriesBuilder = series.Bar(data);
    }
    else if (seriesDefinition.Type == ChartType.Area) {
        seriesBuilder = series.Area(data);
    }
    seriesBuilder.Name(seriesDefinition.Name);
})

What can I replace ??? with that satisfies this type declaration?

Edit: Here is the class hierarchy starting with the return types of Bar() or Area().

// Area() returns this; Bar() returns ChartBarSeriesBuilder
public class ChartAreaSeriesBuilder<T> :
    ChartAreaSeriesBuilderBase<IChartAreaSeries, ChartAreaSeriesBuilder<T>>
    where T : class
{
}

T is the type of the data item for the series, which I have control over. It's inferred when I pass in an object of that type to Area().

// or ChartBarSeriesBuilderBase
public abstract class ChartAreaSeriesBuilderBase<TSeries, TSeriesBuilder> :
    ChartSeriesBuilderBase<TSeries, TSeriesBuilder>
    where TSeries : IAreaSeries
    where TSeriesBuilder : ChartAreaSeriesBuilderBase<TSeries, TSeriesBuilder>
{
}

and finally

public abstract class ChartSeriesBuilderBase<TSeries, TSeriesBuilder> : IHideObjectMembers
    where TSeries : IChartSeries
    where TSeriesBuilder : ChartSeriesBuilderBase<TSeries, TSeriesBuilder>
{
}
ArrowCase
  • 209
  • 1
  • 8
  • 1
    That would depend on what `Bar()` and `Area()` return. – Jeroen Mostert Mar 07 '18 at 16:16
  • @JeroenMostert There are a lot more generics and builders and bases involved. But how do those types help me make a decision about the less derived type? The point is that the `ChartSeriesBuilderBase` is the first type we come to in the hierarchy that isn't specifically a `Bar` or `Area` or `Pie` type. – ArrowCase Mar 07 '18 at 16:27
  • Well, you just can't do that. – Evk Mar 07 '18 at 16:42
  • I'm also confused quite how anything could ever satisfy that generic constraint. Can you post the class definition from a class that derives from `ChartSeriesBuilderBase` to suggest how the library uses that class itself? – Trevor Mar 07 '18 at 17:25
  • Check out the `Tree where T : Tree` example from here https://stackoverflow.com/a/25166761/8915494 although I'm still trying to get my head around it. – Trevor Mar 07 '18 at 17:28
  • @ArrowCase: no, the point is that if you get to the point where such code can even be written, then `Bar()` and `Area()` have return types that allow you to know what the type of `seriesBuilder` should be. I'm a simple man: for `x = foo()` to compile, the type of `x` must be compatible with the type of `foo()`, so let's use that knowledge. (If you *can't* get to the point where such code can be written, then there's your answer.) – Jeroen Mostert Mar 07 '18 at 17:51

1 Answers1

1

tl;dr You can't unless ChartSeriesBuilderBase implements some other non-generic interfaces.


You can create a class which inherits from ChartSeriesBuilderBase and then instantiate it and this works, as follows:

public class MyBuilder : ChartSeriesBuilderBase<IChartSeries, MyBuilder>
{
}

It's a bit confusing, but as MyBuilder is being defined, it is able to reference itself as the generic type for its base class.

I would imagine that Bar and Area are also defined in a similar manner and so reference themselves in their own generic definition. As Bar and Area inherit from ChartSeriesBuilderBase, we can declare the following variables:

ChartSeriesBuilderBase<IChartSeries, Bar> myBarBuilder;
ChartSeriesBuilderBase<IChartSeries, Area> myAreaBuilder;

But again, as these variables are typed to either Bar or Area, they don't help us to create a variable capable of containing both object types.

Now consider this similar, but simplified situation:

public class MyGeneric<T>
{
}

void q49156521()
{
    var myObject = new MyGeneric<string>();
    var myOtherObject = new MyGeneric<int>();
}

While both variables are MyGenerics, there is no base class that I can use to define a variable which would be able to contain both myObject and myOtherObject because they differ by their generic type.

Note that many generic classes allow you to solve this exact problem by implementing non-generic interfaces. For example, such asList<T> implements IList so therefore it is possible to use an IList variable to contain both a List<string> and a List<int>. However, it looks like ChartSeriesBuilderBase has no such interface that could be used.

So I'm afraid it looks like the answer is, you can't.

I had to reference another question here on SO to even know this was a thing: https://stackoverflow.com/a/25166761/8915494

Hope this helps

Trevor
  • 1,251
  • 1
  • 9
  • 11
  • Thank you for the thorough explanation of both why it's possible for the derived types to work, and why declaring a base type doesn't work. The lack of a non-generic interface implementation is exactly the issue. – ArrowCase Mar 07 '18 at 18:22