Chapter 6: Generics

Chapter 6: Generics

Overview

The definition of generic, as found in the Merriam-Webster's Collegiate Dictionary, is "adj. of a whole genus, kind, class, etc.; general; inclusive." Based on this definition, a person is generic, whereas Donis Marshall is quite specific. A city is generic, whereas Seattle is specific. For developers, 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. To lesser extent, general abstractions can use generic implementation. An arithmetic class is generic, whereas integer calculations are specific.

In .NET, a type is specific, such as a class or structure. A StackInt class is a specific type and represents a specialization of the stack pattern, which targets integers. A stack of strings or floats would require additional 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();
            stack.Pop();
            Console.WriteLine("Popped 1 value");
            stack.ListItems();
        }
    }

    public class StackInt {

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

        public int Pop() {
            if(top!=(-1)) {
                 return list[top--]; //was 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);
    }

}

The implementation works, but not without some issues:

  • Replicating the algorithm is time-consuming and is the antithesis of code reuse.

  • This approach is not maintainable. What if the core algorithm is refashioned for better performance? It requires updating and testing multiple sets of nearly identical source code.

  • Specific types are not extensible. As an application matures, the number of implementations could become unwieldy. In a graphics program, as additional geometric shapes are supported, the list of stack classes could expand to include StackEllipse, StackOval, StackLine, StackTriangle, StackCurve, StackText, ad infinitum.

Inheritance polymorphism, which is polymorphism using inheritance and virtual functions, is an alternate solution to type specialty. In a personnel application, ExemptEmployee, HourlyEmployee, and TempEmployee are specific classes that are related through inheritance, where Employee is the generic base class. Using it solves some of the aforementioned problems, such as code reuse. This is a potential solution to generic abstraction but not a generic data algorithm. 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. For a stack solution, inheritance polymorphism is a contrived solution at best.

A simple solution is generic implementation, which is single implementation for all types—a stack of anything, versus a stack of integers. This is the function of collections in the .NET Framework class library (FCL), which are general-purpose containers. All types—value or reference—are derived directly or indirectly from System.Object, making it the ubiquitous type. System.Object can be anything. General-purpose collections are containers of System.Object instances. These collections are discussed in Chapter 5, "Arrays and Collections."

In the following code, a stack of integers is implemented using the stack collection class from the .NET FCL. The stack collection is reused as 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 the previous chapter) are valuable, but there are some drawbacks. Performance is the first problem. These collections manage instances of System.Object. Casting is required to access, remove, and insert items into a collection. For integers, boxing occurs as items are added to the collection. Unboxing happens when items are accessed in the collection. Boxing occurs if a collection contains value types—integers are value types. Frequent boxing can prompt earlier garbage collection, which is especially expensive. The incremental penalty for boxing and unboxing can be substantial for a collection of value types. For a collection of reference types, there is the cost of down-casting, which is lighter. The second problem is type-safeness. Although the StackInt type is type-safe, the general-purpose collection of integers is not. The stack collection stores elements in memory as System.Object. As a System.Object type, the behavior of each element is limited to ToString, GetHashCode, Equals, and GetType. Casting is required for expanded behavior. However, you can cast to anything, which is inherently type-unsafe. Incorrect casts cause run-time exceptions. The third problem is clarity. Frequent casting, as required with collection classes, obfuscates the source code. Generics are the solution.

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

Generics address the shortcomings of generic collections. Generics perform better because needless boxing and unboxing are eliminated. At run time, generic types with value type arguments expand to type-specific instances. For example, a generic stack with an integer type parameter becomes an actual stack of integers, which make generics inherently type-safe. Generic types become specific types, which avoids the necessity of casting. Because casting is avoided, clarity is enhanced.

Generic types and methods are not suitable in all circumstances:

  • The entry point method cannot be a member of an generic type.

  • Unmanaged types cannot be generic.

  • Constructors cannot be generic.

  • Operator member functions cannot be generic methods.

  • Properties cannot be generic.

  • Indexers cannot be generic.

  • Attributes cannot be generic.