Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лаба №1 / books / csharp_ebook.pdf
Скачиваний:
51
Добавлен:
03.03.2016
Размер:
3.69 Mб
Скачать

Programmers Heaven: C# School

15. New Features In C# 2.0

C# evolves

Everything presented so far in this book should work with both the first and second versions of the C# language and the underlying .Net framework. This chapter looks at the major additions that were made to the second version of C#, known as C# 2.0.

Much care is needed when adding new features to a language, particularly with regard to what effect they will have on existing programs. Usually it is desirable to maintain backward compatibility with programs written in previous versions of the language. C# 2.0 is a superset of C# 1.0, meaning that programs written in C# 1.0 should still compile and run as expected with C# 2.0 and version 2.0 of the .Net runtime. The converse, however, is not true. Clearly C# 2.0 programs cannot be expected to compile with a C# 1.0 compiler, but you might expect that the compiled .Net bytecode might run on the first version of the CLR. This is not the case – additions have also been made at the runtime level, for good reasons that will be explored later. This means that if you write, compile and distribute C# 2.0 programs, your users will need to have the second version of the .Net runtime to run them.

The need for generics

The addition of generics is the most significant change in version 2.0 of the C# language. The concept behind generics is known as parametric polymorphism, which provides us with more of a clue as to what generics is about. We have already seen polymorphism in C#, way back in chapter 4. Polymorphism is about types, and the form of polymorphism we have seen so far in C# has been subtyping, achieved through inheritance. Here more specialized classes can be used anywhere that the parent class they inherit from can, and the compiler knows that this is safe since the child classes have the same behavior and representation as the parent classes.

Parametric polymorphism is just another way of introducing abstractions into your program. It aims to deal with a different problem to subtyping; the two complement each other rather than compete. Consider the following snippet of C# code.

//Create a list that we'll put values of factorial in. ArrayList factorial = new ArrayList();

//Compute first 10 factorials.

int fact = 1;

for (int i = 1; i <= 10; i++)

{

fact *= i; factorial.Add(fact);

}

322

Programmers Heaven: C# School

// Print them.

for (int i = 0; i < 10; i++)

{

int value = (int) factorial[i]; Console.WriteLine(value);

}

Here we are using an ArrayList collection to hold a list of integers – the first 10 values of the factorial function in this case. This code works as expected, so what’s the problem? The first issue is that we have to use a cast (to type int) when retrieving values from the collection. A cast implies a runtime check – this means that every time we retrieve a value we have to check it really is an integer. Aside from this runtime cost, we can see from looking at the program that the value will always be an integer and it would be preferable not to have to write the cast. The second issue is that, since a collection needs to be able to hold objects of any type, there is no way to optimize the cases where an integer (or any other non-reference type) is being stored. This means that boxing and unboxing must be performed when adding items to and fetching items from the collection. A third and more general issue not directly exhibited in this program is that it is possible to insert an object that isn’t an integer into this collection. This will result in the program failing at runtime.

All of these problems have one common cause – there is no way to state that the collection will only hold integers. If there was a way to state this, the need for the cast would disappear, it would become possible to avoid boxing and unboxing and the compiler can check that only integers are added to the collection and fail at compile time if any other object is added. Enter generics.

Generic collections

The snippet of code below is a re-write of the previous one, but this time generics have been used.

//Create a list that we'll put values of factorial in. List<int> factorial = new List<int>();

//Compute first 10 factorials.

int fact = 1;

for (int i = 1; i <= 10; i++)

{

fact *= i; factorial.Add(fact);

}

// Print them.

for (int i = 0; i < 10; i++)

{

323

Programmers Heaven: C# School

int value = factorial[i];

Console.WriteLine(value);

}

Only three changes have been made to the program. Two of them are on the second line.

List<int> factorial = new List<int>();

The first thing to note is that the type List rather than ListArray is being used. List is a new type in C# 2.0 and is located in the new System.Collections.Generic namespace. The bigger difference is that the new generics syntax is used here. After the type List, the type int has also been mentioned, placed in angle brackets. This is called a type parameter (thus the name parametric polymorphism), and it specifies the type of value that will be stored inside the collection.

The third change to the program is that the cast has now disappeared. The compiler now knows that the list can only hold integers, and therefore there is no need to do a check at runtime. Further to this, it is now possible for the runtime to optimize the collection, removing the need for boxing and unboxing, so there is a performance gain to be had here too.

Finally, consider what happens if they following line is added after the first loop.

factorial[2] = "Monkey!";

With the original program, this would compile. However, the program would crash at runtime.

1

2

System.InvalidCastException was unhandled

at CSharp2Examples.Program.Main(String[] args)

Using the generic List type means the compiler now has enough information to know that this line is invalid – only integers should be added to the collection. Trying to compile the program now results in an error message.

Cannot implicitly convert type 'string' to 'int'

Another example of a generic collection is the new Dictionary type. Imagine that you wanted to store a list of names of people (of type string) with their associated ages (of type int). A common solution would be to use a HashTable collection, with names as the keys and ages as the values.

HashTable ages = new HashTable();

324

Programmers Heaven: C# School

The Dictionary class is a drop-in replacement for the HashTable, but with the types of the keys and values parameterized. It is instantiated as follows.

Dictionary<string, int> ages = new Dictionary<string, int>();

This generic type takes two type parameters – one for the keys and the other for the values. When more than one type parameter is provided, they are separated by commas, just as parameters being passed to a method would be. In fact, as we’ll go on to see, the analogy to method parameters runs deeper when you implement generic types of your own.

Finally, it’s worth making clear that it is possible to have a list of lists of integers or similar. This is written as you might expect:

List<List<int>> nested = new List<List<int>>();

Author's Note: I’m guilty of mixing terminology somewhat: generics with parametric polymorphism and generic type with parametric type. You’re now either familiar with both sets of terminology or horribly confused. You’ll likely find most users of C# prefer to talk about generics. Parametric polymorphism is the more academic term, but I think it captures the concept more clearly, which is why I’ve used it here.

Creating generic types

It is possible to create your own generic types in C# as well as using the supplied ones. This is useful when you have a class and that provides the same functionality for more than one type or group of types. For example, imagine I want a class with one property that takes some default value or object in the constructor. If another value has been stored through property it will then hand that back. If not, it will hand back the default value. I want to use this for integers, floats and some object types.

One option I have is to implement it as follows, using the object type so that anything can be stored in the class.

class DefaultProxy

{

private object defaultObject; private object stored;

//Set the default object we'll return if nothing has been

//set.

public DefaultProxy(object o)

{

defaultObject = o;

}

325

Programmers Heaven: C# School

// Property for object o. public object target

{

get

{

return stored == null ? defaultObject : stored;

}

set

{

stored = value;

}

}

}

This will work, but has the same issue as the non-parameterized collections we saw earlier. Another option would be to write a specialized copy of the class for each type, for example IntegerDefaultProxy, FloatDefaultProxy etc. This is also a bad idea, since if a bug is found then every class needs to be updated.

Generics provide the solution. Turning the class above into a generic type involves two steps. The first is to parameterize the type – that is, state that the class takes a type parameter. This is done using the angle bracket syntax, and is analogous to writing the parameter list when defining a method.

class DefaultProxy<T>

We call “T” a type variable. Just like we can use a variable anywhere that we would use a value, we can use a type variable anywhere that we would use a type. Therefore, we can replace the use of the “object” type in the above code with the type variable, as shown below.

class DefaultProxy<T>

{

private T defaultObject; private T stored;

//Set the default object we'll return if nothing has been

//set.

public DefaultProxy(T o)

{

defaultObject = o;

}

// Property for object o. public T target

326

Соседние файлы в папке books