Static Members

Generic types can have static members. Static members cannot be bound to an open constructed type. Static members belong to the type where the member is defined. However, for a generic type, the type where the member is defined is an open constructed type, and static members of a generic type belong to the closed constructed type. There can be more than one closed constructed type for a generic type, each one having its own set of static members from the same generic type. 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 for accessing a static member:

  • classname<type_argumentlist>.staticmember

In this syntax, classname is the name of the generic type, type_argumentlist is a comma-delimited list of type arguments, and staticmember is the name of the static member.

Static members are frequently used as counters. The following code counts the instances of a generic type. There are several generic type instantiations, each using a closed constructed type. The static count is specific to each closed constructed type. Running this code demonstrates that there are different sets of static members, one for each closed constructed type. There are separate counts for ZClass<double> and ZClass<int>:

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>.DisplayCount(obj1);
            ZClass<double>.DisplayCount(obj2);
        }
    }

    public class ZClass<T> {

        public ZClass() {
            ++counter;
        }

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

        private static int counter = 0;
    }
}

Operator Functions

Generic types can contain operator member functions. As explained in Chapter 9, operator member functions are static members. Operator member functions cannot be generic. However, operator member functions can use type parameters from the surrounding generic type.

In the following code, an operator+ member function 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, the compiler won’t allow you to add variables of type T, and you aren’t allowed to cast from a type parameter to an integer type. For these reasons, a helper function called Add is provided to add the values in two cells. (Error handling code could be added to ensure that the Sheet generic type is used only with addable types.)

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+ member function 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. The second parameter is not similarly restricted and could be anything. For this reason, it is possible that the first and second parameters of the operator+ member function are not compatible.

Serialization

Serialization persists the state of an object to a stream. Serializing an instance of a generic type is similar to serializing a regular type. This book does not present a detailed explanation of serialization. This section provides only essential information on serialization for generic types.

Serialization is performed mostly with the SerializationInfo object. SerializationInfo.AddValue and SerializationInfo.GetValue methods add data to and get data from the serialization data stream. For generic types, use the SerializationInfo.AddValue(string, object, Type) and SerializationInfo.GetValue(string, Type) overloaded methods.

SerializationInfo.GetValue returns an object type that should be cast to the target type.

Generic types must be adorned with the Serializable attribute to support serialization.

The GetObjectData method is where the serialization of an object is implemented. GetObjectData has a SerializationInfo and StreamingContext parameter. The SerializationInfo.AddValue method is called to serialize states, including any generic 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 SerializationInfo and StreamingContext parameters. Call the SerializationInfo.GetValue method to rehydrate the instance, as shown in the following code. (The StreamingContext parameter provides user-defined information, which is not required in this sample code.)

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

Objects can be serialized in different formats, such as binary or Simple Object Access Protocol (SOAP). This is done with formatters, such as the BinaryFormatter type. The SoapFormatter type cannot be used with generic types. Serialization also requires creating an appropriate stream for the formatter, such as a FileStream. The stream is where the instance is serialized or deserialized. For example, call BinaryFormatter.Serialize to serialize a generic type instance and BinaryFormatter.Deserialize to deserialize a generic type instance.

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

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;
        }

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

        public void GetObjectData(SerializationInfo info,
            StreamingContext ctx) {
            info.AddValue("fielda", 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 parametric polymorphism in other languages. The difference is found in the compile-time and run-time semantics of generics. This section focuses on improvements in these areas compared with parameterized types in C++, which is a widely recognized and well-documented implementation of parametric polymorphism.

Although an inspection of parameterized templates in C++ might unveil basic similarities with generics, there are considerable differences. These differences make generics more efficient and better-performing than parameterized templates. The exact implementation of templates is specific to each C++ compiler. Yet 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 at compile time. For example, the Standard Template Library (STL) of C++ offers a stack collection, which is a template. If ellipse, rectangle, triangle, and curve versions 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 bloat.

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 written in C# but also can be used in Microsoft Visual Basic .NET. With C++, the particulars of the template, such as the parameterized types, are lost at compile time and are 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 7-1 shows the MSIL-specific code for a generic type.

An MSIL view of a generic type

Figure 7-1. An 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 arguments and reference type arguments.

If a generic type has a value type argument, it is expanded into a class at run time. This new class has the type argument (value type) substituted for the type parameter throughout the class. The resulting class is cached in memory. Future instances of similar generic types reference the cached class. The code is shared across multiple instances of generic types with the same type arguments. This prevents additional class expansion as found in C++ to eliminate unnecessary code bloat.

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 type parameter. The new class is cached in memory. Future instances of the generic type with any reference type argument will use this same class. Essentially, generic type instantiations that have a reference type argument all 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 single class. Sheet<double> is a separate class. Sheet<XClass> and Sheet<YClass> share a class intended for type arguments that are references.

Generic Collections

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

The parameterized collections are found in the System.Collections.Generic namespace. Generic interfaces are also included in that namespace. Table 7-3 lists some of the types and interfaces of this namespace.

Table 7-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>

..................Content has been hidden....................

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