Chapter 5: Using Constants and Enumerations
When you hard-code numbers in your procedures, a myriad of things can go wrong. Hard-coded numbers are generally referred to as "magic numbers" because they're often shrouded in mystery; the meaning of such a number is obscure because the digits themselves give no indication as to what the number represents. This chapter discusses the drawbacks of magic numbers and offers alternatives using constants and enumerations.
A constant is much like a variable; you create a name and assign it a value. However, unlike a variable, a constant is given its value at design time, and this value cannot be changed at run time. You should always use constants in place of magic numbers, for reasons I'll discuss in this section.
When you hard-code a string in a procedure, the effect is similar to that of using magic numbers. All the reasons for eliminating magic numbers also apply to eliminating hard-coded strings; if you know the value at design time, use a constant rather than hard-coding the text or the number.
Magic Numbers Are Prone to Data Entry Problems
One of the critical problems with magic numbers is that you can easily mistype a number, transposing its digits. When you type the number 10876, for instance, it's not at all difficult to mistakenly type 10867 or 18076. In contrast to the way it handles variables and reserved words, Microsoft Visual Basic's compiler takes no issue with transposed or incorrect numbersit's happy to use whatever magic number you supply it. Sometimes the problems caused by a simple mistake don't surface immediately, and when they do they can appear as random miscalculations that are difficult to pinpoint. When you use a constant in place of a magic number, Visual Basic checks the validity of the constant at compile time. If the constant does not exist, Visual Basic tells you so and refuses to compile. This eliminates the problem of inaccurately typed numbers; as long as the single constant has the correct value, all the code that uses that constant will use the correct value as well.
If you don't include the Option Explicit statement within the Declarations section of a module and you mistype a constant name, Visual Basic implicitly declares a variable with the incorrect name of the constant, causing inaccurate results. This is just one more reason to explicitly declare your variables. See Chapter 6, "Variables," for more information on explicit vs. implicit variable declaration.
Magic Numbers Are Difficult to Update
Another serious drawback to magic numbers is that they're difficult to keep updated. Say you're developing a financial application and the current mortgage interest rate is 7.25 percent. Also assume that this value is hard-coded in a number of procedures that perform calculations based on the interest rate. What do you do when the rates change (which they do regularly)? You could perform a global search and replace, but that exposes your code to errors. Another loan rate used within the application might also have the value 7.25. If you perform a global search and replace on 7.25 percent, you'll change that loan rate as well. If you manually change each value in the code, you risk transposing or otherwise mistyping the new values. A selective search and replace with a confirmation on each change would be time-consuming. If you use a constant instead, you simply change the value once (in the constant's declaration), and every line of code that uses the mortgage interest rate instantly uses the new, updated rate.
Constants Make Code Easier to Read
A by-product of using constants to create more error-free code is code that is much more readable. Generally, magic numbers are anything but intuitive. They might be obvious to you, but they can be difficult for others to decipher. By intelligently naming your constants, the code that uses those constants becomes a bit more self-documenting and a lot easier to read. Consider the following two code statements. Which makes the most sense to you?
curInterestAmount = (curLoanAmount * .06) / 12
curInterestAmount = _ (curLoanAmount * c_sngInterestRate) / c_intMonthsInTerm
One last note on constants: it's quite acceptable to give constants higher scope, unlike when you use variables, a situation in which it's highly advisable to reduce scope. As a matter of fact, you should never create the same constant twice within an application. If you find that you're duplicating a constant, move the original declaration to a higher scope until the constant becomes available to all procedures that reference it.
Enumerations are similar to constants in that they are named entities that are assigned values. However, enumerations behave like groups of public constants in a module. They're treated as data types, and you use them to create constants of suitable values for variables and properties. You might already be using enumerations in Visual Basic. For instance, when you use the MsgBox statement, Visual Basic's Auto List Members feature displays a drop-down list (commonly referred to as the "code helper drop-down list") for the Buttons parameter. (See Figure 5-1.)
Figure 5-1. Enumerations eliminate the need to memorize many different parameter values.
The time that the developers of the MsgBox statement invested to provide the Buttons parameter's values in an enumeration pays you dividends as a programmer. You never have to remember the numeric values of the parameter, and the chance of incorrectly specifying a value is greatly diminished. Although you can still specify a numeric value for a parameter rather than the name of an enumeration member, you should never do so. To actually use a magic number when an associated member name is available is just south of insane.
Creating Custom Enumerations
You create enumerations much like you do user-defined data types. In the Declarations section of a module, you type the word Public or Private, type Enum, and then type the name of your custom enumeration. The following is a sample enumeration:
Public Enum otBorderStyle otNone = 0 otRaised_Light = 1 otRaised_Heavy = 2 otSunken_Light = 3 otSunken_Heavy = 4 End Enum
This enumeration creates an enumerated type with five values. Although you always refer to the name of an enumeration member when you write code, the name simply represents its numeric value, much like a constant. All members of enumerations are long integers; you can't use other data types.
Using a Custom Enumeration
Once you've created an enumeration, you can use it as the data type for any variable, Function procedure, or Property procedure within the scope of the enumeration. For instance, to create a BorderStyle property that uses the enumerated type shown previously, you can declare a procedure like this:
Public Property Let BorderStyle(lngNew_BorderStyle _ As otBorderStyle) End Property
This property procedure accepts a value into the parameter lngNew_BorderStyle. lngNew_BorderStyle is a long integer because it is declared as an enumerated type and all members of enumerations are long integers. When you reference this property in code, Visual Basic displays the code helper drop-down list with all the enumeration members, as shown in Figure 5-2.
Figure 5-2. Custom enumerations offer the same advantages and behavior as system enumerations.
You can also use an enumerated type as the data type returned by a function or a Property procedure. For instance, to create the Property Get procedure that corresponds to the previous Property Let procedure, you can use code like this:
Public Property Get BorderStyle() As otBorderStyle End Property
Note that simply declaring a parameter as an enumerated type does not guarantee that the value passed to the parameter will be one of the defined enumeration members. As a matter of fact, this is one of the most common misconceptions about enumerated types. When you define an enumeration, you define a list of named values, but these are not the only values that a parameter of that enumerated type will accept. As mentioned previously, parameters that are defined as enumerated types are actually long integers. As such, they accept any value that fits into a long integer; Visual Basic does not confirm that the number passed into a parameter corresponds to a member of the enumeration. For this reason, you should always validate the data passed to a parameter declared as an enumerated type, as I'll discuss in Directive 5.8.
Goals of Using Constants and Enumerations
The goals of using constants and enumerations include
5.1 Prefix all constants with c_ and a scope designator.
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
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
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 correctaccording to the directives presented in this bookbecause 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
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.Practical Applications
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.
Const USDATE = "mm/dd/yyyy" Const KEYCONTROL = 17
Const c_USDate = "mm/dd/yyyy" Const c_KeyControl = 17
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
Private Const mc_US_Date = "mm/dd/yyyy" Public Const gc_KeyControl = 17
5.2 Use constants in place of magic numbers, regardless of scope.
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."
'* 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
'* 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
5.3 Use enumerations whenever they are available.
When a procedure or a parameter is declared as an enumerated type, it's common courtesynay, it's your dutyto 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.
MsgBox "Print all documents?", 36
MsgBox "Print all documents?", vbYesNo Or vbQuestion
5.4 Use constants when you refer to elements of a control array.
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 numbercomplete 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.
txtPhone(0).Text txtPhone(1).Text txtPhone(2).Text
Const c_txtHomePhone = 0 Const c_txtWorkPhone = 1 Const c_txtFax = 2 txtPhone(c_txtHomePhone).Text txtPhone(c_txtWorkPhone).Text txtPhone(c_txtFax).Text
5.5 Use an application or company prefix for enumeration members.
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
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.
Public Enum BackTrackItem Account = 0 ServiceOrder = 1 Quote = 2 Contact = 3 PriceBookItem = 4 PurchaseOrder = 5 Task = 6 End Enum
Public Enum BackTrackItem tpsAccount = 0 tpsServiceOrder = 1 tpsQuote = 2 tpsContact = 3 tpsPriceBookItem = 4 tpsPurchaseOrder = 5 tpsTask = 6 End Enum
Public Enum tpsBackTrackItem tpsAccount = 0 tpsServiceOrder = 1 tpsQuote = 2 tpsContact = 3 tpsPriceBookItem = 4 tpsPurchaseOrder = 5 tpsTask = 6 End Enum
5.6 Use system constants when enumerations aren't available.
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 enumerationwell, 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.
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.
With Me .BorderStyle = 1 .WindowState = 0 .ScaleMode = 3 .DrawMode = 13 End With
With Me .BorderStyle = vbFixedSingle .WindowState = vbNormal .ScaleMode = vbPixels .DrawMode = vbCopyPen End With
5.7 Use an enumeration whenever a parameter accepts a limited number of values.
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.
Public Sub ShowAVIFile(lngType As Long)
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)
5.8 Validate values that are passed as enumerated types.
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.
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.
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
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.
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
Practical Standards for Microsoft® Visual Basic®
Last Updated: Friday, July 6, 2001