0

Consider the following classes:

abstract class Cog {}
class BigCog: Cog {}
class SmallCog: Cog {}

abstract class Machine {}
class BigMachine: Machine {}
class SmallMachine: Machine {}

Now all the machines work with cogs, but BigMachine only works with BigCog and SmallMachine only works with SmallCog. I'd like to make this nicely typed, so that I could to this:

Machine machine;
BigMachine bigMachine;
SmallMachine smallMachine;

Cog cog;
BigCog bigCog;
SmallCog smallCog;

bigMachine.cog = bigCog; // OK
smallMachine.cog = smallCog; // OK

bigMachine.cog = smallCog; // Compiler error, type mismatch
smallMachine.cog = bigCog; // Compiler error, type mismatch

machine.cog = cog; // Runtime error if type mismatch
machine.cog = bigCog; // Runtime error if type mismatch
machine.cog = smallCog; // Runtime error if type mismatch

bigMachine.cog.BigCogMethod(); // OK, BigMachine.cog is of type BigCog
smallMachine.cog.SmallCogMethod(); // OK, SmallMachine.cog is of type SmallCog

The only way I can think of achieving this is if I can BOTH override AND shadow the cog property:

abstract class Machine
{
    public virtual Cog cog { get; set; }
}
class BigMachine
{
    public override Cog cog
    {
        get
        {
            return base.cog;
        }
        set
        {
            if ( value != null && !(value is BigCog) )
                throw new ArgumentException();
            base.cog = value;
        }
    }
    new public BigCog cog // Compiler error, member "cog" already declared
    {
        get { return (BigCog)base.cog; }
        set { base.cog = value; }
    }
}

Unfortunately the compiler complains that there already is a class member named cog, so I cannot both override it and shadow at the same time. Is there some other elegant pattern that can solve this dilemma? Note that I do need to act on abstract Machine type variables and do the machine.cog = cog; thing, so making a generic class like class BigMachine: Machine<BigCog> doesn't work.

Vilx-
  • 104,512
  • 87
  • 279
  • 422
  • Is there a reason why cog is a property? Can a machine work without a cog? – Radu Cojocari Jul 03 '17 at 12:37
  • What would you expect to be called when you use the cog property? The shadowed, the overriden or both? – Camilo Terevinto Jul 03 '17 at 12:42
  • @CamiloTerevinto - Well, depends on how it's called. That's how shadowing works. If you call it on a variable of type `BigMachine`, then the shadowed gets called. If you call it on a variable of type `Machine`, then the overriden gets called. Anyways, they both do the same thing so it doesn't matter. – Vilx- Jul 03 '17 at 12:51
  • @stely000 - Yes, it my case, the `cog` property is optional and can be `null`. – Vilx- Jul 03 '17 at 12:54
  • Is there any difference between BigCog and SmallCog?Do they have different interfaces? – Radu Cojocari Jul 03 '17 at 13:34
  • @stely000 - Yes, very different. – Vilx- Jul 03 '17 at 13:43
  • Because they are very different I would try to encapsulate a cog in a method that does make sense for a machine to have (Demeter's Law). Then, instead of calling bigMachine.cog.BigCogMethod(). you can call bigMachine.Method(Cog cog); You will have to write a guard clause at the start of the method to throw a runtime exception if it's not the right cog. You can also break the inheritance chain and shadow that method if you need too but I would not advise that. – Radu Cojocari Jul 03 '17 at 13:54
  • @stely000 - Well, in the real situation, the "cogs" are lean data objects which just have a bunch of different properties pertaining to the "machine". They are assigned to the machine by some generic code that retrieves and then matches them. Afterwards they are only read, never written. – Vilx- Jul 03 '17 at 14:03
  • By the sounds of it, you have to have that cog property on the machine and can't change it. Fine, back to the drawing board :) – Radu Cojocari Jul 03 '17 at 14:14

7 Answers7

2

How about:

abstract class Machine<T> where T:Cog
{
    public virtual T cog { get; set; }
}

class BigMachine : Machine<BigCog>
{

}

You can create generic base class with Constraint on type parameter. This way you will limit Type of cog property to only types that derives from Cog type.

Pablo notPicasso
  • 3,031
  • 3
  • 17
  • 22
  • Because you cannot use the base class with that as the OP already mentioned *Note that I do need to act on abstract Machine type variables and do the machine.cog = cog; thing* and Machine m = new BigMachine() does not work! – Sir Rufo Jul 03 '17 at 12:39
  • 1
    And if `Machine` happens to also have bolts, springs, pistons, belts, etc? Do you think of a machine as a machine *of* cogs or a machine *that has* cogs? This can get way out of hand in no time with generics. – InBetween Jul 03 '17 at 12:45
  • Yes, indeed, as I stated in the question, this generic approach does not work, because I need to do stuff like: `WareHouse.FindMachine(id).cog = WareHouse.FindCog(id);` - and I don't know what kind of a machine and cog I will get, just that they will match (unless, of course, something is very, very wrong, in which case I need a runtime exception). – Vilx- Jul 03 '17 at 12:58
  • If the subtype of the machine is not known at compile time, then obviously there cannot be a compile time type check which assures that you put the right cog into the right machine. – Helmut D Jul 03 '17 at 13:10
  • @HelmutD - Yes, which is why in those cases I want a runtime exception. When it's possible, I'll use compile time checks, when not - runtime. – Vilx- Jul 03 '17 at 13:12
2

If I understand you correct you want RunTime error when cog type does not match? How about using interface + Explicit implementation?

interface IMachine
{
    Cog cog { get; set; }
}

abstract class Machine : IMachine
{
    Cog IMachine.cog { get; set; }
}

class BigMachine : Machine, IMachine
{
    public BigCog cog { get; set; }

    Cog IMachine.cog
    {
        get { return cog; }
        set { cog = (BigCog) value; }
    }
}

class SmallMachine : Machine, IMachine
{
    public SmallCog cog { get; set; }
    Cog IMachine.cog
    {
        get { return cog; }
        set { cog = (SmallCog)value; }
    }
}

Than instead of Machine class use IMachine interface

List<IMachine> list = new List<IMachine>();
list.Add(new BigMachine{cog =  new BigCog()});
list.Add(new SmallMachine{ cog= new SmallCog()});
list[1].cog = new BigCog(); // runtime error. Can not convert BigCog to SmallCog
Pablo notPicasso
  • 3,031
  • 3
  • 17
  • 22
  • Hmm... well, casting a `BigMachine` to `Machine` would allow you to set the wrong kind of cog, even without a runtime error. But, perhaps interfaces can help in my situation. I'll think about it, thanks! – Vilx- Jul 03 '17 at 13:47
1

C# does not support return type variance which makes this whole situation a mess.

I'd take a step back and thoughtfully reconsider if having BigMachine return a BigCog typed Cog property is really worth it.

Let's consider the scenarios:

  1. I have a Machine typed reference. Ok, I can't really leverage a strongly typed property to begin with, machine.Cog will return a Cog typed cog and thats all I can ask for.
  2. I have a BigMachine typed reference. Ok, so now bigMachine returns a Cog typed cog but I know its really a BigCog. I can simply do a cast: bigCog = (BigCog)(bigMachine.Cog) if I really need to. Or better yet, simply implement strongly typed properties that belong exclusively to BigMachine: BigMachine.BigCog for instance.

Also worth noting, is that return type variance does not provide type safety at all. There'd always need to be a runtime type check to make sure no one adds a wrongly typed Cog via a Machine typed reference. This type of set up can always blow up at runtime, you can't statically make it safe (without generics).

So, all this mess really just saves you one cast in a very specific scenario (chances are most of your Machine references are going to be Machine typed to begin with).

There is a reason return type variance is not in the language, and its because, although convenient, its not really an awesome feature. You can live without it just fine. So if the type system is not helping at all, don't fight it, chances are you are overcomplicating things.

InBetween
  • 32,319
  • 3
  • 50
  • 90
0

Oh, and another approach, which I think is even worse than the previous.

abstract class Machine
{
    public virtual Cog cog { get; set; }
}
abstract class BigMachineBase: Machine
{
    public override Cog cog
    {
        get { return base.cog; }
        set
        {
            if ( value != null && !(value is BigCog) )
                throw new ArgumentException();
            base.cog = value;
        }
    }
}

class BigMachine: BigMachineBase
{
    new public BigCog cog
    {
        get { return (BigCog)base.cog; }
        set { base.cog = cog; }
    }
}
Vilx-
  • 104,512
  • 87
  • 279
  • 422
0

This is what I have to offer (not perfect either, but I guess that's the way I would do it):

abstract class Cog { };
class BigCog : Cog { }
class SmallCog : Cog { }

abstract class Machine
{
    public Cog Cog
    {
        get { return GetCog(); }
        set { SetCog(value); }
    }
    protected abstract Cog GetCog();
    protected abstract void SetCog(Cog value);
}
abstract class TypedMachine<T> : Machine where T: Cog
{
    private T typeSafeCog;
    public new T Cog
    {
        get { return typeSafeCog; }
        set { typeSafeCog = value; }
    }
    protected override Cog GetCog()
    {
        return typeSafeCog;
    }
    protected override void SetCog(Cog value)
    {
        if (value is T)
            typeSafeCog = (T)value;
        else
            throw new Exception("type error!");
    }
}
class BigMachine : TypedMachine<BigCog> { }
class SmallMachine : TypedMachine<SmallCog> { }

class Program
{
    static void Main(string[] args)
    {
        var bigCog = new BigCog();
        var smallCog = new SmallCog();
        var bigMachine = new BigMachine();
        bigMachine.Cog = smallCog; // compile error
        bigMachine.Cog = bigCog; // ok
        Machine[] anyMachine = { new SmallMachine(), new BigMachine() };
        anyMachine[0].Cog = smallCog; // ok
        anyMachine[1].Cog = smallCog; // runtime exception
        anyMachine[1].Cog = bigCog; // ok
        Console.ReadKey();
    }
}
Helmut D
  • 630
  • 6
  • 11
-1

Hmm, here's one approach I came up with, although I'm not very happy about it, so I'll see if anything better comes up:

abstract class Machine
{
    protected virtual Cog _cog { get; set; }
    public Cog cog
    {
        get { return this._cog; }
        set { this._cog = value; }
    }
}

class BigMachine: Machine
{
    protected override Cog _cog
    {
        get { return base._cog; }
        set
        {
            if ( value != null && !(value is BigCog) )
                throw new ArgumentException();
            base._cog = value;
        }
    }
    new public BigCog cog
    {
        get { return (BigCog)base._cog; }
        set { base._cog = value; }
    }
}
Vilx-
  • 104,512
  • 87
  • 279
  • 422
  • 1
    C# does not have return type variance, which is a big PIA when it comes to scenarios like yours. The question here is: do you really need `BigMachine` to return `BigCog`? Is all this mess worth saving the consumer *one* cast? Not really. Don't fight the type system, its not worth it in this case. – InBetween Jul 03 '17 at 13:23
  • @InBetween - That's also an option, true, though I do like the extra type safety when it's possible. But maybe you're right. – Vilx- Jul 03 '17 at 13:25
  • but you dont have that safety; anyone downcasting your `BigMachine` can always set a wrong `Cog` that will blow up at runtime, thats why you have to implement the type check safety net. Typesafety is only great if you can get it at compile time. In this scenario you simply can't. – InBetween Jul 03 '17 at 13:32
  • @InBetween - Well, yes, but it is there partially. There are also methods that work explicitly with `BigMachine` only and those would benefit. There's also IntelliSense benefits when typing `bigMachine.cog.xxx`. – Vilx- Jul 03 '17 at 13:48
  • Intellisense works only with static types. If your reference is `BigMachine` then *solve the problem leveraging the fact that you know about it*: write specialized methods that strongly type the overriden implementation: a `BigCog` property for instance. If you *statically know* the type, you *dont need* variance, simply use strongly typed versions of the methods/properties that the base type needs to know nothing about. – InBetween Jul 03 '17 at 13:51
  • @InBetween - It's like this - I have a bunch of different `Machine` objects with their `cog`s set to `null` initially. Then in the background extra data is retrieved and the `cog` instances are assigned to their matching `Machine`s. The retrieving/assigning code doesn't know anything about specific machine/cog types. They all have an `id` field by which they are matched and that's it. Afterwards they are operated on in a strongly-typed manner. – Vilx- Jul 03 '17 at 14:07
  • ... which just made me realize, that the setter is always used as "type-less" and the getter is always used typed. In other words, I could just make an abstract `SetCog(cog)` method in the base `Machine class`, and then strongly typed getters in all the derived classes. And that solves my problem. Still, I think that the theoretical question like stated is also interesting. Or should I delete it? – Vilx- Jul 03 '17 at 14:10
-2

Assuming you don't have to use inheritance this would be another approach:

public class BigMachine
{
    private readonly Machine _machine;

    public BigMachine(Machine machine)
    {
        _machine = machine;
    }

    public BigCog Cog
    {
        get { return (BigCog)_machine.cog; }
        set { _machine.cog = value; }
    }
}

Although I don;t like that cast there.

Radu Cojocari
  • 1,759
  • 1
  • 22
  • 25
  • No, because I cannot cast `BigMachine` to `Machine` and `BigCog` to `Cog` and then work with them without knowing their particular types. – Vilx- Jul 03 '17 at 13:23