C#: Thoughts on class design with "override" and "new" modifier

Class Member Structure With new

From Microsoft’s DotNet documentation, the new modifier is associated with hiding base class members by creating a new one. But first, let’s see what it does to the class.

Let’s assume, we have three classes declared as bellow -

namespace ConsoleAppHelloWorld.App.CsharpClassTest
{
    class SimpleClassBase
    {
        public virtual void TestMethod()
        {
        }
    }

    class SimpleClassWithNew : SimpleClassBase
    {
        public new void TestMethod()
        {
        }
    }

    class SimpleClassWithOverride : SimpleClassBase
    {
        public override void TestMethod()
        {
        }
    }
}

Now, let’s write a function that prints out the declaring type of each member in the class -

using Reflection = System.Reflection;
using System;

namespace ConsoleAppHelloWorld.App.CsharpClassTest
{
    class AppMain
    {
        public static void GetMemberInfoFromClass(ref Type type)
        {
            Reflection.BindingFlags flags =
                Reflection.BindingFlags.Instance
                | Reflection.BindingFlags.Static
                | Reflection.BindingFlags.Public
                | Reflection.BindingFlags.NonPublic
                | Reflection.BindingFlags.FlattenHierarchy;
            Reflection.MemberInfo[] memberInfos = type.GetMembers(flags);
            Console.WriteLine(
                $"----------\n" +
                $"Type {type.Name} has {memberInfos.Length} members:" +
                $"\n----------"
               );
            foreach (var member in memberInfos)
            {
                System.Text.StringBuilder str = new();
                str.Append($"{member.Name} ({member.MemberType}): ");
                str.Append($"Declared by {member.DeclaringType}");
                Console.WriteLine(str.ToString());
            }
        }
    }
}

Now, let’s print the member information -

namespace ConsoleAppHelloWorld.App.CsharpClassTest

{
    class AppMain
    {
        public static void Run()
        {
            Type type1 = typeof(SimpleClassWithOverride);
            GetMemberInfoFromClass(ref type1);
            Type type2 = typeof(SimpleClassWithNew);
            GetMemberInfoFromClass(ref type2);
        }
}

The diff of the output will be -

// ----------
// Type SimpleClassWithOverride has 10 members:
// ----------
-TestMethod (Method): Declared by ConsoleAppHelloWorld.App.CsharpClassTest.SimpleClassWithOverride
+TestMethod (Method): Declared by ConsoleAppHelloWorld.App.CsharpClassTest.SimpleClassWithNew
+TestMethod (Method): Declared by ConsoleAppHelloWorld.App.CsharpClassTest.SimpleClassBase
GetType (Method): Declared by System.Object
MemberwiseClone (Method): Declared by System.Object
Finalize (Method): Declared by System.Object
ToString (Method): Declared by System.Object
Equals (Method): Declared by System.Object
Equals (Method): Declared by System.Object
ReferenceEquals (Method): Declared by System.Object
GetHashCode (Method): Declared by System.Object
.ctor (Constructor): Declared by ConsoleAppHelloWorld.App.CsharpClassTest.SimpleClassWithOverride

From this, we can come to the following resolution -

  • override modifier has removed the TestMethod() member from the base type and added a new member.

  • new modifier has left TestMethod() member from the base class but also added the new member to its type.

Now the question arises, why should we leave the method from the base in the first place? Because -

  • We can access that method with base.TestMethod()

  • Executing the TestMethod() from the extended class will invoke the method from the extended class, but not from the base class.

But, the members with new modifier can also be accessed by type-casting the base class type to the extended class instance. Because access to the member of an instance is decided by the declaring type.

Defining behavior with new

Let’s create some better classes to test this -

using System;

namespace ConsoleAppHelloWorld.App.NewVsOverride
{
    class BaseCar
    {
        protected int HorsePower = 300;

        public virtual int GetHorsePower()
        {
            return HorsePower;
        }
    }

    class SedanCar : BaseCar
    {
        protected int CoolerUpgrade = 30;

        public new int GetHorsePower()
        {
            return base.GetHorsePower() + CoolerUpgrade;
        }
    }

    class SportSedanCar : SedanCar
    {
        protected int TurboHorsePower = 50;
        public new int GetHorsePower()
        {
            return base.GetHorsePower() + TurboHorsePower;
        }
    }
}

Here, we have a base model car which is BaseCar. SedanCar is build on top of BaseCar, so SedanCar extends BaseCar. We have another car that is an upgrade to the SedanCar which is SportSedanCar, so SportSedanCar extends SedanCar.

Now, let’s print their horse-power-

using System;
namespace ConsoleAppHelloWorld.App.NewVsOverride
{
    class AppMain
    {
        public static void Run()
        {
            SportSedanCar sportSedanCar = new();

            Console.WriteLine($"Sedan : HP : {((SedanCar)sportSedanCar).GetHorsePower()}");
            Console.WriteLine($"SportSedan : HP : {sportSedanCar.GetHorsePower()}");
        }
    }
}

The output is the following-

Base : HP : 300
Sedan : HP : 330
SportSedan : HP : 380

Because we used new modifier instead of override, we added a new GetHorsePower() member for the extending class, thus each type can invoke the method that it has declared.

If we had used override then it would remove the previous member and invoke the method that was declared in the instance.

    class SedanCar : BaseCar
    {
        protected int CoolerUpgrade = 30;

-       public new int GetHorsePower()
+       public override int GetHorsePower()
        {
            return base.GetHorsePower() + CoolerUpgrade;
        }
    }

Output:

-Base : HP : 300
+Base : HP : 330
Sedan : HP : 330
SportSedan : HP : 380

How this affects the application

In our previous program, we saw that we can get the horse-power of BaseCar and SedanCar by type-casting it to a single instance of SportSedanCar.

So, if we want to get the difference of horse-power among themselves, we can easily do the following -

SportSedanCar sportSedanCar = new();

Console.WriteLine($"HP Diff : Base -> Sedan : " +
    $"{((SedanCar)sportSedanCar).GetHorsePower() - ((BaseCar)sportSedanCar).GetHorsePower()}"
);
Console.WriteLine($"HP Diff : Sedan -> SportSedan : " +
    $"{sportSedanCar.GetHorsePower() - ((SedanCar)sportSedanCar).GetHorsePower()}"
);
Console.WriteLine($"HP Diff : Base -> SportSedan : " +
    $"{sportSedanCar.GetHorsePower() - ((BaseCar)sportSedanCar).GetHorsePower()}"
);

Output -

HP Diff : Base -> Sedan : 30
HP Diff : Sedan -> SportSedan : 50
HP Diff : Base -> SportSedan : 80

If we used override, then we would have to create different instances of BaseCar, SedanCar, and SportSedanCar to derive that comparison.

SportSedanCar sportSedanCar = new();
SedanCar sedanCar = new();
BaseCar baseCar = new();

Console.WriteLine($"HP Diff : Base -> Sedan : " +
    $"{sedanCar.GetHorsePower() - baseCar.GetHorsePower()}"
);
Console.WriteLine($"HP Diff : Sedan -> SportSedan : " +
    $"{sportSedanCar.GetHorsePower() - sedanCar.GetHorsePower()}"
);
Console.WriteLine($"HP Diff : Base -> SportSedan : " +
    $"{sportSedanCar.GetHorsePower() - baseCar.GetHorsePower()}"
);

If we compare, then we will see that the override version of the code requires 3 instances but the new requires only 1 instance.

This may not affect much in normal applications, but imagine that it was executing in a REST API with 1000 requests per second. 2 extra instances means we have a total of 2000 memory allocation of BaseCar and SedanCar. Those 2000 instances will need to be garbage collected which will waste precious CPU time that the server could have used. This CPU waste is a real-world problem because the CPU is far more expensive than other hardware. Discord moved from Go back-end to Rust back-end because they ran into CPU spikes by frequent garbage collection triggers. You can find that article here.

Should we always use new instead of override?

Of course not!!!

Usage of both override and new depends on the design principle. override is used most of the time because we design classes to inherit common functionality from the base class. new is best suited for classes that add the features on top of the inheriting class and those features are distinguishable.

References