Skip to main content
Published / updated

Procedures and Arrays

Before you start

You need: conditions and loops (Article 02).

Time: about 45 minutes, plus the practice.

Learning objective

Read any VB.NET procedure signature and state exactly what it returns, which arguments it can modify, and how its arrays are sized.

Topics

  • Sub versus Function
  • ByVal and ByRef, and the VB6 default trap
  • Optional parameters and ParamArray
  • Overloads and overload resolution
  • Declaring, sizing, and iterating arrays
  • ReDim and ReDim Preserve
  • Multi-dimensional and jagged arrays
  • When to stop using arrays

Sub versus Function

A Sub returns nothing. A Function returns a value and declares its type with As.

Public Sub MarkAttendance(studentId As Integer, isPresent As Boolean)
_attendanceRepository.Save(studentId, Date.Today, isPresent)
End Sub

Public Function CalculatePercentage(marksObtained As Integer, maxMarks As Integer) As Decimal
If maxMarks <= 0 Then
Throw New ArgumentOutOfRangeException(NameOf(maxMarks))
End If

Return CDec(marksObtained) / CDec(maxMarks) * 100D
End Function

Legacy code frequently assigns to the function name instead of using Return:

Public Function GetGrade(percentage As Decimal) As String
If percentage >= 90D Then
GetGrade = "A+"
ElseIf percentage >= 80D Then
GetGrade = "A"
Else
GetGrade = "C"
End If
' no Return statement — the assigned value is returned at End Function
End Function

Both forms work. The assignment form is a VB6 inheritance and is worth knowing because of one trap: assigning to the function name does not exit the procedure. Code after the assignment still runs and can overwrite the result. Return both sets the value and exits.

If a Function reaches End Function with nothing assigned, it returns the type's default — 0, False, or Nothing — with no warning.

Calling procedures

Modern VB.NET requires parentheses on Function calls and permits them on Sub calls. Very old code uses the Call keyword and bare Sub invocation:

' Modern
MarkAttendance(studentId, True)

' Legacy, still valid
Call MarkAttendance(studentId, True)
MarkAttendance studentId, True

ByVal and ByRef

ByVal passes a copy of the argument. ByRef passes a reference to the caller's variable, so the procedure can replace it.

Public Sub ApplyDiscount(ByVal totalFees As Decimal, ByRef finalAmount As Decimal)
finalAmount = totalFees - 500D
End Sub
Dim payable As Decimal = 0D
ApplyDiscount(50000D, payable)
' payable is now 49500

The trap: in VB6, parameters defaulted to ByRef. In VB.NET the default is ByVal. Code migrated from VB6 often carries explicit ByRef that nobody intended, and a procedure quietly reassigns a caller's variable.

For reference types the distinction is subtler. ByVal still lets the procedure mutate the object's properties — it only prevents replacing the object itself:

Public Sub Deactivate(ByVal student As Student)
student.Status = StudentStatus.Inactive ' caller sees this
student = Nothing ' caller does NOT see this
End Sub

Write ByVal explicitly even though it is the default. It removes all doubt for the next reader.

Optional parameters

Public Function BuildRollNumber(year As Integer,
sequence As Integer,
Optional schoolCode As String = "NCA") As String
Return schoolCode & "-" & year.ToString() & "-" & sequence.ToString("D4")
End Function

Optional parameters must come last and must have a default. Callers can skip them or name them:

Dim roll As String = BuildRollNumber(2024, 12)
Dim other As String = BuildRollNumber(2024, 12, schoolCode:="NCB")

The default is compiled into the calling assembly. Changing a default in a library does not affect callers already compiled against the old value — a genuine source of confusion across a multi-project solution.

ParamArray

Accepts a variable number of arguments as an array.

Public Function SumMarks(ParamArray marks() As Integer) As Integer
Dim total As Integer = 0

For Each mark As Integer In marks
total += mark
Next

Return total
End Function
Dim total As Integer = SumMarks(87, 72, 91, 65)

ParamArray must be the last parameter and must be ByVal. It is C#'s params.

Overloads

Several procedures may share a name with different signatures.

Public Overloads Function FindStudent(id As Integer) As Student
End Function

Public Overloads Function FindStudent(rollNumber As String) As Student
End Function

The Overloads keyword is optional when all versions are in the same class. It becomes required when a derived class adds an overload of a name that exists in the base class — without it, the derived version hides the base one instead of joining it. That distinction is covered with Shadows in the next article.

Return type alone cannot distinguish overloads. Neither can ByVal versus ByRef when nothing else differs.

Arrays

VB.NET array declarations state the upper bound, not the length. This is the single most common array error in the language.

Dim rollNumbers(4) As String ' indices 0 to 4 — FIVE elements
// C# equivalent
string[] rollNumbers = new string[5];

Initialising with values:

Dim sections() As String = {"A", "B", "C"}
Dim marks() As Integer = New Integer() {87, 72, 91}

Iterating:

For index As Integer = 0 To rollNumbers.Length - 1
Console.WriteLine(rollNumbers(index))
Next

For index As Integer = 0 To UBound(rollNumbers)
Console.WriteLine(rollNumbers(index))
Next

UBound returns the highest index; Length returns the count. UBound(a) equals a.Length - 1. Mixing them produces an off-by-one, and with Option Strict Off the resulting IndexOutOfRangeException may only appear for certain data.

Elements are accessed with parentheses, not brackets. That means students(3) could be an array index, a method call, or a default property access — you must look at the declaration to know which.

ReDim and ReDim Preserve

ReDim resizes an existing array. On its own it discards all contents:

ReDim rollNumbers(9) ' now 10 elements, all Nothing
ReDim Preserve rollNumbers(9) ' now 10 elements, first 5 kept

ReDim Preserve allocates a new array and copies every element, so calling it inside a loop is O(n²). A loop that grows an array one element at a time is a classic legacy performance defect:

' Slow — reallocates and copies on every iteration
For Each row As DataRow In table.Rows
ReDim Preserve results(count)
results(count) = row("RollNumber").ToString()
count += 1
Next

Replace with List(Of String), which grows in amortised constant time. That is covered in the next article.

ReDim Preserve can only change the last dimension of a multi-dimensional array.

Multi-dimensional and jagged arrays

' Rectangular: 3 classes x 5 subjects, one comma
Dim classMarks(2, 4) As Integer
classMarks(0, 3) = 87

' Jagged: an array of arrays, each row a different length
Dim sectionStudents()() As String = New String(2)() {}
sectionStudents(0) = New String() {"Ravi Kumar", "Priya Sharma"}
sectionStudents(1) = New String() {"Arjun Reddy"}

Rectangular arrays use one set of parentheses with commas; jagged use chained parentheses. Length on a rectangular array returns the total element count, not a dimension — use GetLength(0) and GetLength(1).

When to stop using arrays

Arrays are fixed-size and are the right choice only when the size is known and stable. In almost all application code, List(Of T) or Dictionary(Of TKey, TValue) is the correct type.

Finding ReDim Preserve inside a loop is a reliable signal that an array is being used where a List(Of T) belongs.

Errors you will hit

MessageCauseFix
BC30455: Argument not specified for parameterMissing argument, or an Optional misunderstoodCheck the signature
BC32029: Option Strict On disallows narrowing in implicit type conversions in a ByRef callPassing a different type by referenceMatch the types exactly
System.IndexOutOfRangeExceptionVB array bounds are 0 To upper, and Dim a(5) gives six elementsRead the declaration carefully
A value changes unexpectedly after a callThe parameter is ByRefCheck for ByRef in the signature
ReDim loses all the dataUsed ReDim without PreserveReDim Preserve

Dim marks(5) As Integer creates six elements, 0 to 5. VB declares the upper bound, not the length — the opposite of C#.

Common mistakes

  • Reading Dim a(5) as five elements instead of six
  • Confusing UBound (highest index) with Length (count)
  • Assigning to the function name and assuming it exits the procedure
  • A Function with a path that assigns nothing, silently returning 0 or Nothing
  • Carrying VB6 ByRef defaults into VB.NET without intent
  • Expecting ByVal on a reference type to prevent property changes
  • ReDim without Preserve, losing all data
  • ReDim Preserve inside a loop instead of using List(Of T)
  • Assuming parentheses mean array indexing when they may be a method call

Practice

Find a procedure in an existing VB.NET application that takes a ByRef parameter. Determine whether the caller actually depends on the reassignment, or whether the ByRef is a migration leftover. Then find every ReDim Preserve in the project, note which sit inside a loop, and rewrite one of them with List(Of T).

You can now

  • Read any VB.NET procedure signature and say what it may modify
  • Tell Sub from Function, and ByVal from ByRef
  • Size and resize arrays without off-by-one errors
  • Use ReDim Preserve and say what plain ReDim costs
  • Recognise Optional and ParamArray parameters

Review questions

  1. How many elements does Dim marks(9) As Integer hold?
  2. What is returned by a Function that reaches End Function without assigning a value?
  3. Why does assigning to the function name behave differently from Return?
  4. What is wrong with calling ReDim Preserve inside a loop?

Next: Classes and OOP