Inheritance #
Inheritance works similarly to C++. We have one mode of inheritance, which would correspond to public inheritance from C++. In C#, we do not have multiple inheritance: we can only inherit from one class at a time.
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
public class Student : Person
{
public string StudentID { get; set; }
}
As we would expect, a Student
is also a Person
and has everything the base class has.
Student alice = new Student() { FirstName = "Alice",
LastName = "Brown",
Age = 25,
StudentID = "X-84355" };
Console.WriteLine($"{alice.FirstName} {alice.LastName}, {alice.Age}, {alice.StudentID}");
Polymorphism #
References are polymorphic; they can be treated as if they were of the base type. We can pass a Student
object to a method that accepts a base class object - after all, it’s the same thing. This allows us to write extensible code that operates on a general type, without worrying about specific implementations.
Register(alice);
public void Register(Person person)
{
// ...
}
We can also implicitly cast a subclass type to a base type. The other way around requires an explicit cast and may result in an InvalidCastException
.
Person person = alice; // Implicit cast
Student student = (Student)person; // Requires explicit cast
as
Operator
#
The as
operator works similarly to dynamic_cast
from C++. We can use it to perform a downcast that returns null
if it fails.
Teacher teacher = alice as Teacher;
if (teacher != null)
{
Console.WriteLine("SAP ID: {teacher.SapID}");
}
public class Teacher : Person
{
public int SapID { get; set; }
}
However, there are better ways to check the type.
is
Operator
#
In general, the is
operator checks if a variable matches a pattern and returns a boolean result. One of the patterns that interests us is the type pattern.
if (alice is Teacher)
{
Teacher teacher = (Teacher)alice;
Console.WriteLine("SAP ID: {teacher.SapID}");
}
Additionally, in the type pattern, we can introduce a variable of that type.
if (alice is Teacher teacher)
{
Console.WriteLine("SAP ID: {teacher.SapID}");
}
Virtual Methods #
Virtual functions work on the same principle as in C++. Under the hood, they use a virtual function tables.
In C#, not only methods can be virtual, but also properties, indexers, and events.
public class Vehicle
{
public float Position { get; protected set; } = 0;
public virtual float Speed { get; protected set; } = 1.0f;
public string Name { get; }
public Vehicle(string name) => Name = name;
public virtual float Run(float dt)
{
Console.WriteLine($"Vehicle.Run({dt})");
return (Position = Position + dt * Speed);
}
}
public class Car : Vehicle
{
public override float Speed { get; protected set; } = 0.0f;
public virtual float Acceleration { get; }
public Car(string name, float acceleration) : base(name) => Acceleration = acceleration;
public override float Run(float dt)
{
Console.WriteLine($"Car.Run({dt})");
Position += dt * Speed;
Speed += dt * Acceleration;
return Position;
}
}
public class Bike : Vehicle
{
public Bike(string name) : base(name) {}
public override float Run(float dt)
{
Console.WriteLine($"Bike.Run({dt})"); // We can skip implementation if not for the output.
return base.Run(dt);
}
}
base
#When it comes to the
base
keyword, it has two meanings here. We can use it to:
- Call overridden methods
- Call the base class constructor
It works analogously to the
this
keyword, but for the base class.
List<Vehicle> vehicles = [new Bike("Romet"), new Car("Honda Civic", 1.5f), new Car("Toyota Yaris", 1.0f)];
const float dt = 1.0f;
for (float time = 0.0f; time < 4.0f; time += dt)
{
Console.WriteLine($"====== time: {time,5:F1}s ======");
foreach (var vehicle in vehicles)
{
vehicle.Run(dt);
}
foreach (var vehicle in vehicles)
{
Console.WriteLine($"Vehicle {vehicle.Name}, Position {vehicle.Position}");
}
}
Source code
The whole thing works analogously to how it would work in C++. If vehicle
is of type Car
, Car.Run
will be called, if it is of type Bike
, Bike.Run
will be called. If the type did not override this method, Vehicle.Run
would be called. The only cosmetic difference is that in C# the override
keyword is required if we want to override a virtual method - if we don’t do this, we just hide it and generate a compiler warning.
Abstract Classes #
Abstract classes are classes with the abstract
keyword. We cannot initialize objects of this class, we can define abstract members in such a class - that is, those that have a signature, but no implementation. These can be abstract methods, properties, indexers, and events. This is an analogy to classes from C++ that have declared pure virtual functions.
Instead of providing an implementation of the Vehicle.Run
method, we can make it abstract. Then, each non-abstract subclass must provide its own implementation.
public abstract class Vehicle
{
public float Position { get; protected set; } = 0;
public virtual float Speed { get; protected set; } = 1.0f;
public string Name { get; }
public Vehicle(string name) => Name = name;
public abstract float Run(float dt);
}
Hiding Members #
A subclass can define the same members as the base class.
public class Base
{
public int Member = 0;
public string Method() => "Base.Method";
public virtual string VirtualMethod() => "Base.VirtualMethod";
}
public class Hider : Base
{
public int Member = 1;
public string Method() => "Hider.Method";
public string VirtualMethod() => "Hider.VirtualMethod";
}
public class Overrider : Base
{
public override string VirtualMethod() => "Overrider.VirtualMethod";
}
In this example, the fields and methods in Hider
are hidden. This is usually not intentional - the compiler generates a warning in this case.
The consequences of hiding can be seen in the example:
Hider hider = new Hider();
Base baseHider = hider;
Overrider overrider = new Overrider();
Base baseOverrider = overrider;
Console.WriteLine(hider.Method()); // Hider.Method
Console.WriteLine(hider.VirtualMethod()); // Hider.VirtualMethod
Console.WriteLine(baseHider.Method()); // Base.Method
Console.WriteLine(baseHider.VirtualMethod()); // Base.VirtualMethod
Console.WriteLine(overrider.Method()); // Base.Method
Console.WriteLine(overrider.VirtualMethod()); // Overrider.VirtualMethod
Console.WriteLine(baseOverrider.Method()); // Base.Method
Console.WriteLine(baseOverrider.VirtualMethod()); // Overrider.VirtualMethod
Source code
Hidden methods are not overridden, usually if a method was marked as virtual
, the intention is to override it. If the intention was actually to hide it, the compiler warnings can be silenced with the new
keyword. This is the only action of this keyword in this context.
public class Hider : Base
{
public new int Member = 1;
public new string Method() => "Hider.Method";
public new string VirtualMethod() => "Hider.VirtualMethod";
}
sealed
#
The sealed
keyword applied to a method means that it cannot be overridden in a subclass. This does not mean, however, that such a method cannot be hidden. Applied to a class, it means that such a class cannot be inherited from.
Constructors #
Constructors are not inherited. A derived class must define its own set of constructors.
A derived class must also take care of initializing the base class. In the constructor of the derived class, we can use base
to call one of the constructors of the base class.
public class Base
{
public int X;
public Base(int x) => X = x;
}
public class Derived : Base
{
public int Y;
public Derived(int x, int y) : base(x) => Y = y;
}
If the base class provides a parameterless constructor, we can omit the explicit call to the base class constructor, but the parameterless constructor of the base class will be called implicitly.
public class Base
{
public int X;
public Base() => X = 1;
}
public class Derived : Base
{
public int Y;
public Derived() => Y = 1; // Can skip implicit base call
}
Initialization Order #
- Fields and properties of the derived class
- Fields and properties of the base class
- Base class constructor
- Derived class constructor