Basic procedure declarations haven't changed dramatically between VB6 and Visual Basic .NET. Procedures that do not have a return value are still defined with the keyword Sub and procedures that return a value are still defined with the keyword Function.
You will notice a few subtle changes when implementing and using subroutines and functions. The first such change is the use of parentheses for calling subroutines and functions. Always use parentheses when calling a function or a subroutine, but you don't need to add the Call keyword. We will cover additional changes to procedures in the subsections that follow.
A subroutine is defined by typing the keyword Sub followed by a name and parentheses. The bounds of the procedure are constrained by the End Sub statement. Together the Sub Name and End Sub statements define the procedure block, or the extent of a particular sub.
Sub MySub() End Sub
Any temporary variables defined in a procedure block have procedure scope. That is, variables with procedure scope are only accessible when the program's execution point is within the subroutine. Except for Static variables, procedurally scoped variables don't maintain their values between successive calls to the procedure.
VB6 supported adding the Static modifier to a procedure heading if you wanted all variables in that procedure to be static. Visual Basic .NET does not support this modifier in the procedure heading. If you want every variable in a procedure to be static, you must use the Static modifier on each variable.
In recent years , best practices have been identified and innovations have evolved from these best practices. The section "Defining Procedure Arguments" covers further specific implementation details regarding subroutines and functions. For now, let's examine the subroutine from the perspective of constraining procedure complexity. (The discussions in the next two sections, "Best Practices" and "Innovations in Programming," apply equally to subroutines and functions.)
For many years there have been several well-reasoned best practices that describe what constitutes a good procedure. Unfortunately these best practices haven't been adopted universally . With innovations in programming, specifically refactoring, it's possible that we will have a forum and a language for demonstrating and teaching these best practices to new and experienced developers.
The strategies described here can be reduced to several common maxims about software development. I will introduce each maxim in a separate statement followed by my comments about the problem presented by the maxim .
The high incidence of failure in our industry is evidence that the way in which developers as a group are currently building software has shortcomings. Further, it seems to be apparent that we have discovered better ways of building software but haven't widely adopted many of them. The following bulleted list describes best practices to implement Maxim 6.
Employing the best practices described in this list will help keep you out of trouble at the procedural level. There are additional strategies to address other problems related to developing complex systems, which will be introduced in later chapters as appropriate.
Innovations in Programming
Apparently the term best practices wasn't catchy enough. Writers like Grady Booch were talking about best practices as early as the 1990s (Booch, 1994). Recently Kent Beck's Extreme Programming (XP) and William Opdike's Refactoringwhich are catchierare gaining a following. Specifically, refactoring, an aspect of XP, describes programming best practices in the context of all code. Instead of prescribing the things you should do to proactively prevent code from becoming obtuse, refactoring describes curatives that can be employed in a systematic manner to recondition code into a condition closer to an ideal. Possibly the best benefit of refactoring is that it introduces a forum in which code quality can be discussed and taught in a constructive and consistent manner.
As mentioned in the introduction, refactorings are introduced in this book where appropriate.
All of the guidelines for writing good subroutines apply to functions. The only difference between a function and a subroutine is that a function is designed to return a value. VB6 return values were assigned to the function name. In Visual Basic .NET, the Return statement is used to return the value from a function. The basic syntax of a function is
Function Name () As DataType Return Value End Function
For example, a function that returns an integer would be implemented as follows :
Function GetInteger() As Integer Return 1 End Function
The precedence for introducing the Return statement is that it is close to the actual Ret instruction found at the machine-language level, and other languages have employed a Return statement (or similar construct) successfully for years.
Use functions when you need to return a value, but in general avoid using functions simply to return an error code. Error codes are used much less frequently since the adoption of exception handlers. Where you would have employed a function with a return error code in VB6, use a subroutine and throw an exception in VB .NET.
Using the Return Statement
To demonstrate the Return statement, Listing 5.1 defines a recursive factorial algorithm. Calculating N -factorial means returning the multiplicative value of all the digits from 1 to N. For example, 5 factorial (written in mathematics as 5!) is the value of 1 * 2 * 3 * 4 * 5, or 120.
Listing 5.1 Calculating N-factorial using the Return statement
1: Public Class Form1 2: Inherits System.Windows.Forms.Form 3: 4: [ Windows Form Designer generated code ] 5: 6: Public Sub Check(ByVal Value As Long) 7: If (Value > 0) Then Exit Sub 8: Const Msg = "Factorial value must be greater than 0" 9: Throw New Exception(Msg) 10: End Sub 11: 12: Public Function Factorial(ByVal Value As Long) As Long 13: Debug.Assert(Value > 0) 14: Check(Value) 15: 16: If (Value > 1) Then 17: Return Value * Factorial(Value - 1) 18: Else 19: Return Value 20: End If 21: 22: End Function 23: 24: Private Sub Button1_Click(ByVal sender As System.Object, _ 25: ByVal e As System.EventArgs) Handles Button1.Click 26: 27: TextBox1.Text = Factorial(CLng(TextBox1.Text)) 28: 29: End Sub 30: End Class
Listing 5.1 defines a recursive Factorial function that calls itself when needed to aid in solving N -factorial. (Refer to the section "Working with Recursion" for more about recursive algorithms.) Lines 13 and 14 combine an Assert with a Check subroutine. The Assert is used to alert the developer to problems with the algorithm. In this instance, the assertion will let the programmer know if a consumer of the Factorial class is passing bad data. When you build the release version, all Assert statements are disabled. Assertions are written for the benefit of the developer. In addition to an assertion, you usually need to write runtime error-checking code. The Check method plays the role of runtime error handler.
Line 13 uses an assertion to ensure that callers are passing values greater than 0 to the Factorial function. Using assertions to encourage callers to pass appropriate values to procedures is referred to as a contract program. The test value of the Assert method constitutes the contract. In Listing 5.1, the contract is Value > 0.
Contract programming is an excellent way to ensure that consumers use methods in ways consistent with the needs of the algorithm.
The Check subroutine doesn't return an error code. Instead it throws an exceptionline 9if the value passed to the Factorial function is less than 1. Notice on the other hand that Button1_Click doesn't throw an exception when converting the value of TextBox1 to a Long. If CLng(TextBox1.Text) fails, an InvalidCastException is thrown. The default behaviorraising an InvalidCastException (see Figure 5.1)is the behavior we want, so no pre-type checking is necessary.
Figure 5.1. InvalidCast Exception can be raised when we try to convert from one data type to another as shown (running the executable outside of the IDE).
The recursive Factorial function returns Value if Value is equal to 1. If Value is greater than 1, the current Value is multiplied by Factorial(Value-1). Figure 5.2 shows the stack frame when 5 is passed to the Factorial procedure. The stack shows successive calls to Factorial with the value of the parameter being decreased by one each time Factorial is called.
Figure 5.2. Stack memory showing recursive calls to the Factorial function.
Throw an Exception Instead of Returning an Error
We could have returned a Boolean value in the Check subroutine. For example, Check could have been implemented as a function returning a Boolean as the result of Return Value < 1. However, this would require every user to check the return value and implement a response to the return value. Unfortunately, programmers can and sometimes do ignore return error codes or Booleans.
Unlike error codes, exceptions can't be ignored, but the exception only needs to be checked when an error condition actually exists. Chapter 3 covers the fundamentals of exceptions. Line 9 demonstrates how we can throw an exception to indicate an error state in our code. As far as the Check subroutine is concerned , we are defining what constitutes an error, although if the code itself will cause an exception, we need do nothing.