Chapter 12
Generics

Wrox.com Code Downloads for this Chapter

You can find the wrox.com code downloads for this chapter at www.wrox.com/go/beginningvisualc#2015programming on the Download Code tab. The code is in the Chapter 12 download and individually named according to the names throughout the chapter.

This chapter begins by looking at what generics are. You learn about generics in fairly abstract terms at first, because learning the concepts behind generics is crucial to being able to use them effectively.

Next, you see some of the generic types in the .NET Framework in action. This will help you understand their functionality and power, as well as the new syntax required in your code. You'll then move on to define your own generic types, including generic classes, interfaces, methods, and delegates. You also learn additional techniques for further customizing generic types: the default keyword and type constraints.

Finally, you'll look at covariance and contravariance, two forms of variance that were introduced in C# 4 and that allow greater flexibility when using generic classes.

What Are Generics?

To best illustrate what generics are, and why they are so useful, recall the collection classes from the previous chapter. You saw how basic collections can be contained in classes such as ArrayList, but that such collections suffer from being untyped. This requires that you cast object items into whatever type of objects you actually stored in the collection. Because anything that inherits from System.Object (that is, practically anything) can be stored in an ArrayList, you need to be careful. Assuming that certain types are all that is contained in a collection can lead to exceptions being thrown, and code logic breaking down. You learned some techniques to deal with this, including the code required to check the type of an object.

However, you discovered that a much better solution is to use a strongly typed collection class initially. By deriving from CollectionBase and providing your own methods for adding, removing, and otherwise accessing members of the collection, you learned how you could restrict collection members to those derived from a certain base type or supporting a certain interface. This is where you encounter a problem. Every time you create a new class that needs to be held in a collection, you must do one of the following:

  • Use a collection class you've already made that can contain items of the new type.
  • Create a new collection class that can hold items of the new type, implementing all the required methods.

Typically, with a new type you need extra functionality, so more often than not, you need a new collection class anyway. Therefore, making collection classes can take up a fair amount of your time!

Generic classes, conversely, make coding a lot simpler. A generic class is built around whatever type, or types, you supply during instantiation, enabling you to strongly type an object with hardly any effort at all. In the context of collections, creating a “collection of type T objects” is as simple as saying it aloud — and achievable in a single line of code. Instead of code such as,

CollectionClass items = new CollectionClass();
items.Add(new ItemClass());
you can use this:
CollectionClass<ItemClass> items = new CollectionClass<ItemClass>();
items.Add(new ItemClass());

The angle bracket syntax is the way you pass type parameters to generic types. In the preceding code, read CollectionClass<ItemClass> as CollectionClass of ItemClass. You will, of course, examine this syntax in more detail later in the chapter.

There's more to the subject of generics than just collections, but they are particularly suited to this area, as you will see later in the chapter when you look at the System.Collections.Generic namespace. By creating a generic class, you can generate methods that have a signature that can be strongly typed to any type you want, even catering to the fact that a type can be a value or reference type, and deal with individual cases as they occur. You can even allow only a subset of types to be used, by restricting the types used to instantiate a generic class to those that support a given interface or are derived from a certain type. Moreover, you're not restricted to generic classes — you can create generic interfaces, generic methods (which can be defined on nongeneric classes), and even generic delegates. All this adds a great deal of flexibility to your code, and judicious use of generics can eliminate hours of development time.

You're probably wondering how all this is possible. Usually, when you create a class, it is compiled into a type that you can then use in your code. You might think that when you create a generic class, it would have to be compiled into a plethora of types, so that you could instantiate it. Fortunately, that's not the case — and given the infinite amount of classes possible in .NET, that's just as well. Behind the scenes, the .NET runtime allows generic classes to be dynamically generated as and when you need them. A given generic class A of B won't exist until you ask for it by instantiating it.

Using Generics

Before you look at how to create your own generic types, it's worth looking at the ones supplied by the .NET Framework. These include the types in the System.Collections.Generic namespace, a namespace that you've seen several times in your code because it is included by default in console applications. You haven't yet used any of the types in this namespace, but that's about to change. This section looks at the types in this namespace and how you can use them to create strongly typed collections and improve the functionality of your existing collections.

First, though, you'll look at another simpler generic type that gets around a minor issue with value types: nullable types.

Nullable Types

In earlier chapters, you saw that one of the ways in which value types (which include most of the basic types such as int and double as well as all structs) differ from reference types (string and any class) is that they must contain a value. They can exist in an unassigned state, just after they are declared and before a value is assigned, but you can't make use of the value type in that state in any way. Conversely, reference types can be null.

There are times, and they crop up more often than you might think (particularly when you work with databases), when it is useful to have a value type that can be null. Generics give you a way to do this using the System.Nullable<T> type, as shown in this example:

System.Nullable<int> nullableInt;

This code declares a variable called nullableInt, which can have any value that an int variable can, plus the value null. This enables you to write code such as the following:

nullableInt = null;

If nullableInt were an int type variable, then the preceding code wouldn't compile.

The preceding assignment is equivalent to the following:

nullableInt = new System.Nullable<int>();

As with any other variable, you can't just use it before some kind of initialization, whether to null (through either syntax shown previously) or by assigning a value.

You can test nullable types to determine whether they are null, just like you test reference types:

if (nullableInt == null)
{
   …
}

Alternatively, you can use the HasValue property:

if (nullableInt.HasValue)
{
   …
}

This wouldn't work for reference types, even one with a HasValue property of its own, because having a null-valued reference type variable means that no object exists through which to access this property, and an exception would be thrown.

You can also look at the value of a nullable type by using the Value property. If HasValue is true, then you are guaranteed a non-null value for Value; but if HasValue is false — that is, null has been assigned to the variable — then accessing Value will result in an exception of type System.InvalidOperationException.

Note that nullable types are so useful that they have resulted in a modification of C# syntax. Rather than use the syntax shown previously to declare a nullable type variable, you can instead use the following:

int? nullableInt;

int? is simply a shorthand for System.Nullable<int> but is much more readable. In subsequent sections, you'll use this syntax.

Operators and Nullable Types

With simple types, such as int, you can use operators such as +, -, and so on to work with values. With nullable type equivalents, there is no difference: The values contained in nullable types are implicitly converted to the required type and the appropriate operators are used. This also applies to structs with operators that you have supplied:

int? op1 = 5;
int? result = op1 * 2;

Note that here the result variable is also of type int?. The following code will not compile:

int? op1 = 5;
int result = op1 * 2;

To get this to work you must perform an explicit conversion or access the value through the Value property, which requires code such as,

int? op1 = 5;
int result = (int)op1 * 2;

or:

int? op1 = 5;
int result = op1.Value * 2;

This works fine as long as op1 has a value — if it is null, then you will get an exception of type System.InvalidOperationException.

This raises the obvious question: What happens when one or both values in an operator evaluation that involves two nullable values are null, such as op1 in the following code?

int? op1 = null;
int? op2 = 5;
int? result = op1 * op2;

The answer is that for all simple nullable types other than bool?, the result of the operation is null, which you can interpret as “unable to compute.” For structs you can define your own operators to deal with this situation (as shown later in this chapter), and for bool? there are operators defined for & and | that might result in non-null return values. The results in the table make perfect sense logically — if there is enough information to work out the answer of the computation without needing to know the value of one of the operands, then it doesn't matter if that operand is null.

The ?? Operator

To further reduce the amount of code you need in order to deal with nullable types, and to make it easier to deal with variables that can be null, you can use the ?? operator. Known as the null coalescing operator, it is a binary operator that enables you to supply an alternative value to use for expressions that might evaluate to null. The operator evaluates to its first operand if the first operand is not null, or to its second operator if the first operand is null. Functionally, the following two expressions are equivalent:

op1 ?? op2
op1 == null ? op2 : op1

In this code, op1 can be any nullable expression, including a reference type and, importantly, a nullable type. This means that you can use the ?? operator to provide default values to use if a nullable type is null, as shown here:

int? op1 = null;
int result = op1 * 2 ?? 5;

Because in this example op1 is null, op1 * 2 will also be null. However, the ?? operator detects this and assigns the value 5 to result. Importantly, note here that no explicit conversion is required to put the result in the int type variable result. The ?? operator handles this conversion for you. Alternatively, you can pass the result of a ?? evaluation into an int? with no problems:

int? result = op1 * 2 ?? 5;

This behavior makes the ?? operator a versatile one to use when dealing with nullable variables, and a handy way to supply defaults without using either a block of code in an if structure or the often confusing ternary operator.

The ?. Operator

This operator, often referred to as the Elvis operator or the null condition operator, helps to overcome code ambiguity caused by burdensome null checking. For example, if you wanted to get the count of orders for a given customer, you would need to check for null before setting the count value:

int count = 0;
if (customer.orders ! = null)
{
  count = customer.orders.Count();
}

If you were to simply write this code and there were no orders existing for the customer (i.e. it's null), a System.ArgumentNullException is thrown:

int count = customer.orders.Count();

Using the ?. operator results in the int? count being set to null instead of an exception happening.

int? count = customer.orders?.Count();

Combining the null coalescing operator ?? discussed in the previous section with the null condition operator ?: makes it possible to set a default value when the result is null.

int? count = customer.orders?.Count() ?? 0;

Another use of the null conditional operator is to trigger events. Events are discussed in detail in Chapter 13. The most common way to trigger an event is by using this code pattern:

var onChanged = OnChanged;
if (onChanged != null)
{
  onChanged(this, args);
}

This pattern is not thread safe because someone might unsubscribe the last event handler just after the null check is done. When that happens an exception is thrown and the application crashes. Avoid this by using the null condition operator as shown here:

OnChanged?.Invoke(this, args);

As mentioned in Chapter 11, use the ?. operator to check for nulls with the == operator overload in the C:BegVCSharpChapter12Ch12CardLibCard.cs class to prevent an exception from being thrown when using the method. For example:

public static bool operator ==(Card card1, Card card2)
        => (card1?.suit == card2?.suit) && (card1?.rank == card2?.rank);

By including the null condition operator in the statement, you are effectively expressing that if the object to the left is not null, (in this case card1 or card2), then retrieve what is to the right. If the object on the left is null (i.e. card1 or card2), then terminate the access chain and return null.

Working with Nullable Types

Use the following Try It Out to experiment with a nullable Vector type.

The System.Collections.Generic Namespace

In practically every application used so far in this book, you have seen the following namespaces:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

The System namespace contains most of the basic types used in .NET applications. The System.Text namespace includes types relating to string processing and encoding. You'll look at the System.Linq namespace later in this book. The System.Threading.Tasks namespace contains types that help you to write asynchronous code, which isn't covered in this book. But what about System.Collections.Generic, and why is it included by default in console applications?

The answer is that this namespace contains generic types for dealing with collections, and it is likely to be used so often that it is configured with a using statement, ready for you to use without qualification.

You'll now look at these types, which are guaranteed to make your life easier. They make it possible for you to create strongly typed collection classes with hardly any effort. Table 12.1 lists two types from the System.Collections.Generic namespace that are covered in this section. More of the types in this namespace are covered later in this chapter.

Table 12.1 Generic Collection Type

Type Description
List<T> Collection of type T objects
Dictionary<K, V> Collection of items of type V, associated with keys of type K

This section also describes various interfaces and delegates used with these classes.

List<T>

Rather than derive a class from CollectionBase and implement the required methods as you did in the last chapter, it can be quicker and easier simply to use the List<T> generic collection type. An added bonus here is that many of the methods you normally have to implement, such as Add(), are implemented for you.

Creating a collection of type T objects requires the following code:

List<T> myCollection = new List<T>();

That's it. You don't have to define any classes, implement any methods, or do anything else. You can also set a starting list of items in the collection by passing a List<T> object to the constructor. List<T> also has an Item property, enabling array-like access:

T itemAtIndex2 = myCollectionOfT[2];

This class supports several other methods, but that's plenty to get you started. The following Try It Out demonstrates how to use List<T> in practice.

Sorting and Searching Generic Lists

Sorting a generic list is much the same as sorting any other list. The last chapter described how you can use the IComparer and IComparable interfaces to compare two objects and thereby sort a list of that type of object. The only difference here is that you can use the generic interfaces IComparer<T> and IComparable<T>, which expose slightly different, type-specific methods. Table 12.2 explains these differences.

Table 12.2 Sorting with Generic Types

Generic Method Nongeneric Method Difference
Int IComparable<T>.CompareTo(T otherObj) int IComparable.CompareTo(object otherObj) Strongly typed in generic versions.
Bool IComparable<T>.Equals(T otherObj) N/A Doesn't exist on a nongeneric interface; can use inherited object.Equals() instead.
Int IComparer<T>.Compare(T objectA, T objectB) int IComparer.Compare(object objectA, object objectB) Strongly typed in generic versions.
Bool IComparer<T>.Equals(T objectA, T objectB) N/A Doesn't exist on a nongeneric interface; can use inherited object.Equals() instead.
Int IComparer<T>.GetHashCode(T objectA) N/A Doesn't exist on a nongeneric interface; can use inherited object.GetHashCode() instead.

To sort a List<T>, you can supply an IComparable<T> interface on the type to be sorted, or supply an IComparer<T> interface. Alternatively, you can supply a generic delegate as a sorting method. From the perspective of seeing how the code works, this is far more interesting because implementing the interfaces described here takes no more effort than implementing their nongeneric cousins.

In general terms, all you need to sort a list is a method that compares two objects of type T; and to search, all you need is a method that checks an object of type T to determine whether it meets certain criteria. It is a simple matter to define such methods, and to aid you there are two generic delegate types that you can use:

  • Comparison<T> — A delegate type for a method used for sorting, with the following return type and parameters:
    int method(T objectA, T objectB)
  • Predicate<T> — A delegate type for a method used for searching, with the following return type and parameters:
    bool method(T targetObject)

You can define any number of such methods, and use them to “snap-in” to the searching and sorting methods of List<T>. The next Try It Out illustrates this technique.

Dictionary<K, V>

The Dictionary<K, V> type enables you to define a collection of key-value pairs. Unlike the other generic collection types you've looked at in this chapter, this class requires instantiating two types: the types for both the key and the value that represent each item in the collection.

Once a Dictionary<K, V> object is instantiated, you can perform much the same operations on it as you can on a class that inherits from DictionaryBase, but with type-safe methods and properties already in place. You can, for example, add key-value pairs using a strongly typed Add() method:

Dictionary<string, int> things = new Dictionary<string, int>();
things.Add("Green Things", 29);
things.Add("Blue Things", 94);
things.Add("Yellow Things", 34);
things.Add("Red Things", 52);
things.Add("Brown Things", 27);

You can iterate through keys and values in the collection by using the Keys and Values properties:

foreach (string key in things.Keys)
{
   WriteLine(key);
}
foreach (int value in things.Values)
{
   WriteLine(value);
}

In addition, you can iterate through items in the collection by obtaining each as a KeyValuePair<K, V> instance, much like you can with the DictionaryEntry objects shown in the last chapter:

foreach (KeyValuePair<string, int> thing in things)
{
   WriteLine($"{thing.Key} = {thing.Value}");
}

One point to note about Dictionary<K, V> is that the key for each item must be unique. Attempting to add an item with an identical key will cause an ArgumentException exception to be thrown. Because of this, Dictionary<K, V> allows you to pass an IComparer<K> interface to its constructor. This might be necessary if you use your own classes as keys and they don't support an IComparable or IComparable<K> interface, or if you want to compare objects using a nondefault process. For instance, in the preceding example, you could use a case-insensitive method to compare string keys:

Dictionary<string, int> things =
   new Dictionary<string, int>(StringComparer.CurrentCultureIgnoreCase);

Now you'll get an exception if you use keys such as this:

things.Add("Green Things", 29);
things.Add("Green things", 94);

You can also pass an initial capacity (with an int) or a set of items (with an IDictionary<K, V> interface) to the constructor.

A feature introduced in C# 6 called index initializers supports the initialization of indices inside the object initializer:

var zahlen = new Dictionary<int, string>()
{
  [1] = "eins",
  [2] = "zwei"
};

Index initializers can be streamlined as in many cases there is no need for a temporary variable as show previously via var zahlen. Using expression-bodied methods, the above example leads to a cascading effect of simplification:

public ZObject ToGerman() => new ZObject() { [1] = "eins", [2] = "zwei"};

Modifying CardLib to Use a Generic Collection Class

One simple modification you can make to the CardLib project you've been building over recent chapters is to change the Cards collection class to use a generic collection class, thus saving many lines of code. The required modification to the class definition for Cards is as follows (you can find this code in Ch12CardLibCards.cs):

   public class Cards : List<Card>, ICloneable { … }

You can also remove all the methods of Cards except Clone(), which is required for ICloneable, and CopyTo(), because the version of CopyTo() supplied by List<Card> works with an array of Card objects, not a Cards collection. Clone() requires a minor modification because the List<T> class does not define a List property to use:

public object Clone()
{
    Cards newCards = new Cards();
    foreach (Card sourceCard in this)
    {
        newCards.Add((Card)sourceCard.Clone());
    }
    return newCards;
}

Rather than show the code here for what is a very simple modification, the updated version of CardLib, called Ch12CardLib, is included in the downloadable code for this chapter, along with the client code from the last chapter.

Defining Generic Types

You've now learned enough about generics to create your own. You've seen plenty of code involving generic types and have had plenty of practice using generic syntax. This section looks at defining the following:

  • Generic classes
  • Generic interfaces
  • Generic methods
  • Generic delegates

You'll also look at the following more advanced techniques for dealing with the issues that come up when defining generic types:

  • The default keyword
  • Constraining types
  • Inheriting from generic classes
  • Generic operators

Defining Generic Classes

To create a generic class, merely include the angle bracket syntax in the class definition:

class MyGenericClass<T> { … }

Here, T can be any identifier you like, following the usual C# naming rules, such as not starting with a number and so on. Typically, though, you can just use T. A generic class can have any number of type parameters in its definition, separated by commas:

class MyGenericClass<T1, T2, T3> { … }

Once these types are defined, you can use them in the class definition just like any other type. You can use them as types for member variables, return types for members such as properties or methods, and parameter types for method arguments:

class MyGenericClass<T1, T2, T3>
{
   private T1 innerT1Object;
   public MyGenericClass(T1 item)
   {
      innerT1Object = item;
   }
   public T1 InnerT1Object
   {
      get { return innerT1Object; }
   }
}

Here, an object of type T1 can be passed to the constructor, and read-only access is permitted to this object via the property InnerT1Object. Note that you can make practically no assumptions as to what the types supplied to the class are. The following code, for example, will not compile:

class MyGenericClass<T1, T2, T3>
{
   private T1 innerT1Object;
   public MyGenericClass()
   {
      innerT1Object = new T1();
   }
   public T1 InnerT1Object
   {
      get { return innerT1Object; }
   }
}

Because you don't know what T1 is, you can't use any of its constructors — it might not even have any, or it might have no publicly accessible default constructor. Without more complicated code involving the techniques shown later in this section, you can make only the following assumption about T1: you can treat it as a type that either inherits from or can be boxed into System.Object.

Obviously, this means that you can't really do anything very interesting with instances of this type, or any of the other types supplied to the generic class MyGenericClass. Without using reflection, which is an advanced technique used to examine types at runtime (and not covered in this chapter), you're limited to code that's no more complicated than the following:

   public string GetAllTypesAsString()
   {
      return "T1 = " + typeof(T1).ToString()
         + ", T2 = " + typeof(T2).ToString()
         + ", T3 = " + typeof(T3).ToString();
   }

There is a bit more that you can do, particularly in terms of collections, because dealing with groups of objects is a pretty simple process and doesn't need any assumptions about the object types — which is one good reason why the generic collection classes you've seen in this chapter exist.

Another limitation that you need to be aware of is that using the operator == or != is permitted only when comparing a value of a type supplied to a generic type to null. That is, the following code works fine:

   public bool Compare(T1 op1, T1 op2)
   {
      if (op1 != null && op2 != null)
      {
         return true;
      }
      else
      {
         return false;
      }
   }

Here, if T1 is a value type, then it is always assumed to be non-null, so in the preceding code Compare will always return true. However, attempting to compare the two arguments op1 and op2 fails to compile:

   public bool Compare(T1 op1, T1 op2)
   {
      if (op1 == op2)
      {
         return true;
      }
      else
      {
         return false;
      }
   }

That's because this code assumes that T1 supports the == operator. In short, to do anything interesting with generics, you need to know a bit more about the types used in the class.

The default Keyword

One of the most basic things you might want to know about types used to create generic class instances is whether they are reference types or value types. Without knowing this, you can't even assign null values with code such as this:

   public MyGenericClass()
   {
      innerT1Object = null;
   }

If T1 is a value type, then innerT1Object can't have the value null, so this code won't compile. Luckily, this problem has been addressed, resulting in a new use for the default keyword (which you've seen being used in switch structures earlier in the book). This is used as follows:

   public MyGenericClass()
   {
      innerT1Object = default(T1);
   }

The result of this is that innerT1Object is assigned a value of null if it is a reference type, or a default value if it is a value type. This default value is 0 for numeric types, while structs have each of their members initialized to 0 or null in the same way. The default keyword gets you a bit further in terms of doing a little more with the types you are forced to use, but to truly get ahead, you need to constrain the types that are supplied.

Constraining Types

The types you have used with generic classes until now are known as unbounded types because no restrictions are placed on what they can be. By constraining types, it is possible to restrict the types that can be used to instantiate a generic class. There are a number of ways to do this. For example, it's possible to restrict a type to one that inherits from a certain type. Referring back to the Animal, Cow, and Chicken classes used earlier, you could restrict a type to one that was or inherited from Animal, so this code would be fine:

MyGenericClass<Cow> = new MyGenericClass<Cow>();

The following, however, would fail to compile:

MyGenericClass<string> = new MyGenericClass<string>();

In your class definitions this is achieved using the where keyword:

class MyGenericClass<T> where T : constraint { … }

Here, constraint defines what the constraint is. You can supply a number of constraints in this way by separating them with commas:

class MyGenericClass<T> where T : constraint1, constraint2 { … }

You can define constraints on any or all of the types required by the generic class by using multiple where statements:

class MyGenericClass<T1, T2> where T1 : constraint1 where T2 : constraint2
{ … }

Any constraints that you use must appear after the inheritance specifiers:

class MyGenericClass<T1, T2> : MyBaseClass, IMyInterface
   where T1 : constraint1 where T2 : constraint2 { … }

The available constraints are shown in Table 12.3.

Table 12.3 Generic Type Constraints

Constraint Definition Example Usage
struct Type must be a value type. In a class that requires value types to function — for example, where a member variable of type T being 0 means something
class Type must be a reference type. In a class that requires reference types to function — for example, where a member variable of type T being null means something
base-class Type must be, or inherit from, base-class. You can supply any class name as this constraint. In a class that requires certain baseline functionality inherited from base-class in order to function
interface Type must be, or implement, interface. In a class that requires certain baseline functionality exposed by interface in order to function
new() Type must have a public, parameterless constructor. In a class where you need to be able to instantiate variables of type T, perhaps in a constructor

It is possible to use one type parameter as a constraint on another through the base-class constraint as follows:

class MyGenericClass<T1, T2> where T2 : T1 { … }

Here, T2 must be the same type as T1 or inherit from T1. This is known as a naked type constraint, meaning that one generic type parameter is used as a constraint on another.

Circular type constraints, as shown here, are forbidden:

class MyGenericClass<T1, T2> where T2 : T1 where T1 : T2 { … }

This code will not compile. In the following Try It Out, you'll define and use a generic class that uses the Animal family of classes shown in earlier chapters.

Inheriting from Generic Classes

The Farm<T> class in the preceding example, as well as several other classes you've seen in this chapter, inherit from a generic type. In the case of Farm<T>, this type was an interface: IEnumerable<T>. Here, the constraint on T supplied by Farm<T> resulted in an additional constraint on T used in IEnumerable<T>. This can be a useful technique for constraining otherwise unbounded types. However, you do need to follow some rules.

First, you can't “unconstrain” types that are constrained in a type from which you are inheriting. In other words, a type T that is used in a type you are inheriting from must be constrained at least as much as it is in that type. For example, the following code is fine:

class SuperFarm<T> : Farm<T>
      where T : SuperCow {}

This works because T is constrained to Animal in Farm<T>, and constraining it to SuperCow is constraining T to a subset of these values. However, the following won't compile:

class SuperFarm<T> : Farm<T>
      where T : struct{}

Here, you can say definitively that the type T supplied to SuperFarm<T> cannot be converted into a T usable by Farm<T>, so the code won't compile.

Even situations in which the constraint is a superset have the same problem:

class SuperFarm<T> : Farm<T>
      where T : class{}

Even though types such as Animal would be allowed by SuperFarm<T>, other types that satisfy the class constraint won't be allowed in Farm<T>. Again, compilation will fail. This rule applies to all the constraint types shown earlier in this chapter.

Also note that if you inherit from a generic type, then you must supply all the required type information, either in the form of other generic type parameters, as shown, or explicitly. This also applies to nongeneric classes that inherit from generic types, as you've seen elsewhere. Here's an example:

public class Cards : List<Card>, ICloneable{}

This is fine, but attempting the following will fail:

public class Cards : List<T>, ICloneable{}

Here, no information is supplied for T, so no compilation is possible.

Generic Operators

Operator overrides are implemented in C# just like other methods and can be implemented in generic classes. For example, you could define the following implicit conversion operator in Farm<T>:

public static implicit operator List<Animal>(Farm<T> farm)
{
   List<Animal> result = new List<Animal>();
   foreach (T animal in farm)
   {
      result.Add(animal);
   }
   return result;
}

This allows the Animal objects in a Farm<T> to be accessed directly as a List<Animal> should you require it. This comes in handy if you want to add two Farm<T> instances together, such as with the following operators:

public static Farm<T> operator +(Farm<T> farm1, List<T> farm2)
{
   Farm<T> result = new Farm<T>();
   foreach (T animal in farm1)
   {
      result.Animals.Add(animal);
   }
   foreach (T animal in farm2)
   {
      if (!result.Animals.Contains(animal))
      {
         result.Animals.Add(animal);
      }
   }
   return result;
}
public static Farm<T> operator +(List<T> farm1, Farm<T> farm2)
         => farm2 + farm1;

You could then add instances of Farm<Animal> and Farm<Cow> as follows:

Farm<Animal> newFarm = farm + dairyFarm;

In this code, dairyFarm (an instance of Farm<Cow>) is implicitly converted into List<Animal>, which is usable by the overloaded + operator in Farm<T>.

You might think that this could be achieved simply by using the following:

public static Farm<T> operator +(Farm<T> farm1, Farm<T> farm2){ … }

However, because Farm<Cow> cannot be converted into Farm<Animal>, the summation will fail. To take this a step further, you could solve this using the following conversion operator:

public static implicit operator Farm<Animal>(Farm<T> farm)
{
   Farm <Animal> result = new Farm <Animal>();
   foreach (T animal in farm)
   {
      result.Animals.Add(animal);
   }
   return result;
}

With this operator, instances of Farm<T>, such as Farm<Cow>, can be converted into instances of Farm<Animal>, solving the problem. You can use either of the methods shown, although the latter is preferable for its simplicity.

Generic Structs

You learned in earlier chapters that structs are essentially the same as classes, barring some minor differences and the fact that a struct is a value type, not a reference type. Because this is the case, generic structs can be created in the same way as generic classes, as shown here:

public struct MyStruct<T1, T2>
{
   public T1 item1;
   public T2 item2;
}

Defining Generic Interfaces

You've now seen several generic interfaces in use — namely, those in the Systems.Collections.Generic namespace such as IEnumerable<T> used in the last example. Defining a generic interface involves the same techniques as defining a generic class:

interface MyFarmingInterface<T>
   where T : Animal
{
   bool AttemptToBreed(T animal1, T animal2);
   T OldestInHerd { get; }
}

Here, the generic parameter T is used as the type of the two arguments of AttemptToBreed() and the type of the OldestInHerd property.

The same inheritance rules apply as for classes. If you inherit from a base generic interface, you must obey the rules, such as keeping the constraints of the base interface generic type parameters.

Defining Generic Methods

The previous Try It Out used a method called GetCows(), and in the discussion of the example it was stated that you could make a more general form of this method using a generic method. In this section you'll see how this is possible. A generic method is one in which the return and/or parameter types are determined by a generic type parameter or parameters:

public T GetDefault<T>() => default(T);

This trivial example uses the default keyword you looked at earlier in the chapter to return a default value for a type T. This method is called as follows:

int myDefaultInt = GetDefault<int>();

The type parameter T is provided at the time the method is called.

This T is quite separate from the types used to supply generic type parameters to classes. In fact, generic methods can be implemented by nongeneric classes:

public class Defaulter
{
   public T GetDefault<T>() => default(T);
}

If the class is generic, though, then you must use different identifiers for generic method types. The following code won't compile:

public class Defaulter<T>
{
   public T GetDefault<T>() => default(T);
}

The type T used by either the method or the class must be renamed.

Constraints can be used by generic method parameters in the same way that they are for classes, and in this case you can make use of any class type parameters:

public class Defaulter<T1>
{
   public T2 GetDefault<T2>()
      where T2 : T1
   {
      return default(T2);
   }
}

Here, the type T2 supplied to the method must be the same as, or inherit from, T1 supplied to the class. This is a common way to constrain generic methods.

In the Farm<T> class shown earlier, you could include the following method (included, but commented out, in the downloadable code for Ch12Ex04):

public Farm<U> GetSpecies<U>() where U : T
{
   Farm<U> speciesFarm = new Farm<U>();
   foreach (T animal in animals)
   {
      if (animal is U)
      {
         speciesFarm.Animals.Add(animal as U);
      }
   }
   return speciesFarm;
}

This can replace GetCows() and any other methods of the same type. The generic type parameter used here, U, is constrained by T, which is in turn constrained by the Farm<T> class to Animal. This enables you to treat instances of T as instances of Animal, should you want to do so.

In the client code for Ch12Ex04, in Program.cs, using this new method requires one modification:

Farm<Cow> dairyFarm = farm.GetSpecies<Cow>();

In a similar vein, you could write:

Farm<Chicken> poultryFarm = farm.GetSpecies<Chicken>();

You can take this same approach with any class that inherits from Animal.

Note here that having generic type parameters on a method changes the signature of the method. This means you can have several overloads of a method differing only in generic type parameters, as shown in this example:

public void ProcessT<T>(T op1){ … }
public void ProcessT<T, U>(T op1){ … }

Which method should be used is determined by the amount of generic type parameters specified when the method is called.

Defining Generic Delegates

The last generic type to consider is the generic delegate. You saw these delegates in action earlier in the chapter when you learned how to sort and search generic lists. You used the Comparison<T> and Predicate<T> delegates, respectively, for this.

Chapter 6 described how to define delegates using the parameters and return type of a method, the delegate keyword, and a name for the delegate:

public delegate int MyDelegate(int op1, int op2);

To define a generic delegate, you simply declare and use one or more generic type parameters:

public delegate T1 MyDelegate<T1, T2>(T2 op1, T2 op2) where T1: T2;

As you can see, constraints can be applied here too. You'll learn a lot more about delegates in the next chapter, including how you can use them in a common C# programming technique — events.

Variance

Variance is the collective term for covariance and contravariance, two concepts that were introduced in .NET 4. In fact, they have been around longer than that (they were available in .NET 2.0), but until .NET 4 it was very difficult to implement them, as this required custom compilation procedures.

The easiest way to grasp what these terms mean is to compare them with polymorphism. Polymorphism, as you will recall, is what enables you to put objects of a derived type into variables of a base type, for example:

Cow myCow = new Cow("Geronimo");
Animal myAnimal = myCow;

Here, an object of type Cow has been placed into a variable of type Animal — which is possible because Cow derives from Animal.

However, the same cannot be said for interfaces. That is to say, the following code will not work:

IMethaneProducer<Cow> cowMethaneProducer = myCow;
IMethaneProducer<Animal> animalMethaneProducer = cowMethaneProducer;

The first line of code is fine, assuming that Cow supports the interface IMethaneProducer<Cow>. However, the second line of code presupposes a relationship between the two interface types that doesn't exist, so there is no way of converting one into the other. Or is there? There certainly isn't a way using the techniques you've seen so far in this chapter, as all the type parameters for generic types have been invariant. However, it is possible to define variant type parameters on generic interfaces and generic delegates that cater to exactly the situation illustrated in the previous code.

To make the previous code work, the type parameter T for the IMethaneProducer<T> interface must be covariant. Having a covariant type parameter effectively sets up an inheritance relationship between IMethaneProducer<Cow> and IMethaneProducer<Animal>, so that variables of one type can hold values of the other, just like with polymorphism (although a little more complicated).

To round off this introduction to variance, you need to look at the other kind, contravariance. This is similar but works in the other direction. Rather than being able to place a generic interface value into a variable that includes a base type as in covariance, contravariance enables you to place that interface into a variable that uses a derived type, for example:

IGrassMuncher<Cow> cowGrassMuncher = myCow;
IGrassMuncher<SuperCow> superCowGrassMuncher = cowGrassMuncher;

At first glance this seems a little odd, as you couldn't do the same with polymorphism. However, this is a useful technique in certain circumstances, as you will see in the section called, “Contravariance.”

In the next two sections, you look at how to implement variance in generic types and how the .NET Framework uses variance to make your life easier.

Covariance

To define a generic type parameter as covariant, you use the out keyword in the type definition, as shown in the following example:

public interface IMethaneProducer<out T>{ … }

For interface definitions, covariant type parameters can be used only as return values of methods or property get accessors.

A good example of how this is useful is found in the .NET Framework, in the IEnumerable<T> interface that you've used previously. The item type T in this interface is defined as being covariant. This means that you can put an object that supports, say, IEnumerable<Cow> into a variable of type IEnumerable<Animal>.

This enables the following code:

static void Main(string[] args)
{
   List<Cow> cows = new List<Cow>();
   cows.Add(new Cow("Geronimo"));
   cows.Add(new SuperCow("Tonto"));
   ListAnimals(cows);
   ReadKey();
}
static void ListAnimals(IEnumerable<Animal> animals)
{
   foreach (Animal animal in animals)
   {
      WriteLine(animal.ToString());
   }
}

Here the cows variable is of type List<Cow>, which supports the IEnumerable<Cow> interface. This variable can, through covariance, be passed to a method that expects a parameter of type IEnumerable<Animal>. Recalling what you know about how foreach loops work, you know that the GetEnumerator() method is used to get an enumerator of IEnumerator<T>, and the Current property of that enumerator is used to access items. IEnumerator<T> also defines its type parameter as covariant, which means that it's okay to use it as the get accessor of a parameter, and everything works perfectly.

Contravariance

To define a generic type parameter as contravariant, you use the in keyword in the type definition:

public interface IGrassMuncher<in T>{ … }

For interface definitions, contravariant type parameters can be used only as method parameters, not as return types.

Again, the best way to understand this is to look at an example of how contravariance is used in the .NET Framework. One interface that has a contravariant type parameter, again one that you've already used, is IComparer<T>. You might implement this interface for animals as follows:

public class AnimalNameLengthComparer : IComparer<Animal>
{
    public int Compare(Animal x, Animal y)
         => x.Name.Length.CompareTo(y.Name.Length);
}

This comparer compares animals by name length, so you could use it to sort, for example, an instance of List<Animal>. However, through contravariance, you can also use it to sort an instance of List<Cow>, even though the List<Cow>.Sort() method expects an instance of IComparer<Cow>:

List<Cow> cows = new List<Cow>();
cows.Add(new Cow("Geronimo"));
cows.Add(new SuperCow("Tonto"));
cows.Add(new Cow("Gerald"));
cows.Add(new Cow("Phil"));
cows.Sort(new AnimalNameLengthComparer());

In most circumstances, contravariance is something that simply happens — and it's been worked into the .NET Framework to help with just this sort of operation. The good thing about both types of variance in .NET 4 and above, though, is that you can now implement them with the techniques shown in this section whenever you need them.

image What You Have Learned in This Chapter

Topic Key Concepts
Using generic types Generic types require one or more type parameters to work. You can use a generic type as the type of a variable by passing the type parameters you require when you declare a variable. You do this by enclosing a comma-separated list of type names in angle brackets.
Nullable types Nullable types are types that can take any value of a specified value type or the value null. You can use the syntax Nullable<T> or T? to declare a nullable type variable.
The ?? operator The null coalescing operator returns either the value of its first operand, or, if the first operand is null, its second operand.
Generic collections Generic collections are extremely useful as they come with strong typing built-in. You can use List<T>, Collection<T>, and Dictionary<K, V> among other collection types. These also expose generic interfaces. To sort and search generic collections, you use the IComparer<T> and IComparable<T> interfaces.
Defining generic classes You define a generic type much like any other type, with the addition of generic type parameters where you specify the type name. As with using generic types, you specify these as a comma-separated list enclosed in angle brackets. You can use the generic type parameters in your code anywhere you'd use a type name, for example, in method return values and parameters.
Generic type parameter constraints In order to use generic type parameters more effectively in your generic type code, you can constrain the types that can be supplied when the type is used. You can constrain type parameters by base class, supported interface, whether they must be value or reference types, and whether they support parameterless constructors. Without such constraints, you must use the default keyword to instantiate a variable of a generic type.
Other generic types As well as classes, you can define generic interfaces, delegates, and methods.
Variance Variance is a concept similar to polymorphism, but applied to type parameters. It allows you to use one generic type in place of another, where those generic types vary only in the generic type parameters used. Covariance allows conversion between two types where the target type has a type parameter that is a base class of the type parameter of the source type. Contravariance allows conversion where this relationship is inverted. Covariant type parameters are defined with the out parameter, and can only be used as return types and property get accessor types. Contravariant type parameters are defined with the in parameter and can only be used as method parameters.
..................Content has been hidden....................

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