The Prism Primer - Part 9
From The Oxygene Language Wiki
This is a General topic
Feel free to add your notes to this topic below.
The Prism Primer: Part 1 – Part 2 – Part 3 – Part 4 – Part 5 – Part 6 – Part 7 – Part 8 – Part 9
Generics
Currently we use the listbox for holding our data. That is not a good approach. The GUI's only purpose should be to display the data. Holding the data should be the task of other parts of your program, the most simple way would be an array, for big applications this will be a database.
We want to use a generic list for this task. Generic lists are in most cases the "modern" replacement for arrays. The problem with arrays is, that they have a fixed size. So, if you don't know how many items you want to store, you'll not be able to use an array efficiently. You would've to backup the items in the array, re-create it with the new size and then copy the saved items into it.
One can easily write a class that holds data, for example a linked list. A list item (also called "node") for a double-linked list could look like this:
IntegerListItem = public class property Value : Integer; property NextItem : IntegerListItem; property PrevItem : IntegerListItem; end;
The problem is, that with this node, you can only store integers. For storing singles, you would've to declare a new class. And another one for strings. That's not very efficient ;-)
Generic Classes
The solution is a generic class. A generic version of this class looks like this:
ListItem<T> = public class property Value : T; property NextItem : ListItem<T>; property PrevItem : ListItem<T>; end;
ListItem<T> is now a generic type (also called open type) with the type parameter T. T is some kind of placeholder for an arbitrary type used in ListItem<T>. You can see this on the way, Value is now declared: It has the type T instead of Integer. And T is also used in NextItem and PrevItem, which are of the generic type (open type), too.
If you want to use an instance of a generic class, you have to specifiy the generic parameters in the constructor:
var aStringListItem := new ListItem<String>;
ListItem<String> is a constructed type, where String is the type argument used to construct it.
By the way: You distinguish between "open" constructed types and "closed" constructed types. The types of NextItem and PrevItem are open constructed types, because their type argument is the generic parameter of the class they are declared in. The type argument isn't "fixed". ListItem<String> is a closed constructed type, because the type argument is an actual type. It will always have the type argument String.
When you now use aStringListItem, its Value property is of type String. You can work with it as if it was declared as String in the first place. Even IntelliSense will behave like this.
Generic Constraints
If you wanted to create a sorted list, you would need a possibility to compare two nodes and determine, which one is the bigger one. This breaks down to comparing their Value properties. But how can you do this, if you don't know the type of these properties? What, if they are of a type, that can't be compared?
We just have to make sure, that they can be. And that is done by using generic constraints. Whatever type is passed as type argument, it has to be comparable. Or speaking .NET: It has to implement the IComparable<T> interface (yes, interfaces can be generic, too)! That's antoher point where interfaces come in very handy.
We can extend the ListItem<T> class by such a constraint:
ListItem<T> = public class(IComparable<T>) where T is IComparable<T>; public property Value : T; property Next : ListItem<T>; property Prev : ListItem<T>; method CompareTo(other: T): Int32; end; implementation method ListItem<T>.CompareTo(other: T): Int32; begin result := self.Value.CompareTo(other); end;
The constraint is the second line. A constraint begins with the where keyword and is followed by the condition the type argument has to match. That's the same we did already with the interfaces in our list of geometrical objects. Note that the constraint for T can be an interface that has T as type argument. Then the interface is of course an open constructed type.
Now we know, that T implements the IComparable<T>-interface, therefore we can do the same thing in the ListItem<T>-class. This interface only has the CompareTo-method. Note, that the method signature also contains the generic parameter T in the parameter list. This ListItem<T> is not compared to another ListItem<T> but to a T!
In the method body, we just call the CompareTo method of the Value. We now from the constraint, that it has this method. And: IntelliSense does know this, too! It will show this method for Value. :-)
There're more types of constraints. For example you can demand, that a type argument is a class and has a (parameter-less) constructor: where T is class, T has constructor (you see here, that you can concatenate two constraints by separating them with a comma). You can also demand, that T is record.
Generic Methods
Not only classes but methods, too, can be generic. For that they don't have to be members of a generic class. You could add a method like this to our non-generic MainForm-class:
public method EnsureOrder<T>(var a, b : T); where T is IComparable<T>; (* .. *) implementation (* .. *) method MainForm.EnsureOrder<T>(var a, b : T); begin if a.CompareTo(b) > 0 then begin var temp := a; a := b; b := temp; end; end; (* .. *) var foo := 10; var bar := 5; EnsureOrder(var foo, var bar);
As you can see, generic constraints work with generic methods, too. Using that the method will swap the parameters, if a > b.
When calling a generic method, the compiler often can infer the type argument. In this case, it is obvious, that T is Integer, because we pass two integers as parameter. If you want explicitely define the type argument, the call looks like this:
EnsureOrder<Integer>(var foo, var bar);
Returning "Generic" Values
Even the return type of a method can be generic:
method GetSomeValue<T> : T; where T has constructor;
When calling that method, you must give the type argument, because the compiler won't be able to infer it (because it has no parameters of type T). If you assign the method result at a previously declared variable, the compiler will not look at its type to infer the type argument of the method!
If you want to return an instance of type T, you can just write
result := new T;
because T has the constraint, that it has to have a constructor.
Also it is possible to retrieve the default value of a type:
result := default(T);
You don't need any constraint for that. If T is a reference type, the default value is nil. For value types, it's their initial value (e.g. zero for Integers, false for Booleans).
Frequently Used: List<T>
Let's get back to our list of geometrical objects. You don't have to write a generic list for yourself, the .NET framework has some of them already built in. We'll use the probably most used one: List<T>.
You can add and insert items in this list, and you can remove arbitrary items from this list (in other words: they don't have to be at the end or the beginning of the list). You can access every item in the list like you do it with arrays:
var myList := new List<Integer>; myList.Add(42); myList.Insert(0, 23); MessageBox.Show(myList[1].ToString);
This will show "42", because first 42 was added to the list and then 23 was inserted at the first position ( = index zero), so the 42 was "pushed" one index up.
Note, that after the first line, T is fixed for the complete lifetime of myList. Every method, every property, every field will now use Integer as type argument!
Of course we need a list for geometrical objects, not for integers. Please add a private field to the form (right below fBitmap):
fObjects : List<MyGeomObject> := new List<MyGeomObject>;
This line first declares the field as List<MyGeomObject> and then initializes the field by creating an instance of the list. That is equivalent to assining the field at the beginning of the form's constructor. We couldn't do this with fBitmap, because at the beginning of the constructor the width and height of the picturebox aren't set yet and we need them for creating the bitmap.
Now search for every occurence lbGeometricalObjects.Items and replace it with fObjects. Note, that fObjects is the list for itself, so it does not have (and need) an Items-property.
You should now for example have this line:
for matching g : IDrawableObject in fObjects do
If we hadn't to check for the IDrawableObject-interface, we could use type inference here, because fObjects is a constructed type and therefore its elements have a defined type (MyGeomObject). The loop would just look like this:
for g in fObjects do
That's (normally) one of the advantages of using such a list for storing the data:
It's type safe. You don't have to cast some Objects into the proper types, the items already have the correct type. And you can be sure that you only have items of the specified type in your list, while you can't be sure when using a list storing only Objects, because an Object can be everything. It's a improvement in performace, too, if you don't have to cast.
By the way, a List<T> offers a variety of usefull methods: You can Find objects in it, you can check if a list Contains an object or Sort the whole list. You can Remove an object you have a reference on (e.g. in a variable), or you can RemoveAt a specified position in the list.
Look What I Can Do! (aka "BindingList<T>")
If you now run the program and add a new geometrical object you will see - nothing!
The listbox does not know about the objects added to the list. So, it doesn't know what it should display. The list and the listbox are completely independent. Of course, one could add the object to both the list and the listbox. But that is not really elegant.
There's a cool mechanism in .NET called "DataBinding". It's (at least at the moment) too much for this article to look into it in detail. In short: It's a mechanism that the objects holding the data can notify the GUI of a change in this data. The GUI then can automatically get the new data and display it. Very, very usefull!
A generic list that notifies the GUI when an item is added or removed is the BindingList<T>. Just change the declaration and instantiation of fObject to BindingList<MyGeomObject>. Then add this line to the end of the MainForm's constructor (below the creation of fBitmap):
lbGeomtricalObjects.DataSource := fObjects;
Run the program and add an object to the list. Because we defined fObjects as DataSource for the listbox, it will notify the listbox about the new item and the listbox will display it.
The down-side of this is, that the BindingList<T> does not offer many of the usefull methods a List<T> offers. For example you cannot find objects in it or sort it automatically. That's why you should use BindingList<T> only when you want to directly reflect your list in the GUI. In all other cases List<T> will be the better choice.
See Also
The Prism Primer: Part 1 – Part 2 – Part 3 – Part 4 – Part 5 – Part 6 – Part 7 – Part 8 – Part 9
Glossaries — Keywords — Types — FAQ — How To