Defining Procedure Arguments

Team-Fly    

 
Visual Basic .NET Unleashed
By Paul Kimmel
Table of Contents
Chapter 5.  Subroutines, Functions, and Structures

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 specifier you use should constrain the argument in a manner appropriate for its intended use. For example, immutable arguments should be passed by value with ByVal.

  • The names of arguments should use whole words or standard abbreviations. Arguments are generally nouns representing data.

  • The data type of an argument should constrain the possible values, such that inappropriate values for arguments are difficult to pass. Simply put, argument types should be as specific as possible. For example, if a limited range of integral values is appropriate, consider using an enumerated type to ensure data-type appropriate values.

  • The number of arguments generally should be less than three. (Although this is a general guideline, you might want to consider further refinement of the solution if you have long parameter lists.)

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 Passing

Default 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 ByVal

Changes 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 subprogram
  1:  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 ByRef

If 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 Types

Memory 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.

Reference Types Value Types
Classes Enumerations
Arrays Structures
Delegates Primitive types
Interfaces  
Modules  

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 demonstrated
  1:  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.

graphics/05fig03.gif

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 Parameters

When 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 parameter
  1:  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 Arguments

Visual 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 list
  1:  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 Name

Visual 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 Name

As 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 name
  1:  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.

graphics/05fig04.jpg

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 Arguments

The 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 arguments
  1:  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 Parameters

Occasionally 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, inclusively
  1:  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 Object

The 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 :

  1. Define a new class whose members are the parameters that you want to replace.

  2. Replace the parameters with an instance of the new class.

  3. Compile and test.

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 object
  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:  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 object
  1:  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 code is more understandable.

  • The parameter list is shortened.

  • We only have to write and test this code one time and then we can use it forever.

  • The Range class is portable, and can be used repeatedly in other applications.

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 Types

Strongly 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 value
  1:  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
 


Visual BasicR. NET Unleashed
Visual BasicR. NET Unleashed
ISBN: N/A
EAN: N/A
Year: 2001
Pages: 222

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net