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

Programmers Heaven: C# School

{

get

{

return stored == null ? defaultObject : stored;

}

set

{

stored = value;

}

}

}

And that’s it – we’ve just created a generic type. It is instantiated by supplying a type to use for type variable “T”; in the examples below we create two instances of this generic type, one parameterized with int and the other with string.

DefaultProxy<int> proxy1 = new DefaultProxy<int>();

DefaultProxy<string> proxy2 = new DefaultProxy<string>();

It may help you to think of the word “int” being textually substituted wherever a “T” is found in the DefaultProxy class, though be aware that under the hood this is not what really happens.

Constraining type parameters

The generic types that we’ve seen so far allow themselves to be parameterized with any type. However, this constrains us to performing operations that only apply to all types. Consider the following program.

class MyGenericType<T>

{

private T item;

public void PerformSomeOperation()

{

foreach(object o in item)

{

// do something

}

}

}

Since not every type is enumerable (for example, an int is not), attempting to compile this class will result in an error:

327

Programmers Heaven: C# School

foreach statement cannot operate on variables of type 'T' because 'T' does not

contain a public definition for 'GetEnumerator'

To get around this, we constrain the type variable “T” such that it must always implement the IEnumerable interface. This is done using the new “where” keyword along with the “:” syntax, as used for specifying which class to inherit from and what interfaces to implement.

class MyGenericType<T> where T : IEnumerable

The class will now compile, and parameterizing it with any type that implements the IEnumerable interface will succeed.

MyGenericType<List<int>> chimp = new MyGenericType<List<int>>();

However, supplying a type for the type parameter that does not implement IEnumerable, such as “int”, will give a compile time error.

The type 'int' must be convertible to 'System.Collections.IEnumerable' in order

to use it as parameter 'T' in the generic type or method

'CSharp2Examples.MyGenericType<T>'

Note that it is possible to constrain a type variable such that it must implement more than one interface or inherit from a certain parent class and implement one or more interfaces. To do this, just separate them by commas.

class MyGenericType<T> where T : IEnumerable, IComparable

To supply constraints for a second type variable, the “where” keyword must be placed again at the start of that constraint. Laying them out as I have here is not required, but perhaps makes the constraints clearer to read.

class MyGenericType<T, U> where T : IEnumerable, IComparable

where U : ICollection

It is also possible to constrain a parameter to only accept reference or value types using the class and struct keywords. Note that these constraints must appear before any interface or inheritance constraints for a given type variable.

class MyGenericType<T, U> where T : struct

where U : class

If when implementing a generic type you wish to instantiate a class whose type is given by a type variable, then you must be certain that it has a constructor. The new() constraint can be used to specify that any type that the

328

Programmers Heaven: C# School

generic type is parameterized with must provide the default parameterless constructor. Note that for a given type variable this constraint must come after any others, and that it is invalid to use it with the struct constraint.

class MyGenericType<T> where T : new()

Final thoughts on generics

If you’re a C++ programmer then you’ve probably read through this thinking “I’ve seen this all before, it’s just templates again”. C++ templates also work to facilitate parametric polymorphism, however they are compiled away. That is, templates do not exist at runtime – the compiler uses the template to produce classes as needed, one for each type the template is parameterized with. It’s somewhat like an extension of C’s macros – it really is textual substitution.

Java has also gained generics, with syntax somewhat similar to that seen in C#, but as with C++ templates generics are compiled away. This was done so that the runtime (the JVM) did not have to be modified to support generics. However, this introduces two problems. The first is that reflection (looking at types at runtime) does not recognize type parameters. The second is that you lose the performance benefit, since the generics implementation in the compiler actually just replaces every type variable with Object and then inserts casts as required. Therefore parameterizing on a primitive (non-reference) type leads to boxing and unboxing.

In .Net, the runtime has been modified to be aware of generics. While this breaks backward compatibility with previous versions of the runtime, it means that generics exist when you do reflection and that the optimizations you’d hope for with regard to eliminating boxing and unboxing are realized.

Author's Note: I was fortunate enough to be at a talk by Andrew Kennedy (from Microsoft Research) who worked on the .Net generics implementation. If you’re interested in how generics are implemented in the .Net runtime, I greatly recommend reading his slides and/or papers. You can find his website at http://research.microsoft.com/~akenn/.

Partial types

When I first heard the term partial type I thought it was going to be something exciting. After all, parametric types are pretty cool. It turns out that they are actually quite boring by comparison, but thankfully trivial to understand. The use of the word “type” here is referring to types that you define yourself – that is, classes and structs (but not enumerations). The “partial” bit simply means that you can choose to define only part of the class at a time.

Essentially, you’re saying to the compiler “here is part of the definition of this class – some of the fields and some of the methods – but there may be other parts elsewhere”.

329

Programmers Heaven: C# School

partial class A {

 

 

int x;

 

class A : B {

void y() { … }

 

 

int x;

}

 

 

void y() { … }

 

Compiler

 

int z;

 

 

partial class A : B {

 

int a() { … }

 

}

int z;

 

 

 

int a() { … }

 

 

}

 

 

The win from partial types is that a class definition can now be split over multiple files. The most common use of this is in Visual Studio or other GUI designers, where part of the class for a window is generated and maintained automatically. With partial types the human generated and machine generated code can go in separate files.

Another use case is where two programmers want to work on different methods of the same class at the same time. If two aspects of what the class does can be cleanly pulled apart, then each aspect can be put into a partial class across separate files. It may also be useful for when a class gets large and there is a sensible way to split it up, but you should probably be asking whether one class is doing too much if you’re reaching that stage.

The syntax is simple – to specify that other parts of a class may be defined elsewhere just add the new “partial” modifier. For example, imagine we had a class that defined an attendee at a party. We may wish to separate out the different types of things a partygoer can do into separate files. For example, in the file “Drinking.cs” we might have:

partial class PartyGoer

{

private int drinksHad; private bool drunk;

public void HaveADrink(string drinkName)

{

// ...

}

}

Whereas the file “Relationship.cs” might perhaps contain:

partial class PartyGoer

{

330

Programmers Heaven: C# School

public string[] FindAttractivePeople()

{

// ...

}

public void ChatUp(string name)

{

// ...

}

}

Note that the private fields defined in one fragment of the partial class are visible in other parts too, since really they are the same class. Therefore in “Relationship.cs” it is perfectly valid to write a method that accesses a private field from the fragment of the class defined in “Drinking.cs”.

public void AskToMarry(string name)

{

if (drunk)

{

// Ask!

}

else

{

throw new ThatIsAReallyStupidIdeaException();

}

}

Partial classes are compiled away – they do not exist at runtime. The compiler collects all of the fragments of the class together and compiles it as if it had all been specified together. For this reason, all fragments of a class must be in the same assembly. Note that it is not required to specify all interfaces that are implemented by the class every time that a part of it is defined – the compiler will take the union of all those mentioned across the partial classes. Therefore, if you try to inherit from different classes in different parts of the class, you’ll get a compile time error. Always remember that it really is just one class with its definition split up.

Partial classes have the potential to make a program more understandable by more clearly splitting up functionality. However, there is a risk that they will actually make debugging and understanding programs harder because you can no longer see right away from the code what a class inherits from and what interfaces it implements – you have to find all of the fragments of the class to be sure. All of the fields and methods are not in one place either, which means more searching through multiple files.

I’m certainly not suggesting that partial classes should not be used, but I would encourage carefully considering whether their use is going to make life easier or harder for anyone who has to maintain the code after you.

331

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