🧠Advanced C# Programming

Explore advanced topics like delegates, events, generics, exception handling, and lambda expressions to write efficient, elegant C# code.

🎯 Delegates, Events, and Lambdas

Welcome to our guide on Delegates, Events, and Lambdas in C#! These powerful features are essential for modern C# development. Let's dive into how they work and why they matter.

💡 What Are Delegates?

A delegate is a type that represents references to methods with a particular parameter list and return type. Think of it as a pointer to a method.

// Delegate declaration
public delegate void MyDelegate(int value);

// Method matching the delegate signature
void MyMethod(int x) {
    Console.WriteLine(x);
}

// Assigning the method to the delegate
MyDelegate del = new MyDelegate(MyMethod);

💡 Key Points About Delegates

  • Delegates are strongly typed.
  • They can be passed as parameters to methods.
  • Multiple delegates can be combined into a single invocation list (multicast delegates).

💡 Multicast Delegates

A multicast delegate can point to more than one method. When invoked, it executes all its registered methods in the order they were added.

MyDelegate del = new MyDelegate(MyMethod);
del += AnotherMethod;
del -= AnotherMethod;

💡 Events and Event Handling

An event is a way for an object to communicate with other objects when something significant happens. Events are based on delegates but provide better encapsulation.

// Event declaration
public event MyDelegate SomethingHappened;

// Raising the event
protected virtual void OnSomethingHappened(int value) {
    SomethingHappened?.Invoke(value);
}

💡 Lambda Expressions

A lambda expression is an anonymous method defined using concise syntax. They are particularly useful for creating delegates and event handlers.

// Lambda as a delegate
MyDelegate del = x => Console.WriteLine(x);

del(42);

💡 Lambdas in LINQ

Lambda expressions are widely used in LINQ (Language Integrated Query) to define queries that filter, order, and project data.

// LINQ query with lambda
var evenNumbers = numbers.Where(n => n % 2 == 0);

💡 Best Practices

  • Always use EventHandler<TEventArgs> when declaring events.
  • Prefer lambdas for concise and readable event handlers.
  • Avoid excessive multicast delegates to prevent performance issues.

Common Mistakes

  • Forgetting to check for null events before invoking them (using the null-conditional operator ?.)
  • Creating overly complex multicast delegates that are hard to maintain.
  • Not using the correct delegate type for events.

💡 Real World Applications

Delegates, events, and lambdas are used in various scenarios: - UI event handling (e.g., button click events) - Asynchronous programming - Data processing with LINQ - Frameworks like ASP.NET Core

🧰 Generics and Constraints

Welcome to our chapter on Generics! In this section, we'll explore how to create reusable and type-safe components in C#. We'll also dive into constraints that allow you to enforce specific types for your generic code.

💡 What are Generics?

Generics provide a way to create classes, interfaces, and methods that work with any data type. They ensure compile-time type safety while maintaining flexibility.

// Without generics
collection.AddItem(123);

// With generics
collection.Add<string>('Hello');

Common Constraints

  • where T : struct - Ensures the type is a value type
  • where T : class - Ensures the type is a reference type
  • where T : new() - Requires the type has a public parameterless constructor
  • where T : TBase - Specifies that T must inherit from a base class or implement an interface

💡 Constraint Examples

// Example with struct constraint
class Box<T> where T : struct {
    public T Value { get; set; }
}

// Example with inheritance constraint
interface IShape {}

class ShapeProcessor<T> where T : IShape {}

Best Practices for Generics

  • Use generics when you need to write reusable code that works with different types
  • Always add appropriate constraints to ensure type safety
  • Avoid over-constraining - only specify what's necessary for the implementation
  • Consider using multiple constraints when needed

Common Mistakes

  • Don't forget to specify necessary constraints that your code requires
  • Don't use object as a generic type unless absolutely necessary
  • Avoid creating overly complex constraint chains that are hard to understand

🧯 Exception Handling & Defensive Programming

💡 Exception Handling Basics

Exception handling is a critical aspect of robust software development. It allows you to handle unexpected errors and ensure your application remains stable even when things go wrong.

try
{
    // Code that might throw an exception
}
catch (Exception ex)
{
    // Handle the exception
}
finally
{
    // Cleanup code that always executes
}

💡 Handling Multiple Exceptions

You can catch multiple types of exceptions using multiple catch blocks or by catching base exception classes.

try
{
    // Code that might throw an exception
}
catch (ArgumentNullException ex)
{
    // Handle null argument specific error
}
catch (FormatException ex)
{
    // Handle format validation errors
}
catch (Exception ex)
{
    // Catch any other exceptions
}

💡 Creating Custom Exceptions

Create custom exceptions by inheriting from the Exception class or one of its derivatives.

public class InvalidDataException : Exception
{
    public InvalidDataException(string message) : base(message)
    {
    }
}

// Throwing custom exception
throw new InvalidDataException("Invalid data format provided");

💡 Defensive Programming Basics

Defensive programming involves writing code that anticipates potential issues and handles them proactively. This includes using guard clauses to validate inputs before proceeding.

// Guard clause example
public void ProcessData(string input)
{
    if (string.IsNullOrEmpty(input))
    {
        throw new ArgumentNullException(nameof(input), "Input cannot be null or empty.");
    }

    // Proceed with processing valid data
}

Best Practices for Defensive Programming

  • Always validate inputs before using them.
  • Use guard clauses to handle invalid states early.
  • Implement fail-fast strategies where appropriate.
  • Document expected input ranges and constraints.

Common Mistakes in Exception Handling

  • Avoid empty catch blocks that swallow exceptions.
  • Don't catch general exceptions without handling them properly.
  • Don't handle exceptions too far from where they occur.
  • Don't use exceptions for normal program flow.

💡 Advanced Defensive Coding Techniques

Implement defensive programming by checking preconditions, postconditions, and invariants to ensure your code behaves as expected under all circumstances.

// Example of validating method arguments
public void CalculateAverage(int[] numbers)
{
    if (numbers == null || numbers.Length == 0)
    {
        throw new ArgumentException("Numbers array must contain at least one element.", nameof(numbers));
    }

    double average = numbers.Average();
    Console.WriteLine($"The average is: {average}");
}

Quiz

Question 1 of 15

Which of the following is a correct way to declare a delegate that takes no parameters and returns void?

  • public delegate void MyDelegate();
  • public delegate int MyDelegate();
  • public delegate string MyDelegate(string input);
  • public delegate bool MyDelegate(bool flag);