Static Members

Static Members

Generic types can have static members. If present, a closed constructed type owns a set of any static members. Therefore, there are possibly multiple instances of the static members—one for each closed constructed type. Static members are not referable from the open constructed type. Static members are usually accessed through the class name. With generic types, static members are accessible using the closed constructed type notation. Static constructors, which are called implicitly, initialize the static fields in the context of the current closed constructed type.

This is the constructed type notation:

  • classname<argumentlist>.staticmember

classname is the name of the generic type; argumentlist is a comma-delimited list of type arguments; staticmember is the name of the static member.

Static members are frequently used as counters. The following code counts the instances of generic types. There are several generic type instantiations—each using a closed constructed type. The static count is specific to each closed constructed type. The count counts the number of instances of a closed constructed type.

using System;

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

    public class ZClass<T> {

        public ZClass() {
            ++counter;
        }

        public static void Count(ZClass<T> _this) {
            Console.WriteLine("{0} : {1}",
                _this.GetType().ToString(),
                counter.ToString());
        }

        private static int counter=0;
    }

}

Several articles describing generics bemoan the extra cost of static fields in generic types. This is a misplaced concern. A generic type often supplants the explicit implementation of separate classes: StackCircle, StackTriangle, and so on. As separate classes, static members would also be replicated. Alternatively, a general-purpose collection would have a single set of static members but incur other costs, such as boxing and unboxing. From every perspective, the overhead from extra sets of static members from generic types is comparable to or better than any alternate solution.

Operator Functions

You can implement operator functions for generic types. Operator member functions are static members. Therefore, the rules of static members also apply to operator member functions. An operator member function cannot be a generic method. However, the operator member function can use type parameters of the generic type.

An operator+ has been added to the Sheet generic type. It adds two Sheet collections. The results of the calculations are placed in a third sheet. Only integral sheets can be added. Because type parameters cannot be constrained by a value type, a helper function called Add is provided:

    public abstract class AddClass<T> {
        public abstract T Add(T op1, T op2);
    }

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

        }

       public static Sheet<int> operator+(Sheet<int> sheet1,
           Sheet<T> sheet2)
                {
           byte dimension=Math.Max(sheet1.m_Dimension,
               sheet2.m_Dimension);
          Sheet<int> total=new Sheet<int>(dimension);

           for(byte row=1; row<=dimension; ++row) {
                for(byte col=1; col<=dimension; ++col) {
                    total[(byte) row,(byte)col]=
                        sheet1.Add(sheet1[(byte) row,(byte)col],(int)
                       (object) (sheet2[(byte) row,(byte)col]));
                }
            }
           return total;
       }

        public override int Add(int op1, int op2) {
            return op1+op2;
        }
…

This is the signature of the operator+ function in the Sheet generic type:

       public static Sheet<int> operator+(Sheet<int> sheet1,
           Sheet<T> sheet2)

An operator+ is a binary operator with two operands. Notice that one operand is a closed constructed type, whereas the other is an open constructed type. Why? The operator+ requires that one of the operands be the containing class, which is the open constructed type, so there is a chance of adding incompatible sheets. This explains the cast:

(int) (object) (sheet2[(byte) row,(byte)col])

sheet2 is the right-hand parameter of the operator+ function. The unknown type in sheet2 is being cast back to an integer. If the type is incompatible, an exception is raised at that moment. Exception handling could mitigate this problem. However, to keep the code simple, the problem is left unresolved. This code is meant as demonstration only.

Serialization

Serialization persists the state of an object to a stream. Serializing the instance of a generic type is similar to a regular type. This book does not provide a detailed overview of serialization. This section presents targeted information on serializing generic types.

Serialization is done mostly with the SerializationInfo object. For generic types, there are additional overloads of the SerializationInfo.AddValue and SerializationInfo.GetValue methods for object types. This requires casting to and from object types.

For serialization, the generic type must be adorned with the Serializable attribute.

The GetObjectData method implements the serialization of an object. This includes serializing both the metadata and instance data of the type. GetObjectData has a SerializationInfo and StreamingContext parameter. The SerializationInfo.AddValue method is called to serialize generic type content:

       public void GetObjectData(SerializationInfo info,
           StreamingContext ctx) {
           info.AddValue("fielda", fielda, typeof(T));
       }

To deserialize, add a two-argument constructor to the generic type. The arguments are a SerializationInfo and StreamingContext parameter. Call the SerializationInfo.GetValue method to rehydrate the instance:

       private ZClass(SerializationInfo info,
           StreamingContext ctx) {
           fielda=(T) info.GetValue("fielda", typeof(T));
       }

Objects can be serialized in different formats, which is accomplished with formatters, such as the BinaryFormatter type. The SoapFormatter type cannot be used with generic types. Serialization also requires creating an appropriate stream, such as a FileStream. The stream is where the instance is serialized or deserialized. Call BinaryFormatter.Serialize to serialize a generic type instance. Conversely, call BinaryFormatter.Deserialize to deserialize.

The following program accepts a command-line argument. The Set command instructs the program to serialize an instance of the ZClass generic type to a file. A Get command asks the program to deserialize the ZClass generic type.

using System;
using System.Runtime.Serialization;

using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

namespace Donis.CSharpBook{

    public class Starter{
        public static void Main(string [] args){
            BinaryFormatter binary=new BinaryFormatter();
            FileStream file=
                new FileStream("data.bin", FileMode.OpenOrCreate);

            if(args[0].ToLower()=="set") {
                ZClass<int> obj=new ZClass<int>(5);
                binary.Serialize(file, obj);
                return;
            }

            if(args[0].ToLower()=="get") {
                ZClass<int> obj=(ZClass<int>)
                    binary.Deserialize(file);
                Console.WriteLine(obj.GetValue());
                return;
            }

        }
    }

    [Serializable] public class ZClass<T> {

        public ZClass(T init) {
            fielda=init;
        }

        public void GetObjectData(SerializationInfo info,
            StreamingContext ctx) {
            info.AddValue("fielda", fielda, typeof(T));
        }

        private ZClass(SerializationInfo info,
            StreamingContext ctx) {
            fielda=(T) info.GetValue("fielda", typeof(T));
        }

        public void SetValue(T data) {
           fielda=data;
        }

        public T GetValue() {
           return fielda;
        }

        private T fielda=default(T);
    }

}

Generics Internals

Generics are economical and expeditious, especially when compared with past implementations of parametric polymorphism. The disparity is found in the compile-time and run-time semantics of code expansion in generics. This section focuses on improvement in this area as compared with parameterized types in C++, which has a widely-recognized implementation of parametric polymorphism.

Although an inspection of parameterized templates in C++ might uncover cursory similarities with generics, there are considerable differences between them. These differences make generics more efficient and better-performing than parameterized templates. The exact implementation of templates is specific to each C++ compiler. However, the concepts of parameterized templates are similar in all implementations.

The major difference between generics and parameterized templates is that the latter is purely compile-time based. Instances of parameterized templates expand into separate classes and are inlined at compile time. The Standard Template Library (STL) of C++ offers a stack collection. If ellipse, rectangle, triangle, and curve instances of the stack are defined, the stack template expands into separate classes—one for each type. The expansion occurs at compile time. What happens when two stacks of circles are defined separately? Is there a consolidation of the code? The answer is no, which can lead to significant code bloating.

The compile-time expansion has some shortcomings. This makes the parameterized templates specific to C++. In .NET, generic types expand at run time and are not language-specific. Therefore, generics are available to any managed language. The Sheet generic type presented in this chapter is usable in Microsoft Visual Basic .NET. With C++, the particulars of the template, such as the parameterized types, are lost at compile time and not available for later inspection. Managed code, including generics, undergoes two compilations. The first compilation, administered by the language compiler, emits metadata and Microsoft intermediate language (MSIL) code specific to generic types. Because the specifics of the generic type are preserved, it is available for later inspection, such as reflection. There are new metadata and MSIL instructions that target generic types. The second compilation, performed by the just-in-time compiler (jitter), performs the code expansion. The jitter is part of the Common Language Runtime (CLR).

Figure 6-1 shows the MSIL-specific code for a generic type.

Image from book
Figure 6-1: MSIL view of a generic type

The CLR performs an intelligent expansion of generic types, unlike C++, which blindly expands parameterized types. Intelligent expansion is conducted differently for value type versus reference type arguments.

When a generic type with a value argument is defined, it is expanded into a class at run time, where the specific type is substituted for the parameter throughout the class. The resulting class is cached in memory. Future instances of the generic type with the same type argument reference the existing class. In this circumstance, there is code sharing between the separate generic type instantiations. Additional class expansion is prevented and unnecessary code bloating is avoided.

If the type argument is a reference type, the CLR conducts intelligent expansion differently. The run time creates a specialized class for the reference type, where System.Object is substituted for the parameter. The new class is cached in memory. Future instances of the generic type with any reference type parameter references this same class. Generic type instantiations that have a reference type parameter share the same code.

Look at the following code. How many specialized classes are created at run time?

           Sheet<int> asheet=new Sheet<int>(2);
           Sheet<double> bsheet=new Sheet<double>(5);
           Sheet<XClass> csheet=new Sheet<XClass>(2);
           Sheet<YClass> dsheet=new Sheet<YClass>(5);
           Sheet<int> esheet=new Sheet<int>(3);

The preceding code results in three specialized classes. The Sheet<int> instantiations share a class. Sheet<double> is a separate class. Sheet<XClass> and Sheet<YClass> also share a class.

Generic Collections

As discussed in the previous chapter, the .NET Framework Class Library includes general-purpose collection classes for commonplace data algorithms, such as a stack, queue, dynamic array, and dictionary. These collections are object-based, which affects performance, hinders type-safeness, and potentially consumes the available memory. The .NET FCL includes parameterized versions of most of the collections.

The parameterized data algorithms are found in the System.Collections.Generic namespace. Generic interfaces are also included in the namespace. Table 6-3 lists some of the types and interfaces members of this namespace.

Table 6-3: Generic Types and Interfaces

Description

Type

Dynamic array

List<T>

LIFO list

Stack<T>

FIFO list

Queue<T>

Collection of key/value pairs

Dictionary<K,V>

Compares a current and other object

IComparable<T>

Compares two objects

IComparer<T>

Returns an enumerator

IEnumerable<T>

Defines an enumerator

IEnumerator<T>