Defining Procedure Arguments Subroutines and functions generally need additional information to perform their roles correctly. However, passing too many arguments is considered bad form. Reducing the number of arguments can be accomplished by introducing a parameter object; refer to the section "Refactoring: Introduce Parameter Object" for an explanation of this refactoring. You are trying to achieve multiple objectives when defining procedure arguments:
The subsections that follow demonstrate how to use various argument specifiers to convey more meaning about your arguments and ensure their proper use. Default Parameter PassingDefault parameter passing has changed in Visual Basic .NET. If you didn't use a parameter specifier in VB6, by default parameters were passed ByRef. Visual Basic .NET passes parameters ByVal by default. A good programming practice is to be verbose; in other words, to explicitly indicate the intent of the code by typing the parameter specifier. Passing Arguments ByValChanges to parameters passed by value using the ByVal specifier aren't reflected in the calling subprogram. When you want to ensure that a procedure doesn't change the value of the parameter, pass the parameter ByVal. From the caller's perspective, the value pass is constant. The called subprogram can modify the value, but the modification is temporary to the called subprogram. Listing 5.2 demonstrates ByVal parameters. Listing 5.2 Changes to arguments passed ByVal aren't reflected in the calling subprogram1: Private Sub Increment(ByVal Value As Long) 2: Value += 1 3: End Sub 4: 5: Private Sub Button1_Click(ByVal sender As System.Object, _ 6: ByVal e As System.EventArgs) Handles Button1.Click 7: 8: Dim Val As Long 9: Val = CLng(TextBox1.Text) 10: Increment(Val) 11: TextBox1.Text = CStr(Val) 12: End Sub The example passes a long integer to Increment. Increment adds 1 to the Value. If you type 5 in TextBox1, Value will be incremented to 6 after line 2 runs, but when the procedure returns, the value of Val is still 5. Keep in mind that assignments to aggregate types, like classes and structures, passed by value can be assigned to new instances of that type but the caller will still refer to the original object. However, you can change the value reference type members passed ByVal and those changes are reflected in the calling program. Changes to aggregate value types passed by value aren't reflected in the calling subprogram. A structure is an example of a value type, and a class is an example of a reference type. (The upcoming section "Value Types Versus Reference Types" provides more information.) Passing Arguments ByRefIf you have a subroutine that needs to change multiple values, you can pass these arguments ByRef. The ByRef specifier indicates to the caller that arguments will be changed by the called subprogram. To make the example in Listing 5.2 work as you might expectIncrement changes the value of the argument on behalf of the callermodify the declaration of Increment, swapping ByVal for ByRef: Private Sub Increment(ByRef Value As Long) With this single revision, let's repeat the example given earlier. Call Increment with the value 5 and Val is incremented to 6. The incremented value is reflected in the calling subprogram and TextBox1 is updated to contain the new value. When you pass a reference type ByRef, what you are indicating to a reader is that the object referred to by the argument may change and refer to a completely new object. Pass a reference type ByVal and you may modify the properties of the reference type. Pass a reference type ByRef and you may modify the actual object referred to. Value Types Versus Reference TypesMemory is assigned to variables in two intrinsic ways: value types and reference types. Value types are variables that are created in stack memory, and reference types are variables that are created in heap memory. Reference types can be assigned to the equivalent of a null value, which is the value Nothing. When you assign variables to reference types, you are getting a copy of the reference to the data. When you assign a variable to the value of a value type, you get a copy of the data. When a value type is destroyed, the data is destroyed with the variable. When a reference type is destroyed, the reference is destroyed , but the object referred to may still exist because other objects may refer to the actual data. A class is an example of reference type and a structure is an example of value type. Hence, when you pass a structure ByVal, you are essentially assigning the structure to a variable and you get a copy. Consequently, any changes made to the ByVal Structure argument are reflected in the copy only, and the calling subprogram doesn't get the modified values. When you pass an instance of a class, you are passing a reference. Because both variablesthe one in the calling subprogram and the argumentrefer to the same physical object, changes to that object in the called subprogram are reflected in the caller. What does ByVal mean where reference objects are concerned ? ByVal means that you can't assign a new object to the argument and have the caller get the reference to the new object, but you can modify the object because you have a reference to the same object as the caller. The following table shows the breakdown of components into reference types and value types.
Listing 5.3 demonstrates the difference between value types and reference types. Listing 5.3 Changes to members of reference types are reflected in calling programs even when the reference types are passed ByVal, as demonstrated1: Public Class Form1 2: Inherits System.Windows.Forms.Form 3: 4: [ Windows Form Designer generated code ] 5: 6: Private Sub Increment(ByRef Value As Long)[...] 7: 8: Private Sub ModifyData(ByVal Arg As Data) 9: Arg.Str = "Modified" 10: End Sub 11: 12: Private Sub ModifyClass(ByVal Arg As ClassData) 13: Arg.Str = "Modified" 14: End Sub 15: 16: Private Sub Button1_Click(ByVal sender As System.Object, _[...] 17: 18: Private Sub Button2_Click(ByVal sender As System.Object, _ 19: ByVal e As System.EventArgs) Handles Button2.Click 20: 21: Dim C As New ClassData() 22: C.Str = Button2.Text 23: ModifyClass(c) 24: Button2.Text = C.Str 25: 26: Dim D As Data 27: D.Str = TextBox1.Text 28: ModifyData(d) 29: TextBox1.Text = D.Str 30: End Sub 31: 32: End Class 33: 34: Public Structure Data 35: Public Str As String 36: End Structure 37: 38: Public Class ClassData 39: Public Str As String 40: End Class Listing 5.3 defines the value type, Data, on line 34 and the reference type, ClassData, on line 38. When Button2_Click is called, an instance of ClassData, C, is created and passed ByVal to ModifyClass on line 23. When line 24 is run, C.Str now contains the value assigned to it on line 13. Comparing C on line 21 with Arg on line 12 using the test C Is Arg yields True whether the test is performed before or after the assignment to Arg. The keyword Is tests reference equality, and both C and Arg refer to the same object. You can't test two value types with Is, but you can use the Equals method. Equals for structures performs memberwise comparisons. Arg.Equals would be True before the assignment to Arg on line 9 and False after the assignment. This is because the two objects refer to different memory locations and the value of Arg isn't the same as the value of D. Reference types refer to the same memory location when reference assignment is performed, and value types refer to separate memory addresses after assignment. Figure 5.3 shows the different views of memory after assignment of two value types versus two reference types. Figure 5.3. Comparing value type assignment to reference type assignment.
Although you can initialize variables of value types with the New keyword, doing so doesn't make them reference types. A reference type without the New keyword refers to Nothing. The only valid operation on such a reference type is assignment or an operation involving a Shared member. Using Optional ParametersWhen you use the Optional specifier, you are indicating that a parameter doesn't need to have an argument supplied for it. The Optional parameter must include a default value that will be used if the procedure consumer doesn't supply an argument for the Optional parameter. Optional parameters must be the last parameters in the parameter list. If a procedure has optional parameters, you can indicate that you are skipping a parameter by placing a comma in the location where an argument and comma would normally go. Listing 5.4 demonstrates the syntax for an optional parameter. Tip When you are reading the help documentation, optional parameters are usually indicated by the parameter name being displayed in square brackets, as in Foo([ arg1 ]). Listing 5.4 An optional parameter1: Private Sub HasOptionalParam(Optional ByVal Str _ 2: As String = "Hello World!") 3: MsgBox(Str) 4: End Sub 5: 6: Private Sub Button3_Click(ByVal sender As System.Object, _ 7: ByVal e As System.EventArgs) Handles Button3.Click 8: 9: HasOptionalParam() 10: HasOptionalParam("New Value") 11: 12: End Sub When you begin typing HasOptionalParam on line 9, IntelliSense shows the signature as HasOptionalParam([Str As String = "Hello World!"]). The square brackets around the Str parameter indicate that Str is Optional. If you do not supply an argument, as demonstrated on line 9, Str is given the value "Hello World!". When HasOptionalParam is called on line 10, Str is given the value "New Value". What is the benefit of optional parameters? Optional parameters are a form of method overloading, explained further in the following section. Overloading and Optional ArgumentsVisual Basic .NET supports overloading procedures and property methods . To overload simply means to define more than one procedure with the same name but with differing parameter lists. Chapter 7 goes into overloading in more detail. The reasons for overloading simply have to do with minimizing the number of names and using identical names for semantically similar operations with different implementations . Optional arguments are similar to overloading because you can invoke a procedure in different ways, with or without the optional parameters. Overloaded methods actually have different parameter lists. When you call an overloaded procedure, you are calling one of various different procedures based on the arguments you pass during the procedure invocation. When you call a procedure with optional parameters, you are always calling the same procedure, but the parameter list can be different during different invocations. The rule for determining whether you need an optional parameter versus an overloaded procedure is straightforward. If the behavior is always the samethat is, the code is always the sameand much of the time you will be passing the same value, you need an optional parameter. If the implementation of the behavior changes based on the argument passed, you need an overloaded procedure. Applying this logic to the Increment procedure from Listing 5.2, we can decide that most of the time we want to increment a value by 1, but sometimes we want to increment by some other value. Because we are still talking about addition here, we need an optional parameter with a default value of 1. Listing 5.5 demonstrates the revised Increment procedure. Listing 5.5 Using optional parameters to create an overloaded invocation list1: Private Sub Increment(ByRef Value As Long, _ 2: Optional ByVal Inc As Long = 1) 3: Value += Inc 4: End Sub Listing 5.5 represents the best implementation of the Increment procedure. The modified value is returned to the caller and most of the time we will increment by 1, but if we need to, we can pass an alternate increment value as the argument for Inc. Passing Arguments by NameVisual Basic .NET supports passing arguments by name. To pass an argument by name, use the name: = value syntax for identifying and supplying a named argument. You can change the order of named arguments, although it may add to confusion to do so, but you must supply all non-optional arguments when using named arguments. The following method demonstrates an arbitrary procedure and a statement passing arguments by name. (Note the reverse order of the arguments.) Public Sub NamedArguments(ByVal I As Integer, ByVal S As String) MsgBox(String.Format("S={0} and I={1} ", S, I), _ MsgBoxStyle.Information, "Named Arguments") End Sub NamedArguments(S:=5, I:=10) The most useful application of named parameters is to use the pass-by-name technique to supply arguments to optional parameters as opposed to comma counting when you skip optional parameters. Passing Optional Arguments by NameAs an alternative to using a comma to skip optional parameters, you pass optional arguments by name. Passing arguments by name allows you to pass parameters in any order, too. Listing 5.6 demonstrates a subroutine with a couple of optional parameters and demonstrates examples of passing parameters using space-comma to skip parameters and using the by-name convention as a convenient alternative. Listing 5.6 An example of optional parameters and passing arguments by name1: Public Sub TakesSeveralParams(Optional ByVal Val As Integer = 5, _ 2: Optional ByVal Str As String = "String Data", _ 3: Optional ByVal ADate As Date = #12:00:00 AM#) 4: 5: Debug.WriteLine(Val) 6: Debug.WriteLine(Str) 7: Debug.WriteLine(ADate) 8: End Sub 9: 10: Private Sub Button4_Click(ByVal sender As System.Object, _ 11: ByVal e As System.EventArgs) Handles Button4.Click 12: 13: TakesSeveralParams() 14: TakesSeveralParams(10, , Today) 15: TakesSeveralParams(, "Text",) 16: TakesSeveralParams(Val:=-3, ADate:=Now) 17: TakesSeveralParams(ADate:=Today, Str:="Some More Text") 18: 19: End Sub The output from Listing 5.6 is shown in Figure 5.4. Each set of three lines represents one call to TakesSeveralParams. Figure 5.4. The output from Listing 5.6 shown in the Output window.
Caution Using the argument-by-name convention doesn't allow you to skip non-optional parameters. Any parameters not preceded by the Optional keyword must have an argument supplied by the consumer. Defining ParamArray ArgumentsThe equivalent of a parameter array has been around for a long time. Languages such as C and C++ have used parameter arrays to define standard input and output and formatting functions for 20 or so years . Visual Basic .NET supports the ParamArray specifier. Preceding a parameter by the ParamArray keyword indicates to consumers that they may pass a varying number of arguments of the type specified in the ParamArray clause. Listing 5.7 demonstrates the syntax for defining a ParamArray parameter and passing and managing arguments passed to a ParamArray. Listing 5.7 Using ParamArray arguments1: Private Sub ParamArrayDemo(ByVal ParamArray Values() As String) 2: Dim Enumerator As IEnumerator = Values.GetEnumerator 3: 4: Dim Output As String 5: While (Enumerator.MoveNext) 6: Output = Output & Enumerator.Current & vbCrLf 7: End While 8: 9: MessageBox.Show(Output, "Formatted Output", _ 10: MessageBoxButtons.OK, MessageBoxIcon.Information, _ 11: MessageBoxDefaultButton.Button1, _ 12: MessageBoxOptions.DefaultDesktopOnly) 13: End Sub 14: 15: Private Sub Button1_Click(ByVal sender As System.Object, _ 16: ByVal e As System.EventArgs) Handles Button1.Click 17: 18: ParamArrayDemo("ParamArrayDemo.Exe", _ 19: "Copyright 2001. All Rights Reserved.", _ 20: "Written by Paul Kimmel. pkimmel@softconcepts.com") 21: 22: End Sub ParamArrayDemo takes a ParamArray of Strings. In the example, copyright information is passed to the ParamArrayDemo subroutine and formatted by tacking on a carriage-return, line-feed pair. The carriage return and line feed are added on line 6 using the vbCrLf constant. Notice that an Enumerator was used to iterate over the parameter data and the MessageBox.Show method was used to display the message box. MsgBox is maintained for backward compatibility with VB6, but the CLR implements the MessageBox class in the System.Windows.Forms namespace. ParamArrays are treated like an infinitely overloaded procedure taking zero, one, two, and so on, arguments of the same type. All arguments of the ParamArray must be of the same type. What this means in a technical sense is that if you define a ParamArray of a very generic type, like Object, you can pass an array of heterogeneous objects. Reducing the Number of ParametersOccasionally you will be coding and find out that you have a procedure that needs a huge number of parameters. Generally, this may imply that a useful entity in the problem domain doesn't have an implemented equivalent in the solution domain. In simpler terms, something that is part of the problem the code is designed to solve isn't defined as a class in the code. You can simplify commonly grouped parameters or long parameter lists by moving the method to the class that contains most of the parameters as fields or by applying the Refactoring "Introduce Parameter Object." A parameter clas s is a class whose members are fields and properties that are passed as arguments to a procedure. Consider the subroutine in Listing 5.8 that takes a value and determines whether the value is between the low and high range values. Listing 5.8 Determines whether the target value is between a low and high value, inclusively1: Function WithinRange(ByVal Target As Integer, ByVal Low As Integer, _ 2: ByVal High As Integer) As Boolean 3: Return (Low <= Target) And (Target <= High) 4: End Function 5: 6: Private Sub Button1_Click(ByVal sender As System.Object, _ 7: ByVal e As System.EventArgs) Handles Button1.Click 8: 9: MsgBox("0 <= " & TextBox1.Text & " <= 100 is " & _ 10: WithinRange(TextBox1.Text, 0, 100)) 11: 12: End Sub The function WithinRange takes a target value and low and high range values, returning True if Target is greater than or equal to Low and is also less than or equal to High. These kinds of functions often result in long parameter lists. By applying "Introduce Parameter Object," we can simplify the parameter list and get more mileage out of our code. Refactoring: Introduce Parameter ObjectThe motivation for "Introduce Parameter Object" is to shorten complicated, long parameter listsoften you get a derived benefit from consolidating code. Because our minds are better at working with fewer elements of information, by consolidating several pieces of data into higher levels of abstraction, we are able to make further refinements. The basic mechanics of "Introduce Parameter Object," as applied to our example in Listing 5.8, are as follows :
Keep in mind that an objective when refactoring is to support the existing behavior in a new way. In Listing 5.8, we defined low and high integer values as our range and a target was the comparison value. What the parameters are used for provides us with a clue for a good name. Low and High represent a range, so we will name our new class Range. Listing 5.9 shows the new Range class. Listing 5.9 The new class used to implement our parameter object1: Public Class Range 2: Private FLow, FHigh As Integer 3: 4: Public Property Low() As Integer 5: Get 6: Return FLow 7: End Get 8: Set(ByVal Value As Integer) 9: FLow = Value 10: End Set 11: End Property 12: 13: Public Property High() As Integer 14: Get 15: Return FHigh 16: End Get 17: Set(ByVal Value As Integer) 18: FHigh = Value 19: End Set 20: End Property 21: 22: Public Sub New(ByVal ALow As Integer, ByVal AHigh As Integer) 23: FLow = ALow 24: FHigh = AHigh 25: End Sub 26: End Class If classes are completely foreign to you, Chapter 7 will fill in the blanks nicely . Without too much explanation, Range defines a class that has two fields, FLow and FHigh. The fields are accessed through the properties Low and High. The Sub New defines a parameterized constructor that allows us to initialize the low and high values when the object is created. Now we can implement WithinRange using an instance of the Range class, as shown in Listing 5.10. Listing 5.10 Using the Range parameter object1: Function WithinRange2(ByVal Target As Integer, ByVal ARange As Range) 2: Return (ARange.Low <= Target) And (Target<= ARange.High) 3: End Function 4: 5: Private Sub Button1_Click(ByVal sender As System.Object, _ 6: ByVal e As System.EventArgs) Handles Button1.Click 7: 8: MsgBox("0 <= " & TextBox1.Text & " <= 100 is " & _ 9: WithinRange2(TextBox1.Text, New Range(0, 100))) 10: 11: End Sub Notice that WithinRange2 takes the target value and an instance of the new Range class defined in Listing 5.9. A Range object is created as it is passed to the new implementation, WithinRange2, on line 9. We have satisfied the first motivation for this refactoring; we have shortened the parameter list. The second motivation is that our new entity may suggest further refinements. In fact, if you think about it, you could easily infer that a range should know whether a value is within the Low and High bounds, suggesting that WithinRange should be a method of Range. Listing 5.11 demonstrates the refinement and the result on the code. Listing 5.11 Range revised to include the WithinRange method.1: Public Class Range 2: Private FLow, FHigh As Integer 3: 4: Public Property Low() As Integer 5: Get 6: Return FLow 7: End Get 8: Set(ByVal Value As Integer) 9: FLow = Value 10: End Set 11: End Property 12: 13: Public Property High() As Integer 14: Get 15: Return FHigh 16: End Get 17: Set(ByVal Value As Integer) 18: FHigh = Value 19: End Set 20: End Property 21: 22: Public Sub New(ByVal ALow As Integer, ByVal AHigh As Integer) 23: FLow = ALow 24: FHigh = AHigh 25: End Sub 26: 27: Public Function WithinRange(ByVal Target As Integer) As Boolean 28: Return Low <= Target <= High 29: End Function 30: 31: End Class The only revision to the class Range is the addition of the WithinRange method. Notice that WithinRange now only has one parameter, Target. This is the idea behind the notion that object-oriented applications have shorter methods and fewer parameters. As demonstrated in the Range class, the parameter list of WithinRange was shortened by two- thirds . The revision had the following impact on the event handler that tests the Range class: Private Sub Button1_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Button1.Click Dim ARange As New Range(0, 100) MsgBox("0 <= " & TextBox1.Text & " <= 100 is " & _ ARange.WithinRange(TextBox1.Text)) End Sub Notice that we only have to pass the target value to the WithinRange method. A reasonable person might pause at this point and say, "Hey, I thought objects were supposed to make applications shorter?" This very short example certainly doesn't illustrate a shortening of the implementation. In fact, the listing almost doubled in size . That's true. The Range class added 31 lines of code to the application. The benefit received accumulates over time. The benefits of all the revisions are as follows:
The more times we need the Range behavior, or any behavior captured in a class, the greater our leverage will be over time and the greater increase in productivity and decrease in lines of code we will achieve. Hyperproductive programmers know these things. A hyperproductive programmer knows that a little extra effort up front will yield dividends now and in the future. Many programmers won't stop to create classes out of types as simple as a Range type. There is still a lot of cutting and pasting going on. Cutting and pasting and structured programming are some of the reasons software projects are still failing at a phenomenal rate. The faulty reasoning is that the changes individually are so trivial that they couldn't possibly matter. This faulty reasoning usually holds true in the very short term , but as development progresses, poor programming practices add to the complexity and confusion until it's too late. The code becomes unmanageable. An objective of XP and refactoring is to get code under control and keep it that way. Using Concise Argument TypesStrongly typed languages allow you to force the compiler to do a lot of work for you. By being as explicit as possible with your argument types, the compiler can help you make sure that you are passing good data to your procedures. How does this work? Well, replacing two random integers with a Range type makes the code more expressive and constrains the way in which the code was written and used. Suppose you have a function that takes a name, phone number, and address. By defining a function that takes these three parameters, you've created an environment where any string data would technically satisfy the arguments. But, if you wanted to convey more meaning and ensure that the data passed were more likely to be the correct values, you could define a structure containing the three elements and pass the structure to the function. The structure (or class) can contain property methods that could encourage the consumer to initialize the members with suitable data, perhaps by validating the format of the phone number or ensuring that the city, state, and zip code of the address were appropriate. Using concise types applies to even simpler kinds of data. Suppose you have a procedure that works on a subset of possible values of a simpler type, like an Integer. By defining an enumerated type containing named values representing the suitable Integers, you could define the procedure to take an argument of the Enumerated type. Now, the enumeration helps to ensure that only suitable arguments are passed. Many times, modeling the data correctly alleviates the need for a lot of extra validation code. Consider the code in Listings 5.14 and 5.15. The first function sets an eye color using an arbitrary integer value (perhaps representing a field in a driver's license application for the Department of Motor Vehicles). The second, more expressive, version of the function uses an enumerated type to make the code more meaningful. Listing 5.12 Using an arbitrary integer to represent a value1: Private FEyeColorInteger As Integer 2: 3: Public Sub SetEyeColor(ByVal Color As Integer) 4: If (Color > 0) And (Color <= 2) Then 5: FEyeColorInteger = Color 6: End If 7: End Sub Listing 5.13 Using an enumerated type, making the code more expressive and deliberate .1: Private FEyeColorEnum As EyeColor 2: 3: Public Enum EyeColor 4: Blue 5: Brown 6: Green 7: End Enum 8: 9: Public Sub SetEyeColor2(ByVal Color As EyeColor) 10: FEyeColorEnum = Color 11: End Sub Listing 5.12 uses an integer that could have four billion possible values, but only three are valid for a certain purpose. Listing 5.13 indicates to the reader that only Blue, Brown, and Green make sense by introducing a new enumerated type named appropriately for this particular scenario and containing the appropriately named values. (See the section "Using Enumerated Types" at the end of this chapter for more on the capabilities of enumerated types.) |
Team-Fly |
Top |