Generics FAQ: Best Practices

 

Juval Lowy

October 2005

Applies to:
   Generic Types

Summary: Review frequently asked questions about best practices for generic types. (16 printed pages)

Contents

When Should I Not Use Generics?
What Naming Convention Should I Use for Generics?
Should I Put Constraints on Generic Interfaces?
How Do I Dispose of a Generic Type?
Can I Cast to and from Generic Type Parameters?
How Do I Synchronize Multithreaded Access to a Generic Type?
How do I Serialize Generic Types?

When Should I Not Use Generics?

The main reason not to use generics is cross-targeting—if you build the same code for both .NET 1.1 and .NET 2.0, then you cannot take advantage of generics, since they are only supported on .NET 2.0.

What Naming Convention Should I Use for Generics?

I recommend using a single capital letter for a generic type parameter. If you have no additional contextual information about the type parameter, you should use the letter T:

[C#]

public class MyClass<T>
{...}

[Visual Basic]

Public Class SomeClass(Of T)
   ...
End Class

[C++]

generic <typename T>
public ref class MyClass
{...};

In all other cases, the official Microsoft guidelines for generic naming conventions are:

  • Name generic type parameters with descriptive names, unless a single letter name is completely self explanatory and a descriptive name would not add value.

    [C#]

    public interface ISessionChannel<TSession> 
    {...}
    public delegate TOutput Converter<TInput,TOutput>(TInput from);
    

    [Visual Basic]

    Public Interface ISessionChannel(Of TSession)
       ... 
    End Interface
    
    Public Delegate Function Converter(Of TInput, TOutput)(ByVal input As TInput) As Toutput
    

    [C++]

    generic <typename TSession>
    public interface class ISessionChannel 
    {...};
    generic <typename TInput, typename TOutput>
    public delegate TOutput Converter(TInput from);
    
  • Consider indicating constraints placed on a type parameter in the name of parameter. For example, a parameter constrained to ISession may be called TSession.

Should I Put Constraints on Generic Interfaces?

An interface can define constraints for the generic types it uses. For example,

[C#]

public interface ILinkedList<T> where T : IComparable<T>
{...}

[Visual Basic]

Public Interface ILinkedList(Of T As IComparable(Of T))
   ...
End Interface

[C++]

generic <typename T> where T : IComparable<T>
public interface class ILinkedList
{...};

However, you should be very mindful about the implications of defining constraints at the scope of an interface. An interface should not have any shred of implementation details, to reinforce the notion of separation of interface from implementation. There are many ways in which one could implement the generic interface. The specific type arguments used are, after all, an implementation detail. Constraining them commonly couples the interface to specific implementation options.

It is better to let the class implementing the generic interface add the constraint and keep the interface itself constraints-free:

[C#]

public class LinkedList<T> : ILinkedList<T> where T : IComparable<T>
{
   //Rest of the implementation  
}

[Visual Basic]

Public Class LinkedList(Of T As IComparable(Of T))
       Implements ILinkedList(Of T)
' Rest of the implementation  
End Class

[C++]

generic <typename T> where T : IComparable<T> 
public ref class LinkedList : ILinkedList<T>
{
   //Rest of the implementation  
};

How Do I Dispose of a Generic Type?

In C# and Visual Basic, when you supply an object of a generic type parameter to the using statement, the compiler has no way of knowing whether the actual type the client will specify supports IDisposable. The compiler will therefore not allow you to specify an instance of a generic type parameter for the using statement:

[C#]

public class MyClass<T> 
{
   public void SomeMethod(T t)
   {
      using(t)//Does not compile 
      {...}
   }
}

[Visual Basic]

Public Class SomeClass(Of T)
   Public Sub SomeMethod(ByVal value As T)
      Using value ' Does not compile
      End Using
   End Sub
End Class

Instead, you can constrain the type parameter to support IDisposable:

[C#]

public class MyClass<T> where T : IDisposable 
{
   public void SomeMethod(T t)
   {
      using(t)
      {...}
   }
}

[Visual Basic]

Public Class SomeClass(Of T As IDisposable)
   Public Sub SomeMethod(ByVal value As T)
      Using value
      End Using
   End Sub
End Class

However, you should not do so. The problem with the IDisposable constraint is that now you cannot use interfaces as type arguments, even if the underlying type supports IDisposable:

[C#]

public interface IMyInterface
{}
public class MyOtherClass : IMyInterface,IDisposable 
{...}
public class MyClass<T> where T : IDisposable 
{
   public void SomeMethod(T t)
   {
      using(t)
      {...}
   }
}
MyOtherClass myOtherClass = new MyOtherClass();
MyClass<IMyInterface> obj = new MyClass<IMyInterface>();//Does not compile
obj.SomeMethod(myOtherClass); 

[Visual Basic]

Public Interface IMyInterface
End Interface

Public Class MyOtherClass
      Implements IMyInterface, IDisposable
   ...
End Class

Public Class SomeClass(Of T As IDisposable)
   Public Sub SomeMethod(ByVal value As T)
      Using value
      End Using
   End Sub
End Class

Dim myOtherClass As New MyOtherClass
Dim obj As New SomeClass(Of IMyInterface) ' Does not compile
obj.SomeMethod(myOtherClass)

Instead of constraining the type parameter to derive from IDisposable, I recommend that you use the as operator in C# or the TryCast operator in Visual Basic with the using statement on generic type parameters to enable its use when dealing with interfaces:

[C#]

public class MyClass<T> 
{
   public void SomeMethod(T t)
   {
      using(t as IDisposable)
      {...}
   }
}

[Visual Basic]

Public Class SomeClass(Of T)
   Public Sub SomeMethod(ByVal value As T)
      Using TryCast(value, IDisposable)
      End Using
   End Sub
End Class

Can I Cast to and from Generic Type Parameters?

The compiler will only let you implicitly cast generic type parameters to object, or to constraint-specified types:

[C#]

interface ISomeInterface
{...}
class BaseClass
{...}
class MyClass<T> where T : BaseClass,ISomeInterface
{
   void SomeMethod(T t)
   {
      ISomeInterface obj1 = t;
      BaseClass      obj2 = t;
      object         obj3 = t;
   }
}

[Visual Basic]

Interface ISomeInterface
   ...
End Interface

Class BaseClass
   ...
End Class

Class SomeClass(Of T As{BaseClass,ISomeInterface})

   Private Sub SomeMethod(ByVal value As T)
      Dim obj1 As ISomeInterface = value
      Dim obj2 As BaseClass = value
      Dim obj3 As Object = value
   End Sub
End Class

[C++]

interface class ISomeInterface
{...};
ref class BaseClass
{...};
generic <typename T> where T : BaseClass,ISomeInterface
ref class MyClass
{
   void SomeMethod(T t)
   {
      ISomeInterface ^obj1 = t;
      BaseClass      ^obj2 = t;
      Object         ^obj3 = t;
   }
};

Such implicit casting is of course type safe, because any incompatibility is discovered at compile-time.

The compiler will let you explicitly cast generic type parameters to any interface, but not to a class:

[C#]

interface ISomeInterface
{...}
class SomeClass
{...}
class MyClass<T> 
{
   void SomeMethod(T t)
   {
      ISomeInterface obj1 = (ISomeInterface)t;//Compiles
      SomeClass      obj2 = (SomeClass)t;     //Does not compile
   }
}

[Visual Basic]

Interface ISomeInterface
   ...
End Interface

Class BaseClass
   ...
End Class

Class SomeClass(Of T)
   Private Sub SomeMethod(ByVal value As T)
      Dim obj1 As ISomeInterface = CType(value,ISomeInterface)' Compiles
      Dim obj2 As BaseClass = CType(value,BaseClass)' Does not compile
   End Sub
End Class

[C++]

interface class ISomeInterface
{...};
ref class SomeClass
{...};
generic <typename T>
ref class MyClass 
{
   void SomeMethod(T t)
   {
      ISomeInterface ^obj1 = (ISomeInterface ^)t;//Compiles
      SomeClass      ^obj2 = (SomeClass ^)t;     //Does not compile
   }
};

However, you can force a cast from a generic type parameter to any other type using a temporary object variable:

[C#]

class MyOtherClass
{...}

class MyClass<T> 
{
  
   void SomeMethod(T t)
   
   {
      object temp = t;
      MyOtherClass obj = (MyOtherClass)temp;
   
   }
}

[Visual Basic]

Class MyOtherClass
   ...
End Class

Class SomeClass(Of T)
   Sub SomeMethod(ByVal value As T)
      Dim temp As Object = value
      Dim obj As MyOtherClass = CType(temp, MyOtherClass)
   End Sub
End Class

[C++]

ref class SomeClass
{...};

generic <typename T>
ref class MyClass 
{
  
   void SomeMethod(T t)
   
   {
      Object ^temp = t;
      SomeClass ^obj = (SomeClass ^)temp;
   
   }
};

Needless to say, such explicit casting is dangerous because it may throw an exception at runtime if the concrete type used instead of the generic type parameter does not derive from the type you explicitly cast to.

[C#]

Instead of risking a casting exception, a better approach is to use the is or as operators. The is operator returns true if the generic type parameter is of the queried type, and as will perform a cast if the types are compatible, and will return null otherwise.

public class MyClass<T> 
{
   public void SomeMethod(T t)
   {
      if(t is int)
      {...} 

      if(t is LinkedList<int,string>)
      {...}

      string str = t as string;
      if(str != null)
      {...}

      LinkedList<int,string> list = t as LinkedList<int,string>;
      if(list != null)
      {...}
   }
}

[Visual Basic]

Instead of risking a casting exception, a better approach is to use the TypeOf and the TryCast operators. The is operator returns true if the generic type parameter is of the queried type. You can also use the TryCast operator to try to perform a cast if the types are compatible, and return Nothing otherwise.

Class SomeClass(Of T)

   Public Sub SomeMethod(ByVal value As T)
      If TypeOf value Is Integer Then
         ...
      End If

      If TypeOf value Is LinkedList(Of Integer, String) Then
         ...
      End If

      Dim str As String = TryCast(value,String)
      If (Not str Is Nothing) Then
         ...
      End If

      Dim list As LinkedList(Of Integer, String) = _
         TryCast(value,LinkedList(Of 
         Integer, String))
      If (Not list Is Nothing) Then
         ...
      End If
   End Sub
End Class

How Do I Synchronize Multithreaded Access to a Generic Type?

In general, you should not use a Monitor on generic type parameters. The reason is that the Monitor can only be used with reference types. When you use generic types, the compiler cannot tell in advance whether you will provide a reference or a value type parameter. In C#, the compiler will let you use the lock() statement, yet if you provide a value type as the type parameter, it will have no effect at runtime. In Visual Basic, the compiler will not let you use the SyncLock on generic type parameters if the compiler is not certain the generic type parameter is a reference type.

In C# and Visual Basic, the only time when you could safely lock the generic type parameter is when you can constrain it to be a reference type, either by constraining it to be a reference type, or to derive from a base class:

[C#]

public class MyClass<T> where T : class
{..}

[Visual Basic]

Public Class SomeClass(Of T As Class)
   ...
End Class

or:

[C#]

public class SomeClass
{...}
public class MyClass<T> where T : SomeClass
{...}

[Visual Basic]

Public Class SomeClass
   ...
End Class
Public Class SomeClass(Of T As SomeClass)
   ...
End Class

Yet in general with synchronization it is better to avoid fragmented locking of individual member variables because that raises the likelihood of deadlocks.

How Do I Serialize Generic Types?

A generic class that has generic type parameters as members can be marked for serialization:

[C#]

[Serializable]
public class MySerializableClass<T>
{
   T m_T;
}

[Visual Basic]

<Serializable()> _
Public Class MySerializableClass(Of T)

   Dim m_T As T
End Class

[C++]

generic <typename T>
[Serializable]
public ref class MyClass
{
   T m_T;
};

However, in such cases, the generic class is only serializable if the generic type parameter specified is serializable. Consider this code:

[C#]

public class SomeClass
{}
MySerializableClass<SomeClass> obj;

[Visual Basic]

Public Class SomeClass
End Class

Dim obj as MySerializableClass(Of SomeClass)

[C++]

public ref class SomeClass
{};
MyClass<SomeClass ^> ^obj;

obj is not serializable because the type parameter SomeClass is not serializable. Consequently, MySerializableClass<T> may or may not be serializable, depending on the generic type parameter used. This may result in a run-time loss of data or system corruption, because the client application may not be able to persist the state of the object.

Presently, .NET does not provide a mechanism for constraining a generic type parameter to be serializable. The workaround is to perform a single run-time check before any use of the type, and abort the use immediately, before any damage could take place. You can place the run-time verification in the static constructor:

[C#]

[Serializable]
class MySerializableClass<T>
{
   T m_T;

   static MySerializableClass()   
   {
      ConstrainType(typeof(T));
   }
   static void ConstrainType(Type type)
   {
      bool serializable = type.IsSerializable;
      if(serializable == false)
      {
         string message = "The type " + type + " is not serializable";
         throw new InvalidOperationException(message);
      }
   }
}

[Visual Basic]

<Serializable()> _
Class SomeClass(Of T)
   Private m_T As T   

   Shared Sub New()
      ConstrainType(GetType(T))
   End Sub
   Private Shared Sub ConstrainType(ByVal t As Type)
      If Not t.IsSerializable Then
         Dim message As String = "The type " + t.ToString() + " is not 
                                                         serializable"
         Throw New InvalidOperationException(message)
      End If
   End Sub
End Class

[C++]

generic <typename T>
[Serializable]
ref class MyClass
{
   T m_T;
public:   
   static MyClass()   
   {
      ConstrainType(typeid<T>);
   }
private:
   static void ConstrainType(Type type)
   {
      bool serializable = type->IsSerializable;
      if(serializable == false)
      {
         String ^message = String::Concat("The type ", type->Name, 
            " is not serializable");
         throw gcnew SerializationException(message);
      }
   }
};

The static constructor is invoked exactly once per type per app domain, upon the first attempt to instantiate an object of that type. Performing the constraint verification in the static constructor is a technique applicable to any constraint that you cannot enforce at compile time, yet you have some programmatic way of determining and enforcing it at runtime.

 

About the author

Juval Lowy is a software architect and the principal of IDesign, specializing in .NET architecture consulting and advanced .NET training. Juval is Microsoft's Regional Director for the Silicon Valley, working with Microsoft on helping the industry adopt .NET. His latest book is Programming .NET Components 2nd Edition (O'Reilly, 2005). Juval participates in the Microsoft internal design reviews for future versions of .NET. Juval published numerous articles, regarding almost every aspect of .NET development, and is a frequent presenter at development conferences. Microsoft recognized Juval as a Software Legend as one of the world's top .NET experts and industry leaders.