Defining Generics


You've now seen enough about generics to be ready to create your own. You've seen plenty of code involving generic types and have had plenty of practice using generic syntax. In this section, you'll look at defining:

  • Generic classes

  • Generic interfaces

  • Generic methods

  • Generic delegates

You'll also see some more advanced techniques for dealing with the issues that come up when defining generic types along the way, namely:

  • The default keyword

  • Constraining types

  • Inheriting from generic classes

  • Generic operators

Defining Generic Classes

To create a generic class, all you need to do is to 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.

A generic class can have any number of types in its definition, separated by commas, for example:

 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. For example:

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.

An important point to note is 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;       }    } }

Since you don't know what T1 is, you can't use any of its constructors — it might not even have any, or have no publicly accessible default constructor at any rate. Without more complicated code involving the techniques you'll see later in this section, you can pretty much 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 very much 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 which you won't be looking at in this chapter, you're limited pretty much to code as complicated as:

 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, since 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 earlier in this chapter exist.

Another limitation that you need to be aware of is that using the operators == and != are only permitted 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 above 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;    } }

The reason for this is that this code assumes that T1 supports the == operator.

What all this boils down to is that to do anything really 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 find out about types used to create generic class instances is whether they are reference or value types. Without knowing this you can't even assign null values with code such as:

 public MyGenericClass() { innerT1Object = null; } 

Should T1 be a value type then innerT1Object can't have the value null, so this code won't compile.

Luckily, this problem has been thought out and has resulted 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 some way to be able to do a little more with the types you are forced to use, but to get further you need to constrain the types that are supplied.

Constraining Types

The types you have used with generic classes up till now are known as unbounded types, since 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 of doing this. For example, it is possible to restrict a type to one that inherits from a certain type. To refer back to the Animal, Cow, and Chicken classes you used earlier, it would be possible to restrict a type to one that was or inherited from Animal, so this code would be fine:

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

whereas the following 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 by commas:

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

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

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

Any constraints must appear after the inheritance specifiers:

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

The available constraints are shown in the following table:

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.

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




Beginning Visual C# 2005
Beginning Visual C#supAND#174;/sup 2005
ISBN: B000N7ETVG
EAN: N/A
Year: 2005
Pages: 278

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net