Advanced C# Concepts

This section delves into powerful and sophisticated features of C# that enable you to write more robust, efficient, and maintainable code.

Generics

Generics allow you to define type-safe classes, interfaces, methods, and delegates without specifying their exact type at compile time. This promotes code reusability and type safety.

Why Use Generics?

  • Type Safety: Compile-time checks prevent type errors.
  • Performance: Avoids boxing and unboxing overhead associated with non-generic collections.
  • Code Reusability: Write a single method or class that can operate on different data types.

Example: Generic List


using System;
using System.Collections.Generic;

public class Example
{
    public static void Main(string[] args)
    {
        List numbers = new List();
        numbers.Add(10);
        numbers.Add(20);

        foreach (int number in numbers)
        {
            Console.WriteLine(number);
        }

        List names = new List();
        names.Add("Alice");
        names.Add("Bob");

        foreach (string name in names)
        {
            Console.WriteLine(name);
        }
    }
}
                

Delegates and Events

Delegates are type-safe function pointers. They are fundamental to implementing callbacks and event-driven programming in C#.

Delegates

A delegate defines the signature of a method. Any method that matches this signature can be assigned to the delegate.


public delegate void MyDelegate(string message);

public class MyClass
{
    public void DisplayMessage(string msg)
    {
        Console.WriteLine($"Message: {msg}");
    }

    public static void Main(string[] args)
    {
        MyClass obj = new MyClass();
        MyDelegate handler = new MyDelegate(obj.DisplayMessage);
        handler("Hello, Delegates!");
    }
}
                

Events

Events are a mechanism that allows an object (the publisher) to notify other objects (the subscribers) when something of interest happens. Events are built upon delegates.


public class Publisher
{
    public delegate void MyEventHandler(object sender, EventArgs e);
    public event MyEventHandler MyEvent;

    public void TriggerEvent()
    {
        OnMyEvent(EventArgs.Empty);
    }

    protected virtual void OnMyEvent(EventArgs e)
    {
        MyEvent?.Invoke(this, e);
    }
}

public class Subscriber
{
    public void Subscribe(Publisher pub)
    {
        pub.MyEvent += Handler;
    }

    private void Handler(object sender, EventArgs e)
    {
        Console.WriteLine("Event received!");
    }

    public static void Main(string[] args)
    {
        Publisher publisher = new Publisher();
        Subscriber subscriber = new Subscriber();
        subscriber.Subscribe(publisher);
        publisher.TriggerEvent();
    }
}
                

Language Integrated Query (LINQ)

LINQ provides a powerful and flexible way to query data from various sources (collections, databases, XML) using a syntax similar to SQL.

Key Concepts

  • Query Syntax and Method Syntax
  • Deferred Execution: Queries are executed only when the results are iterated.
  • Query Providers: LINQ to Objects, LINQ to SQL, LINQ to XML, etc.

Example: Querying a List


using System;
using System.Collections.Generic;
using System.Linq;

public class LinqExample
{
    public static void Main(string[] args)
    {
        List numbers = new List { 5, 10, 15, 20, 25, 30 };

        // Query syntax
        var evenNumbersQuery = from num in numbers
                               where num % 2 == 0
                               orderby num descending
                               select num;

        Console.WriteLine("Even numbers (Query Syntax):");
        foreach (var num in evenNumbersQuery)
        {
            Console.Write(num + " ");
        }
        Console.WriteLine();

        // Method syntax
        var oddNumbersMethod = numbers.Where(num => num % 2 != 0)
                                    .OrderBy(num => num)
                                    .ToList();

        Console.WriteLine("Odd numbers (Method Syntax):");
        foreach (var num in oddNumbersMethod)
        {
            Console.Write(num + " ");
        }
        Console.WriteLine();
    }
}
                

Asynchronous Programming (async/await)

The async and await keywords simplify writing asynchronous code, allowing your application to remain responsive while performing long-running operations.

Benefits

  • Responsiveness: Keeps the UI thread free.
  • Scalability: Efficiently handles I/O-bound operations.
  • Simplified Code: Makes asynchronous code look more like synchronous code.

Example: Asynchronous Download


using System;
using System.Net.Http;
using System.Threading.Tasks;

public class AsyncExample
{
    public static async Task Main(string[] args)
    {
        Console.WriteLine("Starting download...");
        string content = await DownloadContentAsync("https://www.example.com");
        Console.WriteLine("Download complete. Content length: " + content.Length);
    }

    public static async Task<string> DownloadContentAsync(string url)
    {
        using (HttpClient client = new HttpClient())
        {
            return await client.GetStringAsync(url);
        }
    }
}
                

Reflection

Reflection allows you to inspect and manipulate types, methods, and properties at runtime. It's useful for frameworks, serializers, and debugging tools.

Capabilities

  • Get type information (name, base type, interfaces).
  • Invoke methods dynamically.
  • Create instances of types.
  • Access and set properties and fields.

Example: Inspecting a Type


using System;
using System.Reflection;

public class ReflectionExample
{
    public static void Main(string[] args)
    {
        Type stringType = typeof(string);

        Console.WriteLine($"Type Name: {stringType.Name}");
        Console.WriteLine($"Full Name: {stringType.FullName}");
        Console.WriteLine("Methods:");

        MethodInfo[] methods = stringType.GetMethods();
        foreach (var method in methods)
        {
            Console.WriteLine($"- {method.Name}");
        }

        // Dynamic invocation
        object instance = "Hello";
        MethodInfo toUpper = stringType.GetMethod("ToUpper");
        string upperCaseString = (string)toUpper.Invoke(instance, null);
        Console.WriteLine($"ToUpper result: {upperCaseString}");
    }
}
                

Memory Management

Understanding how C# manages memory, including the Garbage Collector (GC) and value vs. reference types, is crucial for performance and avoiding memory leaks.

Key Concepts

  • Stack vs. Heap: Where value types and reference types are stored.
  • Garbage Collector (GC): Automatic memory management.
  • Generations: How the GC categorizes objects for efficient collection.
  • IDisposable and using statement: For deterministic resource cleanup.

The using Statement

The using statement ensures that an object implementing IDisposable has its Dispose() method called, releasing unmanaged resources.


using System;
using System.IO;

public class MemoryManagementExample
{
    public static void ProcessFile(string filePath)
    {
        // The 'using' statement ensures fileStream.Dispose() is called
        // even if an exception occurs.
        using (FileStream fileStream = new FileStream(filePath, FileMode.Open))
        using (StreamReader reader = new StreamReader(fileStream))
        {
            string line;
            while ((line = reader.ReadLine()) != null)
            {
                Console.WriteLine(line);
            }
        } // fileStream and reader are disposed here
    }

    public static void Main(string[] args)
    {
        // Create a dummy file for demonstration
        string tempFilePath = "temp_doc.txt";
        File.WriteAllText(tempFilePath, "Line 1\nLine 2\nLine 3");

        Console.WriteLine($"Reading file: {tempFilePath}");
        ProcessFile(tempFilePath);

        // Clean up the dummy file
        File.Delete(tempFilePath);
    }
}
                

Note on Garbage Collection

While the GC is powerful, understanding its behavior, especially in high-performance scenarios, can help optimize your applications. Avoid creating unnecessary large objects or long-lived object graphs that can put pressure on the GC.

Tip for Resource Management

Always use the using statement for any object that implements IDisposable (like file streams, database connections, graphics objects) to ensure proper cleanup of unmanaged resources.