Thư viện tri thức trực tuyến
Kho tài liệu với 50,000+ tài liệu học thuật
© 2023 Siêu thị PDF - Kho tài liệu học thuật hàng đầu Việt Nam

accelerated c# 2010 trey nash phần 7 potx
Nội dung xem thử
Mô tả chi tiết
CHAPTER 11 ■ GENERICS
329
public double TotalArea {
get {
double acc = 0;
foreach( T shape in shapes ) {
// DON'T DO THIS!!!
IShape theShape = (IShape) shape;
acc += theShape.Area;
}
return acc;
}
}
public void Add( T shape ) {
shapes.Add( shape );
}
private List<T> shapes = new List<T>();
}
This modification to Shapes<T> indeed does compile and work most of the time. However, this
generic has lost some of its innocence due to the type cast within the foreach loop. Just imagine that if
during a late-night caffeine-induced trance, you attempted to create a constructed type Shapes<int>.
The compiler would happily oblige. But what would happen if you tried to get the TotalArea property
from a Shapes<int> instance? As expected, you would be treated to a runtime exception as the TotalArea
property accessor attempted to cast an int into an IShape. One of the primary benefits of using generics
is better type safety, but in this example I tossed type safety right out the window. So, what are you
supposed to do? The answer lies in a concept called generic constraints. Check out the following correct
implementation:
public class Shapes<T>
where T: IShape
{
public double TotalArea {
get {
double acc = 0;
foreach( T shape in shapes ) {
acc += shape.Area;
}
return acc;
}
}
public void Add( T shape ) {
shapes.Add( shape );
}
private List<T> shapes = new List<T>();
}
Notice the extra line under the first line of the class declaration using the where keyword. This says,
“Define class Shapes<T> where T must implement IShape.” Now the compiler has everything it needs to
enforce type safety, and the JIT compiler has everything it needs to build working code at runtime. The
CHAPTER 11 ■ GENERICS
330
compiler has been given a hint to help it notify you, with a compile-time error, when you attempt to
create constructed types where T does not implement IShape.
The syntax for constraints is pretty simple. There can be one where clause for each type parameter.
Any number of constraints can be listed following the type parameter in the where clause. However, only
one constraint can name a class type (because the CLR has no concept of multiple inheritance), so that
constraint is known as the primary constraint. Additionally, instead of specifying a class name, the
primary constraint can list the special words class or struct, which are used to indicate that the type
parameter must be any class or any struct. The constraint clause can then include as many secondary
constraints as possible, such as a list of interfaces that the parameterized type must implement. Finally,
you can list a constructor constraint that takes the form new() at the end of the constraint list. This
constrains the parameterized type so it is required to have a default parameterless constructor. Class
types must have an explicitly defined default constructor to satisfy this constraint, whereas value types
have a system-generated default constructor.
It is customary to list each where clause on a separate line in any order under the class header. A
comma separates each constraint following the colon in the where clause. That said, let’s take a look at
some constraint examples:
using System.Collections.Generic;
public class MyValueList<T>
where T: struct
// But can't do the following
// where T: struct, new()
{
public void Add( T v ) {
imp.Add( v );
}
private List<T> imp = new List<T>();
}
public class EntryPoint
{
static void Main() {
MyValueList<int> intList =
new MyValueList<int>();
intList.Add( 123 );
// CAN'T DO THIS.
// MyValueList<object> objList =
// new MyValueList<object>();
}
}
In this code, you can see an example of the struct constraint in the declaration for a container that
can contain only value types. The constraint prevents one from declaring the objList variable that I have
commented out in this example because the result of attempting to compile it presents the following
error:
CHAPTER 11 ■ GENERICS
331
error CS0453: The type 'object' must be a non-nullable value type in order to use it as
parameter 'T' in the generic type or method 'MyValueList<T>'
Alternatively, the constraint could have also claimed to allow only class types. Incidentally, in the
Visual Studio version of the C# compiler, I can’t create a constraint that includes both class and struct.
Of course, doing so is pointless because the same effect comes from including neither struct nor class
in the constraints list. Nevertheless, the compiler complains with an error if you try to do so, claiming
the following:
error CS0449: The 'class' or 'struct' constraint must come before any
other constraints
This looks like the compiler error could be better stated by saying that only one primary constraint
is allowed in a constraint clause. You’ll also see that I commented out an alternate constraint line, in
which I attempted to include the new() constraint to force the type given for T to support a default
constructor. Clearly, for value types, this constraint is redundant and should be harmless to specify.
Even so, the compiler won’t allow you to provide the new() constraint together with the struct
constraint. Now let’s look at a slightly more complex example that shows two constraint clauses:
using System;
using System.Collections.Generic;
public interface IValue
{
// IValue methods.
}
public class MyDictionary<TKey, TValue>
where TKey: struct, IComparable<TKey>
where TValue: IValue, new()
{
public void Add( TKey key, TValue val ) {
imp.Add( key, val );
}
private Dictionary<TKey, TValue> imp
= new Dictionary<TKey, TValue>();
}
I declared MyDictionary<TKey, TValue> so that the key value is constrained to value types. I also
want those key values to be comparable, so I’ve required the TKey type to implement IComparable<TKey>.
This example shows two constraint clauses, one for each type parameter. In this case, I’m allowing the
TValue type to be either a struct or a class, but I do require that it support the defined IValue interface as
well as a default constructor.
Overall, the constraint mechanism built into C# generics is simple and straightforward. The
complexity of constraints is easy to manage and decipher with few if any surprises. As the language and
the CLR evolve, I suspect that this area will see some additions as more and more applications for
generics are explored. For example, the ability to use the class and struct constraints within a
constraint clause was a relatively late addition to the standard.
CHAPTER 11 ■ GENERICS
332
Finally, the format for constraints on generic interfaces is identical to that of generic classes and
structs.
Constraints on Nonclass Types
So far, I’ve discussed constraints within the context of classes, structs, and interfaces. In reality, any
entity that you can declare generically is capable of having an optional constraints clause. For generic
method and delegate declarations, the constraints clauses follow the formal parameter list to the
method or delegate. Using constraint clauses with method and delegate declarations does provide for
some odd-looking syntax, as shown in the following example:
using System;
public delegate R Operation<T1, T2, R>( T1 val1,
T2 val2 )
where T1: struct
where T2: struct
where R: struct;
public class EntryPoint
{
public static double Add( int val1, float val2 ) {
return val1 + val2;
}
static void Main() {
var op =
new Operation<int, float, double>( EntryPoint.Add );
Console.WriteLine( "{0} + {1} = {2}",
1, 3.2, op(1, 3.2f) );
}
}
I declared a generic delegate for an operator method that accepts two parameters and has a return
value. My constraint is that the parameters and the return value all must be value types. Similarly, for
generic methods, the constraints clauses follow the method declaration but precede the method body.
Co- and Contravariance
Variance is all about convertibility and being able to do what makes type-sense. For example, consider
the following code, which demonstrates array covariance that has been possible in C# since the 1.0 days:
using System;
static class EntryPoint
{
static void Main() {
string[] strings = new string[] {
"One",
CHAPTER 11 ■ GENERICS
333
"Two",
"Three"
};
DisplayStrings( strings );
// Array covariance rules allow the following
// assignment
object[] objects = strings;
// But what happens now?
objects[1] = new object();
DisplayStrings( strings );
}
static void DisplayStrings( string[] strings ) {
Console.WriteLine( "----- Printing strings -----" );
foreach( var s in strings ) {
Console.WriteLine( s );
}
}
}
At the beginning of the Main method, I create an array of strings and then immediately pass it to
DisplayStrings to print them to the console. Then, I assign a variable of type objects[] from the variable
strings. After all, because strings and objects are reference type variables, at first glance it makes
logical sense to be able to assign strings to objects because a string is implicitly convertible to an
object. However, notice right after doing so, I modify slot one and replace it with an object instance.
What happens when I call DisplayStrings the second time passing the strings array? As you might
expect, the runtime throws an exception of type ArrayTypeMismatchException shown as follows:
Unhandled Exception: System.ArrayTypeMismatchException: Attempted to access an
element as a type incompatible with the array.
Array covariance in C# has been in the language since the beginning for Java compatibility. But
because it is flawed, and some say broken, then how can we fix this problem? There are a few ways
indeed. Those of you familiar with functional programming will naturally suggest invariance as the
solution. That is, if an array is invariant similar to System.String, a copy is made typically in a lazy
fashion at the point where one is assigned into another variable. However, let’s see how we might fix this
problem using generics:
using System;
using System.Collections.Generic;
static class EntryPoint
{
static void Main() {
List<string> strings = new List<string> {
"One",
"Two",
"Three"
CHAPTER 11 ■ GENERICS
334
};
// THIS WILL NOT COMPILE!!!
List<object> objects = strings;
}
}
The spirit of the preceding code is identical to the array covariance example, but it will not compile.
If you attempt to compile this, you will get the following compiler error:
error CS0029: Cannot implicitly convert type
'System.Collections.Generic.List<string>' to
'System.Collections.Generic.List<object>'
The ultimate problem is that each constructed type is an individual type, and even though they
might originate from the same generic type, they have no implicit type relation between them. For
example, there is no implicit relationship between List<string> and List<object>, and just because
they both are constructed types of List<T> and string is implicitly convertible to object does not imply
that they are convertible from one to the other.
Don’t lose hope, though. There is a syntax added in C# 4.0 that allows you to achieve the desired
result. Using this new syntax, you can notate a generic interface or delegate indicating whether it
supports covariance or contravariance. Additionally, the new variance rules apply only to constructed
types in which reference types are passed for the type arguments to the generic type.
Covariance
Within strongly typed programming languages such as C#, an operation is covariant if it reflects and
preserves the ordering of types so they are ordered from more specific types to more generic types. To
illustrate, I’ll borrow from the example in the previous section to show how array assignment rules in C#
are covariant:
string s = "Hello";
object o = s;
string[] strings = new string[3];
object[] objects = strings;
The first two lines make perfect sense; after all, variables of type string are implicitly convertible to
type object because string derives from object. The second set of lines shows that variables of type
string[] are implicitly convertible to variables of type object[]. And because the ordering of types
between the two implicit assignments is identical that is, from a more specialized type (string) to a
more generic type (object) the array assignment operation is said to be covariant.
Now, to translate this concept to generic interface assignment, an interface of type IOperation<T> is
covariance-convertible to IOperation<R> if there exists an implicit reference conversion from T to R and
IOperation<T> to IOperation<R>. Simply put, if for the two conversion operations just mentioned, T and R