The approach for indexing collections differs between VB6 and Visual Basic .NET. In Visual Basic .NET you can define a property that permits you to treat an instance of a class as if it were itself an array. This property can be the default property, and only indexed properties can be default properties. This is a powerful capability in that it supports defining typed collections with flexible growth unavailable in an array and type-safety awareness in a generic collection. While you are not obliged to use indexers for just typed collections, this represents the best practical use for them.
In this article I will describe the refined behavior of the Default modifier and how you can define indexers for your custom, typed collections. These default indexers will allow you to index elements of an object using the customary integer as well as a string key value.
Expressing Default Properties
Every language has a grammar and rules that make up the grammar. Visual Basic 6 supported the Set keyword. When you defined a default property Visual Basic 6 could tell the difference between object assignment and default properties because you had to use Set when you were assigning objects; without Set VB6 could assume it had to look for a default property.
Visual Basic. NET does not use the Set keyword for object assignment, but like Visual Basic 6, .NET needs a code cue to indicate to the compiler when it should look for a default property. This code cue is the array operator pair (). For example, if you write object() then Visual Basic .NET assumes it is looking at an array or a default indexer property. If object is System.Array then it is the indexer operator and if object is some other type then the compiler looks for a default property. Thus, the presence of () becomes the replacement cue for Set in Visual Basic .NET.
Unlike VB6 though, only properties that have required parameters can be default properties. Listing 1 demonstrates a property without a parameter and Listing 2 shows a property with a parameter. Only the property in Listing 2 can be designated as a default property.
Listing 1: A property without a required argument.
Public Property UnitPrice() As Decimal Get Return FUnitPrice End Get Set(ByVal Value As Decimal) FUnitPrice = Value End Set End Property
Listing 2: A property with a required argument, or an indexed property.
Default Public Property Item(ByVal Index As Integer) As InventoryItem Get Return CType(List(Index), InventoryItem) End Get Set(ByVal Value As InventoryItem) List(Index) = InventoryItem End Set End Property
The argument Index is required. (That is, it is present and it doesn’t use the Optional modifier.) Hence, Item can be expressed as a default property. This means that instances of classes containing Item could be written with or without the express use of the property named Item, as in the following:
Object.Item(5) = New InventoryItem()
is equivalent to
Object(5) = New InventoryItem()
To summarize, there are a couple of differences between properties that can be default and those that can not. Default properties use the Default modifier (see listing 2). Default properties must have a required argument. Objects containing default properties can use the long or short version of the property access.
Defining a Numeric Indexer
The question then becomes where are default properties useful. The answer is that they are useful in places you used them in the past, probably collections, and in .NET they are specifically useful in defining default integer and string indexers for strongly typed collections (see “Implementing Strongly Typed Collections“, codeguru.com May, 2002).
As a refresher, the strongly typed collection can be treated like an array, grows dynamically like a collection, contains a known homogeneous data type, and can be serialized and bound to controls like the DataGrid. Specifically, a strongly typed collection is as easy to use as an array and as powerful as a collection. In Visual Basic .NET we implement strongly typed collections by inheriting from System.Collections.CollectionBase and System.Collections.ReadOnlyCollectionBase. The code in listing 3 demonstrates a strongly typed collection. (I won’t repeat the basic information here; refer to the aforementioned article.) Here we’ll emphasize the indexers.
Listing 3: A strongly typed collection lacking an indexer.
1: Public Class InventoryList 2: Inherits System.Collections.CollectionBase 3: 4: Public Shared Function Instance() As InventoryList 5: Instance = New InventoryList() 6: Instance.Add(New InventoryItem("Sporks", "1234S", 0.02)) 7: Instance.Add(New InventoryItem("Napkins", "2345N", 0.03)) 8: End Function 9: 10: Default Public Property Item(ByVal Index As Integer) _ As InventoryItem 11: Get 12: Return CType(List(Index), InventoryItem) 13: End Get 14: Set(ByVal Value As InventoryItem) 15: List(Index) = InventoryItem 16: End Set 17: End Property 18: 19: Public Function Add(ByVal Value As InventoryItem) As Integer 20: Return List.Add(Value) 21: End Function 22: End Class
(The line numbers were added for reference only.) The inheritance statement is expressed on line 2. The factory method, Instance, on lines 4 through 8 facilitate creating an instance of the collection and are there for test purposes in this instance. However, we could pass a key to the factory method and read the data from a database or some other repository. Lines 19 through 21 implement an Add method; thus allowing us to add new elements of an existing instance of the collection. The indexer is defined on lines 10 through 17.
The indexer is named Item by convention. This is helpful for cross-language programming when a vendor does not implement the indexer idiom. (Indexers are not required parts of the CLI.) The keyword Default indicates that Item is a default property and the required argument, Index, assures that it can be. As a result we could declare an integer and iterate over the elements of the InventoryList as if instances of InventoryList were arrays. The getter-lines 11 through 13-performs the typecast from the internal generic exception back to our known type for the convenience of the consumer, and the setter simply assigns the InventoryItem object to the indexed position.
Defining a String Indexer
Indexers do not have to be a specific type. You can define an indexer that indexes on a string key for instance. The code for a default string indexer is very similar to that of an integer indexer, and both may co-reside in the same class. The biggest difference is that the type of the indexer would be a string instead of an integer, and you will need to implement a search algorithm based on the string value.
Suppose that InventoryItem objects contained a property named Identifier. You could add a default string indexer to InventoryList and search for the identifier based on the passed in value. The revision might be implemented as shown in listing 4.
Listing 4: Adding a string-based default indexer to a strongly typed collection.
Default Public Property Item(ByVal Key As String) As InventoryItem Get Return Me(IndexOf(Key)) End Get Set(ByVal Value As InventoryItem) Me(IndexOf(Key)) = Value End Set End Property Private Function IndexOf(ByVal Key As String) As Integer Dim I As Integer For I = 0 To List.Count - 1 If (Me(I).Identifier.Equals(Key)) Then Return I Next Return -1 End Function
The first part of the listing is the additional default indexer. The index-value is a string. The indexer is implemented in terms of the integer indexer defined in listing 3. All we really did is implement an IndexOf method that searches on our desired key value; in this case, the key value is a property in InventoryItem named Identifier.
To be clear we can expand one statement to a more verbose form. The code in listing 4 may appear to be terse to some. The statement
performs ultimately the same steps as the following verbose form
Dim I As Integer = Indexof(Key) Dim Item As InventoryItem = CType(List(I), InventoryItem) Return Item
It is simply a matter of preference rather you write the long version or the short version. Subjectively speaking, the short version is considered better form.
Visual Basic .NET supports some very powerful idioms. As was demonstrated in this article, you can express inheritance. Implicit in the two versions of the default property is property overloading, indexable properties, and default properties. Combining all of these elements will yield expressive and robust code that is both useful and a pleasure to write.
About the Author
Paul Kimmel is a freelance writer for Developer.com and CodeGuru.com. Look for his recent book “Advanced C# Programming” from McGraw-Hill/Osborne on Amazon.com. Paul will be a keynote speaker in “Great Debate: .NET or .NOT?” at the Fall 2002 Comdex in Las Vegas, Nevada. Paul Kimmel is available to help design and build your .NET solutions and can be contacted at [email protected].
# # #