Practical Standards for Microsoft Visual Basic .NET (Pro-Developer)
[Previous] [Next]
11.1 Use If Then Else when the decision is based on one condition being True or False.
If Then is well suited for making a decision based on the evaluation of a single condition, and it's the most commonly used decision construct. If Then has the following syntax:
If condition Then statement |
or
If condition Then [ statements ] EndIf |
If condition evaluates to True, the statement or statements are executed. If condition evaluates to False, the statement or statements are not executed. Expressions can be simple or complex. Regardless of complexity, the expression used for condition must evaluate to True or False. For instance, all the following expressions are valid for condition .
sngCoordinate>=8 strFirstName="Adam" blnInHere |
Although at first glance the last item doesn't appear to be a condition, remember that a condition always evaluates to either True or False. Since blnInHere refers to a Boolean variable (as denoted by its prefix), blnInHere inherently evaluates to True or False and is therefore a valid condition.
NOTE
If condition evaluates to a numerical value, Microsoft Visual Basic interprets the result as True or False; a zero value is considered False, and all nonzero numeric values are considered True.
To execute a statement or a set of statements when condition evaluates to False, use an Else statement. All statements between Else and End If are executed when condition evaluates to False. All statements between If Then and Else execute only if condition evaluates to True; never do both sets of statements execute in a single pass through an If End If construct.
Practical Applications
11.1.1 Consider using End If , even if only one statement is executed. If only one statement executes when condition evaluates to True, the statement can be put on the same line as If and End If can be omitted. However, to make the code more legible, you might consider placing the statement on its own line and closing the construct with End If .
Incorrect:
'*Iflockingconflictswereencountered,buildthisinformation '*intothemessagestring. If lngLockingConflicts>0 Then strMessage=strMessage&","&_ lngLockingConflicts&_ "contact(s)couldnotbeadded"&_ "duetolockingconflicts!" |
Correct:
'*Iflockingconflictswereencountered,buildthisinformation '*intothemessagestring. If lngLockingConflicts>0 Then strMessage=strMessage&","&lngLockingConflicts&_ "contact(s)couldnotbeaddeddueto"&_ "lockingconflicts!" EndIf |
11.1.2 Don't assume that Visual Basic will short-circuit a compound condition. When creating an If Then decision structure, it's possible to create a compound condition composed of multiple smaller conditions. Consider the following code:
IfNot ((rstAlarms IsNothing ) Or (rstAlarms.EOF)) Then rstAlarms.Delete EndIf |
This decision structure attempts to delete a record if the Recordset's EOF (end-of-file) property is False. First, the If statement makes sure that the variable rstAlarms actually contains a Recordset, and then it makes sure that the EOF property is False. This code, however, has the potential to generate a run-time error.
If rstAlarms doesn't contain a Recordset ( rstAlarms Is Nothing evaluates to True), you might think that Visual Basic wouldn't evaluate the second part of the condition, which is rstAlarms.EOF . With an Or compound condition, if either of the individual conditions evaluates to True, the entire condition evaluates to True. Therefore, with the first part of the compound condition being True ( rstAlarms Is Nothing ), there's really no need to evaluate the second part. Unfortunately, this is not how Visual Basic behaves: Visual Basic evaluates each component of a compound condition regardless of whether it's necessary. In this example, if rstAlarms indeed contains no Recordset, attempting to determine whether the Recordset's EOF property is True causes a run-time error. Some programming languages evaluate the individual conditions (from left to right) in a compound condition only until the value of the compound condition is clear. The remaining individual conditions are not evaluated and therefore cannot cause run-time errors. This behavior is called short-circuiting , and although it works nicely in some languages, relying on it in Visual Basic creates potential land mines in your code.
Incorrect:
PublicFunction FirstValidItemID() AsString '*Purpose:ReturntheItemIDofthefirstitemin '*tblPhysicalInventory. '*Returns:TheItemIDofthefirstitem,ifithasone. '*Otherwisereturns"<notvalid>". Dim strSQL AsString Dim rstInventory As Recordset Const c_NotValid="<notvalid>" '*OpentblPhysicalInventory. Set rstInventory=dbInventory.OpenRecordset_ ("tblPhysicalInventory",dbOpenForwardOnly) '*SincetheRecordsetisForwardOnly,itwillbepositioned '*onthefirstrecordifthereareanyrecords. '*ChecktoseewhetherthefirstrecordhasanItemID. IfNot (rstInventory.EOF) And rstInventory![ItemID]<>"" Then FirstValidItemID=rstInventory![ItemID] Else FirstValidItemID=c_NotValid EndIf PROC_EXIT: ExitFunction EndFunction |
Correct:
PublicFunction FirstValidItemID() AsString '*Purpose:ReturntheItemIDofthefirstitemin '*tblPhysicalInventory. '*Returns:TheItemIDofthefirstitem,ifithasone. '*Otherwisereturns"<notvalid>". Dim strSQL AsString Dim rstInventory As Recordset Const c_NotValid="<notvalid>" '*OpentblPhysicalInventory. Set rstInventory=dbInventory.OpenRecordset_ ("tblPhysicalInventory",dbOpenForwardOnly) '*Defaultthereturnvalueas"<notvalid>". FirstValidItemID=c_NotValid '*SincetheRecordsetisForwardOnly,itwillbepositioned '*onthefirstrecordifthereareanyrecords. '*Checktoseewhetherthereareanyrecords. IfNot (rstInventory.EOF) Then '*Thereisarecord;returntheItemIDifithasone. If rstInventory![ItemID]<>"" Then FirstValidItemID=rstInventory![ItemID] EndIf EndIf PROC_EXIT: ExitFunction EndFunction |
11.2 Use Select Case when comparing a non-Boolean expression to a variety of possible values.
When evaluating an expression that has only two possible values (True or False), If Then is the best decision construct to use. If you must take some action when the expression evaluates to False as well as when it evaluates to True, add an Else clause. Because there's no easy way to use If Then to compare an expression to more than two possible values, however, Select Case becomes the best choice in that situation. A typical Select Case construct has the following syntax:
SelectCase testexpression [ Case expressionlist1 [ statementblock-1 ]] [ Case expressionlist2 [ statementblock-2 ]] |
NOTE
Select Case can be used in many advanced ways, including putting multiple result values on a single Case line. My intent in this chapter is simply to show you when to use it and how to use it properly.
At times, you might find that you want to perform an action only when a given condition is True. If the condition evaluates to False, you want to evaluate a second condition and execute code based on the results of this new condition. You can create quite complex decision structures by using this method. The following shows a skeleton of just such a decision construct:
If condition1 Then |
Notice that these conditions can be entirely unrelated. This is in contrast to the Select Case structure where the conditions are formed by successively comparing one (typically non-Boolean) test expression to each of the expressions in the Case statements. If you find that you are evaluating the same expression and comparing it to a variety of possible values, you should use Select Case rather than If Then ElseIf End If .
Incorrect:
'*Setthepushedstateoftheproperoptionbuttoninthecontrol '*array. If rstTask![Type]="PhoneCall" Then optType(c_PhoneCall).Value= True ElseIf rstTask![Type]="Appointment" Then optType(c_Appointment).Value= True ElseIf rstTask![Type]="To-do" Then optType(c_Todo).Value= True EndIf |
Correct:
'*Setthepushedstateoftheproperoptionbuttoninthecontrol '*array. SelectCase rstTask![Type] CaseIs ="PhoneCall" optType(c_PhoneCall).Value= True CaseIs ="Appointment" optType(c_Appointment).Value= True CaseIs ="To-do" optType(c_Todo).Value= True CaseElse '*Noothervaluesareexpected. EndSelect |
NOTE
You can use Select Case in ways that might not be immediately apparent. For instance, when you have a number of option buttons on a form, you often need to determine which of the option buttons is selected ( Value = True ). If you consider True as your test expression, you can create a fairly nifty construct to determine which option button is selected:
'*Determinewhichoptionbuttonisselected. SelectCaseTrue CaseIs = optDragBehavior(c_CopyFile).Value |
Practical Applications
11.2.1 Always include a Case Else with every Select Case construct, even when it's not needed. Generally, you design a Select Case construct such that it handles every possible value of testexpression . Case Else is useful for creating code to execute when none of your specifically expected results are encountered. However, many developers often leave out Case Else if they've included Case statements for all expected results. In general, it's a good idea to always include Case Else . If you want to ignore the results not specifically accounted for in the Case statements, simply note this in a comment in the Case Else clause. If you firmly believe that no value would cause Case Else to execute ”that is, if all possible results are accounted for in the Case statements ”consider raising an error in the Case Else clause. That way, if a value slips through ”which could happen if additional possible results are created during future development and the Select Case structure is not properly updated ”you'll know about it. Always including a Case Else clause makes your code more self-documenting and you don't force other developers to guess your intentions for handling results not specifically accounted for in Case statements.
Incorrect:
SelectCase m_intActiveSearchType CaseIs =c_ListSearch txtSearch.Text=grdListFind.Columns(c_ListFindName) CaseIs =c_PhoneSearch txtSearch.Text=grdPhones.Columns(c_PhoneFindName) CaseIs =c_PriceBookSearch txtSearch.Text=grdPriceBook.Columns(c_PriceBookFindName) EndSelect |
Correct:
SelectCase m_intActiveSearchType CaseIs =c_ListSearch txtSearch.Text=grdListFind.Columns(c_ListFindName) CaseIs =c_PhoneSearch txtSearch.Text=grdPhones.Columns(c_PhoneFindName) CaseIs =c_PriceBookSearch txtSearch.Text=grdPriceBook.Columns(c_PriceBookFindName) CaseElse '*Allpossiblevaluesshouldbecovered,butjustincase MsgBox"Unexpectedvalueencounteredfor"&_ "m_intActiveSearchTypein"&Me.Name&_ "txtSearch_KeyDown.",vbCritical EndSelect |
11.2.2 Use an intelligible ordering sequence for all Case statements. The order of the various Case statements in a Select Case construct might seem superficial, but it's often quite important. When ordering the statements, consider speed and readability. When a Select Case statement is encountered, the Case statements are evaluated in their listed order until a condition is found to be True. In a large list of items, and when speed is the primary concern, you might consider putting the most frequently expected values at the top of the Case list. More often than not, however, speed is second in importance to readability and ease of maintenance. In these cases, put the list of items in alphabetical or numerical order, which makes it easier to debug the code and to add new values to the Case list.
Incorrect:
SelectCase rstBilling![Basis] CaseIs ="Metered" CallComputeMeteredContract CaseIs ="Hourly" Call ComputeHourlyContract CaseIs ="Units" Call ComputeUnitsContract CaseIs ="Incidents" Call ComputeIncidentsContract EndSelect |
Correct:
SelectCase rstBilling![Basis] CaseIs ="Hourly" Call ComputeHourlyContract CaseIs ="Incidents" Call ComputeIncidentsContract CaseIs ="Metered" CallComputeMeteredContract CaseIs ="Units" Call ComputeUnitsContract CaseElse '*Noothervaluesareexpected. EndSelect |
11.2.3 Don't create a Case statement that will never produce a True result. When creating Select Case structures that evaluate a numeric value, you can create Case statements that never produce a True result. This usually occurs as a result of incorrectly ordering the Case statements, causing an earlier statement to evaluate to True before a later statement is encountered. Notice how the Case Is <= 0 statement in the incorrect code below never evaluates to True. A value of less than 0 causes the preceding Case statement ( Case Is <= 5 ) to evaluate to True, stopping all further evaluations.
Incorrect:
SelectCase sngTaxRate CaseIs <=5 CaseIs <=0 CaseIs <=10 CaseElse EndSelect |
Correct:
SelectCase sngTaxRate CaseIs <=0 CaseIs <=5 CaseIs <=10 CaseElse EndSelect |
11.3 Use end-of-line comments to add clarity to nested decision structures.
Chapter 9, "Commenting Code," describes the proper way to comment a program in great detail. Although Chapter 9 argues that in-line comments are superior to end-of-line comments, end-of-line comments are appropriate at times ”for example, when nested decision structures significantly complicate code. In long procedures, it can be difficult to determine which end-of-construct statement ( End Select or End If ) corresponds to which beginning-of-construct statement. In these situations, use an end-of-line comment after the last statement of each decision structure to state which decision construct the closing statement belongs to.
NOTE
If you prefer, you can use end-of-line comments after the terminating statements of all your decision structures, regardless of whether they're part of a nested group .
Incorrect:
If otnumHours.Value<=0 Then MsgBox"Thisisanhourlycontract.Youmustenterapositive"&_ "numberofhours",vbExclamation otnumHours.SetFocus Else '*Determinewhethertheuserhasenteredarate. If otnumRate.Value>0 Then otnumPrice.Value=otnumHours.Value*otnumRate.Value Else If otnumPrice.Value>0 Then '*Norateentered;checkwhetheracontractpriceisset. '*Ifacontractpriceisset,figuretheratebasedonthe '*hoursentered. otnumRate.Value=otnumPrice.Value/otnumHours.Value EndIf EndIf EndIf |
Correct:
'*Makesuretheuserhasenteredapositivenumberofhours. If otnumHours.Value<=0 Then MsgBox"Thisisanhourlycontract.Youmustenterapositive"&_ "numberofhours",vbExclamation otnumHours.SetFocus Else '*Determinewhethertheuserhasenteredarate. If otnumRate.Value>0 Then otnumPrice.Value=otnumHours.Value*otnumRate.Value Else '*Thereisnorateentered,seeifthereisaprice. If otnumPrice.Value>0 Then '*Acontractpriceisset.Figuretheratebasedonthe '*hoursentered. otnumRate.Value=otnumPrice.Value/otnumHours.Value EndIf '*otnumPrice.Value>0 EndIf '*otnumRate.Value>0 EndIf '*otnumHours.Value<=0 |
11.4 Format expressions for accurate evaluation and ease of understanding.
The evaluation of expressions is at the heart of creating decision structures. Most expressions can be written in multiple ways. Properly formatting an expression reduces the possibility of errors in your code and increases its readability. Since most expressions used in decision structures evaluate to a Boolean value (True or False), correctly working with Boolean expressions is crucial.
Practical Applications
11.4.1 Never compare a Boolean expression to True or False. This seems like a basic principle, but it's one that is often violated. Boolean values are True or False, so there's no need to compare them to True or False. The incorrect code below came from a well-respected Visual Basic magazine. Lack of a naming convention aside, this procedure suffers from a case of overcomplication. BOF and EOF are properties of the Recordset object that indicate that the Recordset is at beginning-of-file or end-of-file, respectively. Each can be either True or False. Since they are inherently Boolean values, comparing them directly to True or False makes the code cluttered and can decrease performance.
Incorrect:
PublicFunction IsEmptyRecordset(rs As Recordset) AsBoolean IsEmptyRecordset=((rs.BOF= True ) And (rs.EOF= True )) EndFunction |
Correct:
PublicFunction IsEmptyRecordset(rs As Recordset) AsBoolean IsEmptyRecordset=rs.BOF And rs.EOF EndFunction |
11.4.2 Create Boolean variable names that reflect the positive rather than the negative. A classic case of overcomplicating a procedure is creating a Boolean variable name that reflects the negative of some condition. Basing decisions on such variables adds an unnecessary layer of complexity ”for instance, why call a variable blnNotLoaded when blnLoaded works just as well and is easier for the mind to deal with? When you work with the negative, you increase the chances for errors in your code because you might not catch problems as you write them. This isn't not like using double negatives in a sentence ”get it? If you must deal with the negative, use Not on the positive form of the variable rather than using the negative form of the variable.
Incorrect:
Dim blnInvalidTemplate AsBoolean '*Attempttoopenthetemplate.ThefunctionOpenTemplatereturns '*successorfailure. blnInvalidTemplate= Not (OpenTemplate(strTemplateName)) '*Ifthetemplateisinvalid,getout. If blnInvalidTemplate Then GoTo PROC_EXIT EndIf |
Correct:
Dim blnValidTemplate AsBoolean '*Attempttoopenthetemplate.ThefunctionOpenTemplatereturns '*successorfailure. blnValidTemplate=OpenTemplate(strTemplateName) '*Ifthetemplateisinvalid,getout. If Not (blnValidTemplate) Then GoTo PROC_EXIT EndIf |
11.4.3 Use parentheses in expressions for clarity, even when they're not required. Parentheses are used in algebraic expressions to override the traditional order of operations. For instance, standard order of operations dictates that multiplication takes place before addition. So, the statement Debug.Print 1 + 5 * 6 prints the value 31. To override this behavior, you use parentheses. Items in parentheses are evaluated first. For instance, Debug.Print (1 + 5) * 6 prints 36. Although you don't have to provide parentheses if you want to use the traditional order of operations, you should use them anyway to add clarity to complicated expressions.
Incorrect:
'*Computetheheightandwidthoftheeditablearea. m_sngEditWidth=m_sngImageWidth*m_intMagnification+1 m_sngEditHeight=m_sngImageHeight*m_intMagnification+1 |
Correct:
'*Computetheheightandwidthoftheeditablearea. m_sngEditWidth=(m_sngImageWidth*m_intMagnification)+1 m_sngEditHeight=(m_sngImageHeight*m_intMagnification)+1 |
11.4.4 Make code flow obvious. When writing decision structures, make the flow of the code as obvious as possible. Pen and paper shouldn't be required for someone to figure out your intentions. The incorrect code below was also culled from a well-known Visual Basic publication. Can you easily tell exactly what's happening here? The code is setting the Cancel parameter equal to the result of the MsgBox function. No the return value of the function is compared to vbCancel , and the result (True or False) is what's stored in the Cancel parameter. Why make the reader work so hard? The revised code performs the same function without any real loss of performance, and it's much easier to understand.
Incorrect:
PrivateSub Form_Unload(Cancel AsInteger ) Cancel=(MsgBox("QuitNow?",vbOKCancel Or _ vbQuestion,"ConfirmationDemo")=vbCancel) EndSub |
Correct:
PrivateSub Form_Unload(Cancel AsInteger ) '*ChecktoseewhethertheuserclickedCancel. If MsgBox("QuitNow?",vbOKCancel Or _ vbQuestion,"ConfirmationDemo")=vbCancel Then Cancel= True EndIf EndSub |
11.5 Refrain from using GoSub whenever possible.
The ability to use GoSub in code dates back to the early days of Basic, when code was linear rather than procedural. GoSub allows you to create a "pseudosubroutine." A GoSub <label> statement causes execution to jump to the specified label, which must be in the same procedure as the GoSub statement. A Return statement causes execution to return to the line following the GoSub statement. GoSubs make code difficult to read and debug. With the advent of procedure-based, event-driven code, a GoSub is rarely needed. If you find yourself writing a GoSub , ask yourself whether the code that you branch to could be handled in-line within the procedure. If not, determine whether it could be turned into a separate procedure. If it can, chances are good that creating a separate procedure is a better approach.
The one time GoSub really comes in handy is when the code in the pseudosubroutine works on a large number of variables local to the procedure. Under certain rare circumstances, the overhead and hassle of passing many local variables to another routine make GoSub a better proposition.
11.6 Use GoTo only when there are no other alternatives or when jumping to an error handler or single exit point.
Although the venerable GoTo statement has been used to force execution to a specific line in a procedure for quite some time, GoTo statements make code difficult to follow because they often redirect the execution path in unintuitive ways. GoTo is perfect, however, for jumping to a single exit point and for jumping to an error handler; otherwise, there's usually a better way to write a process than by using GoTo . Rarely, if ever, should a GoTo statement send code execution back in a procedure (using GoTo PROC_EXIT in an error handler comes to mind). Often, when a GoTo statement sends execution back, some sort of standard looping construct would be a better solution. The following code illustrates how code with a GoTo statement can be better written as a Do loop.
Incorrect:
PrivateFunction StripDoubleQuotes(strString AsString ) AsString '*Purpose:DoublequotescauseerrorsinSQLstatements. '*Thisprocedurestripsthemfromtheenteredtext '*andreplacesthemwithsinglequotes. '*Accepts:strString-thestringinwhichtosearchfor '*doublequotes. '*Returns:Theoriginalstringwithalldoublequotesreplaced '*withsinglequotes. OnErrorGoTo PROC_ERR Dim intLocation AsInteger Const c_DoubleQuote="""" START_CHECK: intLocation=InStr(strString,c_DoubleQuote) '*Determinewhetherornotadoublequotewasfound. If intLocation>0 Then '*Thereisatleastonedoublequote. Mid$(strString,intLocation,1)="'" GoTo START_CHECK Else StripDoubleQuotes=strString EndIf PROC_EXIT: ExitFunction PROC_ERR: Call ShowError(Me.Name,"StripDoubleQuotes",Err.Number,_ Err.Description) GoTo PROC_EXIT EndFunction |
Correct:
PublicFunction StripDoubleQuotes( ByVal strString AsString ) AsString '*Purpose:DoublequotescauseerrorsinSQLstatements. '*Thisprocedurestripsthemfromtheenteredtext '*andreplacesthemwithsinglequotes. '*Accepts:strString-thestringinwhichtosearchfor '*doublequotes. '*Returns:Theoriginalstringwithalldoublequotesreplaced '*withsinglequotes. OnErrorGoTo PROC_EXIT Dim intLocation AsInteger Dim strText AsString Dim blnQuotesFound AsBoolean Const c_DoubleQuote="""" blnQuotesFound= False '*Determinewhetherornotadoublequotewasfound. Do intLocation=InStr(1,strString,c_DoubleQuote) '*Seewhetheradoublequotewasfound. If intLocation>0 Then Mid$(strString,intLocation,1)="'" blnQuotesFound= True Else blnQuotesFound= False EndIf '*Continuelookingfordoublequotesuntilnonearefound. LoopWhile blnQuotesFound StripDoubleQuotes=strString PROC_EXIT: ExitFunction PROC_ERR: Call ShowError(Me.Name,"StripDoubleQuotes",Err.Number,_ Err.Description) GoTo PROC_EXIT EndFunction |
Practical Application
11.6.1 Use all uppercase letters for GoTo labels. GoTo statements make code hard to read. Reduce the amount of effort required to scan a procedure for GoTo labels by using all uppercase letters for those labels.
Incorrect:
PrivateSub lvwPhones_MouseDown(Button AsInteger ,Shift AsInteger ,_ x AsSingle ,y AsSingle ) '*Purpose:Ensurethattheitemclickedisalwaysselected, '*evenifclickedwiththerightmousebutton. OnErrorGoTo proc_err '*Determinewhetherthereisalistitemwheretheuserclicked. If lvwPhones.HitTest(x,y) IsNothingThen Set lvwPhones.SelectedItem= Nothing Else '*Thereisalistitemwheretheuserclicked.Selectitnow. lvwPhones.SelectedItem=lvwPhones.HitTest(x,y) EndIf proc_exit: ExitSub proc_err: Call ShowError(Me.Name,"lvwPhones_MouseDown",Err.Number,_ Err.Description) GoTo proc_exit EndSub |
Correct:
PrivateSub lvwPhones_MouseDown(Button AsInteger ,Shift AsInteger ,_ x AsSingle ,y AsSingle ) '*Purpose:Ensurethattheitemclickedisalwaysselected, '*evenifclickedwiththerightmousebutton. OnErrorGoTo PROC_ERR '*Determinewhetherthereisalistitemwheretheuserclicked. If lvwPhones.HitTest(x,y) IsNothingThen Set lvwPhones.SelectedItem= Nothing Else '*Thereisalistitemwheretheuserclicked.Selectitnow. lvwPhones.SelectedItem=lvwPhones.HitTest(x,y) EndIf PROC_EXIT: ExitSub PROC_ERR: Call ShowError(Me.Name,"lvwPhones_MouseDown",Err.Number,_ Err.Description) GoTo PROC_EXIT EndSub |