3.6 - Advanced OOP Features
C# provides a rich set of advanced object-oriented programming features that go beyond the basic principles of OOP. These features enhance the language's expressiveness, flexibility, and power, allowing developers to write more elegant and maintainable code.
3.6.1 - Indexers
Indexers allow objects to be indexed like arrays. They provide a way to access elements of a class or struct using the array-like syntax with square brackets []
.
public class StringDataStore
{
private string[] data = new string[10];
// Basic indexer
public string this[int index]
{
get
{
if (index < 0 || index >= data.Length)
throw new IndexOutOfRangeException("Index is out of range");
return data[index];
}
set
{
if (index < 0 || index >= data.Length)
throw new IndexOutOfRangeException("Index is out of range");
data[index] = value;
}
}
// Overloaded indexer with a different parameter type
public string this[string name]
{
get
{
int index = Array.FindIndex(data, s => s != null && s.StartsWith(name));
if (index >= 0)
return data[index];
else
return null;
}
}
// Multi-parameter indexer
public string this[int row, int column]
{
get
{
int index = row * 5 + column;
if (index < 0 || index >= data.Length)
throw new IndexOutOfRangeException("Index is out of range");
return data[index];
}
set
{
int index = row * 5 + column;
if (index < 0 || index >= data.Length)
throw new IndexOutOfRangeException("Index is out of range");
data[index] = value;
}
}
}
Using Indexers
StringDataStore store = new StringDataStore();
// Using the basic indexer
store[0] = "Apple";
store[1] = "Banana";
store[2] = "Cherry";
Console.WriteLine($"Item at index 0: {store[0]}");
Console.WriteLine($"Item at index 1: {store[1]}");
Console.WriteLine($"Item at index 2: {store[2]}");
// Using the string indexer
Console.WriteLine($"Item starting with 'A': {store["A"]}");
Console.WriteLine($"Item starting with 'B': {store["B"]}");
// Using the multi-parameter indexer
store[0, 0] = "Red";
store[0, 1] = "Green";
store[0, 2] = "Blue";
Console.WriteLine($"Item at [0, 0]: {store[0, 0]}");
Console.WriteLine($"Item at [0, 1]: {store[0, 1]}");
Console.WriteLine($"Item at [0, 2]: {store[0, 2]}");
3.6.2 - Extension Methods
Extension methods allow you to add methods to existing types without modifying the original type. They are defined as static methods in static classes, with the first parameter having the this
modifier.
// Static class containing extension methods
public static class StringExtensions
{
// Extension method for string
public static bool IsNullOrEmpty(this string str)
{
return string.IsNullOrEmpty(str);
}
// Extension method with parameters
public static string Truncate(this string str, int maxLength)
{
if (str == null)
return null;
return str.Length <= maxLength ? str : str.Substring(0, maxLength) + "...";
}
// Extension method that uses other extension methods
public static string SafeTruncate(this string str, int maxLength)
{
if (str.IsNullOrEmpty())
return string.Empty;
return str.Truncate(maxLength);
}
}
// Extension methods for IEnumerable<T>
public static class EnumerableExtensions
{
// Extension method that returns a new collection
public static IEnumerable<T> WhereNot<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
return source.Where(item => !predicate(item));
}
// Extension method that performs an action on each element
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
foreach (T item in source)
{
action(item);
}
}
}
Using Extension Methods
// Using string extension methods
string text = "Hello, World!";
string nullText = null;
Console.WriteLine($"Is null or empty: {text.IsNullOrEmpty()}");
Console.WriteLine($"Truncated: {text.Truncate(5)}");
Console.WriteLine($"Safe truncate on null: {nullText.SafeTruncate(5)}");
// Using IEnumerable<T> extension methods
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Using the WhereNot extension method
IEnumerable<int> notEven = numbers.WhereNot(n => n % 2 == 0);
Console.WriteLine("Not even numbers:");
foreach (int n in notEven)
{
Console.Write($"{n} ");
}
Console.WriteLine();
// Using the ForEach extension method
Console.WriteLine("Squaring each number:");
numbers.ForEach(n => Console.Write($"{n * n} "));
Console.WriteLine();
3.6.3 - Partial Classes and Methods
Partial classes allow a class, struct, or interface definition to be split into multiple files. Partial methods allow the declaration of a method in one part of a partial class and its implementation in another part.
Partial Classes
// File: Person.cs
public partial class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string GetFullName()
{
return $"{FirstName} {LastName}";
}
}
// File: Person.Address.cs
public partial class Person
{
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
public string GetAddress()
{
return $"{Street}, {City}, {State} {ZipCode}";
}
}
// File: Person.Contact.cs
public partial class Person
{
public string Email { get; set; }
public string Phone { get; set; }
public string GetContactInfo()
{
return $"Email: {Email}, Phone: {Phone}";
}
}
Partial Methods
// File: Order.cs
public partial class Order
{
private string orderId;
private decimal total;
public string OrderId
{
get => orderId;
set
{
orderId = value;
OnOrderIdChanged();
}
}
public decimal Total
{
get => total;
set
{
total = value;
OnTotalChanged();
}
}
// Partial method declarations
partial void OnOrderIdChanged();
partial void OnTotalChanged();
public void Process()
{
Console.WriteLine($"Processing order {OrderId} with total {Total:C}");
OnProcessing();
}
partial void OnProcessing();
}
// File: Order.Events.cs
public partial class Order
{
// Partial method implementations
partial void OnOrderIdChanged()
{
Console.WriteLine($"Order ID changed to {OrderId}");
}
partial void OnTotalChanged()
{
Console.WriteLine($"Total changed to {Total:C}");
}
partial void OnProcessing()
{
Console.WriteLine("Order is being processed");
}
}
Using Partial Classes and Methods
// Using a partial class
Person person = new Person
{
FirstName = "John",
LastName = "Doe",
Street = "123 Main St",
City = "Anytown",
State = "CA",
ZipCode = "12345",
Email = "john.doe@example.com",
Phone = "555-123-4567"
};
Console.WriteLine(person.GetFullName());
Console.WriteLine(person.GetAddress());
Console.WriteLine(person.GetContactInfo());
// Using a class with partial methods
Order order = new Order();
order.OrderId = "ORD-12345";
order.Total = 123.45m;
order.Process();
3.6.4 - Nested Types
Nested types are types (classes, structs, interfaces, enums) that are defined within another type. They are useful for encapsulating helper types that are only used by the containing type.
public class OuterClass
{
private int outerField;
public OuterClass(int value)
{
outerField = value;
}
// Nested class
public class NestedClass
{
private OuterClass parent;
public NestedClass(OuterClass parent)
{
this.parent = parent;
}
public void DisplayOuterField()
{
Console.WriteLine($"Outer field value: {parent.outerField}");
}
}
// Nested struct
public struct NestedStruct
{
public int X;
public int Y;
public NestedStruct(int x, int y)
{
X = x;
Y = y;
}
public double Distance()
{
return Math.Sqrt(X * X + Y * Y);
}
}
// Nested interface
public interface INestedInterface
{
void Method();
}
// Nested enum
public enum NestedEnum
{
One,
Two,
Three
}
// Method that uses nested types
public void UseNestedTypes()
{
NestedClass nestedObj = new NestedClass(this);
nestedObj.DisplayOuterField();
NestedStruct point = new NestedStruct(3, 4);
Console.WriteLine($"Distance: {point.Distance()}");
NestedEnum value = NestedEnum.Two;
Console.WriteLine($"Enum value: {value}");
}
// Class that implements the nested interface
public class ImplementationClass : INestedInterface
{
public void Method()
{
Console.WriteLine("Implementing the nested interface");
}
}
}
Using Nested Types
// Creating an instance of the outer class
OuterClass outer = new OuterClass(42);
// Creating an instance of the nested class
OuterClass.NestedClass nested = new OuterClass.NestedClass(outer);
nested.DisplayOuterField();
// Creating an instance of the nested struct
OuterClass.NestedStruct point = new OuterClass.NestedStruct(3, 4);
Console.WriteLine($"Distance: {point.Distance()}");
// Using the nested enum
OuterClass.NestedEnum value = OuterClass.NestedEnum.Two;
Console.WriteLine($"Enum value: {value}");
// Using the nested interface
OuterClass.INestedInterface implementation = new OuterClass.ImplementationClass();
implementation.Method();
// Using the method that uses nested types
outer.UseNestedTypes();
3.6.5 - Generics in OOP
Generics allow you to define type-safe classes, interfaces, methods, and delegates without committing to specific data types. They provide better type safety, reduce the need for casting, and enable code reuse across different data types.
Generic Classes
// Generic class with a type parameter
public class GenericList<T>
{
private T[] items;
private int count;
public GenericList(int capacity)
{
items = new T[capacity];
count = 0;
}
public void Add(T item)
{
if (count < items.Length)
{
items[count] = item;
count++;
}
else
{
throw new InvalidOperationException("List is full");
}
}
public T GetItem(int index)
{
if (index < 0 || index >= count)
{
throw new IndexOutOfRangeException("Index is out of range");
}
return items[index];
}
public int Count => count;
}
// Generic class with multiple type parameters
public class KeyValuePair<TKey, TValue>
{
public TKey Key { get; set; }
public TValue Value { get; set; }
public KeyValuePair(TKey key, TValue value)
{
Key = key;
Value = value;
}
public override string ToString()
{
return $"{Key}: {Value}";
}
}
// Generic class with constraints
public class Repository<T> where T : class, new()
{
private List<T> items = new List<T>();
public void Add(T item)
{
items.Add(item);
}
public T CreateNew()
{
T item = new T();
Add(item);
return item;
}
public IEnumerable<T> GetAll()
{
return items;
}
}
Generic Interfaces
// Generic interface
public interface IRepository<T>
{
void Add(T item);
void Remove(T item);
T GetById(int id);
IEnumerable<T> GetAll();
}
// Generic interface with constraints
public interface IEntity<TId> where TId : IComparable<TId>
{
TId Id { get; set; }
bool IsValid();
}
// Implementing generic interfaces
public class Product : IEntity<int>
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public bool IsValid()
{
return Id > 0 && !string.IsNullOrEmpty(Name) && Price >= 0;
}
}
public class ProductRepository : IRepository<Product>
{
private List<Product> products = new List<Product>();
public void Add(Product item)
{
products.Add(item);
}
public void Remove(Product item)
{
products.Remove(item);
}
public Product GetById(int id)
{
return products.FirstOrDefault(p => p.Id == id);
}
public IEnumerable<Product> GetAll()
{
return products;
}
}
Generic Methods
public class Utilities
{
// Generic method
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
// Generic method with constraints
public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
// Generic method with multiple type parameters
public static TResult Transform<TInput, TResult>(TInput input, Func<TInput, TResult> transformer)
{
return transformer(input);
}
}
Using Generics
// Using generic classes
GenericList<int> numbers = new GenericList<int>(5);
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
for (int i = 0; i < numbers.Count; i++)
{
Console.WriteLine(numbers.GetItem(i));
}
KeyValuePair<string, int> pair = new KeyValuePair<string, int>("Age", 30);
Console.WriteLine(pair);
Repository<Product> productRepo = new Repository<Product>();
Product newProduct = productRepo.CreateNew();
newProduct.Name = "Generic Product";
newProduct.Price = 19.99m;
// Using generic interfaces
ProductRepository repository = new ProductRepository();
repository.Add(new Product { Id = 1, Name = "Product 1", Price = 9.99m });
repository.Add(new Product { Id = 2, Name = "Product 2", Price = 19.99m });
Product product = repository.GetById(1);
Console.WriteLine($"Product: {product.Name}, Price: {product.Price:C}");
// Using generic methods
int a = 5, b = 10;
Console.WriteLine($"Before swap: a = {a}, b = {b}");
Utilities.Swap(ref a, ref b);
Console.WriteLine($"After swap: a = {a}, b = {b}");
string str1 = "apple", str2 = "banana";
Console.WriteLine($"Max of {str1} and {str2}: {Utilities.Max(str1, str2)}");
int result = Utilities.Transform(5, x => x * x);
Console.WriteLine($"Transformed result: {result}");
3.6.6 - Covariance and Contravariance
Covariance and contravariance are concepts that allow for flexibility in the assignment compatibility of generic types. Covariance allows a method to return a more derived type than specified by the generic parameter, while contravariance allows a method to accept a less derived type than specified by the generic parameter.
Covariance with Interfaces
// Covariant interface (out keyword)
public interface IProducer<out T>
{
T Produce();
}
// Implementing the covariant interface
public class Producer<T> : IProducer<T>
{
private T item;
public Producer(T item)
{
this.item = item;
}
public T Produce()
{
return item;
}
}
// Classes for demonstration
public class Animal
{
public string Name { get; set; }
}
public class Dog : Animal
{
public string Breed { get; set; }
}
Contravariance with Interfaces
// Contravariant interface (in keyword)
public interface IConsumer<in T>
{
void Consume(T item);
}
// Implementing the contravariant interface
public class Consumer<T> : IConsumer<T>
{
public void Consume(T item)
{
Console.WriteLine($"Consuming {item}");
}
}
Using Covariance and Contravariance
// Covariance example
Dog dog = new Dog { Name = "Buddy", Breed = "Golden Retriever" };
IProducer<Dog> dogProducer = new Producer<Dog>(dog);
IProducer<Animal> animalProducer = dogProducer; // Covariance allows this
Animal animal = animalProducer.Produce();
Console.WriteLine($"Produced animal: {animal.Name}");
// Contravariance example
IConsumer<Animal> animalConsumer = new Consumer<Animal>();
IConsumer<Dog> dogConsumer = animalConsumer; // Contravariance allows this
dogConsumer.Consume(dog);
Covariance and Contravariance with Delegates
// Delegate types
public delegate T Producer<out T>();
public delegate void Consumer<in T>(T item);
// Methods for delegates
public static Animal ProduceAnimal()
{
return new Animal { Name = "Generic Animal" };
}
public static Dog ProduceDog()
{
return new Dog { Name = "Buddy", Breed = "Golden Retriever" };
}
public static void ConsumeAnimal(Animal animal)
{
Console.WriteLine($"Consuming animal: {animal.Name}");
}
public static void ConsumeDog(Dog dog)
{
Console.WriteLine($"Consuming dog: {dog.Name}, Breed: {dog.Breed}");
}
Using Covariance and Contravariance with Delegates
// Covariance with delegates
Producer<Dog> dogProducer = ProduceDog;
Producer<Animal> animalProducer = dogProducer; // Covariance allows this
Animal producedAnimal = animalProducer();
Console.WriteLine($"Produced animal: {producedAnimal.Name}");
// Contravariance with delegates
Consumer<Animal> animalConsumer = ConsumeAnimal;
Consumer<Dog> dogConsumer = animalConsumer; // Contravariance allows this
dogConsumer(new Dog { Name = "Rex", Breed = "German Shepherd" });
3.6.7 - Delegates and Events in OOP
Delegates are type-safe function pointers that can reference methods with a specific signature. Events are a way to implement the observer pattern, allowing objects to notify other objects when something of interest occurs.
Delegates
// Delegate declaration
public delegate void MessageHandler(string message);
public delegate TResult Transformer<T, TResult>(T input);
public class DelegateExample
{
// Method that matches the MessageHandler delegate
public void DisplayMessage(string message)
{
Console.WriteLine($"Message: {message}");
}
// Method that matches the Transformer delegate
public int StringToLength(string input)
{
return input?.Length ?? 0;
}
// Method that takes a delegate as a parameter
public void ProcessMessages(string[] messages, MessageHandler handler)
{
foreach (string message in messages)
{
handler(message);
}
}
// Method that returns a delegate
public Transformer<string, int> GetTransformer()
{
return StringToLength;
}
}
Events
// Custom event args
public class OrderEventArgs : EventArgs
{
public string OrderId { get; }
public decimal Total { get; }
public OrderEventArgs(string orderId, decimal total)
{
OrderId = orderId;
Total = total;
}
}
// Class with events
public class OrderProcessor
{
// Event declaration using a built-in delegate type
public event EventHandler<OrderEventArgs> OrderProcessed;
// Event declaration using a custom delegate type
public delegate void OrderFailedEventHandler(object sender, string orderId, Exception ex);
public event OrderFailedEventHandler OrderFailed;
// Method that raises events
public void ProcessOrder(string orderId, decimal total)
{
try
{
// Simulate order processing
Console.WriteLine($"Processing order {orderId} with total {total:C}");
// Raise the OrderProcessed event
OnOrderProcessed(new OrderEventArgs(orderId, total));
}
catch (Exception ex)
{
// Raise the OrderFailed event
OnOrderFailed(orderId, ex);
}
}
// Protected method to raise the OrderProcessed event
protected virtual void OnOrderProcessed(OrderEventArgs e)
{
OrderProcessed?.Invoke(this, e);
}
// Protected method to raise the OrderFailed event
protected virtual void OnOrderFailed(string orderId, Exception ex)
{
OrderFailed?.Invoke(this, orderId, ex);
}
}
Using Delegates and Events
// Using delegates
DelegateExample example = new DelegateExample();
// Creating a delegate instance
MessageHandler handler = example.DisplayMessage;
// Adding methods to a delegate (multicast delegate)
handler += message => Console.WriteLine($"Lambda: {message}");
handler += message => Console.WriteLine($"Length: {message.Length}");
// Invoking the delegate
handler("Hello, World!");
// Using a delegate as a parameter
string[] messages = { "Message 1", "Message 2", "Message 3" };
example.ProcessMessages(messages, handler);
// Using a delegate with generics
Transformer<string, int> transformer = example.GetTransformer();
int length = transformer("Hello");
Console.WriteLine($"Length: {length}");
// Using events
OrderProcessor processor = new OrderProcessor();
// Subscribing to events
processor.OrderProcessed += (sender, e) =>
{
Console.WriteLine($"Order {e.OrderId} processed with total {e.Total:C}");
};
processor.OrderFailed += (sender, orderId, ex) =>
{
Console.WriteLine($"Order {orderId} failed: {ex.Message}");
};
// Triggering events
processor.ProcessOrder("ORD-12345", 123.45m);
3.6.8 - Pattern Matching with Objects
Pattern matching allows you to test if an object has a specific type or structure. It's a powerful feature for working with object hierarchies and polymorphism.
For more advanced pattern matching techniques and C# 9+ enhancements, see Section 7.2.9 - Pattern Matching Enhancements (C# 9+) in the Advanced C# Topics chapter.
Type Patterns
public void ProcessObject(object obj)
{
// Type pattern with 'is'
if (obj is string str)
{
Console.WriteLine($"String: {str}, Length: {str.Length}");
}
else if (obj is int num)
{
Console.WriteLine($"Integer: {num}, Squared: {num * num}");
}
else if (obj is List<int> list)
{
Console.WriteLine($"List of integers with {list.Count} items");
foreach (int item in list)
{
Console.WriteLine($" {item}");
}
}
else
{
Console.WriteLine($"Unknown type: {obj?.GetType().Name ?? "null"}");
}
}
Switch Expression with Patterns
public string DescribeObject(object obj)
{
// Switch expression with patterns (C# 8+)
return obj switch
{
string s => $"String: {s}, Length: {s.Length}",
int n => $"Integer: {n}, Squared: {n * n}",
List<int> list => $"List of integers with {list.Count} items",
Person p => $"Person: {p.FirstName} {p.LastName}",
null => "Null object",
_ => $"Unknown type: {obj.GetType().Name}"
};
}
Property Patterns
public string DescribePerson(Person person)
{
// Property pattern (C# 8+)
return person switch
{
{ FirstName: "John", LastName: "Doe" } => "This is John Doe",
{ FirstName: "Jane", LastName: "Doe" } => "This is Jane Doe",
{ FirstName: var first, LastName: "Smith" } => $"This is {first} Smith",
{ FirstName: var first, LastName: var last } => $"This is {first} {last}",
null => "Null person"
};
}
Tuple Patterns
public string DescribePoint(int x, int y)
{
// Tuple pattern (C# 8+)
return (x, y) switch
{
(0, 0) => "Origin",
(var a, 0) => $"On the X-axis at {a}",
(0, var b) => $"On the Y-axis at {b}",
(var a, var b) when a == b => $"On the diagonal at ({a}, {b})",
(var a, var b) => $"At point ({a}, {b})"
};
}
Positional Patterns
public class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
// Deconstruct method for positional patterns
public void Deconstruct(out int x, out int y)
{
x = X;
y = Y;
}
}
public string DescribePoint(Point point)
{
// Positional pattern (C# 8+)
return point switch
{
(0, 0) => "Origin",
(var x, 0) => $"On the X-axis at {x}",
(0, var y) => $"On the Y-axis at {y}",
(var x, var y) when x == y => $"On the diagonal at ({x}, {y})",
(var x, var y) => $"At point ({x}, {y})",
null => "Null point"
};
}
Using Pattern Matching
// Using type patterns
ProcessObject("Hello");
ProcessObject(42);
ProcessObject(new List<int> { 1, 2, 3 });
ProcessObject(new Person { FirstName = "John", LastName = "Doe" });
ProcessObject(null);
// Using switch expressions with patterns
Console.WriteLine(DescribeObject("Hello"));
Console.WriteLine(DescribeObject(42));
Console.WriteLine(DescribeObject(new List<int> { 1, 2, 3 }));
Console.WriteLine(DescribeObject(new Person { FirstName = "John", LastName = "Doe" }));
Console.WriteLine(DescribeObject(null));
// Using property patterns
Console.WriteLine(DescribePerson(new Person { FirstName = "John", LastName = "Doe" }));
Console.WriteLine(DescribePerson(new Person { FirstName = "Jane", LastName = "Doe" }));
Console.WriteLine(DescribePerson(new Person { FirstName = "Alice", LastName = "Smith" }));
Console.WriteLine(DescribePerson(new Person { FirstName = "Bob", LastName = "Johnson" }));
// Using tuple patterns
Console.WriteLine(DescribePoint(0, 0));
Console.WriteLine(DescribePoint(5, 0));
Console.WriteLine(DescribePoint(0, 7));
Console.WriteLine(DescribePoint(3, 3));
Console.WriteLine(DescribePoint(2, 4));
// Using positional patterns
Console.WriteLine(DescribePoint(new Point(0, 0)));
Console.WriteLine(DescribePoint(new Point(5, 0)));
Console.WriteLine(DescribePoint(new Point(0, 7)));
Console.WriteLine(DescribePoint(new Point(3, 3)));
Console.WriteLine(DescribePoint(new Point(2, 4)));
These advanced OOP features in C# provide powerful tools for creating more expressive, flexible, and maintainable code. By mastering these features, you can take full advantage of C#'s object-oriented capabilities and write code that is both elegant and efficient.