[Previous] [Next]
In the past, one convention for denoting a constant was to use all uppercase letters for the constant's name . For instance, when you created a constant to store a column index in a grid, you would use a statement like this:
Const COLUMN_INDEX = 7 |
Typing anything in code in all uppercase letters is now considered antiquated and undesirable. Mixed-case text is much easier to read. However, since variable and procedure names are also entered in mixed case, it's important to denote when an item is a constant. A better convention is to prefix the constant name with c_ . For example, the constant shown above would be declared like this:
Const c_Column_Index = 7 |
This constant name is a bit easier to read, and you can still immediately tell that you're looking at a constant as opposed to a variable. The second underscore is optional. Some developers (including me) prefer not to use an underscore in this way. This is fine, as long as your approach is consistent. The same constant declaration without the second underscore would look like the following line of code. (Remember that you'll always have an underscore in the constant prefix.)
Const c_ColumnIndex = 7 |
NOTE
Labels for use with GoTo are one of the few exceptions to using mixed-case letters. Such labels, which should be used sparingly, appear in all uppercase letters. Refer to Chapter 11, "Controlling Code Flow," for more information on using these labels.
Another identifying characteristic of a constant as opposed to a variable is the lack of a data type prefix. For instance, if you were storing the column indicator in a variable, you would probably declare the variable by using a statement like this:
Dim intColumnIndex As Integer |
NOTE
Some external libraries still use uppercase constants. For instance, if you use the API viewer to locate and copy API- related constants, you'll often see these constants in uppercase letters. In such cases, leave the constants as they are to promote cross-application consistency.
Many developers don't realize that you can actually create a constant of a specific data type. For instance, the following statement is completely legal:
Const c_InterestRate As Single = 7.5 |
You can specify a data type for a constant, but it adds complexity, and I don't know of a good reason to do so. If you decide to do it anyway, you should use the variable-naming prefixes discussed in Chapter 4, "Naming Conventions." The previous declaration, for instance, is not correct ”according to the directives presented in this book ”because the data type prefix is omitted. The proper declaration would be as follows :
Const c_sngInterestRate As Single = 7.5 |
Although the prefix for constants is different from the prefixes for variables, you should still use the same prefix scheme for indicating the scope of constants that you use for variables . For constants declared locally (within a procedure), no scope indicator is necessary. For constants declared as Private in the Declarations section of a module, you should use the prefix m . For global constants (constants declared as Public within a standard module), you should use the prefix g . The following are declarations of the same constant at different levels of scope:
Procedure: Const c_InterestRate = 7.5 Module (private): Private Const mc_InterestRate = 7.5 Global: Public Const gc_InterestRate = 7.5 |
NOTE
Constants are declared Private by default if you don't explicitly declare them with the Public keyword. As with procedures and variables, constants should always have a clearly defined scope. If you want to create a private constant, explicitly declare the constant using the Private keyword.
By consistently specifying the scope of a constant in addition to denoting the constant with c_ , you'll make your code easier to read and to debug. If you're ever unsure where a constant is declared, simply place the cursor anywhere within the name of the constant and press Shift+F2. Visual Basic will take you directly to the constant's declaration.
When you uniquely identify constants and denote their scope, you create more readable code.
5.1.1 Declare constants using mixed-case characters , prefixing each constant with c_. Remember that identifying constants by using all uppercase letters is out.
Incorrect:
Const USDATE = "mm/dd/yyyy" Const KEYCONTROL = 17 |
Correct:
Const c_USDate = "mm/dd/yyyy" Const c_KeyControl = 17 |
Also correct:
Const c_US_Date = "mm/dd/yyyy" Const c_Key_Control = 17 |
5.1.2 Denote a constant's scope using a scope designator prefix. Knowing a constant's scope is extremely important for debugging. All constants declared in the Declarations section of any type of module need a g or an m designator.
Incorrect (module level or global level):
Private Const c_US_DATE = "mm/dd/yyyy" Public Const c_KeyControl = 17 |
Correct:
Private Const mc_US_Date = "mm/dd/yyyy" Public Const gc_KeyControl = 17 |
I hope that the first part of this chapter has convinced you of the importance of replacing hard-coded numbers (magic numbers) with constants, regardless of scope. It might be tempting to use a hard-coded number within a procedure because it seems silly to create a constant for a single use in a single place. Certainly, maintaining the value is easy enough; you don't need to perform a search and replace when the number exists only once. However, readability is still a problem. Magic numbers are called "magic" for a reason. When someone else looks at the code, how can you be sure that the number will make complete sense to him or her? Regardless of scope, you should always replace magic numbers with constants. All together now: "Use constants in place of magic numbers, regardless of scope."
Incorrect:
'* Fill a most recently used list. For intCount = 1 To 4 strFileName = RetrieveRecentFileName(intCount) '* If an entry was found, add it to the list. If strFileName <> "" Then Set objItem = lvwRecent.ListItems.Add() objItem.Text = strFileName objItem.SmallIcon = "Database" End If Next intCount |
Correct:
'* Fill a most recently used list. Const c_Max_Recently_Used = 4 For intCount = 1 To c_Max_Recently_Used strFileName = RetrieveRecentFileName(intCount) '* If an entry was found, add it to the list. If strFileName <> "" Then Set objItem = lvwRecent.ListItems.Add() objItem.Text = strFileName objItem.SmallIcon = "Database" End If Next intCount |
When a procedure or a parameter is declared as an enumerated type, it's common courtesy ”nay, it's your duty ”to reference the enumeration member names rather than their values. When you use an enumeration, your code is much easier to read and less likely to contain errors. Your code is also less likely to fail if someone later breaks backward compatibility by changing the values that correspond to the member names. For instance, say you're using an ActiveX component and one of its properties is BorderStyle . The developer has designated 0 - Flat and 1 - 3D as the possible values and has exposed them as members of an enumeration, as shown here:
Public Enum BorderStyle psFlat = 0 ps3D = 1 End Enum |
Say that you use the literal values rather than the enumeration's member names and you're updating to a new component. The developer has added a new BorderStyle value called Chiseled to the component. However, he wasn't really thinking of backward compatibility, and he changed the enumeration structure to the following:
Public Enum BorderStyle psFlat = 0 psChiseled = 1 ps3D = 2 End Enum |
You can see that if you hard-code 1 to designate a 3-D border, you'll get unexpected results after you upgrade. Obviously, component developers should not break backward compatibility in this way, but it does happen. If you use the member's name rather than its value, your code will not be affected by such an oversight. Whether to use enumerations shouldn't even be a question. If a function supports them, use them. In new versions of software components , enumerations are often provided where there were none before. As enumerations and constants become available, make sure to change your code appropriately to use them.
Incorrect:
MsgBox "Print all documents?", 36 |
Correct:
MsgBox " Print all documents?", vbYesNoOrvbQuestion |
One area in which constants really shine but are often underused is as references to indexes of control arrays. When you create a control array, you end up with a number of controls all with the same name. To reference a particular control, you use its shared name and its unique index. When you hard-code an index, you create a magic number ”complete with all the drawbacks discussed earlier. The problem with hard-coding index references is that your code can be difficult to debug if you've referenced the wrong index. Since controls within a control array are always of the same type, you can switch indexes all day long with little chance of generating an error message because all the actions you might perform on one member can be performed on another member.
To diminish the possibility of errors when you use control arrays, you should create constants that relate to each index. For example, say you have three text boxes that store a home phone number, work phone number, and mobile phone number, respectively. You use a control array because you have some standard code that runs on the Validate event to verify that each number is indeed a valid phone number. If you hard-code indexes, you have to remember which index references which type of phone number. If you're in a hurry or you haven't had your morning Mountain Dew, you can easily confuse the numbers. However, if you assign a constant to each index and always reference a control by its constant, never by its index directly, it's easier to ensure accuracy. Control arrays can make certain development situations much easier, and they can make an application less resource- intensive . However, the more elements you create for a control array, the more likely it is that an incorrect element will be referenced in code. Constants can help reduce the chances of this happening. In general, module-level scope is best for constants that reference elements of a control array, although local scope might be appropriate in some cases. The following constants have been given the prefix txt to denote that they reference the indexes of a text box control array.
Incorrect:
txtPhone(0).Text txtPhone(1).Text txtPhone(2).Text |
Correct:
Const c_txtHomePhone = 0 Const c_txtWorkPhone = 1 Const c_txtFax = 2 txtPhone(c_txtHomePhone).Text txtPhone(c_txtWorkPhone).Text txtPhone(c_txtFax).Text |
Just as it's important to use a naming convention for variables, it's important to use a naming convention for enumeration members. You don't have to use a prefix to denote the type of an enumeration member because all members are always long integers. However, you should use a unique prefix that indicates that the values are from your application or component.
You should prefix your enumeration members with an identifier because when Visual Basic encounters an enumeration member name, it might get confused if other referenced type libraries contain the same name. For example, all of Visual Basic's system constants have the prefix vb. When you encounter a constant such as vbFixedSingle , you immediately know that the constant belongs to Visual Basic's type library. Although Visual Basic uses two-character prefixes, you should use three or four, but no more than that. If you were to use two characters, you would find it difficult to come up with an identifier that isn't used by another application or vendor. For instance, my company is called Tigerpaw Software. When we declare enumeration members, we use the prefix tps , as shown in the enumeration declaration on the following page.
Public Enum tpsPrintDestination tpsScreen = 0 tpsPrinter = 1 End Enum |
NOTE
It's also acceptable to prefix the name of an enumeration (as well as the names of its members), as I have done in the previous example.
No application is an island. Even the simplest program uses many external libraries. To confirm this, just create a new Standard EXE and then choose References from the Project menu to see all the ActiveX components being referenced. With the increasing complexity of integrated components comes the need to be more aware of the potential for collisions between components. For this reason, give your enumeration members names that make such collisions unlikely .
Incorrect:
Public Enum BackTrackItem Account = 0 ServiceOrder = 1 Quote = 2 Contact = 3 PriceBookItem = 4 PurchaseOrder = 5 Task = 6 End Enum |
Correct:
Public Enum BackTrackItem tpsAccount = 0 tpsServiceOrder = 1 tpsQuote = 2 tpsContact = 3 tpsPriceBookItem = 4 tpsPurchaseOrder = 5 tpsTask = 6 End Enum |
Also correct:
Public Enum tpsBackTrackItem tpsAccount = 0 tpsServiceOrder = 1 tpsQuote = 2 tpsContact = 3 tpsPriceBookItem = 4 tpsPurchaseOrder = 5 tpsTask = 6 End Enum |
Creating custom enumerations for your modules is highly encouraged, but Visual Basic has been slow to adopt them for all of its own objects. For instance, when you set the WindowState property of a form, there are only three possible values: 0 - Normal, 1 - Minimized, and 2 - Maximized . Looking in the Properties window shows you this. Each value (0, 1, and 2) has a name associated with it. These names look very much like members of an enumeration ”well, in the Properties window, at least.
Although Visual Basic should use true enumerations for these properties, more often than not it doesn't. Figure 5-3 shows what the code window looks like when you attempt to change the WindowState property of a form. Notice that there is no code helper drop-down list in which you can select from a set of values.
Figure 5-3. When a parameter or property doesn't support an enumeration, you must remember the possible values; you don't get any help from Visual Basic.
Visual Basic doesn't have defined enumerations for most of its objects, but it does often support system constants for the values. System constants are global constants that are part of the Visual Basic type library. You don't have to define them or reference a library to use them because they're always available. However, since system constants don't appear in the code helper drop-down list, as enumerations do, many developers are unaware that these constants exist. Whenever you must type a numeric value as a parameter of a Visual Basic function or as the value of a standard Visual Basic object property, chances are good there's an associated system constant.
To determine whether a system constant exists for a property, type the property name (such as WindowState ), place the cursor anywhere within the property text, and press F1. The help displayed for the property will usually include a list of any constants that are available, as shown in Figure 5-4.
Figure 5-4. When you have to hard-code a value for a parameter, check online Help to see whether a system constant is available.
NOTE
Many parameters, such as the Buttons parameter of the MsgBox statement, have associated system constants.
To use a system constant, you simply enter it as if you were referencing a constant that you've defined. For instance, to change a form to maximized, you can use a statement such as this:
Me.WindowState = vbMaximized |
One way to know whether you've typed a system constant correctly is to type it in all lowercase letters. If the constant is valid, Visual Basic converts it to its proper case. If the constant remains in all lowercase letters, you've typed the name wrong and you have to correct it. Unlike enumerations, system constants can be used anywhere in code, not just with variables defined as an enumerated type. Therefore, you must be careful not to use the wrong constant because Visual Basic can't detect this type of error. Anything you can do to eliminate magic numbers is a good thing. If an enumeration is available for a procedure, use it. If not, check to see whether a system constant is defined. If that fails, consider creating your own constant to replace the magic number.
Incorrect:
With Me .BorderStyle = 1 .WindowState = 0 .ScaleMode = 3 .DrawMode = 13 End With |
Correct:
With Me .BorderStyle = vbFixedSingle .WindowState = vbNormal .ScaleMode = vbPixels .DrawMode = vbCopyPen End With |
Even developers who truly believe in enumerations sometimes miss the opportunity to use them. As you develop code, you might not always think about creating an enumeration because an enumeration might seem like overkill in certain situations. In general, whenever a procedure accepts a limited set of values, use an enumeration. For instance, if you have a procedure in which a parameter can use one of two values only, that parameter is a prime candidate for an enumeration.
Creating an enumeration for two values might seem excessive, but it's not. You still get the benefits of avoiding magic numbers, including reduced data entry and greater legibility. Also, if you decide to add members in the future, an enumeration will make it easier to do so. Whether the values are strings or numbers is irrelevant; you can benefit from using an enumeration in both situations.
Incorrect:
Public Sub ShowAVIFile(lngType As Long ) |
Correct:
Public Enum tpsAVIFile tpsFileCopy = 0 tpsFileDelete = 1 tpsFileDeleteToRecycle = 2 tpsFileNuke = 3 tpsFindComputer = 4 tpsFindFile = 5 tpsFileMove = 6 tpsSearch = 7 End Enum Public Sub ShowAVIFile(lngType As tpsAVIFile) |
You must validate any value passed to a parameter declared as an enumerated type to ensure that it is acceptable. When a parameter is declared as an enumerated type, it's really a long integer with a fancy code helper drop-down list. While other developers should use the named enumeration members, they are free to pass the parameter any valid long integer. Unfortunately, Visual Basic does not have the capability to automatically require that parameters be valid members of the enumeration. I hope this is added someday; it would greatly reduce the amount of code you have to write when you use many different enumerations.
There are essentially two methods you can use to validate the data:
In general, unless you are validating that the value falls within an acceptable range, use the Select Case construct rather than If End If . The Select Case construct gives you more flexibility if you need to add more values to the enumeration later.
NOTE
When you use Select Case , always include an Else clause to deal with an invalid value passed into the procedure.
Making assumptions is one of the leading causes of errors in code. Visual Basic won't ensure that values passed to an enumerated type actually correspond to named members within the enumeration. Never assume you have good data.
5.8.1 Always validate data by using comparisons to the named members, not to magic numbers. Refrain from using magic numbers for data validation, just as you refrain from using magic numbers elsewhere in your code.
Incorrect:
Public Enum tpsPrintDestination tpsScreen = 0 tpsPrinter = 1 End Enum Public Sub PrintReport( ByVal strFileName As String , _ ByVal lngDestination As tpsPrintDestination) '* Verify that a valid location has been specified. If lngDestination < 0 Or lngDestination > 1 Then GoTo PROC_EXIT End If '* Print the specified report. End Sub |
Correct:
Public Enum tpsPrintDestination tpsScreen = 0 tpsPrinter = 1 End Enum Public Sub PrintReport( ByVal strFileName As String , _ ByVal lngDestination As tpsPrintDestination) '* Verify that a valid location has been specified. If (lngDestination <> tpsScreen) And _ (lngDestination <> tpsPrinter) Then GoTo PROC_EXIT End If '* Print the specified report. End Sub |
5.8.2 Use Select Case to validate that a value is a valid member of a discrete set of values. Don't forget to include the Case Else clause to handle invalid data.
Correct:
Public Sub PrintDocument(lngCallingForm As tpsCallingForm) '* Perform necessary actions based on the calling form. Select Case lngCallingForm Case Is = tpsContactView Case Is = tpsServiceOrderView Case Is = tpsCustomerInventoryView Case Else '* The value passed as the calling form parameter '* is invalid! MsgBox " An incorrect calling form " & _ "has been specified!", vbInformation GoTo PROC_EXIT End Select End Sub |