Write-once/read-many properties
Write-once/read-many properties are a bit more interesting and useful than pure write-only properties. For example, the LoginDialog object described in the previous paragraph might expose a UserName property of this type. Once a user logs in, your code assigns his or her name to this property; the rest of the application can then read it but can’t modify it. Here’s another example: in an Invoice class, the Number property might be rendered as a write-once/read-many property because once you assign a number to an invoice, arbitrarily changing it might cause serious problems in your accounting system.
Visual Basic doesn’t offer a native system to implement such write-once/read-many properties, but it’s easy to do that with some additional lines of code. Let’s say that you want to provide our CPerson class with an ID property that can be assigned only once but read as many times as you need. Here’s a possible solution, based on a Static local variable:
Private m_ID As Long
Public Property Get ID() As Long
ID = m_ID
End Property
Public Property Let ID(ByVal newValue As Long)
Static InitDone As Boolean
If InitDone Then Err.Raise 1002, , "Write-once property"
InitDone = True
m_ID = newValue
End Property
Here’s an alternative solution, which spares you the additional Static variable but consumes some additional bytes in memory (16 bytes instead of 6):
Private m_ID As Variant
Public Property Get ID() As Long
ID = m_ID
End Property
Public Property Let ID(ByVal newValue As Long)
If Not IsEmpty(m_ID) Then Err.Raise 1002, , "Write-once property"
m_ID = newValue
End Property
In both cases, the interface that the class exposes to the outside is the same. (ID is a Long property.) This is another example of how a good encapsulation scheme lets you vary the internal implementation of a class without affecting the code that uses it.
Read-only properties vs. methods
From the point of view of the client code (that is, the code that actually uses your class), a read-only property is similar to a function. In fact, in all cases a read-only property can be invoked only in expressions and can never appear to the left of an assignment symbol. So this raises a sort of semantic problem: When is it preferable to implement a read-only property and when is a function better? Just a few suggestions:
• Most programmers expect properties to be quick shortcuts to values stored in the class. If the routine that you’re building serves mostly to return a value stored inside the class or can be quickly and easily reevaluated, create a property because this is probably the way the client code will look at it anyway. If the routine serves mostly to evaluate a complex value, use a function.
• If you can find it useful to call the routine and discard its return value (in other words, it’s more important what the routine does than what it returns), write a function. VBA lets you call a function as if it were a Sub, which isn’t possible with a Property Get procedure.
• If you can imagine that in the future the value returned by the routine could be assigned to, use a Property Get procedure, and reserve for yourself the chance to add a Property Let routine when it’s needed.
Let’s take the CompleteName member of the CPerson class as an example. It has been implemented as a method, but most programmers would undoubtedly think of it as a read-only property. Moreover—and this is the really important point—nothing prevents you from morphing it into a read/write property:
Property Get CompleteName() As String
CompleteName = FirstName & " " & LastName
End Property
Property Let CompleteName(ByVal newValue As String)
Dim items() As String
items() = Split(newValue)
‘ We expect exactly two items (no support for middle names).
If UBound(items) <> 1 Then Err.Raise 5
‘ If no error, assign to the "real" properties.
FirstName = items(0): LastName = items(1)
End Property
You have increased the usability of the class by letting the client code assign the FirstName and LastName properties in a more natural way, for example, directly from a field on the form:
pers.CompleteName = txtCompleteName.Text
And of course you can still assign individual FirstName and LastName properties without the risk of creating inconsistencies with the CompleteName property. This is another of those cute little things you can do with classes.
Properties with arguments
So far, I’ve illustrated Property Get procedures with no arguments and their matching Property Let procedures with just one argument, the value being assigned to the procedure. Visual Basic also lets you create Property procedures that accept any number of arguments, of any type. This concept is also used by Visual Basic for its own controls: for example, the List property of ListBox controls accepts a numerical index.
Let’s see how this concept can be usefully applied to the CPerson sample class. Suppose you need a Notes property, but at the same time you don’t want to limit yourself to just one item. The first solution that comes to mind is using an array of strings. Unfortunately, if you declare a Public array in a class module as follows:
Public Notes(1 To 10) As String ‘ Not valid!
the compiler complains with the following message, "Constants, fixed-length strings, arrays, user-defined types, and Declare statements not allowed as Public member of object modules." But you can create a Private member array and expose it to the outside using a pair of Property procedures:
‘ A module-level variable
Private m_Notes(1 To 10) As String
Property Get Notes(Index As Integer) As String
Notes = m_Notes (Index)
End Property
Property Let Notes(Index As Integer, ByVal newValue As String)
‘ Check for subscript out of range error.
If Index < LBound(m_Notes) Or Index > UBound(m_Notes) Then Err.Raise 9
m_Notes(Index) = newValue
End Property
Caution You might be tempted not to check the Index argument in the Property Let procedure in the preceding code, relying instead on the default behavior of Visual Basic that would raise an error anyway. Think about it again, and try to imagine what would happen if you later decide to optimize your code by setting the Remove Array Bounds Checks optimization for the compiler. (The answer is easy: Can you spell "G-P-F"?)
Now you can assign and retrieve up to 10 distinct notes for the same person, as in this code:
pers.Notes(1) = "Ask if it’s OK to go fishing next Sunday"
Print pers.Notes(2) ‘ Displays "" (not initialized)
You can improve this mechanism by making Index an optional argument that defaults to the first item in the array, as in the following code:
Property Get Notes(Optional Index As Integer = 1) As String
‘ ... (omitted: no need to change code inside the procedure)
End Property
Property Let Notes(Optional Index As Integer = 1, _
ByVal newValue As String)
‘ ... (omitted : no need to change code inside the procedure)
End Property
In the client code, you can omit the index for the default note.
pers.Notes = "Ask if it’s OK to go fishing next Sunday"
‘ You can always display all notes with a simple For-Next loop.
For i = 1 To 10: Print pers.Notes(i): Next
You can also use optional Variant arguments and the IsMissing function, as you would do for regular procedures in a form or standard module. In practice, this is rarely required, but it’s good to know that you can do it if you need to.
Saturday, December 26, 2009
Subscribe to:
Post Comments (Atom)
No comments:
Post a Comment