C# Inheritance: override vs new and Constructor Traps

C# inheritance on .NET 10: the real output gap between override and new, warning CS0108, sealed and abstract, and why virtual calls in constructors crash.

Revised November 2026. This tutorial first appeared in Korean in 2021. We recompiled and ran every example on .NET 10.0.14 (C# 14). The output did not change, but the claim that "private members are not inherited" was inaccurate, and the new example never showed where new and override actually behave differently. The original compiler error quotes contained a personal Windows user-folder path, so only the error codes and messages remain. New in this version: the constructor/virtual-call trap, abstract and sealed override, and the C# 14 field keyword.

This article is for C# developers who know the inheritance syntax but are not sure when override and new diverge, or what happens inside a base constructor. The short answer: when the variable's type is the base class, override calls the method of the real object and new calls the base method. And when a base constructor calls a virtual method, the derived override runs before the derived fields are set, which can throw a NullReferenceException without a single compiler warning.

Inheritance basics and constructor order

You inherit by adding : BaseClass after the class name. A C# class can have only one base class; if a type needs several contracts, use interfaces.

Here is the 2021 example, run unchanged on .NET 10 (messages translated from the Korean original):

class Parent
{
    public int num;
    public Parent() => Console.WriteLine("Base constructor called");
}
class Child : Parent
{
    public Child(int num)
    {
        this.num = num;
        Console.WriteLine("Derived constructor called");
    }
    public void DisplayValue() => Console.WriteLine("num is {0}", num);
}
// Main: new Child(20).DisplayValue();
Base constructor called
Derived constructor called
num is 20

Constructors themselves are not inherited, and the base constructor runs before the derived one. But only a parameterless base constructor is called automatically. If the base class only has Parent(int num), the derived constructor must chain with : base(num), or you get:

error CS7036: There is no argument given that corresponds to the required parameter 'num' of 'Parent.Parent(int)'

The 2021 text also said that destruction happens in reverse, derived class first. C# finalizers (~Child()) do run derived-first, but the garbage collector decides when, so you cannot rely on them the way you would rely on a C++ destructor. Closing files and connections is the job of IDisposable and using; see the using section of exception handling.

Private members are inherited, just not accessible

The original said "members declared private cannot be inherited." More precisely, the private member is part of the derived object, but derived code cannot name it:

class Parent { private int secret = 42; public int Peek() => secret; }
class Kid : Parent { public int Read() => secret; }   // compile error
error CS0122: 'Parent.secret' is inaccessible due to its protection level

Remove Read() and call new Kid().Peek(), and you get 42. The value lives inside the Kid object. If derived classes should see the member, declare it protected.

this vs base

If the derived class does not declare a field with the same name, this.num and base.num are the same field. They only differ when the derived class redeclares the name, which is the "hiding" covered next. In real code, base is mostly used not for fields but inside an override, to call the base implementation, as in base.Speak().

virtual/override vs new: the output is different

The original virtual/override example still works. Remove virtual from the base method and you get:

error CS0506: 'Child.A()': cannot override inherited member 'Parent.A()' because it is not marked virtual, abstract, or override

So you can override a method that is virtual, abstract, or already an override.

Take the new example, delete the new keywords, and it still compiles with two warnings:

warning CS0108: 'Child.x' hides inherited member 'Parent.x'. Use the new keyword if hiding was intended.
warning CS0108: 'Child.A()' hides inherited member 'Parent.A()'. Use the new keyword if hiding was intended.

The 2021 article only called the method through Child child = new Child();, so the result looked exactly like override. Call it through a base-typed variable and the difference shows. We added three lines to the original:

Parent asParent = child;          // same object, base-typed variable
asParent.A();
Console.WriteLine("x : {0}", asParent.x);
Parent.A() called
x : 100
Child.A() called
x : 200
Parent.A() called
x : 100

Same Child object, but the third pair used the base version. Putting override and new side by side makes it clearer:

Animal[] zoo = [new Dog(), new Cat()];
foreach (Animal a in zoo)
    Console.WriteLine($"{a.GetType().Name,-4} Speak()={a.Speak(),-6} Name()={a.Name()}");
Cat c = new Cat();
Console.WriteLine($"Cat as Cat   : Name()={c.Name()}");

abstract class Animal
{
    public virtual string Speak() => "...";
    public string Name() => "animal";               // not virtual
}
class Dog : Animal
{
    public override string Speak() => "Woof";
    public new string Name() => "dog";              // hides
}
sealed class Cat : Animal
{
    public override string Speak() => base.Speak() + "Meow";
    public new string Name() => "cat";
}
Dog  Speak()=Woof   Name()=animal
Cat  Speak()=...Meow Name()=animal
Cat as Cat   : Name()=cat
Diagram: the variable Animal a holds a Dog object. Calling a.Speak(), which is virtual and overridden, follows the run-time type Dog and prints Woof. Calling a.Name(), which Dog hides with new, follows the variable type Animal and prints animal.
override is chosen by the run-time type of the object, new by the compile-time type of the variable. Output from .NET 10 for Animal a = new Dog().

override picks the method by the run-time type of the object; new picks it by the compile-time type of the variable. The moment you handle objects as List<Animal> or an Animal parameter, methods hidden with new are bypassed. That makes new a patch for name collisions (for example, a base library later added a method with your name), not a design tool. If you want polymorphism, it is always virtual/override. Do not ignore CS0108: if the hiding is intentional, write new so the next reader knows.

sealed, abstract and sealed override

The original sealed example still fails the same way:

error CS0509: 'Child': cannot derive from sealed type 'Parent'

Three things are worth adding in 2026:

  • An abstract class cannot be instantiated (error CS0144: Cannot create an instance of the abstract type or interface 'Shape'), and each abstract method must be implemented by derived classes. Miss one and you get error CS0534: 'Square' does not implement inherited abstract member 'Shape.Area()'.
  • sealed override seals a single method rather than the whole class. A grandchild that tries again gets error CS0239: 'Ring.Area()': cannot override inherited member 'Circle.Area()' because it is sealed.
  • A class that was not designed for inheritance is safer sealed from day one. Unsealing later is a compatible change; sealing a class that others already derive from is a breaking one. As a side effect, the JIT can turn virtual calls on a sealed type into direct calls.
abstract class Shape
{
    public abstract double Area();
    public override string ToString() => $"{GetType().Name} area={Area():F2}";
}
class Circle(double r) : Shape                     // C# 12 primary constructor
{
    public sealed override double Area() => Math.PI * r * r;
}

The trap: calling a virtual method from a constructor

This is the part of the update we most want readers to take away. When a base constructor calls a virtual method, the object already has its derived type, so the derived override runs. At that moment, though, the derived constructor body has not executed.

var r = new Report("2026-11");
Console.WriteLine(r.Describe());

class Base
{
    public Base() => Console.WriteLine($"Base ctor: {Describe()}");
    public virtual string Describe() => "base";
}
class Report : Base
{
    private readonly string _title = "Monthly";   // field initializer
    private readonly string _month;                // assigned in the ctor body
    public Report(string month) { _month = month; }
    public override string Describe() => $"{_title} {_month.ToUpper()}";
}
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Report.Describe() in …/Program.cs:line 14
   at Base..ctor() in …/Program.cs:line 6
   at Report..ctor(String month) in …/Program.cs:line 13

The default project has nullable reference types enabled, and there was still no warning. _month is declared as a non-nullable string, so the compiler considers it safe. Drop the ToUpper() and print the state instead, and the order becomes visible:

Base ctor -> Describe() = "Monthly (null) len=-1"
Report ctor -> Describe() = "Monthly 2026-11 len=7"

_title is already set; only _month is null. In C#, field initializers run before the base constructor, and the constructor body runs after it. Java runs field initializers after the base constructor, so developers coming from Java are especially likely to be surprised.

Flow diagram. Step 1: new Report runs the field initializers, so _title becomes Monthly. Step 2: the Base constructor runs and calls the virtual Describe method, which goes to Report.Describe while _month is still null and throws NullReferenceException. Step 3, which is reached only if no exception occurs: the Report constructor body assigns _month.
Construction order of new Report("2026-11") in C#. The virtual call inside Base() reaches Report.Describe() before the Report constructor body has assigned _month.

The fixes are simple:

  • Do not call virtual or abstract members from a base constructor. Pass what the base needs through : base(value).
  • If you really must, populate the fields the override uses from field initializers or primary-constructor parameters rather than the constructor body.
  • Move post-construction work into an Initialize() method or a factory that runs after the object is fully built.

Properties (get/set) and the C# 14 field keyword

The get/set examples at the end of the original article also produced the same output: assigning "Kelley" (six characters) leaves the name as "John" or the previous value.

mc.Name : John
mc.Name : Bree
mc.Name : Bree      <- the "Kelley" assignment is silently ignored

A setter that silently drops bad values makes bugs hard to find, because the caller believes the assignment worked. Throwing is the usual choice today, and with the C# 14 field keyword you no longer need a separate private backing field:

class MyClass
{
    public string Name
    {
        get;
        set => field = value.Length < 5 ? value
            : throw new ArgumentException($"Name must be shorter than 5: '{value}'", nameof(value));
    } = "John";
}
Bree
Name must be shorter than 5: 'Kelley' (Parameter 'value')

Setting LangVersion to 13 produces error CS9260: Feature 'field keyword' is not available in C# 13.0., which confirms it is a C# 14 feature.

Summary: which tool for which job

You want to Use
Vary behavior per subclass and treat objects as the base type virtual + override
Force every subclass to implement a member, with no base default abstract class + abstract member
Stop further overriding sealed override (method) / sealed (class)
Resolve an accidental name clash new (silences CS0108), or better, rename
Give the base constructor data from the subclass : base(value); never call virtuals in constructors
Reuse behavior from several sources Interfaces or composition (hold the helper in a field)

When several classes must honor the same contract, the choice between an abstract class and an interface continues in the decision table of our interfaces article. Structs cannot inherit at all; why, and what to do instead, is covered in structs.

Tested with .NET SDK 10.0.403 / runtime 10.0.14, C# 14 (the net10.0 default), macOS on an Apple Silicon Mac. Compiler messages are the English SDK output.

FAQ

What changes if I use new instead of override?

When the variable is typed as the derived class, both call the derived method. The difference appears when you call through a base-class variable: override runs the method of the actual object, new runs the base method. With Animal a = new Dog(); a.Speak() printed Woof, while the hidden a.Name() printed animal. If you forget the new keyword the behavior is the same and you only get warning CS0108.

Why is calling a virtual method from a constructor dangerous?

The base constructor runs first, so an override called from it sees an object whose derived constructor body has not run yet. Fields assigned in that body are still null; on .NET 10 this threw a NullReferenceException with no compiler warning at all. Field initializers (= value) run before the base constructor, so those fields already had their values.

Are private members inherited?

Yes, the private field is still part of the derived object. Derived code just cannot refer to it by name (error CS0122). A public method of the base class can still read it. If derived classes should see a member, make it protected.

  • #C#
  • #inheritance
  • #override
  • #virtual
  • #abstract
  • #.NET

t.laboratory

A technical blog that records real problems from the apps, servers and services we build and run — and the code that fixed them.

About · Apps

↑