- The Class Hierarchy
- Creating a New Class
- Declaration and Instantiation
- Constructors and Destructors
- Garbage Collection
- Object Operators
- Adding and Overriding Methods
- Calling the Overridden Method
- Access Scope: Public, Private, Protected
- Setting Properties with Methods
- Default and Optional Parameters
- Declaring Variables Static and Const
- Revisiting the StringParser Module
- Example: Creating a Properties Class
- Data-Oriented Classes and Visual Basic Data Types
- Advanced Techniques
Example: Creating a Properties Class
In the following pages, I will create a new class called Properties, which will be a subclass of the Dictionary class.
There are three potential scenarios to consider for the Constructor. You may get the data to be parsed for the Properties class from a file or a string, and you also may want to be able to instantiate the class without any data so that you can add it later, either by individually setting the key/value pairs or by some other method.
To accomplish this, I'll overload the Constructor method with three versions. If I did nothing, the default Constructor would be used, which takes no parameters. However—and this is a big "however"—if I overload the Constructor class with any other Constructor, the default Constructor goes away and is not accessible. The reason is that it allows you to require that the class accept a parameter in the Constructor because you do not always want the default Constructor available.
This means that you have to reimplement the default Constructor to retain the capability to instantiate the class with no parameters, so that is what I do first:
Sub Constructor() // Do nothing End Sub
The second implementation will accept a string in the parameter. Whenever this Constructor is called, the class automatically parses the file as part of the Constructor. This means that after the class is instantiated, the key/value pairs are accessible, with no further steps required.
Sub Constructor(myPropertyString as String) If myPropertyString <> " " Then Me.parsePropertyFile(myPropertyString) Else // An error as occurred End If End Sub
The following implementation accepts a FolderItem. It tests to make sure the FolderItem is readable and writeable, both of which are functions of the FolderItem class. If the FolderItem instance is readable and writeable, it sends the file to the overloaded parsePropertyFile function.
Sub Constructor(myPropertyFile as FolderItem) If myPropertyFile.exists Then If myPropertyFile.IsReadable and myPropertyFile.IsWriteable Then Me.parsePropertyFile(myPropertyFile) Else // An error has occurred End If Else // An error has occurred End If End Sub
Functionget(aKey as string, aDefault as string) As string Dim s as String s = Me.Lookup(aKey, aDefault) End Function
The get() function is also overloaded to take only one string as a parameter. When this occurs, I need to test first to see whether the Dictionary has the key passed in the parameter, and if it doesn't, respond appropriately.
Function get(aKey as string) As string Dim s as String If Me.HasKey(aKey) = True Then s = Me.Value(aKey) Return Else // Handle the Error Return "" End If End Function
You can accomplish the equivalent by using the Dictionary's Lookup() function. Remember that nothing is stopping you from calling the Lookup() function directly from other parts of your program because it is a public method of Dictionary.
Function get(aKey as string) As string Dim s as String s = Me.Lookup(aKey, "") End Function
I can also replicate the same functionality by calling the first get() function from the second:
Function get(aKey as string) As string Dim s as String s = Me.get(aKey, "") End Function
Of the three options I have given for implementing function get(aKey) as String, the last option is preferable, in my opinion. You may be wondering why you should bother implementing the get() method at all, because you can use the native Dictionary methods anyway. One reason is because get() provides a simpler syntax. Dictionaries can use Variants for keys and values, but I really care only about strings, so get() makes the assumption that it's a string. More importantly, because of the two versions of get() that I am using, I can change the implementation in the future of how the default value is generated when no default value is given, or add tests to the code to make sure that the default value that is passed is a valid default value.
There is yet another approach to implementing get() that accomplishes the same goals, more or less: instead of overloading get(), you can override and overload Lookup() to achieve the same effect.
I'll override Lookup() first. Remember that to override a method, the signature has to be the same, which means the parameters must accept Variants. Also, the reason I said I liked using get() was that it gave me an opportunity to test for legal values first. You can do the same here, by calling the parent class's version of Lookup after you've tested the values:
Function Lookup(aKey as Variant, aDefaultValue as Variant) as String Dim key,default as String key = aKey.StringValue default = aDefaultValue.StringValue If default <> "" Then Return Dictionary.Lookup(key, default).StringValue() End If End Function
I can also keep things simple by using an overloaded version of Lookup() that accepts only a key in the parameter and handles the default value automatically:
Function Lookup(aKey as Variant) as String Dim key,default as String key = aKey.StringValue default = aDefaultValue.StringValue If default <> "" Then Return Dictionary.Lookup(key, default).StringValue() End If End Function
Note that in both cases, I returned a string rather than a Variant. REALbasic doesn't pay attention to the return value when determining whether a method is overloaded. This allows me to force the return of a string, even though the original Lookup() implementation returns a Variant.
In addition to get(), I also implemented a set() subroutine. Again, this is for simplicity's sake because I could also just call the Dictionary.Value() function directly. It also means I can test the values before assigning them, which always helps. I also use the Assigns keyword, which alters the way that the method can be called in your program. An example of how it is used follows this example:
Sub set(aKey as string, assigns aProperty as string) If aKey <> "" and aProperty <> "" Then Me.Value(aKey) = aProperty Else // An error occurred End if End Sub
By using the Assigns keyword in the parameter, you can set a key/value pair using the following syntax, which mirrors the syntax used for the Value() function of Dictionary:
Dim prop as Properties prop = New Properties() prop.set("FirstKey") = "FirstValue"
The first step to parsing the file is to split the string into individual lines. I have two functions for this—one that accepts a string and another that accepts a FolderItem. The string version splits the string on a newline character (ASCII value of 13) and then cycles through each line, parsing the lines individually.
Protected Sub parsePropertyFile(myFile as string) Dim my_array(-1) as String Dim line as String my_array = myFile.split(Chr(13)) For Each line In my_array Me.parseLine(line) Next End Sub
A second version of parsePropertyFile accepts a FolderItem as a parameter. FolderItems are discussed at length later in the book, but the code in this example is sufficiently self-explanatory that you get the general idea of what is happening. When you open a FolderItem as a text file, it returns a TextInputStream. A TextInputStream allows you to read it line by line by calling the ReadLine() function.
Protected Sub parsePropertyFile(myPropertyFile as folderItem) Dim textInput as TextInputStream Dim astr as string If myPropertyFile Is Nil Then // An error has occurred Return End If If myPropertyFile.exists Then Me.file = myPropertyFile textInput = me.file.OpenAsTextFile Do astr=textInput.ReadLine() Me.parseLine(astr) Loop Until textInput.EOF textInput.Close Else // An error has occurred End If End Sub
Each line is parsed individually.
Protected Sub parseLine(my_line as string) Dim aKey, aValue as string // Split line at "=", and ignore comments If Left(my_line, 1) <> "#" Then If CountFields(my_line, "=")= 2 Then aValue = Trim(NthField(my_line, "=", 2)) aKey = Trim(NthField(my_line, "=", 1)) Me.set(aKey) = aValue End If End If // starts with "#" End Sub
The first thing I test for is to see whether the line starts with a # character. In property files, a # at the beginning of a line indicates a comment, and because comments are only for human consumption, I can safely ignore them in my program.
The next step is to split the line into two values, one for the property and the other for the value. Although I could use the Split() function, I have chosen to use the CountFields/NthField combination primarily because it makes it a little easier to count how many fields there are. Because each property is separated from its value by an equal sign, I will parse the line only if there are two fields in the line (which is the same thing as saying that there is only one "=" sign on the line).
The property name, or key, is in the first field, and the value is in the second field. When extracting the values using NthField, I trim the results, which removes whitespace on either side of the string. This accounts for situations where there are extra spaces between the "=" sign and the values themselves. As a result, both of these formats will be parsed correctly:
aProperty=aValue aProperty = aValue
Finally, I use the set() method to add the key/value pair to the Dictionary.
Now that all the members of the class are implemented, here is an example of how you can use the class:
Dim prop as Properties Dim s as String Dim propStr as String propStr = "First=FirstValue" + Chr(13) + "Second=SecondValue" prop = New Properties(propStr) s = prop.get("First") // s equals "FirstValue" prop.set("Third") = "ThirdValue" s = prop.get("Third") // s equals "ThirdValue"