[C# Programming] Exception handling, generics

1. Exception handling

1.1 Multiple exception types

C# allows code to throw derived from System.Exception. For example:

public sealed class TextNumberParser
{
    public static int Parse(string textDigit)
    {
        string[] digitTexts = { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" };
        int result = Array.IndexOf(digitTexts, textDigit.ToLower());
        if(result < 0)
            throw new ArgumentException( "The argument did not represent a digit", nameof(textDigit));
        return result;
    }
}

Two similar exceptions are ArgumentNullException and NullReferenceException. Generally, when dereferencing a null value, the underlying NullReferenceException is triggered.

An important feature of parameter exception types (ArgumentException, ArgumentNullException, and ArgumentOutRangeException) is that each exception has a constructor parameter that allows the actual parameter name to be identified as a string. The C# specification is that for parameter names in parameter type exceptions, you should use nameof operator.

Do not throw System.SystemException and its derived exception types System.stackoverflowException, System.OutOfMemoryException, System.Runtime.InteropServices.COMException, System.ExecutionEngineException and System.Runtime.InteropServices.SEHException

Do not throw System.Exception or System.ApplicationException

Consider calling System.Environment.FailFast() to terminate the process when it becomes unsafe to continue executing.

1.2 Catching Exceptions

C# allows the use of multiple catch blocks, each of which can locate a specific exception type.

public static void Main(string[] args)
{
    try
    {
        // throw new Win32Exception(42);
        throw new InvalidOperationException("Arbitrary exception");
    }
    catch(Win32Exception exception)
    when(args.Length == exception.NativeErrorCode)
    {
        //....
    }
    catch(NullReferenceException exception) {
        // Handle NullReferenceException
    }
    catch(ArgumentException exception) {
        // Handle ArgumentException
    }
    catch(InvalidOperationException exception) {
        // Handle ApplicationException
    }
    catch(Exception exception) {
        //…
    }
    finally {
        // Handle any cleanup code here as it runs regardless of whether there is an exception
    }
}

Re-throw an existing exception: If a specific exception is thrown, all stack information will be updated to match the new throwing location. This causes all stack information indicating the call location where the exception originally occurred to be lost. Therefore C# can only rethrow exceptions in catch statements, for example:

public static void Main(string[] args)
{
    //….
    catch(InvalidOperationException exception)
    {
        bool exceptionHandled = false ;
        if (!exceptionHandled )
            throw;
    }
    //…
}

Throw existing exceptions without replacing stack information: C# 5.0 adds a mechanism that allows previously thrown exceptions to be thrown without losing the stack trace information in the original exception. This way the exception can be re-thrown even outside the catch. System.Runtime.ExceptionServices.ExceptionDispatchInfo handles this situation, for example:

try{
    //....
}
catch(AggregateException exception){
    exception = exeption.Flatten();
    ExceptionDispatchInfo.Capture(exception.InnerException).Throw();
}

1.3 General catch block

Beginning with C# 2.0, all exceptions (whether derived from System.Exception or not) will be wrapped as derived from System.Exception when entering the assembly.

C# also supports a regular catch block, namely catch {}. It behaves exactly the same as the catch(System.Exception exception) block, except that it does not have a type name and a variable name. In addition, the regular catch block must be the last of all catch blocks.

1.4 Specifications for exception handling

Exception handling specifications:

  • Only catch exceptions that can be handled
  • Don’t hide unhandled exceptions
  • Use System.Exception and regular catch blocks as little as possible
  • Avoid logging or reporting exceptions lower in the call stack
  • Use throw in the catch block instead of the throw statement
  • Be careful when re-throwing different exceptions

1.5 Custom exception

The only requirement for a custom exception is to derive from System.Exception or one of its subclasses. For example:

class DatabseException: System.Expection {
    public DatabaseException()
    {
        //…..
    }
    public DatabaseException(string message)
    {
        //…..
    }
    public DatabaseException(string message, Exception innerException)
    {
        InnerException= innerException;
        //…..
    }
    public DatabaseException(System.Data.OracleclientException exception)
    {
        InnerException= innerException;
        //…..
    }
}

When using custom exceptions, you should adhere to the following practices:

  • All exceptions should use the “Exception” suffix
  • In general, all exceptions should contain the following three constructors: a no-argument constructor, a constructor that takes a string parameter, and a constructor that takes both a string and an inner exception as parameters. In addition, any exception should be allowed. data as part of constructor
  • Avoid using deep inheritance class hierarchies (should generally be less than 5 levels)

2. Generics

2.1 Use of generics

Similar to C++, generic classes and structs in C# require the use of angle brackets to declare generic type parameters and specify generic type arguments.

public void Sketch()
{
    Stack<Cell> path = new Stack<Cell>();
    Cell currentPosition;
    ConsoleKeyInfo key; // Added in C# 2.0
    Console.WriteLine("Use arrow keys to draw. X to exit.");
    for(int i = 2; i < Console.WindowHeight; i + + )
        Console.WriteLine();
    currentPosition = new Cell(Console.WindowWidth / 2, Console.WindowHeight / 2);
    path.Push(currentPosition);
    FillCell(currentPosition);
    do {
        bool bFill = false;
        key = Move();
        switch(key.Key) {

        case ConsoleKey.Z:
            if(path.Count >= 1)
            {
                // Undo the previous Move.
                currentPosition = path.Pop();
                Console.SetCursorPosition(currentPosition.X, currentPosition.Y);
                FillCell(currentPosition, ConsoleColor.Black);
                Undo();
            }
            break;

        case ConsoleKey.DownArrow:
            if(Console.CursorTop < Console.WindowHeight - 2)
                currentPosition = new Cell(Console.CursorLeft, Console.CursorTop + 1);
            bFill =true;
            break;

        case ConsoleKey.UpArrow:
            if(Console.CursorTop > 1)
                currentPosition = new Cell(Console.CursorLeft, Console.CursorTop - 1);
            bFill =true;
            break;

       case ConsoleKey.LeftArrow:
            if(Console.CursorLeft > 1)
                currentPosition = new Cell(Console.CursorLeft - 1, Console.CursorTop);
            bFill =true;
            break;

       case ConsoleKey.RightArrow:
            if(Console.CursorLeft < Console.WindowWidth - 2)
                currentPosition = new Cell(Console.CursorLeft + 1, Console.CursorTop);
            bFill =true;
            break;
       default:
            Console.Beep(); // Added in C# 2.0
            break;
       }
       if (bFill){
           path.Push(currentPosition); // Only type Cell allowed in call to Push().
           FillCell(currentPosition);
       }
    }
    while(key.Key != ConsoleKey.X); // Use X to quit.
}
private static ConsoleKeyInfo Move() => Console.ReadKey(true);

    private static void Undo() {
        // stub
    }
    private static void FillCell(Cell cell)
    {
        FillCell(cell, ConsoleColor.White);
    }
    private static void FillCell(Cell cell, ConsoleColor color)
    
        Console.SetCursorPosition(cell.X, cell.Y);
        Console.BackgroundColor = color;
        Console.Write(' ');
        Console.SetCursorPosition(cell.X, cell.Y);
        Console.BackgroundColor = ConsoleColor.Black;
    }

public struct Cell
{
    readonly public int X;
    readonly public int Y;
    public Cell(int x, int y)
    {
        X = x;
        Y = y;
    }
}

2.2 Definition of simple generic classes

When defining a generic class, you only need to specify the type parameters using a pair of angle brackets after the class name. When using a generic class, the type argument replaces all specified type parameters.

public class Stack<T>
{
    // Use read-only field prior to C# 6.0
    private T[] InternalItems { get; }

    public void Push(T data)
    {
        //...
    }

    public T Pop()
    {
        //...
        return InternalItems[0];//just for the example.
    }
}

Advantages of generic classes:

  • Generics promote type safety. It ensures that in parameterized classes, only data types explicitly expected by the members are available
  • Using value types for generic class members does not cause object boxing operations.
  • C# generics alleviate code bloat
  • Performance has been improved
  • Generics reduce memory consumption
  • Code is more readable

Naming convention for type parameters:

Similar to method parameters, type parameters should be descriptive. When defining a generic class, the parameter type name should include the T prefix, for example: public class EntityCollection { //…. }

2.3 Generic interfaces and structures

C# supports the use of generics across the language, including interfaces and structs. The syntax is exactly the same as that of classes. For example:

interface IPair<T>
{
    T First { get; set; }
    T Second { get; set; }
}

The syntax for implementing an interface is the same as that for non-generic classes. The type arguments of one generic type can become the type parameters of another generic type.

public struct Pair<T> : IPair<T>
{
    public T First { get; set; }
    public T Second { get; set; }
}

2.4 Implementing the same interface multiple times in a class

Different constructs of the same generic interface are considered different types, so a class or structure can implement the “same” generic interface multiple times.

public interface IContainer<T>
{
    ICollection<T> Items { get; set;}
}
public class Person : IContainer<Address>, IContainer<Phone>, IContainer<Email> {
    ICollection<Address> IContainer<Address>.Items {
        get {
            //...
            return new List<Address>();
        }
        set { //… }
    }
    ICollection<Phone> IContainer<Phone>.Items {
        get {
            //...
            return new List<Phone>();
        }
        set { //… }
    }
    ICollection<Email> IContainer<Email>.Items {
        get {
            //...
            return new List<Email>();
        }
        set { //… }
    }
}
public class Address { } // For example purposes only
public class Phone { } // For example purposes only
public class Email { } // For example purposes only

2.5 Definition of constructors and destructors

The constructor (destructor) of a generic class or structure does not require type parameters. For example:

public struct Pair<T> : IPair<T>
{
    public Pair(T first, T second)
    {
        First = first;
        Second = second;
    }
    public Pair(T first)
    {
        First = first;
        Second = default(T); // must be initialized
    }
    public T First { get; set; }
    public T Second { get; set; }
}

2.6 Multiple type parameters

Generic types can take any number of type parameters.

interface IPair<TFirst, TSecond>
{
    TFirst First { get; set; }
    TSecond Second { get; set; }
}
public struct Pair<TFirst, TSecond> : IPair<TFirst, TSecond>
{
    public Pair(TFirst first, TSecond second)
    {
        First = first;
        Second = second;
    }
    public TFirst First { get; set; }
    public TSecond Second { get; set; }
}

2.7 Tuple

Starting from C# 4.0, the CLR team has defined 9 new generic types, all called Tuple. Like Pair<…>, the same name can be reused as long as the arity is different.

public class Tuple
{ // ... }
public class Tuple<T1> // : IStructuralEquatable, IStructuralComparable, IComparable
{ // ... }
public class Tuple<T1, T2> // : IStructuralEquatable, IStructuralComparable, IComparable
{ // ... }
public class Tuple<T1, T2, T3> // : IStructuralEquatable, IStructuralComparable, Comparable
{ // ... }
public class Tuple<T1, T2, T3, T4> // : IStructuralEquatable, IStructuralComparable, IComparable
{ // ... }
public class Tuple<T1, T2, T3, T4, T5> // : IStructuralEquatable, IStructuralComparable, IComparable
{ // ... }
public class Tuple<T1, T2, T3, T4, T5, T6> // : IStructuralEquatable, IStructuralComparable, IComparable
{ // ... }
public class Tuple<T1, T2, T3, T4, T5, T6, T7> // : IStructuralEquatable, IStructuralComparable, IComparable
{ // ... }
public class Tuple<T1, T2, T3, T4, T5, T6, T7, TRest> // : IStructuralEquatable, IStructuralComparable, IComparable
{ // ... }

2.8 Nested generic types

Nested types automatically obtain the type parameters of the containing type. For example: If the type declaration contains type parameter T, then type T can also be used in the nested type. If the nested type contains its own type parameter T, then it will hide the containing type. Type parameters of the type with the same name.

class Container<T, U>
{
    // Nested classes inherit type parameters.
    // Reusing a type parameter name will cause
    // a warning.
    class Nested<U>
    {
        void Method(T param0, U param1)
        {}
    }
}

A type parameter is accessible to everyone and everywhere in the type body in which the type parameter is declared.

2.9 Constraints

Generics allow you to define constraints on type parameters that force the types provided as type arguments to obey various rules.

public class BinaryTree<T> {
    public T Item { get; set; }
    public Pair<BinaryTree<T>> SubItems
    {
        get { return _SubItems; }
        set {
            IComparable<T> first;
            first = (IComparable<T>)value.First.Item;
            if(first.CompareTo(value.Second.Item) < 0) {
                //… // first is less than second.
            }
            else {
                // second is less than or equal to first.
            }
            _SubItems = value;
        }
    }
    private Pair<BinaryTree<T>> _SubItems;
}

If the type parameter of BinaryTree does not implement the IComparabe interface, an execution-time error occurs.

2.10 Interface constraints

Constraints describe the characteristics of type parameters required by a generic. To declare a constraint, use the where keyword, followed by a pair of parameters: requirements. Among them, the “parameter” must be a parameter declared in the generic type, and the “requirement” describes whether the class or interface that the type parameter must be converted to must have a default constructor, or whether it is a reference type or a value type.

Interface constraints specify that a certain data type must implement a certain interface. For example:

public class BinaryTree<T>
where T : System.IComparable<T>
{
    public T Item { get; set; }
    public Pair<BinaryTree<T>> SubItems
    {
        get { return _SubItems; }
        set {
            IComparable<T> first = value.First.Item; // Notice that the cast can now be eliminated.
            if(first.CompareTo(value.Second.Item) < 0) {
                // … // first is less than second.
            }
            else {
                //... // second is less than or equal to first.
            }
            _SubItems = value;
        }
    }
    private Pair<BinaryTree<T>> _SubItems;
}

2.11 Class type constraints

Sometimes it may be required to convert a type argument to a specific class type, which is done through the class type. For example:

public class EntityDictionary<TKey, TValue>
    : System.Collections.Generic.Dictionary<TKey, TValue>
where TValue : EntityBase
{
    //...
}
public class EntityBase
{}

If multiple constraints are specified at the same time, the class type constraint must appear first. Multiple class type constraints for the same parameter are not allowed. Similarly, class type constraints cannot specify sealed classes or types that are not classes.

2.12 struct/class constraints

Another important constraint is to restrict type parameters to any non-nullable value type or any reference type. The compiler does not allow System.ValueType to be specified as a base class in constraints. However, C# provides the keyword struct/class to specify whether the parameter type is a value type or a reference type.

public struct Nullable<T> :
    IFormattable, IComparable,
    IComparable<Nullable<T>>, INullable
where T : struct
{
    // ...
}

Since class type constraints require specifying a specific class, using class type and struct/class constraints together will conflict with each other. Therefore, struct/class constraints and class type constraints cannot be used together.

There is a special thing about the struct constraint. The nullable value type does not comply with this constraint. It should be that the nullable value type is derived from Nullable, which has applied the struct constraint to T.

2.13 Multiple constraints

For any given type parameter, any number of interface constraints can be specified, but there can only be one class type constraint. Each constraint is declared in a comma-separated list, and each parameter type needs to be preceded by the where keyword.

public class EntityDictionary<TKey, TValue>
    : Dictionary<TKey, TValue>
    where TKey : IComparable<TKey>, IFormattable
    where TValue : EntityBase
    {
        // ...
    }

2.14 Constructor constraints

In some cases, it is necessary to create instances of type arguments in a generic class. But not all objects are guaranteed to have a public default constructor, so the compiler does not allow calling the default constructor for unconstrained types.

To overcome this limitation, classes use new() after specifying other constraints. This is called a constructor constraint. It requires that the actual parameter type must have a default constructor. Only the default constructor can be constrained, and constraints cannot be specified for a constructor with parameters.

public class EntityBase<TKey>
{
    public TKey Key { get; set; }
}
public class EntityDictionary<TKey, TValue> :
    Dictionary<TKey, TValue>
    where TKey : IComparable<TKey>, IFormattable
    where TValue : EntityBase<TKey>, new()
{
    // ...
    public TValue MakeValue(TKey key)
    {
        TValue newEntity = new TValue();
        newEntity.Key = key;
        Add(newEntity.Key, newEntity);
        return newEntity;
    }
    // ...
}

2.15 Constraint inheritance

Neither generic type parameters nor their constraints are inherited by derived classes because generic type parameters are not members.

Since the derived generic type parameters are now type arguments of the generic base class, the type parameters must have the same (or stronger) constraints as the base class.

class EntityBase<T> where T : IComparable<T>
{
    // ...
}

//ERROR:
// The type 'T' must be convertible to 'System.IComparable<T>'
// to use it as parameter 'T' in the generic type or
// method.
// class Entity<T> : EntityBase<T>
// {
// ...
// }

2.16 Constraints

Constraint limitations:

  • Class type constraints and struct/class constraints cannot be combined.
  • Inheritance from some special classes cannot be restricted. For example object, array, System.ValueType, System.Enum, System.Delegate and System.MulticastDelegate
  • Operator constraints are not supported.
  • Constraint type parameters are not supported to restrict a type to implementing a specific method or operator. Incomplete support can only be provided through class type constraints (restricting methods and operators) and interface constraints (restricting methods).
public abstract class MathEx<T>
{
    public static T Add(T first, T second)
    {
        // Error: Operator ' + ' cannot be applied to
        // operands of type 'T' and 'T'.
        // return first + second;
        return default(T);
    }
}
  • OR conditions are not supported. If multiple interface constraints are provided for a type parameter, the compiler considers that there is always an AND relationship between different constraints. OR relationships cannot be specified between constraints.
public class BinaryTree<T>
    // Error: OR is not supported.
    //where T: System.IComparable<T> || System.IFormattable
{
    // ...
}
  • Constraints on delegate and enumeration types are invalid Delegate types, array types and enumeration types cannot be used in base classes because they are actually “sealed”.
// Error: Constraint cannot be special class 'System.Delegate'
//public class Publisher<T>
// where T : System.Delegate{
// public event T Event;
// public void Publish()
// {
// if(Event != null)
// Event(this, new EventArgs());
// }
//}
  • Constructor constraints only apply to the default constructor. To overcome this limitation, one approach is to provide a factory interface that contains a method to instantiate the type. The factory that implements the interface is responsible for instantiating the entity.
public class EntityBase<TKey>
{
    public EntityBase(TKey key)
    {
        Key = key;
    }
    public TKey Key { get; set; }
}
public interface IEntityFactory<TKey, TValue>
{
    TValue CreateNew(TKey key);
}
public class EntityDictionary<TKey, TValue, TFactory> :
    Dictionary<TKey, TValue>
    where TKey : IComparable<TKey>, IFormattable
    where TValue : EntityBase<TKey>
    where TFactory : IEntityFactory<TKey, TValue>, new()
{
    public TValue New(TKey key)
    {
        TFactory factory = new TFactory();
        TValue newEntity = factory.CreateNew(key);
        Add(newEntity.Key, newEntity);
        return newEntity;
    }
    //...
}
public class Order : EntityBase<Guid>
{
    public Order(Guid key):
        base(key)
    {
            // ...
    }
}
public class OrderFactory : IEntityFactory<Guid, Order>
{
    public Order CreateNew(Guid key)
    {
        return new Order(key);
    }
}

2.17 Generic methods

Generic methods use generic type parameters, which is consistent with generic types. Generic methods can be used in both generic and non-generic types. To use a generic method, add a type parameter after the method name, for example:

public static class MathEx
{
    public static T Max<T>(T first, params T[] values)
        where T : IComparable<T>
    {
        T maximum = first;
        foreach(T item in values)
        {
            if(item.CompareTo(maximum) > 0)
            {
                maximum = item;
            }
        }
        return maximum;
    }
    //…..
}

2.18 Generic method type inference

When calling a generic method, provide type arguments after the method’s type name, for example:

public static void Main()
{
    Console.WriteLine(
        MathEx.Max<int>(7, 490));
        //….
}

In most cases, you can call without specifying type arguments. This is called type inference. The method type inference algorithm only considers method actual parameters, actual parameter types, and formal parameter types when inferring.

2.20 Specification of constraints

Type parameters of generic methods also allow constraints to be specified in the same way as type parameters are specified in generic types, for example:

public class ConsoleTreeControl {
    public static void Show<T>(BinaryTree<T> tree, int indent)
        where T : IComparable<T>
   {
       Console.WriteLine("\
{0}{1}", " + --".PadLeft(5 * indent, ' '), tree.Item.ToString());
       if(tree.SubItems.First != null)
            Show(tree.SubItems.First, indent + 1);
       if(tree.SubItems.Second != null)
           Show(tree.SubItems.Second, indent + 1);
    }
}

Since BinaryTree imposes constraints on type T, and Show uses BinaryTree, Show needs to impose constraints.

2.21 Instantiation of generics

Generic instantiation based on value types: When a generic type is constructed for the first time with a value type as a type parameter, the “runtime” will put the specified type parameter into the appropriate location of CIL, thereby creating a specific generic type. In short, the “runtime” will create a new reified generic type for each new “parameter value type”.

Generic instantiation based on reference types: When a generic type is first constructed using a reference type as a type parameter, the runtime will replace the parameter type with object in the CIL code to create a specific generic type (instead of based on the provided Type parameters create a materialized generic type). In the future, each time a constructed type is instantiated with a reference type parameter, the runtime will reuse the previously generated version of the generic type.