virtual, override and new

virtual offers a method up for replacement, override replaces it, and new only hides it, which is rarely what anyone wanted.

Syntaxpublic virtual string Format(string m) { } / public override string Format(string m) { }

A derived class often needs a method to behave differently from the base class version. virtual and override are how that is done. The base class marks a method virtual, meaning "a derived class may replace this". The derived class marks its own version override, meaning "this replaces the base version".

The payoff is that calling code does not need to know which kind of object it holds. It calls the method and the object supplies the behaviour.

Calling the version you replaced

Inside an override, base.Method() calls the base class version. That lets you add to the old behaviour instead of discarding it. Without base. the call would find your own override and recurse forever.

What override requires

You can only override a member marked virtual, abstract or override. The signature must match: same name, same parameter types, same accessibility. The return type must match too, with one exception that has applied since C# 9: an override, or a read-only property override, may return a type derived from the base member's return type, which is called a covariant return type. Properties and indexers can be virtual too, not just methods. sealed override replaces the member and forbids any further class from replacing it again.

new hides, and that is different

If the base member is not virtual, you can still declare a member with the same name and mark it new. That hides the base member rather than replacing it. Which one runs is decided by the declared type of the variable, not by the object, so the same object answers differently through two variables. Omitting new in that situation produces a compiler warning, and writing new and override on the same member is an error.

Example

Example · csharp
Notifier plain = new Notifier();
Notifier loud = new LoudNotifier();

Console.WriteLine(plain.Format("deploy finished"));
Console.WriteLine(loud.Format("deploy finished"));

class Notifier
{
    // virtual: a derived class is allowed to replace this
    public virtual string Format(string message)
    {
        return $"[info] {message}";
    }
}

class LoudNotifier : Notifier
{
    // override: this replaces Notifier.Format for LoudNotifier objects
    public override string Format(string message)
    {
        // base.Format calls the version in Notifier, not this one
        string original = base.Format(message);
        return original.ToUpperInvariant();
    }
}

// Output:
// [info] deploy finished
// [INFO] DEPLOY FINISHED

When to use it

  • Give a base report class a default Render and let the PDF subclass change only the header.
  • Override a retry count on one job type while every other job keeps the default.
  • Wrap a logging method so the derived version adds a prefix and then calls base.

More examples

new hides, so the declared type decides

This is the behaviour that makes new a mistake in almost every case. Both variables point at the same FancyButton, yet the answer depends on how the variable was declared. The legitimate use is narrow: a base class you do not control added a member whose name you already use, and new silences the warning without changing anything.

Example · csharp
Button asButton = new FancyButton();
FancyButton asFancy = new FancyButton();

// One object, two variables, two different answers
Console.WriteLine(asButton.Label());
Console.WriteLine(asFancy.Label());

class Button
{
    // not virtual, so FancyButton cannot override it
    public string Label() => "plain";
}

class FancyButton : Button
{
    // new hides Button.Label; it does not replace it
    public new string Label() => "fancy";
}

// Output:
// plain
// fancy

Virtual property, and sealed override to stop the chain

Use a virtual property when the derived class should supply a value rather than run an action. Use sealed override when a method has become load-bearing for correctness and further replacement would break an assumption, for example a method that has to produce a stable format for an audit log.

Example · csharp
class Job
{
    // properties can be virtual too
    public virtual int Retries => 3;

    public virtual string Render() => "job";
}

class PaymentJob : Job
{
    public override int Retries => 10;

    // sealed override: replaces Render, and no class below may replace it again
    public sealed override string Render() => "payment job";
}

class CardPaymentJob : PaymentJob
{
    public override int Retries => 1;

    // public override string Render() => "card";  // error: Render is sealed
}

The virtual call from a constructor trap

The base constructor finishes before the derived constructor body starts, but the virtual call already dispatches to the derived override. Reach for this example when a field is mysteriously null inside a method that is only ever called after construction. The fix is to move the call out of the constructor into an explicit initialisation step.

Example · csharp
new Derived();

class Base
{
    public Base()
    {
        // Runs the override in Derived, before Derived's constructor body
        Console.WriteLine(Describe());
    }

    public virtual string Describe() => "base";
}

class Derived : Base
{
    private readonly string? _name;

    public Derived()
    {
        _name = "ready";
    }

    public override string Describe() => $"name is {_name ?? "not set yet"}";
}

// Output:
// name is not set yet

Discussion

  • Be the first to comment on this lesson.