1

I am using the Range<T> class described in this answer.

And I created the following sub-class in order to create a range of a specific type:

public class IntRange : Range<Int32>
{
    public static List<IntRange> ParseRanges(string ranges)
    {
        //...
    }
}

In order to finish the ParseRanges function, I need to cast from Range<Int32> to IntRange. This is because I am calling a helper function I added to the Range<T> class:

//Create a range that automatically determines the min/max between two bounds.
public static Range<T> Create(T bound1, T bound2)
{
    if (bound1.CompareTo(bound2) <= 0)
    {
        return new Range<T>() { Minimum = bound1, Maximum = bound2 };
    }
    else
    {
        return new Range<T>() { Minimum = bound2, Maximum = bound2 };
    }
}

The method calls look like the following:

IntRange range = (IntRange)Create(2, 0);

First of all, I understand that I cannot legally make this cast because it is possible for another person to create a class:

public class VerySpecialIntRange : Range<Int32> { /* ... */ }

And it isn't possible for the compiler to know if Range<Int32> should be an IntRange or a VerySpecialIntRange, hence the error it spits out when I try to cast.

How can I make this cast so that I do not need to roll out the Create function for every subclass? Sure, I have my own source code and can do this with relative ease, but if the Range<T> is a part of some closed third-party library, and I need to subclass it, I cannot easily determine how to properly implement the Create function.

Community
  • 1
  • 1
Nicholas Miller
  • 4,205
  • 2
  • 39
  • 62
  • Why do you need `IntRange` in the first place? Just to contain the static method `ParseRanges`? Did you add any new instance members to `IntRange`? – Yacoub Massad Oct 28 '15 at 23:13
  • 1
    Rather than create a `Create` method for every derived `Range` class, **don't create derived `Range` classes. Deal exclusively with `Range`. Seal it even. Create a static method in another class that can take a `string` and return a `List>` if you want to; but you're getting nothing but trouble out of inheriting `Range` like this. – Servy Oct 28 '15 at 23:14
  • 1
    Oh, and it's probably best for `Range` to be immutable, rather than mutable. It's all the more true if you want to maintain invariant, such as that the maximum is actually larger. If it's mutable someone can violate that invariant. – Servy Oct 28 '15 at 23:15
  • @Servy Thanks. I never considered immutability. By the way, it might be I should seal the class in this example, but surely there are other instances where subclassing is acceptable, right? [The MSDN generics article](https://msdn.microsoft.com/en-us/library/ms379564(v=vs.80).aspx#csharp_generics_topic6) even show's its valid, so I assume there must be a use case. – Nicholas Miller Oct 29 '15 at 13:24
  • 1
    Yes, there are. I even said as much. It's simply a harmful approach *in this situation*. – Servy Oct 29 '15 at 13:25
  • @YacoubMassad Yes it only contains the helper function. Seemed to be like the most suitable place for it. While that is the example I used, I was trying to focus on the cast. – Nicholas Miller Oct 29 '15 at 13:26

3 Answers3

1

It's a little bit tricky to do, but you basically need to create an abstract factory pattern.

First up I would change the original Range<T> class to have the signature public class Range<T> : Range where T : IComparable<T>.

Now I define the Range class as this:

public class Range
{
    private static Dictionary<Type, Delegate> _factories = new Dictionary<Type, Delegate>();

    public static void AddFactory<T>(Func<T, T, Range<T>> factory) where T : IComparable<T>
    {
        _factories.Add(typeof(T), factory);
    }

    public static Range<T> Create<T>(T bound1, T bound2) where T : IComparable<T>
    {
        Func<T, T, Range<T>> factory =
            _factories.ContainsKey(typeof(T))
                ? (Func<T, T, Range<T>>)_factories[typeof(T)]
                : (Func<T, T, Range<T>>)((n, x) => new Range<T>()
                {
                    Minimum = bound1,
                    Maximum = bound2
                });

        return
            bound1.CompareTo(bound2) <= 0
                ? factory(bound1, bound2)
                : factory(bound2, bound1);
    }
}

What this does is creates a Dictionary<Type, Delegate> that can hold as many different factories that you need to create all of the special range types that you want to define. When you call Create<T> the type of T is used to check if you have a special factory, and, if you do, it uses this factory to create your range, and otherwise it uses the default Range<T> class.

A non-generic Range class is required to make this work. If you try to put the factory code in the Range<T> you'll get a separate _factories instance per T. With the non-generic class you only get one.

You can define a factory like this:

Range.AddFactory<int>((n, x) => new IntRange() { Minimum = n, Maximum = x });

Now you can call your create code like this:

IntRange range = (IntRange)Range.Create<int>(4, 9);

This runs and works perfectly fine.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • I fail to see the real difference in this versus rolling out my own create function. I'm asking for the situation that I must subclass, but rely on the parent to create. But in the factory method, we've told the factory what the logic must be. If we instead try to rely on the default (parent) behavior, we'll be trying to cast `Range` in to an `IntRange` once again. Despite this, thanks for showing me how factories look like. ^^ – Nicholas Miller Oct 29 '15 at 13:38
  • 1
    @NickMiller - This has the difference of separating the mechanism to create from the place that you want to create it. It also allows you to choose at run-time which class you want returned without needing to change the `Range` class. How do you expect your code to be able to differentiate between `IntRange` & `VerySpecialIntRange` without you coding for the correct one? This approach even lets you inject new subclasses from different assemblies into your code. This is a run-time rather than compile-time approach. I don't think any other method will get closer to what you want. – Enigmativity Oct 29 '15 at 22:21
0

You'll need to change your create method to create the correct range:

public class IntRange : Range<Int32>
{
    public static List<IntRange> ParseRanges(string ranges)
    {
        return new[] { Create<IntRange, int>(1, 2) }.ToList();
    }
}

public static TRangeType Create<TRangeType, TItemType>(TItemType bound1, TItemType bound2)
 where TRangeType : Range<TItemType>, new()
 where TItemType : IComparable<TItemType>
{
    if (bound1.CompareTo(bound2) <= 0)
    {
        return new TRangeType { Minimum = bound1 };
    }
    else
    {
        return new TRangeType { Minimum = bound2, Maximum=bound2 };
    }
}
Rob
  • 26,989
  • 16
  • 82
  • 98
  • And of course now you've lost the ability to infer the types of `Create`, making it quite cumbersome to use. And if `Range` becomes immutable (which it really should) then this ceases to become an option. – Servy Oct 28 '15 at 23:14
0

Use constructors instead of a static method, which also allows you to make the setters of the Minimum and Maximum properties protected/private, as they really should be (should a user be able to change their values, ultimately redefining the object after instantiation?). Pass the parameters to the base constructor when the default logic is sufficient, use the default base() constructor when you want to override the logic in your derived classes.

public class Range<T> 
{
    Range() { }
    Range( T bound1, T bound2 ) { /* your static method logic here */ }
}

public class IntRange : Range<int>
{
    IntRange( int bound1, int bound2 ) 
        : base( bound1, bound2 )
    { }

    IntRange( int b1, int b2, bool someExampleFlag )
    {
        // custom logic here
    }
}
Moho
  • 15,457
  • 1
  • 30
  • 31