2

Let's say I have following classes:

public class Animal {
  public string name;
  public int age;
  public bool canBark;
}

public class Dog : Animal {
  }

And instantiate an Animal like this:

Animal a = new Animal();

Is there anyway of downconverting this instance to a Dog instance after it's created? Or am I forced to recognize that my Animal is a Dog when I create it? The reason I'm having this problem is because my instance is created by calling a factory class that returns a type based on a JSON file that I feed it. The canBark attribute value is calculated by looking at several seemingly unrelated fields in that file. It seems it make more sense to determine that something is a Dog by looking at the canBark field, rather than those JSON fields.

public class AnimalFactory{
  public static Animal Create(JObject json){
    Animal a = new Animal();
    a.canBark = (json["someField"]["someOtherField"].ToString() == "abc")
    .....
}

I kind of solve this problem right now by having a constructor in the Dog class that takes an Animal as its argument and simply assigns the values of that instance's variables to its own. But this is not a very flexible way as I would need to add a line in each subclass constructor everytime I add a field.

So, is the best way of determining the type by moving that decision to the instance creation or is there some other way?

wasmachien
  • 969
  • 1
  • 11
  • 28

4 Answers4

5

Is there anyway of downconverting this instance to a Dog instance after it's created?

No. The type of an object can never be changed after construction, in either Java or .NET. You'll need to change your factory code to create an instance of the right class. It's not immediately clear how the canBark part relates to the rest of the question, but fundamentally you need to have somewhere that decides what kind of object to create based on the JSON content - or change your design so it can use the same object type in all cases.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • Followup question - could you write the classes so that `Dog fido = (Dog)myAnimal;` would work? I'm pretty sure you can't but I'm having a moment and can't pin down why. – Sam W Oct 30 '18 at 19:37
  • 2
    If `myAnimal` is allocated as an `Animal`, you can't for two reasons: subclasses can be larger than base classes (but you allocated memory just for the base class) and the virtual table pointers are pointing to `Animal` functions, not `Dog` functions. – Blindy Oct 30 '18 at 19:43
  • "or change your design to not need to use the same object type in all cases." Or change the code so that it always can use the same type in all cases. If you handle the differences between different animals in some way other than polymorphism (which may or may not be appropriate depending on the specifics of the problem space) the whole problem goes away. – Servy Oct 30 '18 at 19:44
  • @Servy: Yes, that was what I meant - I'm slightly woozy right now! Edited. – Jon Skeet Oct 30 '18 at 20:02
2

You can have a factory method having an Animal return type but creating and returning a Dog. But you cannot convert an Animal to a Dog after its creation.

Note also that a true conversion and a type cast are not the same thing. Even if the cast operator is the same for both in C#. If you have a variable Animal a = new Dog();, then you can do the valid cast Dog d = (Dog)a;. But a must reference a Dog object. If it was assigned a Cat, the casting would throw an exception.

The best you can do, if you really want to convert an Animal to a Dog, is to add a constructor to Dog accepting an animal as argument (C#):

public Dog(Animal a)
{
    name = a.name;
    age = a.age;
    canBark = true;
}

Then you can create a dog with:

Animal a = new Animal();
// Initialize animal

Dog dog = new Dog(a);

But you cannot do it without creating a new object. Therefore, create the right type from the beginning in the factory:

public class AnimalFactory
{
    public static Animal Create(JObject json)
    {
        string animalType = Get animal type string from json;

        Animal a;
        switch (animalType) {
            case "dog":
                var dog = new Dog();
                // Fill dog specific stuff.
                a = dog;
                break;
            case "cat":
                var cat = new Cat();
                // Fill cat specific stuff.
                a = cat;
                break;
            default:
                return null;
        }
        // Fill stuff common to all animals into a.
        return a;
    }
}

It also probably makes no sense to instantiate the Animal class directly. Therefore I would declare it as abstract.


You could also have constructors with a JObject parameter in the animal classes to delegate the initialization of the fields.

public abstract class Animal
{
    public Animal(JObject json)
    {
        // Initialize common fields.
    }

    public string name;
    public int age;
    public bool canBark;
}

public class Dog : Animal
{
    public Dog(JObject json)
        : base(json) // Pass the json object to the Animal constructor
    {
        // Initialize dog specific fields.
    }
}

The factory then becomes

public class AnimalFactory
{
    public static Animal Create(JObject json)
    {
        string animalType = Get animal type string from json;
        switch (animalType) {
            case "dog":
                return new Dog(json);
            case "cat":
                return new Cat(json);
            default:
                return null;
        }
    }
}
Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
  • Not that you can't write a conversion from `Animal` to `Dog` - conversion operators can't convert from a base class to a derived class or vice versa. – Jon Skeet Oct 30 '18 at 20:03
  • Yes, I've added a reference to the `explicit` operator and some more explanation. – Olivier Jacot-Descombes Oct 30 '18 at 20:05
  • I don't see it - you've still got "As it is explained here explicit (C# Reference), you could declare a user-defined type conversion" but that's just not true if `Dog` is a subclass of `Animal`. You can't write a user-defined type conversion in that situation, so I don't think it's a good idea to even mention it. – Jon Skeet Oct 30 '18 at 20:23
  • Uhhh. I did not know about this restriction. I will reword it. Thank you for pointing it out. – Olivier Jacot-Descombes Oct 30 '18 at 20:52
1

Animal a = new Animal();

You should never do this at all, since in any non-academic environment, Animal is an abstract type.

Is there anyway of downconverting this instance to a Dog instance after it's created?

It should be obvious, but new allocates memory. If you're telling it you're allocating memory for an Animal, but later decide it's actually a Dog, you can't add to the memory allocated.

In other words, you are what you are. If tomorrow you decide you want to be a monkey instead of (presumably) a human, you're not going to literally become a monkey.

Perhaps less obvious if you're not familiar at all with how virtual functions work is that the virtual table is initialized during new, so the compiler needs to know what function pointers to assign at the point of creation.

a type based on a JSON file that I feed it

That's what dictionaries or dynamic objects (ExpandoObject in C#) are for. Or better yet, use proper libraries that handle this for you, like the infamous Newtonsoft JSON library.

Blindy
  • 65,249
  • 10
  • 91
  • 131
0

In languages like Java and C#, the type of an object is determined when it is created. You can declare an Animal reference and then point it at a Dog, but the Dog object itself will always be a Dog. The user of the object may not know its actual type, and it's usually better if it doesn't.

Animal a = new Dog();
a.onOwnerHome();

But you're onto something with your question. The fundamental problem with the Animal examples that you see so often in textbooks is that they tend to be very bad OOP. They model things that really should be data as behavior, and the student misses an opportunity to think about the kinds of things that should be modeled as behavior.

StackOverthrow
  • 1,158
  • 11
  • 23