GoDiagram 4.0: upgrading to .NET 2.0 Generics and Collections
Northwoods Software, www.nwoods.com
I just wanted to document the relevant differences I found between System.Collections and System.Collections.Generic, since I found them somewhat confusing. You may find the information useful even if you are not working on GoDiagram.
Also I talk some about the design and coding changes involving this upgrade to require .NET 2.0.
If you aren’t thoroughly familiar with C# generics already, you can read:
http://msdn.microsoft.com/en-us/library/512aeb7t(VS.80).aspx
In the tables below, for each pair of non-generic and generic types, I list their corresponding methods and properties. On the left side is the non-generic type/method/property. I use “O” as a short hand for Object. On the right side is the generic type/method/property. “T”, “K”, “V” are generic type parameters. The table for ICollection also includes IGoCollection for comparison. There, “GO” is short hand for GoObject.
In each table I list all of the methods and properties, including inherited ones. Those that are inherited I prefix with the interface that they come from, just as you would when defining a class that implements the interface using explicit interface definitions.
IEnumerable and IEnumerable<T>
In the case of defining a class that implements IEnumerable<T>, this means implementing IEnumerable.GetEnumerator() as well as IEnumerable<T>.GetEnumerator().
IEnumerable |
IEnumerable<T> : IEnumerable |
IEnumerator GetEnumerator() |
IEnumerator IEnumerable.GetEnumerator() |
|
IEnumerator<T> GetEnumerator() |
But sometimes there’s more – you’ll want to implement a specific GetEnumerator() for your enumerable class. That’s the case in GoDiagram, where for efficiency reasons I defined types that were both IEnumerable and IEnumerator – for example, the GoCollection class uses the GoCollectionEnumerator struct that implements both. To make this struct accessible to the compiler for efficiency, one has to define GetEnumerator() to return the type GoCollectionEnumerator. Hence:
public class GoCollection : ICollection<GoObject> { // actually IGoCollection, which inherits from ICollection<GoObject>
// explicit interface definition for IEnumerable – cannot have access modifiers
IEnumerator IEnumerable.GetEnumerator() {
return this.GetEnumerator(); // need “this.” to disambiguate
}
// explicit interface definition for IEnumerable<T> – cannot have access modifiers
IEnumerator<GoObject> IEnumerable<GoObject>.GetEnumerator() {
return this.GetEnumerator(); // need “this.” to disambiguate
}
// this is the definition seen by a use like: foreach (GoObject obj in aGoCollection) { . . . }
public virtual GoCollectionEnumerator GetEnumerator() {
. . .
}
. . .
}
IEnumerator and IEnumerator<T>
Here’s how IEnumerator<T> compares with IEnumerator. The minor feature is that it adds inheriting from IDisposable.
IEnumerator |
IEnumerator<T> : IDisposable, IEnumerator |
|
IDisposable.Dispose() |
MoveNext() |
IEnumerator.MoveNext() |
Reset() |
IEnumerator.Reset() |
Current |
IEnumerator.Current of type Object |
|
Current of type T |
ICollection and ICollection<T>
ICollection<T> is quite different from ICollection. The non-generic ICollection only provided read-only access to a collection. The generic ICollection<T> includes methods such as Add and Clear. The generic ICollection<T> also gets rid of IsSynchronized and SyncRoot, members that had always seemed superfluous to me.
The generic ICollection<T> also adds the IsReadOnly property. This was an issue for those sample GoDocument-derived classes that defined their own IsReadOnly property. As an expediency, I have just changed those definitions to “override” the one now provided by GoDocument.
For IGoCollection I have removed the members CopyTo(Array, int), IsSynchronized, and SyncRoot, despite the additional incompatibility that that causes. I removed CopyTo(Array, int) to reduce type errors and to improve efficiency. And nobody cares about synchronization.
Since IGoCollection now implements IEnumerable<GoObject>, you can now use LINQ on GoDiagram collections, if you are using .NET 3.5. (But we can’t in our libraries, since they need to compile/run with .NET 2.0.)
ICollection : IEnumerable |
ICollection<T> : IEnumerable<T>, IEnumerable |
IGoCollection : ICollection<T> now inherits from ICollection<GoObject> |
|
Add(T) |
Add(GO) |
|
Clear() |
Clear() |
|
Contains(T) |
Contains(GO) |
|
|
CopyArray() |
CopyTo(Array, int) |
CopyTo(T[], int) |
CopyTo(GO[], int) removed Array overload |
IEnumerable.GetEnumerator() |
IEnumerable.GetEnumerator() |
GetEnumerator() returns IEnumerable |
|
IEnumerable<T>.GetEnumerator() |
GetEnumerator() added, returns IEnumerator<GO> |
|
|
GetEnumerator() returns GoCollectionEnumerator |
|
Remove(T) |
Remove(GO) |
|
|
Backwards |
Count |
Count |
Count |
|
|
IsEmpty |
|
IsReadOnly |
IsReadOnly added |
IsSynchronized |
|
removed |
SyncRoot |
|
removed |
IList and IList<T>
IList<T> is reasonably similar to IList. The main annoyance in the conversion is that I had to change all of the overrides of Remove to return a boolean in all of the sample classes. ArrayList was the principal implementation of IList. We have already replaced practically all uses of ArrayList with the appropriate List<T>.
IList : ICollection, IEnumerable |
IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable |
Add(O) |
ICollection.Add(T) |
Clear() |
ICollection.Clear() |
Contains(O) |
ICollection.Contains(T) |
ICollection.CopyTo(Array, int) |
ICollection.CopyTo(T[], int) |
IEnumerable.GetEnumerator() |
IEnumerable.GetEnumerator() returns IEnumerator |
|
IEnumerable<T>.GetEnumerator() returns IEnumerator<T> |
IndexOf(O) |
IndexOf(T) |
Insert(int, O) |
Insert(int, T) |
Remove(O) returns void |
ICollection.Remove(T) returns bool |
RemoveAt(int) |
RemoveAt(int) |
ICollection.Count |
ICollection.Count |
IsFixedSize |
|
IsReadOnly |
ICollection.IsReadOnly |
ICollection.IsSynchronized |
|
Item[int] indexer of type O |
Item[int] indexer of type T |
ICollection.SyncRoot |
|
IDictionary and IDictionary<K,V>
IDictionary<K,V> is only moderately similar to IDictionary. Dictionaries use the generic KeyValuePair<K,V>, instead of DictionaryEntry. Enumeration uses IEnumerator<KeyValuePair<K,V>>, instead of IDictionaryEnumerator. Hashtable was the principal implementation of IDictionary. I have just replaced most uses with the appropriate IDictionary<K,V>.
However, the biggest change is in how to look up values. Non-generic code is used to doing:
Hashtable ht = …
MyClass c1 = (MyClass)ht[“one”];
// now c1 is either null or has a MyClass object reference
But this won’t work correctly any more. The main reason is that if there is no key “one” in the hashtable, it will raise an exception. You could do:
Dictionary<String, MyClass> ht = …
MyClass c1 = null;
if (ht.Contains(“one”)) c1 = ht[“one”];
But this is relatively inefficient, since if the key is present it requires two lookups. Instead do:
Dictionary<String, MyClass> ht = …
MyClass c1;
ht.TryGetValue(“one”, out c1);
TryGetValue return true if it finds the key, false if it does not. If it finds the key, the out parameter is set to the value it finds. (Note no need for the cast.)
If the key is not present, the out parameter is set to the default value for the value type. Since MyClass is a reference type, it would be set to null.
IDictionary : ICollection, IEnumerable |
IDictionary<K,V> : ICollection<KeyValuePair<K,V>>, IEnumerable<KeyValuePair<K,V>>, IEnumerable |
|
ICollection.Add(KeyValuePair<K,V>) |
Add(O, O) |
Add(K, V) |
Clear() |
ICollection.Clear() |
Contains(O) |
ContainsKey(K) |
|
ICollection.Contains(KeyValuePair<K,V>) |
ICollection.CopyTo(Array, int) |
ICollection.CopyTo(KeyValuePair<K,V>[], int) |
IEnumerable.GetEnumerator() |
IEnumerable.GetEnumerator() |
GetEnumerator() returns IDictionaryEnumerator |
IEnumerable<KeyValuePair<K,V>>.GetEnumerator() returns IEnumerator<KeyValuePair<K,V>> |
|
ICollection.Remove(KeyValuePair<K,V>) |
Remove(O) |
Remove(K) |
|
TryGetValue(K, out V) use this when both testing presence of key and getting the value |
ICollection.Count |
ICollection.Count |
IsFixedSize |
|
IsReadOnly |
ICollection.IsReadOnly |
Item[O] |
Item[K] throws exception when not present! |
Keys |
Keys |
Values |
Values |
General Comments
There are still a number of uses of System.Collections in the GoDiagram libraries. Because System.Collections.Generic.IEnumerable<T> inherits from System.Collections.IEnumerable, and because System.Collections.Generic.IEnumerator<T> inherits from System.Collections.IEnumerator, there necessarily are some uses of System.Collections types, particularly with explicit interface definitions.
But even ignoring those cases, there remain some uses of System.Collections.
GoCopyDictionary still inherits from Hashtable. Because a lot of code, particularly in overrides of GoGroup.CopyChildren, makes use of the indexer lookup of newly copied objects given an original object, I didn’t want to require everyone to reimplement that code to make use of TryGetValue.
Similarly, in GoDiagram Web, the table of parsed arguments that is passed to GoViewDataRenderer.HandleClientRequest is still a Hashtable instead upgrading to use a Dictionary<String, String>. Again, the same compatibility reasoning applies.
GoText.Choices used to be of type ArrayList. That was a design mistake. I should not have made the implementation type of the list a part of the API. It should have been of type IList. This permits a much broader range of data types that programmers can use to initialize the list of choices for a combobox.
But for that reason, we should leave the type of the property to be IList. Changing the type to be generic, presumably to IList<Object> or worse to IList<String>, would be counterproductive since that would unnecessarily constrain the kinds of data that programmers could use.
The same reasoning applies to other cases where the data is should be of type Object – to use IEnumerable or IList. Examples: Northwoods.Go.Xml.XmlWriter.Objects, Northwoods.Go.Xml.XmlReader.RootObject which might be of type IList, and Northwoods.Go.Instruments.GraduatedScale.LabelChoices.
Upgrading Hints
If you are upgrading your application to use GoDiagram version 4 from version 3, you should find the task to be fairly easy.
First, add the following statement to your source files that use collections:
using System.Collections.Generic;
Second, consider these important .NET collection differences when upgrading your application:
- ICollection<T> includes collection-modifying methods that ICollection does not have
- ICollection<T>.Remove now returns a boolean – you may have some overrides of GoGroup.Remove that you'll need to update
- IDictionary<K,V>.Item gets now throw an exception if the key is not found – use TryGetValue instead