Generic Types

Generic Types

Generics are types with parameters, and parameters are placeholders for future types. Generic types include classes, structures, and interfaces. The essence of the type remains. The persona of a class, even a generic class, remains a class—it is simply a class with type parameters.

Type Parameters

Parameters appear after the class header and are placed in anchor brackets (< and >). The type parameters are accessible to the class declaration, header, body, and constraints. Because collections are typically general-purpose, generics are ideal for implementing collections. .NET provides generic collections for arrays, stacks, queues, and other common data structures. However, there is no predefined spreadsheet collection, which is a collection of rows and columns. Sounds like an opportunity to me.

Here is sample code for a sheet collection:

using System;

namespace Donis.CSharpBook{
    public class Starter{
        public static void Main(){
            int count=1;
            Sheet<int> asheet=new Sheet<int>(2);
            for(byte row=1; row<3; ++row) {
                for(byte col=1; col<3; ++col) {
                    asheet[row,col]=count++;
                }
            }
            for(byte row=1; row<3; ++row) {
                for(byte col=1; col<3; ++col) {
                    Console.WriteLine("R{0} C{1}= {2}",
                        row, col, asheet[row,col]);
                }
            }

            Console.WriteLine("Current[{0},{1}] = {2}",
                asheet.R, asheet.C, asheet.Current);
            asheet.MoveDown();
            asheet.MoveRight();
            Console.WriteLine("Current[{0},{1}] = {2}",
                asheet.R, asheet.C, asheet.Current);
        }
    }

    class Sheet<T> {
        public Sheet(byte dimension) {
            if(dimension<0) {
                throw new Exception("Invalid dimensions");
            }
            m_Dimension=dimension;
            m_Sheet=new T[dimension, dimension];
            for(byte row=0; row<dimension; ++row) {
                for(byte col=0; col<dimension; ++col) {
                    m_Sheet[row,col]=default(T);
                }
            }

        }

    public T this[byte row, byte col] {
        get {
            ValidateCell(row, col);
            return m_Sheet[row-1, col-1];
        }
        set {
            m_Sheet[row-1, col-1]=value;
        }
    }

    public void ValidateCell(byte row, byte col) {
        if((row < 0) || (row > m_Dimension)) {
            throw new Exception("Invalid Row");
        }
        if((col < 0) || (col > m_Dimension)) {
            throw new Exception("Invalid Col");
        }
    }


    public T Current {
        get {
            return m_Sheet[curRow-1, curCol-1];
        }
        set {
            m_Sheet[curRow-1, curCol-1]=value;
        }
    }

    public void MoveLeft() {
        curCol=Math.Max((byte) (curCol-1), (byte) 1);
    }

    public void MoveRight() {
        curCol=Math.Min((byte) (curCol+1), (byte) m_Dimension);
    }

    public void MoveUp() {
        curRow=Math.Max((byte) (curRow-1), (byte) 1);
    }

    public void MoveDown() {
        curRow=Math.Min((byte) (curRow+1), (byte) m_Dimension);
    }

    private byte curRow=1;
    public byte R {
        get {
            return curRow;
        }
    }

    private byte curCol=1;
    public byte C {
        get {
            return curCol;

            }
        }

        private byte m_Dimension;
        private T [,] m_Sheet;
    }
}

Sheet is a generic type and collection, with a single parameter. For generics with a single parameter, by convention, T is the name of the parameter. (T is for type.) In the sheet generic type, the type parameter is used as a function return, field type, and local variable. When the sheet is instantiated, the specific type is specified. This defines a spreadsheet of strings, which has two rows and columns:

        Sheet<string> asheet=new Sheet<string>(2);

Review the constructor of the sheet generic type. Notice the use of the default keyword:

        for(byte row=0; row<dimension; ++row) {
            for(byte col=0; col<dimension; ++col) {
                m_Sheet[row,col]=default(T);
            }
        }

The preceding for loop initializes the internal collection of the Sheet generic type. However, the implementation is not based on a specific type. A generic type implementation has placeholders instead of specific types. The challenge is initializing a nonspecific type, which could be a reference or value type. The default expression returns null for a reference type and bitwise zero for a value type. This is the syntax of the default expression:

  • zerovalue default(type)

The Sheet generic type has a single parameter. Generics can have multiple parameters. The type parameter list is contained within the anchor brackets. Parameters in the type parameter list are comma-delimited. If a generic has two parameters, the parameters are normally named K and V, for key and value. Here is sample code of a generic type that has two parameters:

    public class ZClass<K, V> {
        static void Method(K key, V data) {

        }
    }

This code creates an instance of the ZClass, which has two parameters:

        ZClass<string, float> obj=new ZClass<string, float>();

Generic types have parameters. Those parameters can themselves be generic types. In the following code, XClass is generic and is also inserted as a nested parameter:

using System;

namespace Donis.CSharpBook{

    public class Starter{
        public static void Main(){
            ZClass<XParameter<string>, float> obj=
                new ZClass<XParameter<string>, float>();
        }
    }

    public class XParameter<P> {
        public static void MethodA(P data) {
        }
    }

    public class ZClass<K, V> {
        public static void MethodB(K key, V data) {
        }
    }

}

The syntax of nested parameters can be somewhat complicated. What if the type parameter was extrapolated even further? You could have a type parameter that is a generic, which has a type parameter that is also generic, and so on, which could become somewhat labyrinthine. If the declaration is repeated, it would be particularly problematic. An alias is a good solution. Alias the declaration containing a nested parameter and rely on the alias in the code. The following alias is used for this purpose:

    using ZClass2=ZClass<XParameter<string>, int>;

    public class Starter{
        public static void Main(){
            ZClass2 obj=new ZClass2();
        }
    }


    public class XParameter<P> {
        public static void MethodA(P data) {
        }
    }

    public class ZClass<K, V> {
        public static void MethodB(K key, V data) {
        }
    }

This is the syntax of a generic type:

attributes accessibility modifiers class classname : baselist
   <parameterlist> where parameter:constraintlist:
   { class body };

Constructed Types

Generic types are also called constructed types. There are open constructed and closed constructed types. An open constructed type has at least one type parameter. The type parameter is a placeholder, which is unbound to a specific type. The implementation of a generic type is an example of an open constructed type. This is an open constructed type:

    public class ZClass<K, V> {
        static void Method(K key, V data) {

        }
    }

For a closed constructed type, all type parameters are bound. Bound parameters are called type arguments and are assigned a specific type. Closed constructed types are used in several circumstances: to create an instance of a generic type, inherit a generic type, and more. In the following code, ZClass<int, decimal> is a closed constructed type. The first parameter is bound to an integer, whereas the second is a decimal. The type arguments are int and decimal.

    ZClass<int, decimal> obj=new ZClass<int, decimal>();