https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/inheritance
Class inheritance is one of the primary characteristics of object-oriented programming. It allows you to create a hierarchy of classes by defining relationships between classes. In C#, you can create a base class that defines common behavior and characteristics, and then create derived classes that inherit and extend that behavior.
For example, a base class named Person might define Name and Age properties. A derived class named Employee might inherit these properties and define other properties such as EmployeeNumber and Salary.
public class Person
{
public string? Name { get; set; }
public int Age { get; set; }
}
public class Employee : Person
{
public int EmployeeNumber { get; set; }
public decimal Salary { get; set; }
}
Notice the colon (:) character in the Employee class declaration. The colon between Employee and Person indicates that the Employee class inherits from the Person class. When an Employee object is created, it has access to the Name and Age properties defined in the Person class, as well as the EmployeeNumber and Salary properties defined in the Employee class. When a Person object is created, it only has access to the Name and Age properties.
When you use inheritance in C#, the system:
Class inheritance ensures modularity at scale by enabling you to build upon existing functionality rather than rewriting code, creating a foundation for complex software architectures.
Inheritance allows you to reuse code defined in a base class in a derived class. This reduces duplication and promotes code reuse across your application.
Inheritance enables you to extend the behavior of a base class by adding new members to a derived class. You can define new properties, methods, and events in a derived class without modifying the base class.
Inheritance promotes encapsulation by allowing you to hide the implementation details of a base class from a derived class. Inheritance and encapsulation enable you to define a clear interface for interacting with objects of a derived class.
Inheritance promotes consistency by allowing you to define common behavior in a base class. Inheriting from a base class ensures that the derived classes share the same base behaviors.
Inheritance enables polymorphism, which allows you to treat objects of a derived class as objects of their base class. Polymorphism enables you to write code that works with objects of different types without knowing their specific type at compile time.
Create hierarchical structures that mirror real-world relationships, making your codebase more intuitive and easier to understand.
Class inheritance in C# creates an "is-a" relationship where a derived class inherits members from a base class and can extend or modify that behavior:
Inheritance Flow:
Base Class Definition
↓
Derived Class Declaration (: BaseClass)
↓
├─ Inherits public/protected members
├─ Can add new members
├─ Can override virtual members
└─ Can hide base members with 'new'
Example - Animal Hierarchy:
// Base class
public class Animal
{
public string Name { get; set; }
public virtual void MakeSound()
{
Console.WriteLine("Some generic animal sound");
}
}
// Derived class
public class Dog : Animal
{
public string Breed { get; set; }
public override void MakeSound()
{
Console.WriteLine("Woof!");
}
}
// Usage
Dog myDog = new Dog { Name = "Buddy", Breed = "Golden Retriever" };
myDog.MakeSound(); // Output: Woof!
Console.WriteLine(myDog.Name); // Output: Buddy (inherited property)
Important: C# supports single inheritance only (a class can inherit from one base class), but a class can implement multiple interfaces.
C# provides two mechanisms for defining relationships between classes: class inheritance and interface implementation. Both mechanisms enable code reuse and promote polymorphism, but they have different characteristics.
The difference between "is-a" and "can-do" relationships is important when designing object-oriented systems:
Use class inheritance when: A derived class is a specialized version of a base class.
Example - "Is-A" Relationship:
public class Dog
{
public string Name { get; set; }
public virtual void Bark()
{
Console.WriteLine("Woof!");
}
}
public class GermanShepherd : Dog
{
public string CoatColor { get; set; }
public override void Bark()
{
Console.WriteLine("Loud Woof!");
}
}
public class GoldenRetriever : Dog
{
public bool IsServiceDog { get; set; }
}
In this example, GermanShepherd and GoldenRetriever are specialized versions of Dog. They inherit common properties and methods from the Dog class and can extend or modify that behavior.
Use interface implementation when: A class can perform a specific set of actions.
Example - "Can-Do" Relationship:
public interface IDrawable
{
void Draw();
}
public class Circle : IDrawable
{
public double Radius { get; set; }
public void Draw()
{
Console.WriteLine($"Drawing a circle with radius {Radius}");
}
}
public class Rectangle : IDrawable
{
public double Width { get; set; }
public double Height { get; set; }
public void Draw()
{
Console.WriteLine($"Drawing a rectangle {Width}x{Height}");
}
}
In this example, Circle and Rectangle can be drawn. They implement the IDrawable interface to define a common set of actions shared by different classes, even though they are not related through inheritance.
Class inheritance and polymorphism are closely related concepts in object-oriented programming. Inheritance allows you to define a hierarchy of classes that share common behaviors, while polymorphism allows you to treat objects of a derived class as objects of their base class.
Consider a base class named HousePet and derived classes named Dog and Cat. The HousePet class defines a Speak method that returns a string representing the sound the pet makes. The Dog class overrides the Speak method to return "Woof", and the Cat class overrides the Speak method to return "Meow".
public class HousePet
{
public virtual string Speak()
{
return "Hello";
}
}
public class Dog : HousePet
{
public override string Speak()
{
return "Woof";
}
}
public class Cat : HousePet
{
public override string Speak()
{
return "Meow";
}
}
You can create a HousePet object that references a Dog or Cat instance and call the Speak method to get the appropriate response:
HousePet myPet1 = new Dog(); // Create a HousePet object that's of type Dog
HousePet myPet2 = new Cat(); // Create a HousePet object that's of type Cat
Console.WriteLine(myPet1.Speak()); // Output: Woof
Console.WriteLine(myPet2.Speak()); // Output: Meow
In this example, the HousePet class defines a common Speak behavior that the Dog and Cat classes share. The Dog and Cat classes override the Speak method to customize the behavior. When you call the Speak method on a HousePet object that references one of the derived classes, you get the appropriate response based on the actual type of the object.
Polymorphism allows you to write code that works with objects of different types without knowing their specific type at compile time. In C#, polymorphism can be achieved by using either class inheritance or interface implementation.
A base class (also called parent or superclass) provides the foundation, while a derived class (also called child or subclass) extends or specializes that foundation.
Structure:
// Base class
public class Vehicle
{
public string Brand { get; set; }
public int Year { get; set; }
public void Start()
{
Console.WriteLine($"{Brand} is starting...");
}
public virtual void DisplayInfo()
{
Console.WriteLine($"Brand: {Brand}, Year: {Year}");
}
}
// Derived class
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
public override void DisplayInfo()
{
base.DisplayInfo(); // Call base class method
Console.WriteLine($"Doors: {NumberOfDoors}");
}
public void Honk()
{
Console.WriteLine("Beep beep!");
}
}
Access modifiers control the visibility of base class members in derived classes:
Member Inheritance Rules:
When a class inherits from a base class, the following rules apply:
1. public
Example:
public class BaseClass
{
public int publicField;
public void PublicMethod() { }
}
public class DerivedClass : BaseClass
{
public void AccessPublicMember()
{
publicField = 10;
PublicMethod();
}
}
In this example, the DerivedClass inherits the publicField and PublicMethod members from the BaseClass. The AccessPublicMember method in the DerivedClass can access these members. Public members are also accessible from code outside the class hierarchy.
2. protected
Example:
public class BaseClass
{
protected int protectedField;
protected void ProtectedMethod() { }
}
public class DerivedClass : BaseClass
{
public void AccessProtectedMember()
{
protectedField = 10;
ProtectedMethod();
}
}
In this example, the DerivedClass inherits the protectedField and ProtectedMethod members from the BaseClass. The AccessProtectedMember method in the DerivedClass can access these members. However, if you try to access protected members from outside the class hierarchy, a compile-time error is generated.
3. internal
Example:
public class BaseClass
{
internal int internalField;
internal void InternalMethod() { }
}
public class DerivedClass : BaseClass
{
public void AccessInternalMember()
{
internalField = 10;
InternalMethod();
}
}
In this example, the DerivedClass inherits the internalField and InternalMethod members from the BaseClass. The AccessInternalMember method in the DerivedClass can access these members because they're in the same assembly. However, if you try to access internal members from outside the assembly, a compile-time error is generated.
4. private
Example:
public class BaseClass
{
private int privateField;
private void PrivateMethod() { }
}
public class DerivedClass : BaseClass
{
public void AccessPrivateMember()
{
// Cannot access privateField or PrivateMethod
// Compile-time error
}
}
In this example, the DerivedClass inherits from the BaseClass, but it cannot access the privateField or PrivateMethod members because they're private. Private members are not directly accessible in derived classes.
5. protected internal
6. private protected
Constructors are not inherited, but derived classes can call base class constructors:
public class Employee
{
public string Name { get; set; }
public int EmployeeId { get; set; }
public Employee(string name, int employeeId)
{
Name = name;
EmployeeId = employeeId;
}
}
public class Manager : Employee
{
public int TeamSize { get; set; }
// Constructor must call base constructor
public Manager(string name, int employeeId, int teamSize)
: base(name, employeeId)
{
TeamSize = teamSize;
}
}
// Usage
Manager manager = new Manager("Alice Johnson", 1001, 5);
Constructor Execution Order:
1. Base class constructor executes
2. Derived class constructor executes
Use virtual in the base class and override in the derived class to implement polymorphic behavior:
public class Shape
{
public virtual double CalculateArea()
{
return 0;
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double CalculateArea()
{
return Width * Height;
}
}
// Polymorphic usage
Shape shape1 = new Circle { Radius = 5 };
Shape shape2 = new Rectangle { Width = 4, Height = 6 };
Console.WriteLine(shape1.CalculateArea()); // Uses Circle's implementation
Console.WriteLine(shape2.CalculateArea()); // Uses Rectangle's implementation
A derived class is used to extend or modify the base class that it inherits from. When you create a derived class, you have several options for how to extend the base class:
new keyword to hide base class membersoverride keyword to extend or modify base class behaviorWhen you create a derived class, you can add new properties and methods that extend the behavior and functionality of the base class without any naming conflicts.
Example:
// Create instances of the base class and the derived classes
BaseClass baseClass = new BaseClass();
DerivedClass derivedClass = new DerivedClass();
BaseClass baseClassReferencingDerivedClass = new DerivedClass();
// Access properties and methods of the base class
Console.WriteLine($"\n{baseClass.Property1}");
baseClass.Method1();
// Access properties and methods of the derived class
Console.WriteLine($"\n{derivedClass.Property1}");
derivedClass.Method1();
Console.WriteLine($"{derivedClass.Property2}");
derivedClass.Method2();
// Access properties and methods of the base class that references the derived class
Console.WriteLine($"\n{baseClassReferencingDerivedClass.Property1}");
baseClassReferencingDerivedClass.Method1();
// baseClassReferencingDerivedClass.Method2(); // Error: 'BaseClass' doesn't contain 'Method2'
/*Output:
Base - Property1
Base - Method1
Base - Property1
Base - Method1
Derived - Property2
Derived - Method2
Base - Property1
Base - Method1
*/
public class BaseClass
{
public string Property1 { get; set; } = "Base - Property1";
public void Method1()
{
Console.WriteLine("Base - Method1");
}
}
public class DerivedClass : BaseClass
{
public string Property2 { get; set; } = "Derived - Property2";
public void Method2()
{
Console.WriteLine("Derived - Method2");
}
}
In this example, the DerivedClass extends the base class with a new property Property2 and a new method Method2. There are no naming conflicts between the base and derived classes, so you can access both new and inherited members from an instance of the derived class.
Key Observations:
baseClass can only access members defined in BaseClassderivedClass can access members from both BaseClass and DerivedClassbaseClassReferencingDerivedClass is declared as BaseClass but references a DerivedClass instancebaseClassReferencingDerivedClass contains a DerivedClass instance, it's treated like a BaseClass and can only access base class membersWhen you add new members to a derived class, you need to be aware of naming conflicts. A naming conflict occurs when a derived class defines a member with the same name as a member in the base class.
Example - Naming Conflict:
// Create instances of the base class and the derived classes
BaseClass baseClass = new BaseClass();
DerivedClass derivedClass = new DerivedClass();
BaseClass baseClassReferencingDerivedClass = new DerivedClass();
// Access properties and methods of the base class
Console.WriteLine($"\n{baseClass.Property1}");
Console.WriteLine($"{baseClass.Property2}");
baseClass.Method1();
baseClass.Method2();
// Access properties and methods of the derived class
Console.WriteLine($"\n{derivedClass.Property1}");
Console.WriteLine($"{derivedClass.Property2}");
derivedClass.Method1();
derivedClass.Method2();
// Access properties and methods of the base class that references the derived class
Console.WriteLine($"\n{baseClassReferencingDerivedClass.Property1}");
Console.WriteLine($"{baseClassReferencingDerivedClass.Property2}");
baseClassReferencingDerivedClass.Method1();
baseClassReferencingDerivedClass.Method2();
/*Output:
Base - Property1
Base - Property2
Base - Method1
Base - Method2
Base - Property1
Derived - Property2
Base - Method1
Derived - Method2
Base - Property1
Base - Property2
Base - Method1
Base - Method2
*/
public class BaseClass
{
public string Property1 { get; set; } = "Base - Property1";
public string Property2 { get; set; } = "Base - Property2";
public void Method1()
{
Console.WriteLine("Base - Method1");
}
public void Method2()
{
Console.WriteLine("Base - Method2");
}
}
public class DerivedClass : BaseClass
{
public string Property2 { get; set; } = "Derived - Property2";
public void Method2()
{
Console.WriteLine("Derived - Method2");
}
}
Notice that Property2 and Method2 in the derived class have the same name as members in the base class. This creates a naming conflict and generates compiler warnings:
warning CS0108: 'DerivedClass.Property2' hides inherited member 'BaseClass.Property2'.
Use the new keyword if hiding was intended.
warning CS0108: 'DerivedClass.Method2()' hides inherited member 'BaseClass.Method2()'.
Use the new keyword if hiding was intended.
Output Analysis:
derivedClass (declared as DerivedClass): The derived class versions of Property2 and Method2 are calledbaseClassReferencingDerivedClass (declared as BaseClass): The base class versions of Property2 and Method2 are calledWhen you have a naming conflict between the base class and the derived class, you can use the new keyword to hide the base class member intentionally. The new keyword allows you to define a new member in the derived class that has the same name as a member in the base class.
Using the new keyword doesn't change the compiler's default behavior for resolving naming conflicts, but it does suppress the warning that occurs when you build the solution, and it makes the intention to hide the base class member explicit.
Example:
// Create instances of the base class and the derived classes
BaseClass baseClass = new BaseClass();
DerivedClass derivedClass = new DerivedClass();
BaseClass baseClassReferencingDerivedClass = new DerivedClass();
// Access properties and methods of the base class
Console.WriteLine($"\n{baseClass.Property1}");
Console.WriteLine($"{baseClass.Property2}");
baseClass.Method1();
baseClass.Method2();
// Access properties and methods of the derived class
Console.WriteLine($"\n{derivedClass.Property1}");
Console.WriteLine($"{derivedClass.Property2}");
derivedClass.Method1();
derivedClass.Method2();
// Access properties and methods of the base class that references the derived class
Console.WriteLine($"\n{baseClassReferencingDerivedClass.Property1}");
Console.WriteLine($"{baseClassReferencingDerivedClass.Property2}");
baseClassReferencingDerivedClass.Method1();
baseClassReferencingDerivedClass.Method2();
/*Output:
Base - Property1
Base - Property2
Base - Method1
Base - Method2
Base - Property1
Derived - Property2
Base - Method1
Derived - Method2
Base - Property1
Base - Property2
Base - Method1
Base - Method2
*/
public class BaseClass
{
public string Property1 { get; set; } = "Base - Property1";
public string Property2 { get; set; } = "Base - Property2";
public void Method1()
{
Console.WriteLine("Base - Method1");
}
public void Method2()
{
Console.WriteLine("Base - Method2");
}
}
public class DerivedClass : BaseClass
{
public new string Property2 { get; set; } = "Derived - Property2";
public new void Method2()
{
Console.WriteLine("Derived - Method2");
}
}
When you run this code, the output is the same as the previous example. The new keyword is used in the derived class to hide the Property2 property and the Method2 method defined in the base class.
Behavior:
Property2 property and Method2 method of the derived class are called when using an object declared as DerivedClassProperty2 property and Method2 method of the base class are called when using an object declared as BaseClass (even if it references a DerivedClass instance)The new keyword can be used to hide a member of the base class intentionally. This behavior is useful when the derived class needs to provide a different implementation or functionality that isn't compatible with the base class member.
Guidelines for using the 'new' keyword:
✅ Use the new keyword when:
new makes your intention explicit❌ Avoid using the new keyword when:
override keyword insteadnew breaks polymorphismOverride vs New:
public class BaseClass
{
public virtual void VirtualMethod()
{
Console.WriteLine("BaseClass VirtualMethod");
}
public void NonVirtualMethod()
{
Console.WriteLine("BaseClass NonVirtualMethod");
}
}
public class DerivedClass : BaseClass
{
// Override - enables polymorphism
public override void VirtualMethod()
{
Console.WriteLine("DerivedClass VirtualMethod");
}
// New - hides base member
public new void NonVirtualMethod()
{
Console.WriteLine("DerivedClass NonVirtualMethod");
}
}
// Usage
BaseClass obj = new DerivedClass();
obj.VirtualMethod(); // Output: DerivedClass VirtualMethod (polymorphism works)
obj.NonVirtualMethod(); // Output: BaseClass NonVirtualMethod (hiding - no polymorphism)
DerivedClass obj2 = new DerivedClass();
obj2.VirtualMethod(); // Output: DerivedClass VirtualMethod
obj2.NonVirtualMethod(); // Output: DerivedClass NonVirtualMethod
Abstract classes cannot be instantiated and are designed to be inherited. They can contain abstract members that must be implemented by derived classes.
The abstract keyword in C# is used to define classes and class members that are incomplete and must be implemented in derived classes. An abstract class cannot be instantiated directly and is intended to be a base class for other classes. Abstract methods and properties are declared without any implementation and must be overridden in nonabstract derived classes.
Example - Shape Hierarchy:
public abstract class Shape
{
public abstract int GetArea();
}
public class Square : Shape
{
private int _side;
public Square(int side)
{
_side = side;
}
public override int GetArea()
{
return _side * _side;
}
}
class Program
{
static void Main()
{
Square square = new Square(5);
Console.WriteLine($"Area of the square = {square.GetArea()}");
}
}
In this example, the Shape class is abstract and contains an abstract method GetArea. The Square class inherits from Shape and provides an implementation for the GetArea method. The Square class can be instantiated, and the GetArea method returns the area of the square.
Abstract Class Rules:
More Complex Example:
public abstract class DatabaseConnection
{
public string ConnectionString { get; set; }
// Abstract method - no implementation
public abstract void Connect();
// Abstract property
public abstract bool IsConnected { get; }
// Concrete method
public void LogConnection()
{
Console.WriteLine($"Connecting to: {ConnectionString}");
}
}
public class SqlConnection : DatabaseConnection
{
private bool _isConnected;
public override void Connect()
{
Console.WriteLine("Connecting to SQL Server...");
_isConnected = true;
}
public override bool IsConnected => _isConnected;
}
public class MongoConnection : DatabaseConnection
{
private bool _isConnected;
public override void Connect()
{
Console.WriteLine("Connecting to MongoDB...");
_isConnected = true;
}
public override bool IsConnected => _isConnected;
}
The abstract keyword in C# is a powerful tool for defining incomplete classes and members that must be implemented in derived classes. It enforces a contract that derived classes must follow, ensuring that certain methods and properties are implemented. Appropriate use of the abstract keyword promotes a clear delineation of responsibilities between base and derived classes.
The sealed keyword in C# is used to prevent a class or class member from being inherited or overridden. When a class is marked as sealed, it cannot be used as a base class for other classes. When a method is marked as sealed, it cannot be overridden in derived classes.
public sealed class FinalImplementation : BaseClass
{
// This class cannot be inherited
}
// Compilation error
// public class CannotDeriveThis : FinalImplementation { }
The sealed keyword is particularly useful when you want to prevent further overriding in an inheritance chain:
Example - Controlling Override Chain:
public class BaseClass
{
public virtual void Method1()
{
Console.WriteLine("Method1 in BaseClass");
}
public virtual void Method2()
{
Console.WriteLine("Method2 in BaseClass");
}
}
public class DerivedClass : BaseClass
{
public sealed override void Method1()
{
Console.WriteLine("Method1 in DerivedClass");
}
public override void Method2()
{
Console.WriteLine("Method2 in DerivedClass");
}
}
public class FinalClass : DerivedClass
{
// This class cannot override Method1 because it's sealed in DerivedClass
public override void Method2()
{
Console.WriteLine("Method2 in FinalClass");
}
}
In this example, the DerivedClass inherits from the BaseClass and overrides the Method1 method, marking it as sealed. The Method2 method is also overridden in the DerivedClass but isn't sealed. The FinalClass inherits from DerivedClass and can override the Method2 method. However, it cannot override the Method1 method because it is sealed in the DerivedClass.
Sealed Keyword Rules:
sealed on members that are already overriding a virtual or abstract memberUse Cases for Sealed:
Sealed classes and methods are useful when you want to prevent further extension or modification of a class or method. They provide a way to restrict inheritance and ensure that certain members remain unchanged.
In C#, you can use the override keyword to extend or modify the behavior of the base class in the derived class. The override keyword enables you to override properties and methods that are inherited from the base class, and provide custom implementations in the derived class. This allows you to reuse code defined in the base class and extend or modify the behavior in the derived class.
To override a property or method in a derived class, follow these steps:
abstract or virtualoverride keywordbase keyword to access the base class implementation from the overridden memberBefore you can override members in a derived class, you must declare the members in the base class as either abstract or virtual:
Example:
public class BaseClass
{
public string Property1 { get; set; } = "Base - Property1";
public string Property2 { get; set; } = "Base - Property2";
public virtual void Method1()
{
Console.WriteLine("Base - Method1");
}
public void Method2()
{
Console.WriteLine("Base - Method2");
}
}
In this example, Property1 and Property2 are not marked as virtual, so they cannot be overridden. Method1 is marked as virtual, so it can be overridden in derived classes. Method2 is not marked as virtual, so it can only be hidden using the new keyword.
After declaring members in the base class as either abstract or virtual, you can override them in the derived class using the override keyword. The override keyword indicates that the member in the derived class replaces the member in the base class.
Example - Override vs New:
// Step 1: Create instances of the base class and the derived classes
BaseClass baseClass = new BaseClass();
DerivedClass derivedClass = new DerivedClass();
BaseClass baseClassReferencingDerivedClass = new DerivedClass();
// Step 2: Access properties and methods of the base class
Console.WriteLine($"\n{baseClass.Property1}");
Console.WriteLine($"{baseClass.Property2}");
baseClass.Method1();
baseClass.Method2();
// Step 3: Access properties and methods of the derived class
Console.WriteLine($"\n{derivedClass.Property1}");
Console.WriteLine($"{derivedClass.Property2}");
derivedClass.Method1();
derivedClass.Method2();
// Step 4: Access properties and methods of the base class that references the derived class
Console.WriteLine($"\n{baseClassReferencingDerivedClass.Property1}");
Console.WriteLine($"{baseClassReferencingDerivedClass.Property2}");
baseClassReferencingDerivedClass.Method1();
baseClassReferencingDerivedClass.Method2();
/*Output:
Base - Property1
Base - Property2
Base - Method1
Base - Method2
Derived - Property1
Derived - Property2
Derived - Method1
Derived - Method2
Derived - Property1
Base - Property2
Derived - Method1
Base - Method2
*/
public class BaseClass
{
public virtual string Property1 { get; set; } = "Base - Property1";
public virtual string Property2 { get; set; } = "Base - Property2";
public virtual void Method1()
{
Console.WriteLine("Base - Method1");
}
public void Method2()
{
Console.WriteLine("Base - Method2");
}
}
public class DerivedClass : BaseClass
{
public override string Property1 { get; set; } = "Derived - Property1";
public new string Property2 { get; set; } = "Derived - Property2";
public override void Method1()
{
Console.WriteLine("Derived - Method1");
}
public new void Method2()
{
Console.WriteLine("Derived - Method2");
}
}
This code demonstrates how to modify and hide properties and methods in a derived class using the override and new keywords. The base class has two properties and two methods. The derived class overrides Property1 and Method1, and hides Property2 and Method2 using the new keyword.
Breakdown:
Step 1: Creates three instances:
baseClass: Instance of BaseClassderivedClass: Instance of DerivedClassbaseClassReferencingDerivedClass: BaseClass reference pointing to a DerivedClass object (polymorphism)Step 2: Accessing baseClass uses all base class implementations
Step 3: Accessing derivedClass uses the derived class implementations for all members
Step 4: Accessing baseClassReferencingDerivedClass demonstrates the critical difference:
Property1 and Method1): The derived class implementation is used (polymorphism works)Property2 and Method2): The base class implementation is used (polymorphism doesn't work)This demonstrates the fundamental difference between method overriding (using override) and method hiding (using new).
When you define a base class as abstract, you indicate that the class is incomplete and must be implemented by derived classes. Abstract classes can contain abstract properties and methods that must be implemented by derived classes. Abstract base classes cannot be instantiated directly, so they're used as a template for derived classes to implement the abstract members.
Example:
// Create instances of the base class and the derived classes
// Note: We can't create an instance of an abstract class
// BaseClass baseClass = new BaseClass(); // This line would cause a compile-time error
DerivedClass derivedClass = new DerivedClass();
BaseClass baseClassReferencingDerivedClass = new DerivedClass();
// Access properties and methods of the derived class
Console.WriteLine($"\n{derivedClass.Property1}");
Console.WriteLine($"{derivedClass.Property2}");
derivedClass.Method1();
derivedClass.Method2();
// Access properties and methods of the base class that references the derived class
Console.WriteLine($"\n{baseClassReferencingDerivedClass.Property1}");
Console.WriteLine($"{baseClassReferencingDerivedClass.Property2}");
baseClassReferencingDerivedClass.Method1();
baseClassReferencingDerivedClass.Method2();
/*Output:
Derived - Property1
Derived - Property2
Derived - Method1
Derived - Method2
Derived - Property1
Base - Property2
Derived - Method1
Base - Method2
*/
public abstract class BaseClass
{
public abstract string Property1 { get; set; }
public virtual string Property2 { get; set; } = "Base - Property2";
public abstract void Method1();
public void Method2()
{
Console.WriteLine("Base - Method2");
}
}
public class DerivedClass : BaseClass
{
public override string Property1 { get; set; } = "Derived - Property1";
public new string Property2 { get; set; } = "Derived - Property2";
public override void Method1()
{
Console.WriteLine("Derived - Method1");
}
public new void Method2()
{
Console.WriteLine("Derived - Method2");
}
}
Notice that when the base class is declared using the abstract keyword, the base class cannot be instantiated directly. Instead, you must create instances of the derived classes that implement the abstract members. The derived class must provide implementations for the abstract members defined in the base class. Members that aren't abstract can be overridden or hidden in the derived class.
All classes in C# implicitly inherit from the Object class. The Object class defines several members that are inherited by all classes, such as ToString, Equals, and GetHashCode. You can override these members in your classes to provide custom implementations.
Example:
// Create instances of the derived classes
DerivedClass1 derivedClass1 = new DerivedClass1();
DerivedClass2 derivedClass2a = new DerivedClass2 { Property1 = "Value1", Property2 = "Value2" };
DerivedClass2 derivedClass2b = new DerivedClass2 { Property1 = "Value1", Property2 = "Value2" };
DerivedClass2 derivedClass2c = new DerivedClass2 { Property1 = "Value3", Property2 = "Value4" };
// Demonstrate Object class methods for DerivedClass1
Console.WriteLine("\nDemonstrating Object class methods for DerivedClass1:");
Console.WriteLine($"ToString: {derivedClass1.ToString()}");
Console.WriteLine($"Equals: {derivedClass1.Equals(new DerivedClass1())}");
Console.WriteLine($"GetHashCode: {derivedClass1.GetHashCode()}");
// Demonstrate overridden Object class methods for DerivedClass2
Console.WriteLine("\nDemonstrating overridden Object class methods for DerivedClass2:");
Console.WriteLine($"ToString: {derivedClass2a.ToString()}");
Console.WriteLine($"Equals (derivedClass2a vs derivedClass2b): {derivedClass2a.Equals(derivedClass2b)}"); // True
Console.WriteLine($"Equals (derivedClass2a vs derivedClass2c): {derivedClass2a.Equals(derivedClass2c)}"); // False
Console.WriteLine($"GetHashCode (derivedClass2a): {derivedClass2a.GetHashCode()}");
Console.WriteLine($"GetHashCode (derivedClass2b): {derivedClass2b.GetHashCode()}");
Console.WriteLine($"GetHashCode (derivedClass2c): {derivedClass2c.GetHashCode()}");
/*Output:
Demonstrating Object class methods for DerivedClass1:
ToString: DerivedClass1
Equals: False
GetHashCode: [HashCode]
Demonstrating overridden Object class methods for DerivedClass2:
ToString: DerivedClass2: Property1 = Value1, Property2 = Value2
Equals (derivedClass2a vs derivedClass2b): True
Equals (derivedClass2a vs derivedClass2c): False
GetHashCode (derivedClass2a): [HashCode]
GetHashCode (derivedClass2b): [HashCode]
GetHashCode (derivedClass2c): [HashCode]
*/
public abstract class BaseClass
{
public abstract string Property1 { get; set; }
public virtual string Property2 { get; set; } = "Base - Property2";
public abstract void Method1();
public void Method2()
{
Console.WriteLine("Base - Method2");
}
}
public class DerivedClass1 : BaseClass
{
public override string Property1 { get; set; } = "Derived1 - Property1";
public new string Property2 { get; set; } = "Derived1 - Property2";
public override void Method1()
{
Console.WriteLine("Derived1 - Method1");
}
public new void Method2()
{
Console.WriteLine("Derived1 - Method2");
}
}
public class DerivedClass2 : BaseClass
{
public override string Property1 { get; set; } = "Derived2 - Property1";
public new string Property2 { get; set; } = "Derived2 - Property2";
public override void Method1()
{
Console.WriteLine("Derived2 - Method1");
}
public new void Method2()
{
Console.WriteLine("Derived2 - Method2");
}
// Override ToString method
public override string ToString()
{
return $"DerivedClass2: Property1 = {Property1}, Property2 = {Property2}";
}
// Override Equals method
public override bool Equals(object obj)
{
if (obj is DerivedClass2 other)
{
return Property1 == other.Property1 && Property2 == other.Property2;
}
return false;
}
// Override GetHashCode method
public override int GetHashCode()
{
return HashCode.Combine(Property1, Property2);
}
}
In this example, the DerivedClass1 and DerivedClass2 classes inherit from the BaseClass class. The DerivedClass2 class overrides the ToString, Equals, and GetHashCode methods inherited from the Object class:
The DerivedClass1 class does not override these methods, so it uses the default implementations from the Object class:
ToString returns the type nameEquals compares object references (not values)GetHashCode returns a hash code based on the object referenceThis example demonstrates how overriding Object class methods allows you to customize the behavior of fundamental operations on your objects.
The base keyword allows access to base class members from within a derived class.
public class Logger
{
public virtual void Log(string message)
{
Console.WriteLine($"[LOG] {message}");
}
}
public class FileLogger : Logger
{
public string FilePath { get; set; }
public override void Log(string message)
{
base.Log(message); // Call base implementation
File.AppendAllText(FilePath, $"{DateTime.Now}: {message}\n");
}
}
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
}
public class Student : Person
{
public string StudentId { get; set; }
public Student(string firstName, string lastName, string studentId)
: base(firstName, lastName)
{
StudentId = studentId;
}
}
public class Product
{
public decimal Price { get; set; }
}
public class DiscountedProduct : Product
{
public decimal DiscountPercentage { get; set; }
public decimal FinalPrice
{
get { return base.Price * (1 - DiscountPercentage / 100); }
}
}
In C#, all classes implicitly inherit from the Object class (also known as System.Object). The Object class defines several methods that are available to all classes. If a class doesn't explicitly inherit from another class, it still inherits from Object by default.
1. ToString()
The ToString method returns a string that represents the current object. By default, it returns the fully qualified name of the class (including namespace).
2. Equals()
The Equals method compares two objects for equality. By default, it compares the references of the objects (reference equality).
3. GetHashCode()
The GetHashCode method returns a hash code for the current object. By default, it returns the hash code of the object's reference.
Person person1 = new Person { Name = "Alice", Age = 30 };
Person person2 = new Person { Name = "Alice", Age = 30 };
Person person3 = person1;
Console.WriteLine(person1.ToString()); // Output: Person (or Namespace.Person)
Console.WriteLine(person1.Equals(person2)); // Output: False
Console.WriteLine(person1.GetHashCode()); // Output: 32854180 (example)
Console.WriteLine(person1.Equals(person3)); // Output: True
public class Person
{
public string? Name { get; set; }
public int Age { get; set; }
}
public class Employee : Person
{
public int EmployeeNumber { get; set; }
public decimal Salary { get; set; }
}
Output Explanation:
ToString() returns the fully qualified name of the Person class (including the namespace if defined)Equals() compares person1 and person2 references and returns False because they are different object instances, even though they have the same property valuesGetHashCode() returns the hash code of the person1 object's referenceEquals() compares person1 and person3 references and returns True because person3 is a reference to the same object as person1You can override these methods in your classes to provide custom behavior:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
// Override ToString
public override string ToString()
{
return $"{Name}, Age: {Age}";
}
// Override Equals for value equality
public override bool Equals(object? obj)
{
if (obj == null || GetType() != obj.GetType())
return false;
Person other = (Person)obj;
return Name == other.Name && Age == other.Age;
}
// Override GetHashCode (must override when overriding Equals)
public override int GetHashCode()
{
return HashCode.Combine(Name, Age);
}
}
// Now with overridden methods:
Person person1 = new Person { Name = "Alice", Age = 30 };
Person person2 = new Person { Name = "Alice", Age = 30 };
Console.WriteLine(person1.ToString()); // Output: Alice, Age: 30
Console.WriteLine(person1.Equals(person2)); // Output: True (value equality)
Best Practices for Object Methods:
GetHashCode() when you override Equals()ToString() to provide meaningful string representationsIEquatable<T> for type-safe equality comparisonsbase.ToString(), base.Equals(), or base.GetHashCode() when appropriate in derived classesComplex applications often require multiple levels of inheritance.
// Level 1
public class LivingThing
{
public bool IsAlive { get; set; } = true;
public virtual void Breathe()
{
Console.WriteLine("Breathing...");
}
}
// Level 2
public class Animal : LivingThing
{
public virtual void Move()
{
Console.WriteLine("Moving...");
}
}
// Level 3
public class Mammal : Animal
{
public virtual void ProduceMilk()
{
Console.WriteLine("Producing milk...");
}
}
// Level 4
public class Dog : Mammal
{
public override void Move()
{
Console.WriteLine("Running on four legs...");
}
public void Bark()
{
Console.WriteLine("Woof!");
}
}
// Usage - Dog has access to all ancestor members
Dog dog = new Dog();
dog.Breathe(); // From LivingThing
dog.Move(); // From Animal (overridden in Dog)
dog.ProduceMilk(); // From Mammal
dog.Bark(); // From Dog
Although a class can inherit from only one base class (single inheritance), inheritance is transitive. This means that if class C inherits from class B, and class B inherits from class A, then class C inherits the members declared in both class B and class A. This property allows for deeper hierarchies and further code reuse.
Example - Vehicle Management System:
public class Vehicle
{
public string Make { get; set; }
public string Model { get; set; }
public void StartEngine()
{
Console.WriteLine("Engine started.");
}
public void StopEngine()
{
Console.WriteLine("Engine stopped.");
}
}
public class Car : Vehicle
{
public int NumberOfDoors { get; set; }
public void OpenTrunk()
{
Console.WriteLine("Trunk opened.");
}
public void HonkHorn()
{
Console.WriteLine("Horn honked.");
}
public void LockDoors()
{
Console.WriteLine("Doors locked.");
}
}
public class ElectricCar : Car
{
public int BatteryCapacity { get; set; }
public void ChargeBattery()
{
Console.WriteLine("Battery charging.");
}
public void DisplayBatteryStatus()
{
Console.WriteLine("Battery status displayed.");
}
}
public class CombustionEngineCar : Car
{
public int FuelCapacity { get; set; }
public void Refuel()
{
Console.WriteLine("Car refueled.");
}
public void CheckOilLevel()
{
Console.WriteLine("Oil level checked.");
}
}
In this example:
The ElectricCar and CombustionEngineCar classes inherit members from both Car and Vehicle, demonstrating the transitive nature of inheritance.
Multiple classes inherit from the same base class:
public class BankAccount
{
public string AccountNumber { get; set; }
public decimal Balance { get; protected set; }
public virtual void Deposit(decimal amount)
{
Balance += amount;
}
public virtual bool Withdraw(decimal amount)
{
if (Balance >= amount)
{
Balance -= amount;
return true;
}
return false;
}
}
public class SavingsAccount : BankAccount
{
public decimal InterestRate { get; set; }
public void ApplyInterest()
{
Balance += Balance * InterestRate;
}
}
public class CheckingAccount : BankAccount
{
public decimal OverdraftLimit { get; set; }
public override bool Withdraw(decimal amount)
{
if (Balance + OverdraftLimit >= amount)
{
Balance -= amount;
return true;
}
return false;
}
}
public class BusinessAccount : BankAccount
{
public string BusinessName { get; set; }
public List<string> AuthorizedSigners { get; set; }
}
Polymorphism allows objects of different types to be treated through a common interface.
public abstract class PaymentMethod
{
public abstract void ProcessPayment(decimal amount);
}
public class CreditCard : PaymentMethod
{
public string CardNumber { get; set; }
public override void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing credit card payment: ${amount}");
// Credit card processing logic
}
}
public class PayPal : PaymentMethod
{
public string Email { get; set; }
public override void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing PayPal payment: ${amount}");
// PayPal processing logic
}
}
public class BankTransfer : PaymentMethod
{
public string AccountNumber { get; set; }
public override void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing bank transfer: ${amount}");
// Bank transfer logic
}
}
// Polymorphic usage
public class PaymentProcessor
{
public void ProcessTransaction(PaymentMethod payment, decimal amount)
{
payment.ProcessPayment(amount); // Calls appropriate implementation
}
}
// Usage
PaymentProcessor processor = new PaymentProcessor();
processor.ProcessTransaction(new CreditCard(), 100.00m);
processor.ProcessTransaction(new PayPal(), 50.00m);
processor.ProcessTransaction(new BankTransfer(), 200.00m);
List<PaymentMethod> payments = new List<PaymentMethod>
{
new CreditCard { CardNumber = "1234-5678" },
new PayPal { Email = "[email protected]" },
new BankTransfer { AccountNumber = "987654321" }
};
// Process all payments polymorphically
foreach (var payment in payments)
{
payment.ProcessPayment(100.00m);
}
Requirement: Model different types of employees with varying salary calculations
Solution:
public abstract class Employee
{
public int EmployeeId { get; set; }
public string Name { get; set; }
public DateTime HireDate { get; set; }
public abstract decimal CalculateSalary();
public virtual void DisplayInfo()
{
Console.WriteLine($"ID: {EmployeeId}, Name: {Name}");
}
}
public class HourlyEmployee : Employee
{
public decimal HourlyRate { get; set; }
public int HoursWorked { get; set; }
public override decimal CalculateSalary()
{
return HourlyRate * HoursWorked;
}
}
public class SalariedEmployee : Employee
{
public decimal AnnualSalary { get; set; }
public override decimal CalculateSalary()
{
return AnnualSalary / 12; // Monthly salary
}
}
public class CommissionEmployee : SalariedEmployee
{
public decimal CommissionRate { get; set; }
public decimal SalesAmount { get; set; }
public override decimal CalculateSalary()
{
return base.CalculateSalary() + (SalesAmount * CommissionRate);
}
}
Requirement: Handle different document types with common operations
public abstract class Document
{
public string Title { get; set; }
public string Author { get; set; }
public DateTime CreatedDate { get; set; }
public abstract void Open();
public abstract void Save();
public virtual void Print()
{
Console.WriteLine($"Printing: {Title}");
}
}
public class TextDocument : Document
{
public string Content { get; set; }
public override void Open()
{
Console.WriteLine($"Opening text document: {Title}");
}
public override void Save()
{
Console.WriteLine($"Saving text document: {Title}");
}
}
public class SpreadsheetDocument : Document
{
public int Rows { get; set; }
public int Columns { get; set; }
public override void Open()
{
Console.WriteLine($"Opening spreadsheet: {Title}");
}
public override void Save()
{
Console.WriteLine($"Saving spreadsheet: {Title}");
}
public void CalculateFormulas()
{
Console.WriteLine("Calculating formulas...");
}
}
public class PresentationDocument : Document
{
public int SlideCount { get; set; }
public override void Open()
{
Console.WriteLine($"Opening presentation: {Title}");
}
public override void Save()
{
Console.WriteLine($"Saving presentation: {Title}");
}
public void PlaySlideshow()
{
Console.WriteLine("Playing slideshow...");
}
}
Requirement: Create a flexible character system for a game
public abstract class GameCharacter
{
public string Name { get; set; }
public int Health { get; set; }
public int Level { get; set; }
public abstract void Attack();
public abstract void Defend();
public virtual void LevelUp()
{
Level++;
Health += 10;
Console.WriteLine($"{Name} leveled up to {Level}!");
}
}
public class Warrior : GameCharacter
{
public int Strength { get; set; }
public override void Attack()
{
Console.WriteLine($"{Name} swings sword! Damage: {Strength * 2}");
}
public override void Defend()
{
Console.WriteLine($"{Name} raises shield! Defense increased.");
}
public void BattleCry()
{
Console.WriteLine($"{Name}: For honor!");
}
}
public class Mage : GameCharacter
{
public int Mana { get; set; }
public override void Attack()
{
if (Mana >= 10)
{
Console.WriteLine($"{Name} casts fireball!");
Mana -= 10;
}
}
public override void Defend()
{
Console.WriteLine($"{Name} creates magic barrier!");
}
public void Meditate()
{
Mana += 20;
Console.WriteLine($"{Name} meditates and restores mana.");
}
}
public class Archer : GameCharacter
{
public int Accuracy { get; set; }
public override void Attack()
{
Console.WriteLine($"{Name} shoots arrow! Accuracy: {Accuracy}%");
}
public override void Defend()
{
Console.WriteLine($"{Name} dodges!");
}
}
Requirement: Implement geometric shapes with area and perimeter calculations
public abstract class Shape
{
public string Color { get; set; }
public abstract double CalculateArea();
public abstract double CalculatePerimeter();
public virtual void Draw()
{
Console.WriteLine($"Drawing a {Color} {GetType().Name}");
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
public override double CalculatePerimeter()
{
return 2 * Math.PI * Radius;
}
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double CalculateArea()
{
return Width * Height;
}
public override double CalculatePerimeter()
{
return 2 * (Width + Height);
}
}
public class Triangle : Shape
{
public double SideA { get; set; }
public double SideB { get; set; }
public double SideC { get; set; }
public override double CalculateArea()
{
// Using Heron's formula
double s = (SideA + SideB + SideC) / 2;
return Math.Sqrt(s * (s - SideA) * (s - SideB) * (s - SideC));
}
public override double CalculatePerimeter()
{
return SideA + SideB + SideC;
}
}
Requirement: Manage different vehicle types with varying rental rates
public abstract class RentalVehicle
{
public string VehicleId { get; set; }
public string Make { get; set; }
public string Model { get; set; }
public int Year { get; set; }
public abstract decimal CalculateDailyRate();
public virtual decimal CalculateRentalCost(int days)
{
return CalculateDailyRate() * days;
}
}
public class EconomyCar : RentalVehicle
{
public override decimal CalculateDailyRate()
{
return 35.00m;
}
}
public class LuxuryCar : RentalVehicle
{
public bool HasChauffeur { get; set; }
public override decimal CalculateDailyRate()
{
decimal baseRate = 150.00m;
return HasChauffeur ? baseRate + 100.00m : baseRate;
}
}
public class Truck : RentalVehicle
{
public double PayloadCapacity { get; set; }
public override decimal CalculateDailyRate()
{
return 75.00m + (decimal)(PayloadCapacity * 0.10);
}
}
public class Motorcycle : RentalVehicle
{
public int EngineSize { get; set; }
public override decimal CalculateDailyRate()
{
return EngineSize > 600 ? 60.00m : 40.00m;
}
}
// ❌ Don't: Deep inheritance hierarchies
public class Animal { }
public class Mammal : Animal { }
public class Carnivore : Mammal { }
public class Feline : Carnivore { }
public class Lion : Feline { }
// ✅ Do: Use composition for flexibility
public class Lion
{
private readonly IMovementBehavior _movement;
private readonly IDietBehavior _diet;
public Lion(IMovementBehavior movement, IDietBehavior diet)
{
_movement = movement;
_diet = diet;
}
}
Recommended: 2-3 levels maximum
├─ Base
├─ Derived1
└─ Derived2
Avoid: Deep hierarchies (4+ levels)
├─ Level1
├─ Level2
├─ Level3
├─ Level4
└─ Level5 (too deep!)
// ✅ Good: Abstract base with common functionality
public abstract class Repository<T>
{
protected readonly DbContext _context;
protected Repository(DbContext context)
{
_context = context;
}
public virtual async Task<T> GetByIdAsync(int id)
{
return await _context.Set<T>().FindAsync(id);
}
public abstract Task<IEnumerable<T>> GetAllAsync();
}
// ✅ Derived classes should be substitutable for base classes
public void ProcessShape(Shape shape)
{
double area = shape.CalculateArea(); // Works for any Shape derivative
Console.WriteLine($"Area: {area}");
}
// Works correctly with any shape
ProcessShape(new Circle { Radius = 5 });
ProcessShape(new Rectangle { Width = 4, Height = 6 });
// Seal classes that shouldn't be extended
public sealed class SecurityToken
{
// Implementation that must not be altered
}
// Seal overrides to prevent further modification
public class CustomList : BaseList
{
public sealed override void Add(object item)
{
// Final implementation
}
}
/// <summary>
/// Base class for all database operations.
/// Derived classes must implement connection-specific logic.
/// </summary>
public abstract class DatabaseRepository
{
/// <summary>
/// Connects to the database.
/// Override to provide connection-specific implementation.
/// </summary>
protected abstract void Connect();
/// <summary>
/// Executes a query. Do not override unless absolutely necessary.
/// </summary>
public virtual void ExecuteQuery(string query)
{
Connect();
// Base implementation
}
}
// ❌ Don't: Protected fields break encapsulation
public class BaseClass
{
protected int _value;
}
// ✅ Do: Use protected properties
public class BaseClass
{
private int _value;
protected int Value
{
get => _value;
set => _value = value;
}
}
// ❌ Don't: Make everything virtual by default
public class BaseClass
{
public virtual void Method1() { }
public virtual void Method2() { }
public virtual void Method3() { }
}
// ✅ Do: Only make methods virtual when overriding is expected
public class BaseClass
{
public void FinalMethod() { } // Cannot override
public virtual void ExtensibleMethod() { } // Can override
}
// ✅ Good: Use base class references
List<Employee> employees = new List<Employee>
{
new HourlyEmployee(),
new SalariedEmployee(),
new CommissionEmployee()
};
foreach (Employee emp in employees)
{
Console.WriteLine(emp.CalculateSalary());
}
public class BaseResource : IDisposable
{
private bool _disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose managed resources
}
// Dispose unmanaged resources
_disposed = true;
}
}
}
public class DerivedResource : BaseResource
{
private bool _disposed = false;
protected override void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose derived class managed resources
}
_disposed = true;
}
base.Dispose(disposing);
}
}
Define the skeleton of an algorithm in a base class, letting derived classes override specific steps:
public abstract class DataProcessor
{
public void ProcessData()
{
LoadData();
ValidateData();
TransformData();
SaveData();
}
protected abstract void LoadData();
protected abstract void TransformData();
protected virtual void ValidateData()
{
Console.WriteLine("Performing basic validation...");
}
protected virtual void SaveData()
{
Console.WriteLine("Saving data...");
}
}
public class CsvDataProcessor : DataProcessor
{
protected override void LoadData()
{
Console.WriteLine("Loading CSV data...");
}
protected override void TransformData()
{
Console.WriteLine("Transforming CSV data...");
}
}
public class XmlDataProcessor : DataProcessor
{
protected override void LoadData()
{
Console.WriteLine("Loading XML data...");
}
protected override void TransformData()
{
Console.WriteLine("Transforming XML data...");
}
protected override void ValidateData()
{
base.ValidateData();
Console.WriteLine("Performing XML schema validation...");
}
}
Define an interface for creating objects, but let derived classes decide which class to instantiate:
public abstract class DocumentCreator
{
public abstract IDocument CreateDocument();
public void OpenDocument()
{
IDocument doc = CreateDocument();
doc.Open();
}
}
public class PdfDocumentCreator : DocumentCreator
{
public override IDocument CreateDocument()
{
return new PdfDocument();
}
}
public class WordDocumentCreator : DocumentCreator
{
public override IDocument CreateDocument()
{
return new WordDocument();
}
}
Encapsulate algorithms in a hierarchy of classes:
public abstract class SortingStrategy
{
public abstract void Sort(int[] array);
protected void Swap(int[] array, int i, int j)
{
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
public class BubbleSort : SortingStrategy
{
public override void Sort(int[] array)
{
for (int i = 0; i < array.Length; i++)
{
for (int j = 0; j < array.Length - 1; j++)
{
if (array[j] > array[j + 1])
{
Swap(array, j, j + 1);
}
}
}
}
}
public class QuickSort : SortingStrategy
{
public override void Sort(int[] array)
{
QuickSortHelper(array, 0, array.Length - 1);
}
private void QuickSortHelper(int[] array, int low, int high)
{
// Quick sort implementation
}
}
Virtual methods have a small performance cost due to virtual method table (vtable) lookups:
public class Base
{
public virtual void Method() { }
}
public class Base
{
public void Method() { }
}
Best Practice: Only use virtual when polymorphism is needed.
Sealing classes and methods can improve performance by allowing the JIT compiler to inline methods:
public sealed class OptimizedClass
{
public void Method()
{
}
}
Avoid deep inheritance hierarchies as they can impact:
// ❌ Avoid: Deep hierarchy
Level1 → Level2 → Level3 → Level4 → Level5
// ✅ Prefer: Shallow hierarchy with composition
Base → Derived (with composed behaviors)
Understanding the constructor chain is important for proper initialization:
public class GrandParent
{
public GrandParent()
{
Console.WriteLine("1. GrandParent constructor");
}
}
public class Parent : GrandParent
{
public Parent()
{
Console.WriteLine("2. Parent constructor");
}
}
public class Child : Parent
{
public Child()
{
Console.WriteLine("3. Child constructor");
}
}
// Creating Child executes all three constructors in order:
Child child = new Child();
// Output:
// 1. GrandParent constructor
// 2. Parent constructor
// 3. Child constructor
Destructors execute in reverse order:
public class GrandParent
{
~GrandParent()
{
Console.WriteLine("3. GrandParent destructor");
}
}
public class Parent : GrandParent
{
~Parent()
{
Console.WriteLine("2. Parent destructor");
}
}
public class Child : Parent
{
~Child()
{
Console.WriteLine("1. Child destructor");
}
}
Animal animal = new Dog();
if (animal is Dog)
{
Console.WriteLine("It's a dog!");
}
// Pattern matching
if (animal is Dog dog)
{
dog.Bark();
}
// Type casting
Dog myDog = (Dog)animal; // Throws exception if not a Dog
Dog safeDog = animal as Dog; // Returns null if not a Dog
// Safe casting with null check
if (animal as Dog is Dog castedDog)
{
castedDog.Bark();
}
Use inheritance when:
Example:
// Good: Clear "is-a" relationship
public class Vehicle { }
public class Car : Vehicle { }
public class Truck : Vehicle { }
Avoid inheritance when:
Example:
// ❌ Bad: "has-a" relationship using inheritance
public class Engine { }
public class Car : Engine { } // Car is not an Engine
// ✅ Good: Use composition
public class Engine { }
public class Car
{
private Engine _engine; // Car has an Engine
}
// ✅ Good: Interface defines contract
public interface IRepository<T>
{
Task<T> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
}
public class SqlRepository<T> : IRepository<T>
{
// Implementation
}
// ✅ Good: Abstract class provides shared implementation
public abstract class RepositoryBase<T>
{
protected readonly DbContext _context;
protected RepositoryBase(DbContext context)
{
_context = context;
}
public virtual async Task<T> GetByIdAsync(int id)
{
return await _context.Set<T>().FindAsync(id);
}
public abstract Task<IEnumerable<T>> GetAllAsync();
}
// Basic inheritance
public class Derived : Base { }
// With interface implementation
public class Derived : Base, IInterface1, IInterface2 { }
// Abstract class
public abstract class Base
{
public abstract void AbstractMethod();
public virtual void VirtualMethod() { }
public void ConcreteMethod() { }
}
// Sealed class
public sealed class Final : Base
{
public override void AbstractMethod() { }
}
| Modifier | Same Class | Derived Class | Same Assembly | Other Assembly |
|---|---|---|---|---|
| public | ✓ | ✓ | ✓ | ✓ |
| protected | ✓ | ✓ | ✗ | ✗ |
| internal | ✓ | ✓ (same assembly) | ✓ | ✗ |
| protected internal | ✓ | ✓ | ✓ | ✗ |
| private protected | ✓ | ✓ (same assembly) | ✗ | ✗ |
| private | ✓ | ✗ | ✗ | ✗ |
| Keyword | Usage | Purpose |
|---|---|---|
| virtual | Base class method | Allows overriding in derived classes |
| override | Derived class method | Overrides base class virtual method |
| abstract | Base class/method | Must be overridden in derived classes |
| sealed | Class or method | Prevents inheritance or overriding |
| new | Derived class member | Hides base class member |
| base | Derived class | Accesses base class members |
// Base class must use virtual, abstract, or override
public class Base
{
public virtual void Method1() { }
public abstract void Method2();
}
// Derived class uses override
public class Derived : Base
{
public override void Method1() { }
public override void Method2() { }
}
// Cannot override non-virtual methods
public class Base
{
public void Method() { } // Not virtual
}
public class Derived : Base
{
// ❌ Error: Cannot override
// public override void Method() { }
// ✅ Can hide with 'new'
public new void Method() { }
}
Class inheritance is a powerful feature in C# that enables code reuse, promotes logical organization, and supports polymorphic behavior.
Key Takeaways:
virtual and override keywords to enable polymorphismbase keywordRemember: Inheritance is a tool, not a goal. Use it when it creates clearer, more maintainable code, but don't force inheritance relationships where composition or interfaces would be more appropriate. Always consider whether an "is-a" relationship truly exists before using inheritance.
Serilog - Structured Logging for .NET
Logging is often treated as an afterthought—sprinkle some Console.WriteLine() calls during development, maybe add ILogger if you remember, and hope for the best in production. Then something breaks at 3 AM, and you're grepping through text files trying to reconstruct what happened. Structured logging changes this game entirely.
C# Collections
C# Collections - List<T>, HashSet<T>, Dictionary<TKey, TValue>, and ObservableCollection<T> for managing data structures.