Home Random Page


CATEGORIES:

BiologyChemistryConstructionCultureEcologyEconomyElectronicsFinanceGeographyHistoryInformaticsLawMathematicsMechanicsMedicineOtherPedagogyPhilosophyPhysicsPolicyPsychologySociologySportTourism






Nbsp;   Verifiability and Constraints

When compiling generic code, the C# compiler analyzes it and ensures that the code will work for any

type that exists today or that may be defined in the future. Let’s look at the following method.

 

private static Boolean MethodTakingAnyType<T>(T o) { T temp = o;

Console.WriteLine(o.ToString()); Boolean b = temp.Equals(o); return b;

}

 

This method declares a temporary variable (temp) of type T, and then the method performs a couple of variable assignments and a few method calls. This method works for any type. If T is a refer- ence type, it works. If T is a value or enumeration type, it works. If T is an interface or delegate type,

it works. This method works for all types that exist today or that will be defined tomorrow because


every type supports assignment and calls to methods defined by Object (such as ToString and

Equals).

 

Now look at the following method.

 

private static T Min<T>(T o1, T o2) { if (o1.CompareTo(o2) < 0) return o1; return o2;

}

 

The Min method attempts to use the o1 variable to call the CompareTo method. But there are lots of types that do not offer a CompareTo method, and therefore, the C# compiler can’t compile this code and guarantee that this method would work for all types. If you attempt to compile the above code, the compiler issues the following message: error CS1061: 'T' does not contain a definition for 'CompareTo' accepting a first argument of type 'T' could be found (are you missing a using directive or an assembly reference?)

 

So it would seem that when using generics, you can declare variables of a generic type, perform some variable assignments, call methods defined by Object, and that’s about it! This makes generics practically useless. Fortunately, compilers and the CLR support a mechanism called constraints that you can take advantage of to make generics useful again.

A constraint is a way to limit the number of types that can be specified for a generic argument. Limiting the number of types allows you to do more with those types. Here is a new version of the Min method that specifies a constraint (in bold).

 

public static T Min<T>(T o1, T o2) where T : IComparable<T>{ if (o1.CompareTo(o2) < 0) return o1;

return o2;

}

 

The C# where token tells the compiler that any type specified for T must implement the generic IComparable interface of the same type (T). Because of this constraint, the compiler now allows the method to call the CompareTo method because this method is defined by the IComparable<T> interface.

Now, when code references a generic type or method, the compiler is responsible for ensuring that a type argument that meets the constraints is specified. For example, the following code causes the compiler to issue the following message: error CS0311: The type 'object' cannot be used as type parameter 'T' in the generic type or method 'SomeType.Min<T>(T, T)'. There is no implicit reference conversion from 'object' to 'System.



IComparable<object>'.

 

private static void CallMin() {

Object o1 = "Jeff", o2 = "Richter";

Object oMin = Min<Object>(o1, o2); // Error CS0311

}

 

The compiler issues the error because System.Object doesn’t implement the IComparable­

<Object> interface. In fact, System.Object doesn’t implement any interfaces at all.


Now that you have a sense of what constraints are and how they work, we’ll start to look a little deeper into them. Constraints can be applied to a generic type’s type parameters as well as to a generic method’s type parameters (as shown in the Min method). The CLR doesn’t allow overloading based on type parameter names or constraints; you can overload types or methods based only on arity. The following examples show what I mean.

 

// It is OK to define the following types: internal sealed class AType {}

internal sealed class AType<T> {} internal sealed class AType<T1, T2> {}

 

// Error: conflicts with AType<T> that has no constraints internal sealed class AType<T> where T : IComparable<T> {}

 

// Error: conflicts with AType<T1, T2> internal sealed class AType<T3, T4> {}

 

internal sealed class AnotherType {

// It is OK to define the following methods: private static void M() {}

private static void M<T>() {} private static void M<T1, T2>() {}

 

// Error: conflicts with M<T> that has no constraints private static void M<T>() where T : IComparable<T> {}

 

// Error: conflicts with M<T1, T2> private static void M<T3, T4>() {}

}

 

When overriding a virtual generic method, the overriding method must specify the same num- ber of type parameters, and these type parameters will inherit the constraints specified on them by the base class’s method. In fact, the overriding method is not allowed to specify any constraints on its type parameters at all. However, it can change the names of the type parameters. Similarly, when

implementing an interface method, the method must specify the same number of type parameters as the interface method, and these type parameters will inherit the constraints specified on them by the interface’s method. Here is an example that demonstrates this rule by using virtual methods.

 

internal class Base {

public virtual void M<T1, T2>() where T1 : struct

where T2 : class {

}

}

 

internal sealed class Derived : Base { public override void M<T3, T4>()

where T3 : EventArgs // Error where T4 : class // Error

{ }

}


Attempting to compile the preceding code causes the compiler to issue the following message: error CS0460: Constraints for override and explicit interface implementa­ tion methods are inherited from the base method, so they cannot be specified directly. If we remove the two where lines from the Derived class’s M<T3, T4> method, the code will compile just fine. Notice that you can change the names of the type parameters (as in the example: from T1 to T3 and T2 to T4); however, you cannot change (or even specify) constraints.

 

Now let’s talk about the different kinds of constraints the compiler/CLR allows you to apply to a type parameter. A type parameter can be constrained by using a primary constraint, a secondary con- straint, and/or a constructor constraint. I’ll talk about these three kinds of constraints in the next three sections.

 

Primary Constraints

A type parameter can specify zero primary constraints or one primary constraint. A primary constraint can be a reference type that identifies a class that is not sealed. You cannot specify one of the following special reference types: System.Object, System.Array, System.Delegate, System.MulticastDelegate, System.ValueType, System.Enum, or System.Void.

When specifying a reference type constraint, you are promising the compiler that a specified type argument will either be of the same type or of a type derived from the constraint type. For example, see the following generic class.

 

internal sealed class PrimaryConstraintOfStream<T> where T : Stream { public void M(T stream) {

stream.Close();// OK

}

}

 

In this class definition, the type parameter T has a primary constraint of Stream (defined in the System.IO namespace). This tells the compiler that code using PrimaryConstraintOfStream must specify a type argument of Stream or a type derived from Stream (such as FileStream). If a type parameter doesn’t specify a primary constraint, System.Object is assumed. However, the

C# compiler issues an error message (error CS0702: Constraint cannot be special class 'object') if you explicitly specify System.Object in your source code.

There are two special primary constraints: class and struct. The class constraint promises the compiler that a specified type argument will be a reference type. Any class type, interface type, del- egate type, or array type satisfies this constraint. For example, see the following generic class.

 

internal sealed class PrimaryConstraintOfClass<T> where T : class { public void M() {

T temp = null;// Allowed because T must be a reference type

}

}


In this example, setting temp to null is legal because T is known to be a reference type, and all reference type variables can be set to null. If T were unconstrained, the preceding code would not compile because T could be a value type, and value type variables cannot be set to null.

The struct constraint promises the compiler that a specified type argument will be a value type. Any value type, including enumerations, satisfies this constraint. However, the compiler and the CLR treat any System.Nullable<T> value type as a special type, and nullable types do not satisfy this constraint. The reason is because the Nullable<T> type constrains its type parameter to struct, and the CLR wants to prohibit a recursive type such as Nullable<Nullable<T>>. Nullable types are discussed in Chapter 19, “Nullable Value Types.”

Here is an example class that constrains its type parameter by using the struct constraint.

 

internal sealed class PrimaryConstraintOfStruct<T> where T : struct { public static T Factory() {

// Allowed because all value types implicitly

// have a public, parameterless constructor return new T();

}

}

 

In this example, newing up a T is legal because T is known to be a value type, and all value types implicitly have a public, parameterless constructor. If T were unconstrained, constrained to a reference type, or constrained to class, the above code would not compile because some reference types do not have public, parameterless constructors.

 

Secondary Constraints

A type parameter can specify zero or more secondary constraints where a secondary constraint rep- resents an interface type. When specifying an interface type constraint, you are promising the com- piler that a specified type argument will be a type that implements the interface. And because you can specify multiple interface constraints, the type argument must specify a type that implements all of the interface constraints (and all of the primary constraints too, if specified). Chapter 13 discusses interface constraints in detail.

There is another kind of secondary constraint called a type parameter constraint (sometimes re- ferred to as a naked type constraint). This kind of constraint is used much less often than an interface constraint. It allows a generic type or method to indicate that there must be a relationship between specified type arguments. A type parameter can have zero or more type constraints applied to it.

Here is a generic method that demonstrates the use of a type parameter constraint.

 

private static List<TBase> ConvertIList<T, TBase>(IList<T> list) where T : TBase {

List<TBase> baseList = new List<TBase>(list.Count); for (Int32 index = 0; index < list.Count; index++) {

baseList.Add(list[index]);

}

return baseList;

}


The ConvertIList method specifies two type parameters in which the T parameter is constrained by the TBase type parameter. This means that whatever type argument is specified for T, the type argument must be compatible with whatever type argument is specified for TBase. Here is a method showing some legal and illegal calls to ConvertIList.

 

private static void CallingConvertIList() {

// Construct and initialize a List<String> (which implements IList<String>) IList<String> ls = new List<String>();

ls.Add("A String");

 

// Convert the IList<String> to an IList<Object> IList<Object> lo = ConvertIList<String, Object>(ls);

 

// Convert the IList<String> to an IList<IComparable> IList<IComparable> lc = ConvertIList<String, IComparable>(ls);

 

// Convert the IList<String> to an IList<IComparable<String>> IList<IComparable<String>> lcs =

ConvertIList<String, IComparable<String>>(ls);

 

// Convert the IList<String> to an IList<String> IList<String> ls2 = ConvertIList<String, String>(ls);

 

// Convert the IList<String> to an IList<Exception> IList<Exception> le = ConvertIList<String, Exception>(ls);// Error

}

 

In the first call to ConvertIList, the compiler ensures that String is compatible with Object.

Because String is derived from Object, the first call adheres to the type parameter constraint. In the second call to ConvertIList, the compiler ensures that String is compatible with IComparable. Be- cause String implements the IComparable interface, the second call adheres to the type parameter constraint. In the third call to ConvertIList, the compiler ensures that String is compatible with IComparable<String>. Because String implements the IComparable<String> interface, the third call adheres to the type parameter constraint. In the fourth call to ConvertIList, the compiler knows that String is compatible with itself. In the fifth call to ConvertIList, the compiler ensures that String is compatible with Exception. Because String is not compatible with Exception, the fifth call doesn’t adhere to the type parameter constraint, and the compiler issues the following message: error CS0311: The type 'string' cannot be used as type parameter 'T' in the generic type or method Program.ConvertIList<T,TBase>(System.Collections.Ge­ neric.IList<T>)'. There is no implicit reference conversion from 'string' to 'System.Exception'.

 

 

Constructor Constraints

A type parameter can specify zero constructor constraints or one constructor constraint. When speci- fying a constructor constraint, you are promising the compiler that a specified type argument will be a non-abstract type that implements a public, parameterless constructor. Note that the C# compiler considers it an error to specify a constructor constraint with the struct constraint because it is


redundant; all value types implicitly offer a public, parameterless constructor. Here is an example class that constrains its type parameter by using the constructor constraint.

 

internal sealed class ConstructorConstraint<T> where T : new() { public static T Factory() {

// Allowed because all value types implicitly

// have a public, parameterless constructor and because

// the constraint requires that any specified reference

// type also have a public, parameterless constructor return new T();

}

}

 

In this example, newing up a T is legal because T is known to be a type that has a public, parame- terless constructor. This is certainly true of all value types, and the constructor constraint requires that it be true of any reference type specified as a type argument.

Sometimes developers would like to declare a type parameter by using a constructor constraint whereby the constructor takes various parameters itself. As of now, the CLR (and therefore the C# compiler) supports only parameterless constructors. Microsoft feels that this will be good enough for almost all scenarios, and I agree.

 

Other Verifiability Issues

In the remainder of this section, I’d like to point out a few other code constructs that have unexpected behavior when used with generics due to verifiability issues and how constraints can be used to make the code verifiable again.

 

Casting a Generic Type Variable

Casting a generic type variable to another type is illegal unless you are casting to a type compatible with a constraint.

 

private static void CastingAGenericTypeVariable1<T>(T obj) { Int32 x = (Int32) obj; // Error

String s = (String) obj; // Error

}

 

The compiler issues an error on both lines above because T could be any type, and there is no guarantee that the casts will succeed. You can modify this code to get it to compile by casting to Object first.

 

private static void CastingAGenericTypeVariable2<T>(T obj) { Int32 x = (Int32) (Object) obj; // No error

String s = (String) (Object) obj; // No error

}

 

Although this code will now compile, it is still possible for the CLR to throw an InvalidCast­ Exception at run time.


If you are trying to cast to a reference type, you can also use the C# as operator. Here is code

modified to use the as operator with String (because Int32 is a value type).

 

private static void CastingAGenericTypeVariable3<T>(T obj) { String s = obj as String; // No error

}

 

 

Setting a Generic Type Variable to a Default Value

Setting a generic type variable to null is illegal unless the generic type is constrained to a ref- erence type.

 

private static void SettingAGenericTypeVariableToNull<T>() {

T temp = null; // CS0403 – Cannot convert null to type parameter 'T' because it could

// be a non­nullable value type. Consider using 'default(T)' instead

}

 

Because T is unconstrained, it could be a value type, and setting a variable of a value type to null is not possible. If T were constrained to a reference type, setting temp to null would compile and run just fine.

Microsoft’s C# team felt that it would be useful to give developers the ability to set a variable to a default value. So the C# compiler allows you to use the default keyword to accomplish this.

 

private static void SettingAGenericTypeVariableToDefaultValue<T>() { T temp = default(T); // OK

}

 

The use of the default keyword above tells the C# compiler and the CLR’s JIT compiler to produce code to set temp to null if T is a reference type and to set temp to all-bits-zero if T is a value type.

 

Comparing a Generic Type Variable with null

Comparing a generic type variable to null by using the == or != operator is legal regardless of whether the generic type is constrained.

 

private static void ComparingAGenericTypeVariableWithNull<T>(T obj) { if (obj == null) { /* Never executes for a value type */ }

}

 

Because T is unconstrained, it could be a reference type or a value type. If T is a value type, obj can never be null. Normally, you’d expect the C# compiler to issue an error because of this. However, the C# compiler does not issue an error; instead, it compiles the code just fine. When this method is called using a type argument that is a value type, the JIT compiler sees that the if statement can nev- er be true, and the JIT compiler will not emit the native code for the if test or the code in the braces. If I had used the != operator, the JIT compiler would not emit the code for the if test (because it is always true), and it will emit the code inside the if’s braces.


By the way, if T had been constrained to a struct, the C# compiler would issue an error because you shouldn’t be writing code that compares a value type variable with null because the result is always the same.

 

Comparing Two Generic Type Variables with Each Other

Comparing two variables of the same generic type is illegal if the generic type parameter is not known to be a reference type.

 

private static void ComparingTwoGenericTypeVariables<T>(T o1, T o2) { if (o1 == o2) { } // Error

}

 

In this example, T is unconstrained, and whereas it is legal to compare two reference type vari- ables with one another, it is not legal to compare two value type variables with one another unless the value type overloads the == operator. If T were constrained to class, this code would compile, and the == operator would return true if the variables referred to the same object, checking for exact identity. Note that if T were constrained to a reference type that overloaded the operator == method, the compiler would emit calls to this method when it sees the == operator. Obviously, this whole discussion applies to uses of the != operator too.

When you write code to compare the primitive value types—Byte, Int32, Single, Decimal, etc.—the C# compiler knows how to emit the right code. However, for non-primitive value types, the C# compiler doesn’t know how to emit the code to do comparisons. So if ComparingTwoGeneric­ TypeVariables method’s T were constrained to struct, the compiler would issue an error. And you’re not allowed to constrain a type parameter to a specific value type because it is implicitly sealed, and therefore no types exist that are derived from the value type. Allowing this would make the generic method constrained to a specific type, and the C# compiler doesn’t allow this because it is more efficient to just make a non-generic method.

 

Using Generic Type Variables as Operands

Finally, it should be noted that there are a lot of issues about using operators with generic type operands. In Chapter 5, I talked about C# and how it handles its primitive types: Byte, Int16, Int32, Int64, Decimal, and so on. In particular, I mentioned that C# knows how to interpret operators (such as +, ­, *, and /) when applied to the primitive types. Well, these operators can’t be applied to variables of a generic type because the compiler doesn’t know the type at compile time. This means that you can’t use any of these operators with variables of a generic type. So it is impossible to write

a mathematical algorithm that works on an arbitrary numeric data type. Here is an example of a generic method that I’d like to write.

 

private static T Sum<T>(T num) where T : struct { T sum = default(T) ;

for (T n = default(T) ; n < num ; n++) sum += n;

return sum;

}


I’ve done everything possible to try to get this method to compile. I’ve constrained T to struct, and I’m using default(T) to initialize sum and n to 0. But when I compile this code, I get the follow- ing three errors.

■ error CS0019: Operator '<' cannot be applied to operands of type 'T' and 'T'

■ error CS0023: Operator '++' cannot be applied to operand of type 'T'

 

■ error CS0019: Operator '+=' cannot be applied to operands of type 'T' and 'T'

This is a severe limitation on the CLR’s generic support, and many developers (especially in the sci- entific, financial, and mathematical world) are very disappointed by this limitation. Many people have tried to come up with techniques to work around this limitation by using reflection (see Chapter 23, “Assembly Loading and Reflection”), the dynamic primitive type (see Chapter 5), operator overload- ing, and so on. But all of these cause a severe performance penalty or hurt readability of the code substantially. Hopefully, this is an area that Microsoft will address in a future version of the CLR and the compilers.


 


C HA P T E R 1 3

Interfaces

In this chapter:

Class and Interface Inheritance............................................................... 296

Defining an Interface............................................................................... 296

Inheriting an Interface............................................................................ 298

More About Calling Interface Methods.................................................. 300


Date: 2016-03-03; view: 636


<== previous page | next page ==>
Nbsp;   Generic Methods | Nbsp;   Defining an Interface
doclecture.net - lectures - 2014-2024 year. Copyright infringement or personal data (0.015 sec.)