web analytics

Working with C# Generics

Options

codeling 1595 - 6639
@2021-04-08 22:02:22

Generics were added to C# language and the common language runtime (CLR) to remove the need for casting, improve type safety, reduce the amount of boxing required, and to make it easier to create generalized classes and methods.

Generics introduce to the .NET Framework the concept of type parameters, which make it possible to design classes and methods that defer the specification of one or more types until the class or method is declared and instantiated by client code. For example, by using a generic type parameter T you can write a single class that other client code can use without incurring the cost or risk of runtime casts or boxing operations, as shown here:

// Declare the generic class.
public class GenericList<T>
{
    void Add(T input) { }
}

class TestGenericList
{
    private class ExampleClass { }
    static void Main()
    {
        // Declare a list of type int.
        GenericList<int> list1 = new GenericList<int>();

        // Declare a list of type string.
        GenericList<string> list2 = new GenericList<string>();

        // Declare a list of type ExampleClass.
        GenericList<ExampleClass> list3 = new GenericList<ExampleClass>();
    }
}

In order to minimize the learning curve for developers, generics are used in much the same way as C++ templates. Programmers can create classes and structures just as they normally have, and by using the angle bracket notation (< and >) they can specify type parameters. When the generic class declaration is used, each type parameter shall be replaced by a type argument that the user of the class supplies.

@2021-04-08 22:04:49

Type Parameters and Type Arguments, Generic Type and Contructed Type

In a generic type/class or method definition, a type parameter is a placeholder for a specific type that a client specifies when they instantiate a variable of the generic type. A generic type or generic class, such as GenericList<T> defined above, cannot be used as-is because it is not really a type; it is more like a blueprint for a type.

To use GenericList<T>, client code must declare and instantiate a constructed type by specifying a type argument inside the angle brackets. The type argument for this particular class can be any type recognized by the compiler. Any number of constructed type instances can be created, each one using a different type argument, as follows:

GenericList<float> list1 = new GenericList<float>();
GenericList<ExampleClass> list2 = new GenericList<ExampleClass>();
GenericList<ExampleStruct> list3 = new GenericList<ExampleStruct>();

In each of these instances of GenericList<T>, every occurrence of T in the class will be substituted at run time with the type argument. By means of this substitution, we have created three separate type-safe and efficient objects using a single class definition.

@2021-04-08 22:09:26

Type Parameter Naming Guidelines

1. Do name generic type parameters with descriptive names, unless a single letter name is completely self explanatory and a descriptive name would not add value.

public interface ISessionChannel<TSession> { /*...*/ }
public delegate TOutput Converter<TInput, TOutput>(TInput from);
public class List<T> { /*...*/ }

2. Consider using T as the type parameter name for types with one single letter type parameter.

public int IComparer<T>() { return 0; }
public delegate bool Predicate<T>(T item);
public struct Nullable<T> where T : struct { /*...*/ }

3. Do prefix descriptive type parameter names with “T”.

public interface ISessionChannel<TSession>
{
    TSession Session { get; }
}

4. Consider indicating constraints placed on a type parameter in the name of parameter. For example, a parameter constrained to ISession may be called TSession.

@2021-04-08 22:21:30

Constraints on Type Parameters

When you define a generic class or type, you can apply restrictions to the kinds of types that client code can use for type arguments when it instantiates your class. If client code tries to instantiate your class by using a type that is not allowed by a constraint, the result is a compile-time error. These restrictions are called constraints. Constraints are specified by using the where contextual keyword. The following table lists the six types of constraints:

Constraint

Description

where T: struct

The type argument must be a value type. Any value type except Nullable can be specified.

where T : class

The type argument must be a reference type; this applies also to any class, interface, delegate, or array type.

where T : new()

The type argument must have a public parameterless constructor. When used together with other constraints, the new() constraint must be specified last.

where T : <base class name>

The type argument must be or derive from the specified base class.

where T : <interface name>

The type argument must be or implement the specified interface. Multiple interface constraints can be specified. The constraining interface can also be generic.

where T : U

The type argument supplied for T must be or derive from the argument supplied for U. This is called a naked type constraint.

If you want to examine an item in a generic list to determine whether it is valid or to compare it to some other item, the compiler must have some guarantee that the operator or method it has to call will be supported by any type argument that might be specified by client code. This guarantee is obtained by applying one or more constraints to your generic class definition. For example, the base class constraint tells the compiler that only objects of this type or derived from this type will be used as type arguments. Once the compiler has this guarantee, it can allow methods of that type to be called in the generic class.

Constraints are applied by using the contextual keyword where. The following code example demonstrates the functionality we can add to the GenericList<T> class by applying a base class constraint.

public class Employee
{
    private string name;
    private int id;

    public Employee(string s, int i)
    {
        name = s;
        id = i;
    }

    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    public int ID
    {
        get { return id; }
        set { id = value; }
    }
}

public class GenericList<T> where T : Employee
{
    private class Node
    {
        private Node next;
        private T data;

        public Node(T t)
        {
            next = null;
            data = t;
        }

        public Node Next
        {
            get { return next; }
            set { next = value; }
        }

        public T Data
        {
            get { return data; }
            set { data = value; }
        }
    }

    private Node head;

    public GenericList() //constructor
    {
        head = null;
    }

    public void AddHead(T t)
    {
        Node n = new Node(t);
        n.Next = head;
        head = n;
    }

    public IEnumerator<T> GetEnumerator()
    {
        Node current = head;

        while (current != null)
        {
            yield return current.Data;
            current = current.Next;
        }
    }

    public T FindFirstOccurrence(string s)
    {
        Node current = head;
        T t = null;

        while (current != null)
        {
            //The constraint enables access to the Name property.
            if (current.Data.Name == s)
            {
                t = current.Data;
                break;
            }
            else
            {
                current = current.Next;
            }
        }
        return t;
    }
}

The constraint enables the generic class to use the Employee.Name property because all items of type T are guaranteed to be either an Employee object or an object that inherits from Employee.

Multiple constraints can be applied to the same type parameter, and the constraints themselves can be generic types, as follows:

class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new()
{
    // ...
}

By constraining the type parameter, you increase the number of allowable operations and method calls to those supported by the constraining type and all types in its inheritance hierarchy. Therefore, when you design generic classes or methods, if you will be performing any operation on the generic members beyond simple assignment or calling any methods not supported by System.Object, you will have to apply constraints to the type parameter.

When applying the where T : class constraint, avoid the == and != operators on the type parameter because these operators will test for reference identity only, not for value equality. This is the case even if these operators are overloaded in a type that is used as an argument. The following code illustrates this point; the output is false even though the String class overloads the == operator.

public static void OpTest<T>(T s, T t) where T : class
{
    System.Console.WriteLine(s == t);
}
static void Main()
{
    string s1 = "target";
    System.Text.StringBuilder sb = new System.Text.StringBuilder("target");
    string s2 = sb.ToString();
    OpTest<string>(s1, s2);
}

The reason for this behavior is that, at compile time, the compiler only knows that T is a reference type, and therefore must use the default operators that are valid for all reference types. If you must test for value equality, the recommended way is to also apply the where T : IComparable<T> constraint and implement that interface in any class that will be used to construct the generic class.

You can apply constraints to multiple parameters, and multiple constraints to a single parameter, as shown in the following example:

class Base { }
class Test<T, U>
    where U : struct
    where T : Base, new() { }

Type parameters that have no constraints, such as T in public class SampleClass<T>{}, are called unbounded type parameters. Unbounded type parameters have the following rules:

  • The != and == operators cannot be used because there is no guarantee that the concrete type argument will support these operators.
  • They can be converted to and from System.Object or explicitly converted to any interface type.
  • You can compare to null. If an unbounded parameter is compared to null, the comparison will always return false if the type argument is a value type.

When a generic type parameter is used as a constraint, it is called a naked type constraint. Naked type constraints are useful when a member function with its own type parameter has to constrain that parameter to the type parameter of the containing type, as shown in the following example:

class List<T>
{
    void Add<U>(List<U> items) where U : T {/*...*/}
}

In the previous example, T is a naked type constraint in the context of the Add method, and an unbounded type parameter in the context of the List class.

Naked type constraints can also be used in generic class definitions. Note that the naked type constraint must also have been declared within the angle brackets together with any other type parameters:

//naked type constraint
public class SampleClass<T, U, V> where T : V { }

The usefulness of naked type constraints with generic classes is very limited because the compiler can assume nothing about a naked type constraint except that it derives from System.Object. Use naked type constraints on generic classes in scenarios in which you want to enforce an inheritance relationship between two type parameters.

Comments

You must Sign In to comment on this topic.


© 2024 Digcode.com