Understanding Generics - Programming in C# - Sams Teach Yourself C# 5.0 in 24 Hours (2013)

Sams Teach Yourself C# 5.0 in 24 Hours (2013)

Part II: Programming in C#

Hour 12. Understanding Generics


What You’ll Learn in This Hour

Why you should use generics

Using generic methods

Creating generic classes

Combining generics and arrays

Variance in generic interfaces

Working with tuples


You have already seen how an object can refer to an instance of any class. This enables you to create classes that operate on any data type. Several significant problems can occur with this approach. By working only with object, there is no way for the class to restrict input to be only of a specific type. To perform meaningful operations on the data, it must be cast from object to a more well-defined type. This not only adds complexity, but also sacrifices type safety at compile time.

Generics in C# solve this problem by enabling generalization that is type safe at compile time by removing the need to cast or otherwise perform boxing and unboxing conversions. Generics combine type safety, reusability, and efficiency in ways nongeneric classes can’t. The most common use for generics is with collections, which you saw in Hour 10, “Working with Arrays and Collections;” however, you can use generics to create your own custom generic types and methods.

In this hour, you learn how generics work and how to create your own generic types.

Why You Should Use Generics

In Hour 10, you saw how to create an array of integer values. Because you explicitly stated the data type for each element in the array was int, the compiler verified that you were assigning only integer values to each element. You also operated on each element using methods and operators defined for integer values. Imagine the code required to find the minimum value of an arbitrary array of integers (see Listing 12.1).

Listing 12.1. Finding the Minimum Value


public int Min(int[] values)
{
int min = values[0];
foreach (int value in values)
{
if (value.CompareTo(min) < 0)
{
min = value;
}
}

return min;
}


What would happen if you wanted this to work with any numeric type? Without generics, you would need to write a different version of Min for each numeric type with the only difference being the data type. Although that is certainly possible, it introduces a lot of redundancy in your code and makes it harder to maintain.

Knowing that the IComparable interface defines a CompareTo method, you could write this in a more generic way using objects. This would allow you to write the code once yet still use it for an array of any numeric type, as shown in Listing 12.2.

Listing 12.2. Finding the Minimum Value Using Objects


public object Min(object[] values)
{
IComparable min = (IComparable)values[0];
foreach (object value in values)
{
if (((IComparable)value).CompareTo(min) < 0)
{
min = (IComparable)value;
}
}

return min;
}


Unfortunately, although you now only have one method to maintain, you also lost any type safety. In addition, an array of integers is not convertible to an array of objects. Because this method works with objects, what happens when it is passed an array that was defined like the following?

object[] array = { 5, 3, "a", "hello" };

This is legal because the array holds elements of type object and any value is implicitly convertible to object.


Try It Yourself: Finding the Minimum Without Generics

By following these steps, you see how creating a generalized method without the use of generics prevents type safety yet still compiles correctly. Keep Visual Studio open at the end of this exercise because you will use this application later.

1. Create a new console application.

2. In the Program.cs file, implement the two functions shown in Listings 12.1 and 12.2. Make sure you declare these methods as static.

3. Change both versions of Min so that the first line of each has a Console.WriteLine to print out which method is executed.

4. In the Main method of the Program.cs file, declare and initialize an integer array named array of five elements followed by a Console.WriteLine statement to print the minimum value.

5. Run the application using Ctrl+F5, and make sure the output displays the correct minimum value for your array.

6. Change the declaration of array from int to long.

7. You should notice the call to Min has a red squiggly line, and two errors are reported in the error window. This happens because there is no implementation of Min that takes a long[] as a parameter.

8. Change the declaration of array from long to object to remove the compiler errors.

9. Run the application again using Ctrl+F5, and make sure the output displays the correct minimum value for your array. (This should be the same value as shown from step 4.)

10. Change the elements of array to the following:

{ 5, 3, "a", "hello", 1 };

11. Run the application using Ctrl+F5, and observe that the output matches what is shown in Figure 12.1.

Image

Figure 12.1. Runtime exception generated from the lack of type safety.


Not only have you lost type safety, but you are also performing n+1 conversion operations, where n is the number of elements in the array. As you might guess, the larger the array, the more expensive this method becomes.

Using generics, this problem becomes simple. You gain the ability to write the code only once without losing type safety or incurring multiple conversion operations. Listing 12.3 shows the same Min method defined using generics. If you compare this with the method defined in Listing 12.1, you can see that they are almost identical.


Note: where T : IComparable<T>

This constraint might be a bit confusing because it looks like there is a circular dependency here. In fact, it’s actually straightforward and means that T must be a type that can be compared with other Ts.


Listing 12.3. Finding the Minimum Value Using Generics


public T Min<T>(T[] values) where T: IComparable<T>
{
T min = values[0];
foreach (T value in values)
{
if (value.CompareTo(min) < 0)
{
min = value;
}
}

return min;
}


The biggest difference is that the generic version uses a generic type parameter T, specified by the <T> syntax, after the method name. The type parameter acts as a placeholder for a real type supplied at compile time. In this example, we know that any real type used in place of T must implement the IComparable<T> interface, where the T of the interface is the type parameter of the generic method. This constrains, or limits, the type parameter to be any type that implements the interface.


Caution: C# Generics, C++ Templates, and Java Generics

Although C# generics, Java generics, and C++ templates all provide support for parameterized types, several significant differences exist between them. The syntax for C# generics is similar to that of Java and simpler than C++ templates.

All the type substitutions for C# generics occur at runtime, thereby preserving the generic type information for instantiated objects. In Java, generics are a language-only construction implemented only in the compiler in a technique known as type erasure. As a result, the generic type information for instantiated objects is not available at runtime. This is different from C++ templates that are expanded at compile time, generating additional code for each template type.

C# generics do not provide the same amount of flexibility as C++ templates and Java generics in some cases. For example, C# generics do not support the idea of a wildcard for the type parameter like Java generics. It is also not possible to call arithmetic operators on a type parameter, as you can in C++ templates.



Try It Yourself: Finding the Minimum with Generics

To modify the code written in the previous exercise to use a generic version of the Min function, follow these steps. If you closed Visual Studio, repeat the previous exercise first.

1. In the Program.cs file, implement the generic function shown in Listing 12.3. Make sure you declare this method as static and make the same change to add as the first line a call to Console.Write to print out the method name.

2. Change the declaration of array so that it is int[] and change the elements to be all numeric values again so that your program compiles and executes correctly.

3. Run the application using Ctrl+F5. Notice that you are still using the nongeneric strongly typed version of Min.

4. Change only the type of array so that it is long[].

5. Run the application again using Ctrl+F5. Notice that you are now using the generic version of Min.

6. Change only the elements of array to the following:

{ 5, 3, "a", "hello", 1 };

7. Run the application again. This time, instead of running and generating a runtime exception, you receive two compiler errors saying that you cannot implicitly convert type ‘string’ to ‘long’, as shown in Figure 12.2.

Image

Figure 12.2. Compiler errors generated by enforcing type safety.


Understanding Generic Type Parameters

Just as methods have parameters and the values of those parameters at runtime are arguments, generic types and methods also have type parameters and type arguments. Type parameters act as placeholders for a type argument supplied at compile time.

This is more than simple text replacement of the type parameter with the supplied type. When a generic type or method is compiled, the Common Intermediate Language (CIL) contains metadata identifying it as having type parameters. At runtime, the Just-In-Time (JIT) compiler creates a constructed type with the supplied type parameter substituted in the appropriate locations.

A generic type or method can have multiple type parameters separated by a comma (,) in the type parameter specification. Several of the generic collection classes, such as Dictionary<TKey, TValue> and KeyValuePair<TKey, TValue>, use more than one type parameter. The Tuple class, which we discuss a bit later, is also generic and has up to eight type parameters.

Constraints

Constraints enable you to apply restrictions on the types used for the type arguments at compile time. These restrictions are specified using the where keyword, as you saw in Listing 12.3. When you constrain a type parameter, the number of allowable operations and methods increase to those supported by the constrained type and all types in its inheritance chain.

There are six possible constraints, as shown in Table 12.1.

Table 12.1. Generic Type Parameter Constraints

Image

Constraints guarantee to the compiler that any operator or method called on a type parameter will be supported. Type parameters that have no constraints are unconstrained type parameters and support only simple assignment and calling methods supported by System.Object. In addition, the != and == operators cannot be used with unconstrained type parameters because the compiler has no guarantee that they are supported.

A single type parameter can have multiple constraints, and you can apply constraints to multiple parameters:

CustomDictionary<TKey, TValue>
where TKey : IComparable
where TValue : class, new()


Note: Testing for Value Equality with Generics

Even if you apply the where T : class constraint, you should still avoid using the == and != operators on the type parameter. These operators test for reference identity, not value equality.

This happens even if those operators are overloaded in the type because the compiler knows only that T is a reference type. As a result, it can use only the default operators defined on System.Object that are valid for all reference types.

The recommended way to test for value equality is to apply the where T : IComparable<T> constraint and ensure that interface is implemented in any class that will be used to construct the generic class. By applying this constraint, you can use the CompareTo method to perform value equality, as you saw in Listing 12.3.


A type parameter constraint is a generic type parameter used as a constraint for another type parameter. Type parameter constraints are most commonly found when a generic method has to constrain its type parameter to the type parameter of the containing type, as shown in Listing 12.4. In this example, T is a type parameter constraint for the Add method that means Add accepts a List<U>, where any type substituted for U must be or derive from T.

Listing 12.4. Type Parameter Constraints on a Method


public class List<T>
{
public void Add<U>(List<U> items) where U : T
{
}
}


Type parameter constraints can also be used with generic classes when you want to enforce a relationship between two type parameters, as shown in Listing 12.5. In this example, you state that Example has three type parameters (T, U, and V) such that any type substituted for T must be, or derive from V, and that U and V are unconstrained.

Listing 12.5. Type Parameter Constraints on a Class


public class Example<T, U, V> where T : V
{
}


Default Values for Generic Types

Remember, C# is a strongly typed language that requires definite assignment of a variable before it can be used. To help simplify this requirement, every type has a default value. Obviously, it isn’t possible for you to know ahead of time whether the default value should be null, 0, or a zero-initialized struct. How then do you specify the default value for a type T that can represent any type?

C# provides the default keyword, which represents the appropriate default value for a type parameter based on the actual type specified. That means it returns null for reference types and zero for all numeric value types. If the type argument is a struct, each member of the struct is initialized to null or zero, as appropriate for the member type. For nullable value types, it returns null.

Using Generic Methods

Generic methods are no different from their nongeneric relatives but are defined using a set of generic type parameters rather than concrete types. A generic method is a blueprint for a method generated at runtime. If you look back at Listing 12.3, you have already used a generic method.


Note: Generic Methods in Nongeneric Classes

Generic methods are not restricted to only generic classes. It is perfectly valid, and fairly common, for a nongeneric class to include generic methods.

It is also possible for generic classes to include nongeneric methods, which have full access to the type parameters of the generic class.


By using constraints on a generic method, you can make use of more specialized operations the constraint guarantees will be available. The type parameters defined by a generic class are available to both generic and nongeneric methods. Because of this, if a generic method defines the same type parameter as the containing class, the argument supplied for the inner T hides the one supplied for the outer T and generates a compiler warning. If you need a method that uses different type arguments than provided when the class was instantiated, you should provide a different identifier for the type parameter, as shown in Listing 12.6.

Listing 12.6. Type Parameter Hiding


class GenericClass<T>
{
void GenerateWarning<T>()
{
}

void NoWarning<U>()
{
}
}


When you call a generic method, you must provide a real data type for the type arguments defined by that method. Listing 12.7 shows one way to call the Min<T> method defined in Listing 12.3.

Listing 12.7. Calling a Generic Method


public static class Program
{
static void Main()
{
int[] array = {3, 5, 7, 0, 2, 4, 6 };
Console.WriteLine(Min<int>(array));
}
}


Although this is acceptable, it isn’t necessary in the majority of calls, thanks to type inference. When you omit the type argument, the compiler attempts to discover, or infer, the type based on the method arguments. Listing 12.8 shows the same call using type inference.

Listing 12.8. Calling a Generic Method Using Type Inference


public static class Program
{
static void Main()
{
int[] array = {3, 5, 7, 0, 2, 4, 6 };
Console.WriteLine(Min(array));
}
}


Because type inference relies on the method arguments, it cannot infer the type only from a constraint or return value. This means you can’t use it with methods that have no parameters.

For generic methods, the type parameters are part of the method signature. This enables a generic method to be overloaded by declaring multiple generic methods with the same formal parameter list but which differ by type parameter.


Tip: Type Inference and Overload Resolution

Type inference occurs at compile time and before the compiler tries to resolve overloaded method signatures. When type substitution occurs, it is possible for a nongeneric method and a generic method to have identical signatures. In such a case, the most specific method will be used, which is always the nongeneric method.


Creating Generic Classes

You have already seen generic classes in action when you looked at the different collection types. Generic classes are most commonly used with collections because the behavior of the collection is the same for any data type stored. Just as a generic method is a blueprint for a method generated at runtime, a generic class is a blueprint for a class constructed at runtime.

Not only are generic classes used within the .NET Framework, you can create your own generic classes as well. This is no different from creating a nongeneric class, except that you provide a type parameter rather than an actual data type.

Keep in mind a few important questions when you create your own generic types:

What types should be type parameters? Generally, the more types you parameterize, the more flexibility your type has. There are practical limits on how many type parameters should be used, however, because the readability of your code can decrease as the number of type parameters increases.

What constraints should be applied? There are multiple ways to determine this. One is to apply as many constraints as possible that will still allow you to work with the types you are expecting. Another is to apply as few constraints as possible so that the generic type is maximally flexible. Both approaches are valid, but you can also take a more pragmatic approach of applying just the constraints necessary to limit the class to implementing its defined purpose. For example, if you know your generic class should work only with reference types, you would apply the class constraint. This prevents your class from being used with value types but enables you to use the as operator and check for null values.

Should behavior be provided in base classes and subclasses? Generic classes can be used as base classes, just like nongeneric classes. As a result, the same design choices apply here as they do with nongeneric classes.

Should generic interfaces be implemented? Depending on the type of generic class you are designing, you might have to implement, and possibly create, one or more generic interfaces. How your class will be used also determines which interfaces, if any, are implemented.

Just as nongeneric classes can inherit from either concrete or abstract nongeneric base classes, generic classes can also inherit from a nongeneric concrete or abstract base class. However, generic classes can also inherit from other generic classes.


Note: Generic Structs and Interfaces

Structs can be generic as well and use the same syntax and type constraints as classes. The only differences between a generic struct and a generic class are the same differences for nongeneric classes and structs.

Generic interfaces use the same type parameter syntax and constraints as generic classes and follow the same rules as nongeneric interface declarations. The one notable exception is that the interfaces implemented by a generic type remain unique for all possible constructed types. What this actually means is that if, after type parameter substitution, two generic interfaces implemented by the same generic class would be identical, the declaration of that generic class is invalid.

Although generic classes can inherit from nongeneric interfaces, a generic interface is the preferred choice for use with generic classes.


To understand inheritance with generic classes, you first need to understand the difference between an open and closed type. An open type is one that involves type parameters. More specifically, it is a generic type that has not been supplied any type arguments for its type parameters. Aclosed type, also called a constructed type, is a generic type that is not open. (That is, it has been supplied a type argument for all its type parameters.)

Generic classes can inherit from either an open type or a closed type. A derived class can provide type arguments for all the type parameters on its base class, in which case it is a constructed type. If the derived class provides no type arguments, it is an open type. Although generic classes can inherit from closed or open types, nongeneric classes can inherit only from closed types; otherwise, there is no way for the compiler to know what type argument should be used.

Some examples of inheriting from open and closed types are shown in Listing 12.9.

Listing 12.9. Inheritance with Generics


abstract class Element { }

class Element<T> : Element { }

class BasicElement<T> : Element<T> { }

class Int32Element : BasicElement<int> { }


In this example, Element<T> derives from Element and is an open type. BasicElement<T> derives from Element<T> and is an open type. Int32Element is a constructed type because it derives from the constructed type BasicElement<int>.

However, a derived class can provide type arguments for some of the type parameters on its base class, in which case it is an open constructed type. Think of an open constructed type as being somewhere in between an open type and a closed type; that is, it has provided arguments for at least one type parameter, but also has at least one type parameter for which an argument must still be provided before it is a constructed type.

Expanding on the example in Listing 12.9 to create an open type Element with two type parameters T and K, the different possibilities for creating open constructed types are shown in Listing 12.10.

Listing 12.10. Inheriting from an Open Constructed Class


class Element<T, K> { }

class Element1<T> : Element<T, int> { }

class Element2<K> : Element<string, K> { }


If the open constructed type specifies constraints, the derived type is required to provide type arguments that meet those constraints. This can be done by specifying constraints itself. The constraints on the subclass can be the same constraints applied to the base class, or they can be a superset of those constraints. Listing 12.11 shows an example of inheriting from an open constructed class with constraints.

Listing 12.11. Inheriting from an Open Constructed Class with Constraints


class ConstrainedElement<T>
where T : IComparable<T>, new()

class ConstrainedElement1<T> : ConstrainedElement<T>

where T : IComparable<T>, new()


Finally, if a generic class implements an interface, all instances of that class can be cast to the interface.

Combining Generics and Arrays

In Hour 10, you learned that all single-dimensional arrays that have a lower bound of zero automatically implement IList<T>. As a result, you can create a generic method that iterates over the contents of an IList<T> that will work for any of the collection types (because they all implementIList<T>) and any single-dimensional array.

Listing 12.12 shows an example of such a generic method.

Listing 12.12. Printing the Items of a Collection or Array with a Generic Method


public static class Program
{
public static void PrintCollection<T>(IList<T> collection)
{
StringBuilder builder = new StringBuilder();
foreach(var item in collection)
{
builder.AppendFormat("{0} ", item);
}

Console.WriteLine(builder.ToString());
}

public static void Main()
{
int[] array = {0, 2, 4, 6, 8};
List<int> list = new List<int>() { 1, 3, 5, 7, 9 };
PrintCollection(array);
PrintCollection(list);

string[] array2 = { "hello", "world" };
List<string> list2 = new List<string>() { "now", "is", "the", "time" };
PrintCollection(array2);
PrintCollection(list2);
}
}


Variance in Generic Interfaces

Type variance refers to the ability to use a type other than the one originally specified. Covariance enables you to use a more-derived type than the one specified, whereas contravariance enables you to use a less-derived type. C# supports covariance for return types and contravariance for parameters.

The generic collections in C# are invariant, meaning you must use an exact match of the formal type specified. As a result, it is not possible to substitute a collection containing a more-derived type where a less-derived type is expected. For example, if you have a collection of cars, you can’t treat it as a collection of police cars because it might contain a car that is not a police car. Similarly, you can’t treat it as a collection of vehicles because you could put a truck (which is clearly not a car) into a collection of vehicles but not into a collection of cars.


Note: Classes Implementing Generic Variant Interfaces

Classes implementing generic variant interfaces are always invariant.


The real problem here is that the collections are mutable. If you could restrict the collection to a read-only subset of behavior, you can make it covariant, allowing a sequence of cars to be treated as a sequence of vehicles with no problem.

In C#, an interface is variant if its type parameters are declared covariant or contravariant. Covariance and contravariance apply only when there is a reference conversion between the two types. This means you can’t use variance with value types. You also can’t use variance with ref or outparameters.

Several of the generic collection interfaces, shown in Table 12.2, support variance.

Table 12.2. Generic Interfaces Supporting Variance

Image


Try It Yourself: Exploring Variance

By following these steps, you explore how variance works by modifying the code shown in Listing 12.12.

1. Create a new console application and modify the content of Program.cs to look as shown in Listing 12.12.

2. Run the application using Ctrl+F5, and observe that the output matches what is shown in Figure 12.3.

Image

Figure 12.3. Printing the contents of an array.

3. Change PrintCollection<T> so that it is no longer a generic method and that the type of collection is IEnumerable<object>.

4. You should immediately see the compiler errors shown in Figure 12.4.

Image

Figure 12.4. Compiler errors.

5. Correct the two errors by commenting out the invalid lines of code. Remember, variance doesn’t work for value types, which is the reason for the compiler errors.

6. Run the application again and observe that the output matches what is shown in Figure 12.5.

Image

Figure 12.5. Results of exploring variance.


Extending Variant Generic Interfaces

The compiler does not infer variance from the inherited interface, which requires that you explicitly specify if the derived interface supports variance, as shown in Listing 12.13.

Listing 12.13. Extending a Generic Variant Interface


interface ICovariant<out T>
{
}

interface IInvariant<T> : ICovariant<T>
{
}

interface IExtendedCovariant<out T> : ICovariant<T>
{
}


Even though the IInvariant<T> interface and the IExtendedCovariant<out T> interface both extend the same covariant interface, only IExtendedCovariant<out T> is also covariant. You can extend contravariant interfaces in the same manner.

You can also extend both a covariant and a contravariant interface in the same derived interface if it is invariant, as shown in Listing 12.14.

Listing 12.14. Extending Both a Covariant and Contravariant Interface


interface ICovariant<out T>
{
}

interface IContravariant<in T>
{
}

interface IInvariant<T> : IContravariant<T>, ICovariant<T>
{
}


However, you cannot extend a contravariant interface with a covariant interface, or the other way around, if the base interface is open, as shown in Listing 12.15.


Note: Creating Your Own Variant Generic Interfaces

Just as you can define your own generic interfaces, you can define your own variant generic interfaces using the in and out keywords with the generic type parameters. These keywords appear only in the interface declaration and are not part of the implementing code.

The out keyword declares a generic type parameter as covariant, whereas the in keyword declares a generic type parameter as contravariant. An interface can also support both covariance and contravariance for different type parameters.


Listing 12.15. Extending Both a Covariant and Contravariant Interface


// Generates a compiler error.
interface IInvalidVariance<in T> : ICovariant<T>
{
}


Working with Tuples

A tuple is commonly used to represent a set of data in a single data structure. Typically, tuples are used to:

• Represent a single set of data.

• Provide easy access and manipulation of a set of data.

• Return multiple values from a method.

• Pass multiple values to a method in a single parameter.

For example, to store a person’s name and date of birth together in a single data structure, you would use a 2-tuple (a tuple with two elements), as shown in the following code:

var t2 = Tuple.Create("Jim Morrison", new DateTime(1943, 12, 8));
Console.WriteLine("{0} was born on {1}.", t2.Item1, t2.Item2);

Although tuples are most frequently found in functional programming languages such as F#, Ruby, or Python, the .NET Framework provides several Tuple classes representing tuples containing from one to seven values. There is also an n-tuple class, where n is any value greater than or equal to eight. This n-tuple class is slightly different from the one-to-seven-tuple classes in that the eighth component of the tuple is another Tuple object that defines the rest of the remaining components.


Try It Yourself: Working with Tuples

To create a new Tuple<T1, T2> instance and access the values of that tuple, follow these steps:

1. Create a new console application.

2. Create a new method in the Program.cs file that returns a Tuple<int, string>:

public static Tuple<int, string> GenerateTuple()
{
return Tuple.Create(1, "hello, world");
}

3. In the Main method, enter the following code:

var t = GenerateTuple();
Console.WriteLine(t.GetType());
Console.WriteLine("{0}:{1}", t.Item1, t.Item2);

4. Run the application by pressing Ctrl+F5; you should see what is displayed in Figure 12.6.

Image

Figure 12.6. Results of working with tuples.


Summary

In this hour, you learned why generic programming is important and how it enables you to solve problems in a way that is reusable no matter what data type is used. You learned how type parameters work and how to constrain those type parameters to provide restrictions on the allowable types.

You learned how to create your own generic classes, interfaces, and methods, including the ability to create generic methods in nongeneric classes. Finally, you learned how to explicitly specify the variance of a type parameter for a generic interface.

Generic programming, both by creating your own generic types and using the existing generic collections, is a flexible and powerful concept that enables you to still have type safety while only having to write a single implementation.

Q&A

Q. What is the most common use of generics?

A. Generics are most commonly used in the collection classes and interfaces, although you can use them for your own classes as well.

Q. What problems do using generics prevent?

A. Using generics enables you to write a single implementation that is type safe and does not require boxing or unboxing operations.

Q. Are C# generics like Java generics or C++ templates?

A. Although the syntax is similar to both, the implementations are different. Java generics are a language-only construction, and the generic type information is not known at runtime. C++ templates are expanded at compile time, which generates additional code for each template type.

Q. What are type constraints?

A. Constraints enable you to apply restrictions on the types that can be used for the type arguments at compile time and guarantee to the compiler that any operator or method called on a type parameter will be supported.

Q. Can a nongeneric class contain a generic method?

A. Yes, a generic method can be defined within either a generic or nongeneric class.

Q. What is co- and contravariance for generic interfaces?

A. Variance is defined as the capability for two generic types to be made assignment-compatible based solely on the known assignment compatibility of their type arguments. Covariance enables interface methods to have more derived return types than originally specified by the type parameters. Contravariance enables interface methods to have argument types that are less derived than originally specified by the type parameters.

Workshop

Quiz

1. What is the correct way to test for value equality with generics?

2. What is a type parameter constraint?

3. What is a closed and open type?

Answers

1. The correct way to test for value equality using generics is to apply the where T : IComparable<T> and use the CompareTo method to perform value equality.

2. A type parameter constraint is when a generic parameter is used as the constraint for another generic parameter.

3. An open type is a generic type that has not been supplied any type arguments for its type parameters. A closed type is a generic type that is not open (that is, it has been supplied a type argument for all its type parameters).

Exercises

1. Replace the implementation of the QueryMetadata<T> method in the ExifMetadata struct of the PhotoViewer project with the following:

Nullable<T> result = new Nullable<T>();

if (metadata.ContainsQuery(query))
{
try
{
object queryResult = metadata.GetQuery(query);
if (queryResult.GetType() == typeof(T))
{
result = (T)queryResult;
}
else
{
try
{
result = (T)Convert.ChangeType(queryResult, typeof(T));
}
catch (InvalidCastException)
{
result = null;
}
catch (FormatException)
{
result = null;
}
catch (OverflowException)
{
result = null;
}
}
}
catch
{
result = null;
}
}

return result;