Serialization for Generic Types in C# .NET
Serialization persists the state of an object to a stream. Serializing the instance of a generic type is similar to a regular type.
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 Examples.Generics { public class Program { 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); } } |
C# .NET Generics: Nested Types
You can nest a generic type inside a nongeneric type. Conversely, a nongeneric type can be nested in a generic type. More intriguing is nesting generic types inside of generic types. The nested generic type can consume the type parameters of the surrounding type. A type parameter of the surrounding type cannot be redefined as a new type parameter in the nested type. However, the nested generic type can declare entirely new type parameters.
This is sample code of nested generic types:
|
using System;
namespace Examples.Generics { public class Program { public static void Main() { ZClass<int>.Nested<double> obj = new ZClass<int>.Nested<double>(); obj.MethodA(10, 12.34); } } public class ZClass<T> { public void MethodA(T arg) { } public class Nested<S> { public void MethodA(T arg1, S arg2) { Console.WriteLine(“arg1: {0}”, arg1.GetType().ToString()); Console.WriteLine(“arg2: {0}”, arg2.GetType().ToString()); } } } } |
C# .NET Generics: Method Overriding
Methods that have a type parameter can be overridden, regardless of where the type parameter is declared. These methods can also override other methods. The Table lists the various combinations of overriding generic and nongeneric methods. If a base class is nongeneric or closed, overriding methods cannot have type parameters. Conversely, if the base class is open, overriding methods can employ type parameters.
|
Base Method |
Derived Method |
Comments |
|
Nongeneric |
Generic (open) |
Permitted |
|
Nongeneric |
Generic (closed) |
Permitted |
|
Generic (open) |
Nongeneric |
Not permitted |
|
Generic (open) |
Generic (open) |
Permitted; must use the same type parameters |
|
Generic (open) |
Generic (closed) |
Not permitted |
|
Generic (closed) |
Nongeneric |
Permitted |
|
Generic (closed) |
Generic (closed) |
Permitted |
|
Generic (closed) |
Generic (open) |
Not permitted |
Here is some example code:
|
using System;
namespace Examples.Generics { public class Program { public static void Main() { } }
public class ZClass<T> { public virtual void MethodA(T arg) { } } public class YClass<T> : ZClass<T> { public override void MethodA(T arg) { } //public override void MethodA(int arg) //{ [ illegal ] //} }
public class XClass<X> : ZClass<int> { public override void MethodA(int arg) { }
//public override void MethodA(X arg) //{ [ illegal ] //} } public class WClass : ZClass<int> { public override void MethodA(int arg) { } } } |
When a generic method overrides another generic method, it inherits the constraints of that method. The overriding method cannot change the inherited constraints.
The following code correctly overrides a generic method:
|
public class ZClass { public virtual void MethodA<T>(T arg) where T : new() { } } public class YClass : ZClass { public override void MethodA<T>(T arg) { T obj = new T(); } } |
C# .NET Generics: Casting
You may need to a cast a generic type. Since generic types are implicit System.Object types, they can always be cast to that type. In addition, generic types can be cast to the derivation constraint, which is also a type. The derivation constraint assures that the generic type is a descendant of the constraint. This assures a safe cast. Finally, generics can be cast to any interface even if the interface is not included in an interface constraint. Since there is no restriction on casting to interfaces, it is not type-safe. For that reason, care should be taken to cast to an implemented interface.
In the following code, ZClass is a generic type. It has a single type parameter (T) that has three constraints: YClass type derivation, IA interface derivation, and the constructor constraint. An instance of the T parameter is created in the method called Cast. The instance is then cast in succession to the YClass, IA, and IB interface. The first two casts work as expected. The third cast fails spectacularly. The type parameter is not related to IB. However, the compiler does not notice. Therefore, an exception is raised at run time, which is the worst possible time and underscores the type-unsafe nature of interface cast of generic types.
|
public class ZClass<T> where T : YClass, IA, new() { static public void Cast() { T obj = new T(); ((YClass)obj).MethodA(); ((IA)obj).MethodA(); ((IB)obj).MethodB(); // Error } } public class YClass : IA { public void MethodA() { Console.WriteLine(“YClass.MethodA”); } } interface IA { void MethodA(); } interface IB { void MethodB(); } |
Generic type parameters cannot be assigned a null or zero. There is no assurance that a type parameter is either a reference or value type, which prevents safely assigning a null or zero value to a type parameter. However, you can test a type parameter against null but not zero. If the comparison succeeds, the type parameter is a reference type. Otherwise, the type parameter is a value type.
C# .NET Generics: Inheritance
Generic and nongeneric classes can inherit a generic type. In addition, a generic type can be the base class to a generic or nongeneric type. Some basic rules apply. For example, the derived class cannot be a closed constructed type. The Table lists all the possible permutations.
|
Base Class |
Derived Class |
Comments |
|
Generic (open) |
Generic (open) |
Permitted when the derived class consumes the type parameters of the base class |
|
Generic (open) |
Generic (closed) |
Not permitted |
|
Generic (open) |
Nongeneric |
Permitted |
|
Generic (closed) |
Generic (open) |
Permitted |
|
Generic (closed) |
Generic (closed) |
Not permitted |
|
Generic (closed) |
Nongeneric |
Not permitted |
|
Nongeneric |
Generic (closed) |
Permitted |
|
Nongeneric |
Generic (open) |
Not permitted |
This sample code shows some of the permitted and not permitted combinations:
|
public class ZClass<T> { }
public class XClass<T> : ZClass<T> { }
public class BClass<Y> { }
public class AClass<Z> : BClass<int> { }
public class YClass : ZClass<int> { }
/* public class AClass<Z> : BClass<Y> { [ illegal ] }
public class YClass: ZClass<T> { [ illegal ] } */ |
When inheriting an open constructed type, the constraints of the base class must be repeated in the derived type. Furthermore, the derived type can provide additional constraints on type parameters declared at the base type. This is not applicable to closed constructed types because closed constructed types do not have type parameters or constraints.
Here is sample code combining inheritance of generic types and constraints:
|
public class ZClass<T> where T : IComparable { }
public class YClass<T> : ZClass<T> where T : IComparable { }
public class XClass<T> : ZClass<T> where T : IComparable, IDisposable { }
public class BClass<Y> where Y : IEnumerable { }
public class AClass<Z> : BClass<int[]> where Z : IDisposable { } |
Generic Constraints in C# .NET: Default Constructor Constraint
Will this code compile? It looks fairly innocuous:
|
class ZClass<T> { public void MethodA() { T obj = new T(); } } |
This code does not compile. The problem is the default constructor. Although prevalent, not every type has a default constructor. A default constructor, or a constructor with no arguments, assigns a default state to an object. The default constructor is called with a parameterless new operator. However, because a default constructor is not guaranteed, the new operator is not universally applicable. Therefore, the new operator is disallowed on type parameters.
The solution is the constructor constraint. The derivation constraint does not help with constructors because derived types do not inherit constructors for the base class. Constructor constraints mandate that a type parameter have a default constructor, which is confirmed at compile time. This allows the new operator to be used with the type parameter. The constructor constraint is added to the where clause and is a new operator. When combined with other constraints, the default constructor constraint must be the last item in the constraint list. The constructor constraint applies only to the default constructor. You are still prevented from using constructors with arguments.
Here is sample code of the constructor constraint. The constructor constraint is used on the ZClass.
|
using System;
namespace Examples.Generics { public class Program { public static void Main() { ZClass obj = new ZClass(); obj.MethodA<XClass>(); } } public class ZClass { public void MethodA<T>() where T : XClass, new() { Console.WriteLine(“ZClass.MethodA”); T obj = new T(); obj.MethodB(); } } public class XClass { public void MethodB() { Console.WriteLine(“XClass.MethodB”); } } } |
Generic Constraints in C# .NET: Reference Type Constraint
A reference type constraint restricts a type parameter to a reference type. Reference types are generally user-defined types, including classes, interfaces, delegates, and array types. A reference type constraint uses the class keyword.
The following code has a reference type constraint. Although this code is similar to the code presented in the previous post, a reference type constraint is used instead of a value type constraint. For this reason, the illegal line has moved. You cannot use an integer type with a reference type parameter.
|
using System; using System.Collections;
namespace Examples.Generics { public class Program { public static void Main() { // ZClass<int> obj1=new ZClass<int>(); [illegal] ZClass<XClass> obj2 = new ZClass<XClass>();
} } public class ZClass<T> where T : class {
public void Iterate(T data) { } } public class XClass { } } |
Generic Constraints in C# .NET: Value Type Constraint
A value type constraint restricts a type parameter to a value type. Value types are derived from the System.ValueType type. Primitives and structures are examples of value types. The exception is the Nullable type. The Nullable type is a value type, but it is not allowed with a value type constraint. A value type constraint is a constraint using the struct keyword.
The following code demonstrates the value type constraint. The commented source line uses a reference type, which would cause compile errors because of the value type constraint.
|
using System.Collections;
namespace Examples.Generics { public class Program { public static void Main() { ZClass<int> obj1 = new ZClass<int>(); // ZClass<XClass> obj2=new ZClass<XClass>(); [illegal] } } public class ZClass<T> where T : struct { public void Iterate(T data) { } } public class XClass { } } |
Generic Constraints in C# .NET: Interface Constraints
Interfaces can also be constraints, which require that the type argument implement the interface. Although a type parameter can have at most one derivation constraint, it can have multiple interface constraints. This is logical because a class can inherit a single base class but can implement many interfaces. The syntax for an interface constraint is identical to a derivation constraint. Class and interface constraints can be combined in a list of constraints. However, a class constraint should precede interface constraints in the list.
Interface and derivation constraints share many of the same rules and restrictions, such as the visibility of the interface constraint exceeding that of the type parameter.
In the following code, the find capability has been added to the Sheet collection. The Find method returns an array of cells that contain a certain value. A comparison is made between the cell and value, where both are the type indicated in the type argument. Types that implement the IComparable interface support comparisons. If a comparison is equal, IComparable .CompareTo returns 0. To support this behavior, an interface constraint for IComparable is added to the type parameter. This is a partial implementation of the Sheet collection that shows Find and related methods. (Some of the code shown previously is omitted.)
|
using System;
namespace Examples.Generics { public class Starter { public static void Main() { Sheet<int> asheet = new Sheet<int>(5); for (byte row = 1; row < 6; ++row) { for (byte col = 1; col < 6; ++col) { asheet[row, col] = row * col; } }
Cell[] found = asheet.Find(6); foreach (Cell answer in found) {
Console.WriteLine(“R{0} C{1}”, answer.row, answer.col); } } } public struct Cell { public byte row; public byte col; } public class Sheet<T> where T : IComparable { //… public Cell[] Find(T searchValue) { int total = Count(searchValue); int counter = 0; Cell[] cells = new Cell[total]; for (byte row = 1; row <= m_Dimension; ++row) { for (byte col = 1; col <= m_Dimension; ++col) { if (m_Sheet[row - 1, col - 1].CompareTo(searchValue) == 0) { cells[counter].row = row; cells[counter].col = col; ++counter; } } } return cells; } public int Count(T searchValue) { int counter = 0; for (byte row = 1; row <= m_Dimension; ++row) { for (byte col = 1; col <= m_Dimension; ++col) { if (m_Sheet[row - 1, col - 1].CompareTo(searchValue) == 0) { ++counter; } } } return counter; } //… } } |
This code works, but there is a subtle problem. The IComparable interface manipulates objects, which causes boxing and unboxing when working with value types. This could become expensive in a large collection of value types. In the preceding code, the type argument is an integer, which is a value type. This causes boxing with the IComparable interface. Generic interfaces obfuscate this problem. .NET Framework 2.0 includes several general-purpose generic interfaces for developers. This is the class header updated for the IComparable generic interface:
|
public class Sheet<T> where T : IComparable<T> |
Generic Constraints in C# .NET: Derivation Constraints
The derivation constraint states the derivation of a type parameter. The type parameter must be derived from that constraint, which is enforced by the C# compiler. This allows the compiler to relax the restriction that access to type parameters is limited to the System.Object. The public interface of the constrained type parameter is expanded to include the derivation type. A type can inherit from a single class because multiple inheritance is not available in C#. For this reason, a type parameter can optionally have a single constraint. However, each parameter can have separate derivation constraints, in which each constraint is space-delimited.
Because the constraint is enforced by the compiler, the type parameter can only be used accordingly. This avoids an unsafe usage of the type parameter. The compiler assures that all access to the type parameter is type-safe as defined in the constraint. This is different in C++, in which the compiler performs no such type-checking on type parameters. In C++, you can basically do anything with a type parameter. Errors are uncovered when the parameter template is expanded at compile time, deep in the bowels of the expansion code. This can lead to cryptic error messages that are hard to debug. Anyone that has used the Active Template Library (ATL) and had expansion errors can attest to this. The C# compiler also updates Microsoft IntelliSense per the derivation constraint. A type parameter reflects the IntelliSense of any constraints on that parameter.
ZClass is a generic type and is defined in the following code. It has a K and V type parameter, each with a separate constraint. Per the constraints, the K parameter must be derived from XClass, while the V parameter should be derived from YClass. Main has three generic type instantiations. The first two are okay, but the third causes compile errors. The problem is the first type argument. WClass is not derived from XClass, which is a requirement of the first parameter per the constraint.
|
using System; namespace Examples.Generics { public class Starter { public static void Main() { // good ZClass< |