Generics preventing virtual function call?

Viewed 81

I'm brushing up on my C# after many years. I'm very rusty so this may be me not the compiler.

Virtual function calls seem to stop working as I'd expect with generics. Here's the code:

namespace ConsoleApp1 {

    public abstract class Root {
        // force me to implenent ToString() in subclasses
        public abstract new string ToString();
    }

    public class Mammal : Root {
        string m;
        public Mammal(string mIn) { m = mIn; }
        public override string ToString() => m;
    }

    // allow me to give a mammal a name:
    public class NamedMammal : Root {
        Mammal m; string name;
        public NamedMammal(Mammal mIn, string nameIn) {
            m = mIn; name = nameIn;
        }
        public override string ToString() =>
            $"({m.ToString()} has name {name})";
    }

    // but I may want to name many things, so make 
    //the thing being named generic:
    public class NamedThing<Tthing> : Root {
        Tthing thing; string name;
        public NamedThing(Tthing thingIn, string nameIn) {
            thing = thingIn; name = nameIn;
        }
        public override string ToString() =>
            $"({thing.ToString()} has name {name})";
    }
    // I can name tractors or planets if I want to now

    class Program {
        static void Main() {

            Mammal c = new Mammal("cow");
            NamedMammal nm = new NamedMammal(c, "daisy");
            Console.WriteLine(nm.ToString());
            // output, as expected:
            // (cow has name daisy)
            
            Mammal f = new Mammal("fox");
            NamedThing<Mammal> nt = 
                new NamedThing<Mammal>(f, "freddie");
            Console.WriteLine(nt.ToString());
            // not what I expected:
            // (ConsoleApp1.Mammal has name freddie)
        }
    }
}

It's clearly not lost the type because it outputting it as 'ConsoleApp1.Mammal' so it knows it's a mammal but it doesn't seem to be calling ToString() on it as I'd expect. I'd guess it's calling the base-most object ToString() instead of the most derived.

Is this as designed, if so could someone give me some pointers, and perhaps an idea of how to deal with this because I don't want to handwrite buckets of boilerplate.

1 Answers

The problem is that the methods object.ToString() and Root.ToString() are not related in any way. Which is by your design.

In the class NamedMammal you expect a thing of type Mammal which implements Root. So the compiler know that the method Root.ToString() is guaranteed to be present on the given instance. Then in the NamedMammal.ToString() method you are calling the Root.ToString() implementation of Mammal because it is shadowing (not overriding) the object.ToString().

But for the NamedThing class your thing does not necessary implement Root.ToString() so in the method NamedThing.ToString() the call thing.ToString() will call object.ToString(). This does not result in the invokation of the Root.ToString() method because it is not an overriding virutal method for object.ToString().

A solution for this would be to condition the type parameter of NamedThing to implement Root like:

public class NamedThing<Tthing> : Root where Tthing : Root 
{ .. }

Another solution would be to replace the new in

public abstract new string ToString();

with override

public abstract override string ToString();

that way the implementing types override object.ToString() and not an unrelated similar named method.


But be aware that you need at least the first solution to force the types being used with NamedThing to implement ToString().

Related