Search⌘ K
AI Features

Virtual Methods and Properties

Explore how to use virtual methods and properties in C# to implement polymorphism. Learn to override base class methods and properties with specific behaviors in derived classes, control inheritance with sealed keywords, and apply base method calls. This lesson helps you understand key object-oriented programming concepts in C#.

When we inherit a method from a base class, we might need to change its behavior in a child class. This ability to provide specific implementations for a general action is a core pillar of polymorphism in C#.

Consider the following example:

public class Animal
{
public void Voice()
{
// Method implementation
}
}
A simple base class

Suppose the Voice() method produces the voice of an animal. For something as generic as the Animal class, we could have a generic implementation of this method. However, derived classes like Cat or Dog require specific implementations of the Voice() method. For example, cats meow and dogs bark.

Providing a different implementation for an inherited method is called overriding. In C#, we can only override a method if it was marked as virtual in a base class.

Overriding a method

We have three classes: Animal, Cat, and Dog. The latter two classes inherit the Voice() method from Animal but override it with different implementations.

First, let's look at the base class where the method is defined.

C# 14.0
namespace VirtualMethodsAndProperties;
public class Animal
{
// Marked the method as virtual
// This method can be overridden in child classes
public virtual void Voice()
{
Console.WriteLine("Grrrrr");
}
}
  • Line 1: We declare a file-scoped namespace.

  • Line 7: We use the virtual keyword to allow derived classes to override the Voice method.

  • Line 9: We provide a default implementation that prints “Grrrrr”.

Now that we have a base class with a virtual method, we can create a derived class that provides a specific implementation.

C# 14.0
namespace VirtualMethodsAndProperties;
// Cat inherits from Animal
public class Cat : Animal
{
// Overrides the Voice method
public override void Voice()
{
Console.WriteLine("Meow...");
}
}
  • Line 4: We define the Cat class, inheriting from Animal.

  • Line 7: We use the override keyword to replace the base class implementation of Voice.

  • Line 9: We provide the specific implementation for Cat, printing “Meow...”.

Similarly, we can create another derived class that offers a different behavior for the same method.

C# 14.0
namespace VirtualMethodsAndProperties;
// Dog inherits from Animal
public class Dog : Animal
{
// Overrides the Voice method
public override void Voice()
{
Console.WriteLine("Bark! Bark!");
}
}
  • Line 4: Defines the Dog class, inheriting from Animal.

  • Line 7: Uses the override keyword to replace the base class implementation of Voice.

  • Line 9: Provides the specific implementation for Dog, printing “Bark! Bark!”.

With all our classes defined, we can now instantiate them and observe how the runtime calls the correct method for each object.

C# 14.0
namespace VirtualMethodsAndProperties;
public class Animal
{
// Marked the method as virtual
// This method can be overridden in child classes
public virtual void Voice()
{
Console.WriteLine("Grrrrr");
}
}
  • Line 3: Creates an instance of the base class Animal.

  • Lines 4–5: Creates instances of the derived classes Dog and Cat.

  • Line 7: Calls Voice on the base object, which prints “Grrrrr”.

  • Lines 8–9: Calls Voice on the derived objects. The runtime executes the overridden versions, printing “Bark! Bark!” and “Meow...” respectively.

There are several constraints to overriding:

  • The overridden version must have the same access modifiers as the base class. For instance, if a virtual method is public, the overridden version must also be public.

  • We cannot override a static method. Static methods cannot be declared virtual.

The base keyword

Sometimes we want to call the base version of the method from within the method we are overriding. This occurs if the base version performs generic operations and our override extends this functionality. We can call the base version using the base keyword.

public override void Voice()
{
base.Voice(); // Calling the base version of this method
Console.WriteLine("Extended functionality.");
}
Invoking the base implementation
  • Line 1: Declares an override of the Voice method.

  • Line 3: Uses base.Voice() to execute the code inside the parent class’s Voice method before continuing.

  • Line 4: Executes the additional code specific to this derived class.

Overriding properties

In C#, we can also override properties. Since property getters and setters are methods, overriding a property effectively overrides these accessors.

In the following example, we define a Number class with a virtual property, and a NonNegativeNumber class that prevents the value from being negative.

C# 14.0
namespace VirtualMethodsAndProperties;
public class Number
{
// Virtual property
public virtual int Value { get; set; }
}
public class NonNegativeNumber : Number
{
private int _value;
// Overriding a property
public override int Value
{
get
{
return _value;
}
set
{
if (value >= 0)
{
// Sets value only if not negative
_value = value;
}
}
}
}
  • Line 6: Declares an auto-implemented property Value marked as virtual.

  • Line 9: Defines NonNegativeNumber inheriting from Number.

  • Line 14: Overrides the Value property using the override keyword.

  • Lines 16–19: Implements the get accessor to return the backing field _value.

  • Lines 21–28: Implements the set accessor with logic to ignore negative values.

Now, let’s see how these properties behave when accessed from the main program.

C# 14.0
using VirtualMethodsAndProperties;
var number = new Number { Value = -50 };
// We have overridden the property
// We can't set negative values
// The value doesn't change if it is set to a negative number
var nonNegativeNumber = new NonNegativeNumber { Value = -50 };
// Output: -50
Console.WriteLine(number.Value);
// Output: 0 (because it is the default value for int)
Console.WriteLine(nonNegativeNumber.Value);
  • Line 3: Instantiates the base Number class and sets Value to -50. This is allowed.

  • Line 8: Instantiates NonNegativeNumber and attempts to set Value to -50.

  • Line 11: Prints the value of the base object (-50).

  • Line 14: Prints the value of the derived object. Since the overridden setter rejected the negative number, it remains 0.

The sealed keyword

To prevent further overriding of a method in derived classes, we can mark it as sealed. This terminates the inheritance chain for that member.

In this example, we create a Wolf class that inherits from Dog (defined earlier). The Wolf class overrides the Voice method and seals it.

C# 14.0
namespace VirtualMethodsAndProperties;
public class Wolf : Dog
{
// This method cannot be overridden by classes inheriting from Wolf
public sealed override void Voice()
{
Console.WriteLine("Howl!");
}
}
  • Line 3: Defines a Wolf class inheriting from Dog.

  • Line 6: Uses sealed override to provide an implementation of Voice.

  • Line 8: Any class inheriting from Wolf will be unable to override Voice further.

If we attempt to inherit from Wolf and override the Voice method again, the compiler will produce an error.

C# 14.0
namespace VirtualMethodsAndProperties;
public class MutantWolf : Wolf
{
// Error CS0239: 'MutantWolf.Voice()': cannot override inherited member 'Wolf.Voice()' because it is sealed
public override void Voice()
{
Console.WriteLine("Roar?");
}
}
  • Line 3: Defines MutantWolf inheriting from Wolf.

  • Lines 6–9: Attempts to override Voice. This code is commented out because it causes a compilation error (CS0239). The sealed keyword in Wolf prevents this modification.