C# > Advanced C# > Collections and Generics > Generic Methods and Classes

Generic Class with Type Constraint

This example shows a generic class with a type constraint, ensuring that the type used must implement a specific interface. This enforces a certain contract and allows the class to work with types in a predictable way.

Code Snippet

This code defines an interface IPrintable with a single method Print(). The Printer class is a generic class with a type constraint where T : IPrintable. This constraint ensures that the type parameter T must implement the IPrintable interface. The Printer class has a constructor that takes an object of type T and a method PrintItem() that calls the Print() method of the object. The Document class implements the IPrintable interface and provides a concrete implementation of the Print() method. The usage example shows how to create a Document object and a Printer object, and then call the PrintItem() method to print the document content. The commented out line Printer intPrinter = new Printer(5); would cause a compile-time error because int does not implement the IPrintable interface, demonstrating the type safety provided by the generic type constraint.

using System;

// Define an interface
public interface IPrintable
{
    void Print();
}

// Generic class with a type constraint
public class Printer<T> where T : IPrintable
{
    private T _itemToPrint;

    public Printer(T itemToPrint)
    {
        _itemToPrint = itemToPrint;
    }

    public void PrintItem()
    {
        _itemToPrint.Print();
    }
}

// Concrete class implementing the interface
public class Document : IPrintable
{
    public string Content { get; set; }

    public Document(string content)
    {
        Content = content;
    }

    public void Print()
    {
        Console.WriteLine($"Printing document: {Content}");
    }
}

// Usage Example:
public class Example
{
    public static void Main(string[] args)
    {
        Document myDocument = new Document("Hello, Generic World!");
        Printer<Document> documentPrinter = new Printer<Document>(myDocument);
        documentPrinter.PrintItem(); // Output: Printing document: Hello, Generic World!

        //The following line would cause a compile-time error because 'int' does not implement IPrintable
        //Printer<int> intPrinter = new Printer<int>(5); // Compiler Error
    }
}

Concepts Behind the Snippet

  • Generics: Enable writing code that can operate on different types without code duplication.
  • Type Constraints: Restrict the types that can be used with a generic type, enforcing a specific contract.
  • Interfaces: Define a contract that classes can implement, ensuring they provide specific functionality.

Real-Life Use Case

Consider a scenario where you have different types of reports (e.g., sales report, financial report) and you want to process them using a generic processing class. You can define an interface IReport with a method GenerateReport(), and then create a generic ReportProcessor class with the constraint where T : IReport. This ensures that the processor can only work with report types that implement the IReport interface.

public interface IReport
{
    string GenerateReport();
}

public class SalesReport : IReport
{
   public string GenerateReport() { return "Sales Report Content"; }
}

public class ReportProcessor where T : IReport
{
    public string ProcessReport(T report)
    {
       return report.GenerateReport();
    }
}

Best Practices

  • Choose the right constraint: Select the most appropriate interface or base class for your type constraint to ensure that the generic type can perform the required operations.
  • Avoid unnecessary constraints: Only use constraints when they are necessary to enforce a specific contract or provide required functionality. Overly restrictive constraints can limit the usability of the generic type.
  • Design for flexibility: Consider using interfaces instead of concrete classes for type constraints to allow for greater flexibility and extensibility.

Interview Tip

Be prepared to explain the purpose of type constraints in generics, how they improve type safety, and how they enable code reuse. Also, be ready to discuss the different types of type constraints (e.g., interface constraint, class constraint, constructor constraint).

When to Use Them

Use generic classes with type constraints when you need to ensure that the type used with the generic class implements a specific interface or inherits from a specific base class. This allows you to write code that relies on the functionality provided by the interface or base class, while still maintaining type safety and reusability.

Memory Footprint

The memory footprint of a generic class with type constraints is similar to that of a regular generic class. Each instantiation of the generic class with a specific type parameter creates a new type, which can contribute to code bloat if many different types are used. The memory footprint of the objects stored in the class depends on their size and the number of objects.

Alternatives

  • Abstract Classes: You can use an abstract class instead of an interface as a type constraint. However, this approach is less flexible because a class can only inherit from one abstract class, while it can implement multiple interfaces.
  • Dynamic Typing: You can use the dynamic keyword to bypass compile-time type checking. However, this approach sacrifices type safety and can lead to runtime errors.

Pros

  • Type Safety: Ensures that the correct type is being used at compile time.
  • Code Reuse: Allows you to write code that can work with different types that implement a specific interface or inherit from a specific base class.
  • Maintainability: Makes the code more maintainable by enforcing a consistent contract for the types used with the generic class.

Cons

  • Complexity: Can be more complex to write and understand than non-generic code, especially for developers new to generics and type constraints.
  • Limited Flexibility: Type constraints can limit the types that can be used with the generic class.

FAQ

  • What happens if I try to create a `Printer` with a type that doesn't implement `IPrintable`?

    You will get a compile-time error because the type constraint is not satisfied.
  • Can I have multiple type constraints?

    Yes, you can have multiple type constraints using the where keyword multiple times (e.g., where T : IPrintable, new()). Note that new() constraint must be the last one if present.
  • Why use an interface constraint instead of a concrete class?

    Using an interface constraint promotes loose coupling and allows for more flexibility. You can use any type that implements the interface, rather than being restricted to a specific class.