Chapter 20. Reusing Code with Procedures

Sometimes a program needs to perform the same action in several places. For example, suppose you're using a simple editor such as WordPad, you make some changes, and then you select the File menu's New command. The program realizes that you have unsaved changes and asks if you want to save them. Depending on whether you click Yes, No, or Cancel, the program saves the changes, discards the changes, or cancels the attempt to create a new file.

Now consider what happens when you try to open a file while you have unsaved changes. The program goes through basically the same steps, asking if you want to save the changes. It does practically the same thing if you select the File menu's Exit command, or click the X in the program's upper-right corner, or open the window's system menu and select Close, or press [Alt]+F4. In all of these cases, the program performs the same checks.

Instead of repeating code to handle unsaved changes everywhere it might be needed, it would be nice if you could centralize the code in a single location and then invoke that code when you need it. In fact, you can do exactly that by using a procedure.

A procedure is a group of programming statements wrapped in a neat package so you can invoke it as needed. A procedure can take parameters that the calling code can use to give it information, it can perform some actions, and then it can return a single value to pass information back to the calling code.

In this lesson, you learn how to use procedures. You learn why they are useful, how to write them, and how to call them from other places in your code.

PROCEDURE ADVANTAGES

The file-editing scenario described in the previous section illustrates one of the key advantages of procedures: code reuse. By placing commonly needed code in a single procedure, you can reuse that code in many places. Clearly, that saves you the effort of writing the code several times.

Much more important, it also saves you the trouble of debugging the code several times. Often, debugging a piece of complex code takes much longer than typing in the code in the first place, so being able to debug the code in only one place can save you a lot of time.

Reusing code also greatly simplifies maintenance. If you later find a bug in the code, you only need to fix it in one place. If you have several copies of the code scattered around, you need to fix each one individually and make sure that all of the fixes are the same. That may sound easy enough, but making synchronized changes is actually pretty hard, particularly for larger projects. It's just too easy to miss one change or to make a slightly different change that later causes a big problem.

Procedures can sometimes make finding and fixing bugs much easier. For example, suppose you're working on an inventory program that can remove items from inventory for one of many reasons: external sales, internal sales, ownership transfer, spoilage, and so forth. Unfortunately, the program occasionally removes items that don't exist, leaving you with negative inventory. If the program has code in many places that can remove items from inventory, figuring out which place is causing the problem can be tricky. Conversely, if all the code uses the same procedure to remove items, you can set breakpoints inside that procedure to see what's going wrong. When you see the problem occurring, you can trace the program's flow to determine where the problem originated.

A final set of advantages to using procedures is that it makes the pieces of the program easier to understand and use. Breaking a complex calculation into a series of simpler procedure calls can make the code easier to understand. No one can keep all the details of a large program in mind at once. Breaking the program into procedures makes it possible to understand the pieces separately.

To provide the most benefit, a procedure should encapsulate its task at an abstract level so other developers don't need to know the details. For example, you could write a FindItemForPurchase procedure that searches through a database of vendors to find the best possible deal on a particular item. This enables developers writing other parts of the program to call that procedure without needing to understand exactly how the search works. The procedure might perform an amazingly complex search to minimize price and long-term expected maintenance costs but the programmer calling the procedure doesn't need to know or care how it works.

In summary, key benefits of using procedures include the following:

  • Code reuse — You write the code once and use it many times.

  • Centralized debugging — You only need to debug the shared code once.

  • Centralized maintenance — If you need to modify or fix the code, you only need to do so in the procedure, not everywhere it is used.

  • Problem decomposition — Procedures can break complex problems into simple pieces.

  • Encapsulation — Procedures can hide complex details from developers.

PROCEDURE SYNTAX

Visual Basic provides several different kinds of procedures that have slightly different syntax:

  • A subroutine, subprocedure, or simply sub is a procedure that performs some action but does not return any value.

  • A function is a procedure that returns a value.

  • A method is a subroutine or function provided by a class. This term is used to emphasize the fact than a class provides the method. Lesson 24 says more about class methods.

  • A property procedure is a procedure used to implement a property for a class. Lesson 23 says more about defining properties.

Note

This book uses the terms subroutine and function when appropriate. When the difference doesn't matter, it uses the term procedure.

Subroutine Syntax

The syntax for defining a subroutine is as follows:

[accessibility] Sub name([parameters])
    ...statements...
End Sub

Where:

  • accessibility — This is an accessibility keyword such as Public or Private. This keyword determines what other code in the project can invoke the procedure. If you omit the accessibility, it defaults to Public.

  • name — This is the name that you want to give the procedure. You can give the procedure any valid name, although by convention most developers use Pascal case. Valid names must start with a letter or underscore and include letters, underscores, and numbers. A valid name cannot be a keyword such as If or While.

  • parameters — This is an optional parameter list that you can pass to the procedure. I'll say more about this shortly.

  • statements — These are the statements that the procedure should execute.

For example, the following subroutine displays a message box that greets the user by name. This subroutine takes no parameters.

Public Sub SayHi()
    MessageBox.Show("Hello " & SystemInformation.UserName)
End Sub

Note

You cannot define a procedure inside another procedure. (You can, however, make anonymous methods that are sort of like procedures inside other procedures. These are quite advanced so I won't discuss them in any depth.)

The following code shows how the program might invoke the subroutine:

Private Sub Form1_Load() Handles MyBase.Load
    SayHi()
End Sub

Note

A subroutine need not be defined before the code that uses it in the file. It can be defined before or after the code that uses it, or even in another file as long as it has the right accessibility.

A subroutine executes its code until it reaches the End Sub statement, at which point control returns to the calling code.

You can also use one or more Return statements to make the subroutine return to the calling code early, skipping any code that comes later.

Note

A subroutine can also return to the calling code by using an Exit Sub statement instead of using Return.

For example, the following subroutine checks the username and password textboxes. If either is blank, the subroutine returns. If both are filled in, the subroutine continues to run other code to validate the user.

Public Sub ValidateUser ()
    If txtUsername.Text.Length < 1 Then Return
    If txtPassword.Text.Length < 1 Then Return

    ' Look up the user in the database.
    ...
End Sub

Note

If you have a long and complicated subroutine, noticing a Return statement buried in the middle can be hard, which can make debugging the code difficult. For that reason, some programmers prefer not to use Return statements in subroutines and always make them exit through the End Sub statement.

You should probably break a long and complicated subroutine into smaller pieces that are easier to understand anyway.

Function Syntax

The syntax for defining a function is similar to the syntax for defining a subroutine, with the addition of a return type:

[accessibility] Function name([parameters]) As returnType
    ...statements...
End Function

The only new piece (aside from using the keyword Function instead of Sub) is returnType. This indicates the data type returned by the function.

To exit a function, use the Return keyword followed by the value the function should return to the calling code.

For example, the following function examines the current hour of the day and builds a greeting message accordingly. The Return statement at the end makes the function return the message.

Public Function Greeting() As String
    Dim message As String
    Select Case Date.Now.Hour
        Case Is < 12
            message = "Good morning "
        Case Is < 17
            message = "Good afternoon "
        Case Else
            message = "Good evening "
    End Select

    Return message
End Function

You can use more than one Return statement to make the function return in several places. For example, you could rewrite the Greeting function like this:

Private Function Greeting() As String
    Select Case Date.Now.Hour
        Case Is < 12
            Return "Good morning "
        Case Is < 17
            Return "Good afternoon "
        Case Else
            Return "Good evening "
    End Select
End Function

Once you have defined a function, you can invoke it and treat its result just as you would treat any literal or variable value of that data type.

For example, the following version of the SayHi subroutine calls the Greeting function and concatenates its result to the value returned by SystemInformation.UserName to produce a greeting message:

Public Sub SayHi()
    MessageBox.Show(Greeting() & SystemInformation.UserName)
End Sub

Parameters

A procedure's declaration can include a comma-separated list of parameters that allow calling code to pass information into the procedure. Each parameter's declaration begins with the keyword ByVal or ByRef, followed by the parameter's name and its data type.

The following section says more about ByVal and ByRef but first it's worth looking at an example.

Recall the definition of the factorial function. The factorial of a number N is written N! and pronounced N factorial. The definition of N! is 1 * 2 * 3 * ... * N.

The following Visual Basic code implements the factorial function:

' Return value!
Public Function Factorial(ByVal value As Long) As Long
    Dim result As Long = 1
    For i As Long = 2 To value
        result *= i
    Next i
    Return result
End Function

This function's parameter list declares a parameter named value of type Long. The name value is the name that the parameter has inside the function. It behaves mostly like a variable declared within the function by using the Dim statement.

The following code invokes the Factorial function:

Dim number As Long = CLng(txtNumber.Text)
Dim result As Long = Factorial(number)
MessageBox.Show(result.ToString())

This code converts text entered by the user into a Long and saves it in the variable number. It then calls the Factorial function, passing number in as a parameter.

At this point control moves to the Factorial function. Inside that function, whatever value was passed to it (in this case, whatever was in the number variable) is known by the parameter name value. The function performs its calculations and returns a result.

Control then returns to the Click event handler, which saves the value returned by Factorial in the variable result. Finally, the code displays the result in a message box.

A procedure's parameter list can include zero, one, or more parameters separated by commas. For example, the following code defines the function Gcd, which returns the greatest common divisor (GCD) of two integers. (The GCD of two integers is the largest integer that evenly divides them both.)

' Calculate GCD(a, b).
Private Function Gcd(ByVal a As Long, ByVal b As Long) As Long
    Dim remainder As Long
    Do
        remainder = a Mod b
        If (remainder <> 0) Then
            a = b
            b = remainder
        End If
    Loop While (remainder > 0)

    Return b
End Function

The following code shows how you might call the Gcd function. The code initializes two integers and passes them to the Gcd function, saving the result. It then displays the result in a message box.

Dim a As Long = CLng(txtA.Text)
Dim b As Long = CLng(txtB.Text)
Dim result As Long = Gcd(a, b)
MessageBox.Show(result.ToString())

ByVal and ByRef

Parameter lists have one more feature that's confusing enough to deserve its own section. Parameters can be passed to a procedure by value or by reference.

When you pass a parameter by value, Visual Basic makes a copy of the value and passes the copy to the procedure. The procedure can then modify its copy without damaging the value used by the calling code.

In contrast, when you pass a value by reference, Visual Basic passes the location of the value's memory into the procedure. If the procedure modifies the parameter, the value is changed in the calling code as well.

Normally values are passed by value. That's less confusing because changes that are hidden inside the procedure cannot confuse the calling code. Sometimes, however, you may want to let a procedure modify a parameter. For example, suppose you want to write a function named GetMatchup that selects two chess players to play against each other. The method should return True if it can find a match and False if no other matches are possible (because they've all been played). The function can only return one value (True or False), so it returns the two players through parameters passed by reference.

The following code shows how the method might be structured:

Private Function GetMatchup(ByRef player1 As Integer,
 ByRef player2 As Integer) As Boolean
    ' Do complicated stuff to pick an even match.
    ...
' Somewhere in here the code sets player1 and player2.
    ...

    ' We found a match.
    Return True
End Function

The method takes two parameters, player1 and player2, that are passed by reference. The method performs some complex calculations not shown here to assign values to player1 and player2. It then returns True to indicate that it found a match.

The following code shows how a program might call this method:

Dim playerA, playerB As Integer
If (GetMatchup(playerA, playerB)) Then
    ' Announce this match.
    ...
Else
    ' No match is possible. We're done.
    '...
End If

This code declares variables playerA and playerB to hold the selected players' names. It calls the GetMatchup method, passing it the two player name variables. Depending on whether the method returns True or False, the program announces the match or does whatever it should when all the matches have been played.

Notice that there's no indication in the calling code that these parameters are being passed by reference. That can make understanding and debugging the code more confusing. Because of that potential for confusion, it's considered bad practice to return results from a procedure through parameters passed by reference.

A better approach is to use a function that takes input parameters by value and returns as a result a structure holding all the output values.

Note

The ByVal and ByRef keywords are more confusing when the parameter is a reference. If you pass a reference variable ByVal, then the procedure can change the properties of the object to which it refers; it just cannot change the object to which the original variable points in the calling code.

For example, suppose a piece of code has a Person variable named customer and passes it ByVal to the SendInvoice subroutine. That subroutine could change the object's FirstName and LastName properties, and the changes will be visible in the calling code. However, if it changes the parameter so it points to a different Person object, the variable customer will still point to the original object when the code returns because ByVal prevents changes to the variable itself.

CODE MODULES

If you place a procedure in a form's code file, it is visible to all the other code inside that form. In fact, if you make the procedure public, then it is visible from the code in other forms, too. For example you could give a dialog a public subroutine that the main program could call to make the dialog perform some action.

But what if you have a procedure that doesn't really apply to a single type of form? For example, consider the Factorial and Gcd functions shown earlier in this lesson. They really don't have much to do with a particular kind of form so why should their code be specific to a particular type of form? Placing them inside a form's code would make it difficult to use them from another form's code.

To solve this problem, Visual Basic provides code modules. A code module is a code file that is not associated with any particular form or other class. All the code in the whole application can see the code in a module, at least if its accessibility allows.

To create a code module, open Visual Studio's Project menu and select Add Module. On the Add New Item dialog, give the module a meaningful name and click Add. Now you can enter procedures inside the module.

In addition to procedures, a code module can contain fields similar to those you can put in a form's code. If you declare a field with public accessibility, then the field is essentially a global variable that is visible from any code in the application.

The following code shows a complete module named MathTools. It defines a global constant named GoldenRatio and a Factorial function, both of which are accessible from any code in the application.

Module MathTools
    Public Const GoldenRatio As Double = 1.61803398874

    ' Return value!
    Private Function Factorial(ByVal value As Long) As Long
        Dim result As Long = 1
        For i As Long = 2 To value
            result *= i
        Next i
        Return result
    End Function
End Module

TRY IT

In this Try It, you make a procedure that calculates the minimum, maximum, and average values for an array of Doubles. You build the program shown in Figure 20-1 to test the procedure.,

FIGURE 20-1

Figure 20.1. FIGURE 20-1

Note

You can download the code and resources for this Try It from the book's web page at www.wrox.com or www.vb-helper.com/24hourvb.html. You can find the code in the Lesson20 folder.

Lesson Requirements

In this lesson:

  • Build the program shown in Figure 20-1.

  • Build a subroutine that takes four parameters: an array of Doubles, and three more Doubles passed by reference. It should loop through the array to find the minimum, maximum, and average.

  • Make the form call the subroutine and display the results.

Hints

  • Use Split to break the input string into pieces. Then loop through the strings, parsing them to make the array of Doubles.

Step-by-Step

  • Build the program shown in Figure 20-1.

    1. This is reasonably straightforward.

  • Build a subroutine that takes four parameters: an array of Doubles, and three more Doubles passed by reference. It should loop through the array to find the minimum and maximum, and to calculate the average.

    1. Initialize minimum and maximum variables to the first entry in the array. Create a total variable and initialize it to the first item, too.

    2. Loop through the rest of the array (skipping the first entry), updating the minimum and maximum variables as needed, and adding the values in the array to the total.

    3. Divide the total by the number of values to get the average.

      The following code shows how you might build this subroutine:

      ' Calculate the minimum, maximum, and average values for the array.
      Private Sub FindMinimumMaximumAverage(ByVal values() As Double,
       ByRef minimum As Double,
       ByRef maximum As Double,
       ByRef average As Double)
          ' Initialize the minimum, maximum, and total values.
          minimum = values(0)
          maximum = values(0)
          Dim total As Double = values(0)
      ' Loop through the rest of the array.
          For i As Integer = 1 To values.Length - 1
              If (values(i) < minimum) Then minimum = values(i)
              If (values(i) > maximum) Then maximum = values(i)
              total += values(i)
          Next i
      
          ' Calculate the average.
          average = total / values.Length
      End Sub
  • Make the form call the subroutine and display the results.

    1. The following code shows how you might use Split to break the inputs apart, loop through them to build the array of Doubles, and call the subroutine:

      ' Find and display the minimum, maximum, and average of the values.
      Private Sub btnCalculate_Click() Handles btnCalculate.Click
          ' Get the values.
          Dim textValues() As String = valuesTextBox.Text.Split()
          Dim values(0 To textValues.Length - 1) As Double
          For i As Integer = 0 To textValues.Length - 1
              values(i) = Double.Parse(textValues(i))
          Next i
      
          ' Calculate.
          Dim smallest, largest, average As Double
          FindMinimumMaximumAverage(values, smallest, largest, average)
      
          ' Display the results.
          minimumTextBox.Text = smallest.ToString()
          maximumTextBox.Text = largest.ToString()
          averageTextBox.Text = average.ToString("0.00")
      End Sub

Note

This lesson mentions that returning values through parameters passed by reference isn't a good practice, so how could you modify this example to avoid that?

You could break the FindMinimumMaximumAverage subroutine into three separate functions, FindMinimum, FindMaximum, and FindAverage, and have each return its result by using a Return statement. In addition to avoiding parameters passed by reference, that makes each routine perform a single, well-focused task, making them easier to understand and use. It also makes them easier to use separately in case you only wanted to find an array's minimum and not its maximum or average.

(Also note that the Array class provides methods that can find these values for you, so you really don't need to write these functions anyway. This Try It is here purely to demonstrate parameters passed by reference.)

Note

Please select Lesson 20 on the DVD to view the video that accompanies this lesson.

EXERCISES

  1. Make a program that calculates the least common multiple (LCM) of two integers. (The LCM of two integers is the smallest integer that the two numbers divide into evenly.) Hints: LCM(a, b) = a * b GCD(a, b). Also, don't write the LCM function from scratch. Instead, make it call the GCD function described earlier in this lesson.

  2. A recursive procedure is one that calls itself. Write a recursive factorial function by using the following definition:

    0! = 1
    N! = N * (N-1)!

    Hint: Be sure to check the stopping condition N = 0 so the function doesn't call itself forever.

  3. Write a program that recursively calculates the Nth Fibonacci number using the following definition:

    Fibonacci(0) = 0
    Fibonacci(1) = 1
    Fibonacci(N) = Fibonacci(N - 1) + Fibonacci(N - 2)
  4. (SimpleEdit) Copy the SimpleEdit program you built in Lesson 18, Exercise 3 (or download Lesson 3's version from the book's web site) and move the code that checks for unsaved changes into a function named IsDataSafe. The IsDataSafe function should perform the same checks as before and return True if it is safe to continue with whatever operation the user is about to perform (new file, open file, or exit).

    Other code that needs to decide whether to continue should call IsDataSafe. For example, the mnuFileNew_Click event handler can now look like this:

    Private Sub mnuFileNew_Click() _
     Handles mnuFileNew.Click, btnNew.Click
        ' See if it's okay to continue.
        If IsDataSafe() Then
            ' Make the new document.
            rchDocument.Clear()
    
            ' There are no unsaved changes now.
            rchDocument.Modified = False
        End If
    End Sub
  5. Recursive procedures can be very confusing to understand and debug, so often it's better to write the procedure without recursion. Some problems have natural recursive definitions but usually a nonrecursive procedure is better and sometimes faster.

    Copy the program you made for Exercise 3 and replace the Fibonacci function with this nonrecursive version:

    ' Calculate the n-th Fibonacci number recursively.
    ' Note that this value is only correct if n >= 0.
    Private Function Fibonacci(ByVal n As Long) As Long
        ' Initialize the base cases.
        Dim fiboN As Long = 0           ' Initially Fibonacci(0).
        Dim fiboNMinus1 As Long = 0     ' Initially Fibonacci(0).
        Dim fiboNMinus2 As Long = 1     ' Initially Fibonacci(1).
    
        ' Calculate the result.
        For i As Long = 1 To n
            ' Calculate fiboN from fibo1 and fibo2.
            fiboN = fiboNMinus1 + fiboNMinus2
    
            ' Update fibo1 and fibo2 for the next loop.
            fiboNMinus2 = fiboNMinus1
            fiboNMinus1 = fiboN
        Next i
    
        Return fiboN
    End Function

    Compare the performance of this program and the one you wrote for Exercise 3 when N is around 35 or 40. Also notice that you don't need to modify the rest of the program to replace the Fibonacci function.

Many of the examples and exercises in earlier lessons use duplicated code. For further practice, rewrite some of them to move the duplicated code into procedures.

Note

You can find solutions to this lesson's exercises in the Lesson20 folder inside the download available on the book's web site at www.wrox.com or www.vb-helper.com/24hourvb.html.

EXERCISES
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset