Chapter 7. Generics

The definition of generic, as found in Merriam-Webster’s Collegiate Dictionary, is "of a whole genus, kind, class, etc.; general; inclusive." Based on this definition, a person is generic, whereas Donis Marshall is quite specific. City is generic, whereas Seattle is specific. More specific to programming, a data algorithm is generic, whereas the implementation is specific. A stack is generic, whereas a stack of integers is specific. A spreadsheet is generic, whereas a spreadsheet of strings is specific.

In the Microsoft .NET Framework, an implementation of a class or structure is specific. A StackInt class is a specific type and a specialization of the stack pattern, which targets integers. A stack of strings or floats would require separate implementations of the same algorithm, such as StackString and StackFloat.

Here is an implementation of the StackInt class:

using System;

namespace Donis.CSharpBook {
    public class Starter {
        public static void Main() {
            StackInt stack = new StackInt(5);
            stack.Push(10);
            stack.Push(15);
            Console.WriteLine("Pushed 3 values");
            stack.ListItems();
            int iPop=stack.Pop();
            Console.WriteLine("Popped 1 value: {0}", iPop);
            Console.WriteLine("Remaining items:");
            stack.ListItems();
        }
    }

    public class StackInt {

        public StackInt(int firstItem) {
            list = new int[1] {firstItem};
            top++;
        }

        public int Pop() {
            if (top != (-1)) {
                 return list[top--];
            }
            throw new Exception("Stack empty");
        }

        public void Push(int topitem) {
            ++top;
            if (top == list.Length) {
                int[] temp = new int[top+1];
                Array.Copy(list, temp, top);
                list = temp;
            }
            list[top] = topitem;
        }

        public void ListItems () {
            for (int item = 0; item <= top; ++item) {
                Console.WriteLine(list[item]);
            }
        }

        private int[] list;
        private int top = (-1);
    }
}

StackInt is not an extensible solution of the stack pattern. It applies only to integers. As the application matures, there could be a need for additional implementations for other data types. Writing separate implementations of the stack pattern is the antithesis of code reuse. Inheritance is an alternative. StackInt, StackFloat, and StackString could be related through inheritance, where Stack is the base class. This approach allows code reuse, which is one of the strengths of an object-oriented language. But abstracting the stack algorithm based on inheritance encourages bad design. Is a stack a kind of integer? Is an integer a kind of stack? Neither statement is true. Inheritance in this scenario is a contrived solution at best.

A better solution is a general implementation, which allows a single implementation for all types—a stack of anything, versus a stack of integers. This is how collections work. Collections are part of the .NET Framework Class Library (FCL) and are general-purpose containers. As general-purpose containers, they are collections of any type. System.Object is the ubiquitous type in .NET and represents any specific type. Why? All types in .NET are either directly or indirectly derived from System.Object. Collections are discussed in Chapter 5.

In the following code, a stack of integers is implemented using the stack collection class from the .NET FCL. The stack collection is also used for a stack of strings, which demonstrates the amorphous nature of the stack collection class:

using System;
using System.Collections;

namespace Donis.CSharpBook {
    public class Starter {

        public static void Main() {
            Console.WriteLine("Integer stack");
            Integers();
            Console.WriteLine("String stack");
            Strings();
        }

        public static void Integers() {
            Stack list = new Stack();
            list.Push(5);
            list.Push(10);
            list.Push(15);
            foreach (int item in list) {
                Console.WriteLine(item);
            }
        }

        public static void Strings() {
            Stack list = new Stack();
            list.Push("a");
            list.Push("b");
            list.Push("c");
            foreach (string item in list) {
                Console.WriteLine(item);
            }
        }
    }
}

General-purpose collections (described in Chapter 5) are helpful, but there are some drawbacks. Performance is the first problem. These collections manage instances of System.Object. Casting is required to access items of a collection. For value types, boxing occurs as items are added to the collection. Conversely, unboxing happens when value-type items are retrieved from the collection. This is true for any collection of value types, such as integers. Frequent boxing can prompt earlier garbage collection, which is especially expensive. The performance penalty for boxing and unboxing can be substantial when iterating large collections of value types. For a collection of reference types, there is the penalty for down-casting, which is less expensive when compared to casting value types. However, there is still a cost. The second problem is type-safety. Although the StackInt type is type-safe, a stack collection of integers is not. The stack collection stores elements as System.Object. As such, the items of the collection are cast to a specific type at run time. This assumes you are not intending the underlying type to be System.Object. Invalid casts at run time will cause an exception. The third problem is clarity. With collections, frequent casts often clutter the source code.

Generics are the solution. Generics are parameterized types and methods. Each type parameter is a placeholder for an unspecified type. The polymorphic behavior of the generic type or method is conveyed through type parameters, which is called parametric polymorphism. There is a single implementation of the algorithm, which is generalized through type parameters. Many developers were introduced to this concept as parameterized templates in C++. Other languages support a similar feature. However, generics in .NET avoid some of the problems found in parametric polymorphism in other languages.

Generics address some of the shortcomings of collections in the .NET FCL. For example, generics eliminate needless boxing and unboxing. At compile time, a generic expands into a specific type. A generic stack of integers becomes a type that represents an actual stack of integers. Because this is type-specific, generics are inherently type-safe. This also avoids the frequent need for casting and clarity is enhanced.

The entry point method cannot be a member of a generic type. The following is the full list of types and member functions that cannot be generic:

  • Unmanaged types

  • Constructors

  • Operator member functions

  • Properties

  • Indexers

  • Attributes

Generic Types

Classes, structures, and interfaces can be generic. Generic types have type parameters, which are placeholders to be completed later. Being generic does not change fundamental rules governing the type. A generic class, for example, remains a class—it is simply a class with type parameters.

Type Parameters

A type parameter is a placeholder for a specific type. You define the type parameter after the generic name at the type definition. The type parameters should be enclosed in angle brackets and comma-delimited. This is considered an open constructed type. Open and closed constructed types are explained in the section titled "Constructed Types," later in this chapter.

Type Arguments

When you declare an instance of a generic type, you must specify type arguments to replace type parameters. This is considered a closed constructed type. When creating an instance of a generic type, specify the type arguments after the name. Enclose the comma-delimited type arguments within angle brackets . A type argument is a specific type that replaces a type parameter (placeholder) of the generic definition.

The following code introduces the Sheet generic type. It abstracts an array of cells, rows, and columns:.

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 type parameter. For generics with a single parameter, by convention, T is the name of the parameter (T stands for type.) In the Sheet generic type, the type parameter is used as a function return and field type. You see T being used as a placeholder at those locations. When the sheet is instantiated, the specific type (type argument) is specified.

The following code defines a spreadsheet of strings that has two rows and columns:

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

The following is the source code from 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 state of the Sheet generic type. The type parameter (T) is used. There is a challenge to assigning default values to data of a generic type. The default value could vary based on type. For example, the default value could be different based on whether the type parameter is a reference or value type. The default expression returns null for a reference type and bitwise zero for a value type. This resolves the problem of initializing generic data correctly. This is the syntax of the default expression:

default(type_parameter)

The Sheet generic type has a single parameter. However, generics can have more than one type parameter. The type_parameter list is contained within angle brackets. Types in the type_parameter list are comma-delimited. If a generic has two type parameters, the parameters are often named K and V, for key and value. This is simply convention, though. You can name the parameters anything. 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 generic ZClass with two type arguments:

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

As already demonstrated, generic types have type parameters. Those type parameters can themselves be generic types. In the following code, XParameter is a generic type. It is also used as a type parameter in the generic ZClass:

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 type parameters can be interesting. What if the type parameter were 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—a structure that could become somewhat labyrinthine. Aliasing the declaration could make the use of nested parameters or arguments clearer. 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) {
    }
}

Here is the syntax of a generic type:

attributes accessibility modifiers class classname: baselist

    <type_parameterlist> where type_parameter: constraintlist:

    {class body};

The constraint clause defined by the where keyword is explained in the section titled "Constraints," later in this chapter

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. Here is an example of 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, including to create an instance of a generic type and to inherit a generic type. In the following code, ZClass<int, decimal> is a closed constructed type. The first type parameter is bound to an integer, whereas the second is bound to a decimal. The type arguments are int and decimal.

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

Overloaded Methods

Methods with generic parameters or a return type can be overloaded, which creates an interesting dilemma. Can a method be overloaded based on type parameters alone? In the following code, MethodA is overloaded:

public void MethodA(T arg) {
}
public void MethodA(U arg) {
}

Both MethodA functions have a single type parameter. Each method has a differently named type parameter. However, when closed, each type could be the same. For example, T and U both could be integers. The overloaded MethodA functions are potentially ambiguous. However, the C# compiler is concerned with actual ambiguity and does not highlight potential ambiguity related to overloading generic methods as an error or warning. A compiler error is manifested when MethodA is called in an ambiguous manner (if it ever is). This circumstance is a potential land mine for developers of libraries. You should test every permutation of type parameters in an overloaded method to uncover any potential ambiguity.

In the following code, MethodA is ambiguous. Both T and U are integers. As a result, both MethodA functions having a single parameter are identical. A compiler error occurs only if the method is actually called:

using System;

namespace Donis.CSharpBook {

    public class Starter {
        public static void Main() {
            ZClass<int, int> obj = new ZClass<int, int>();
            obj.MethodA(5); // ambiguous error
        }
    }

    public class ZClass<T, U> {
        public void MethodA(T arg) {
            Console.WriteLine("ZClass.MethodA(T arg)");
        }

        public void MethodA(U arg) {
            Console.WriteLine("ZClass.MethodA(U arg)");
        }

        public void MethodA() {
            Console.WriteLine("ZClass.MethodA()");
        }
    }
}

Overloaded methods can be a mix of generic and non-generic methods. If the combination of generic and non-generic method is ambiguous, the non-generic method is called. The compiler prefers non-generic methods over generic methods of the same signature.

The following code contains a generic and a non-generic version of MethodA. MethodA is overloaded. MethodA is called with an integer parameter, which seems to be ambiguous. However, the compiler simply calls the non-generic MethodA, because it always prefers non-generic methods over generic methods. When the type argument for MethodA is a double, there is no ambiguity. There is not an overloaded, non-generic version of MethodA with a single parameter of type double:

using System;

namespace Donis.CSharpBook {
    public class Starter {
        public static void Main() {
            ZClass<int> obj1 = new ZClass<int>();
            obj1.MethodA(5);
            ZClass<double> obj2 = new ZClass<double>();
            obj2.MethodA(5.0);
        }
    }

    public class ZClass<T> {
        public void MethodA(T arg) {
            Console.WriteLine("ZClass.MethodA(T arg)");
        }

        public void MethodA(int arg) {
            Console.WriteLine("ZClass.MethodA(int arg)");
        }

        public void MethodA() {
            Console.WriteLine("ZClass.MethodA()");
        }
    }
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.35.185