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
SubversusFunctionByValandByRef, and the VB6 default trapOptionalparameters andParamArrayOverloadsand overload resolution- Declaring, sizing, and iterating arrays
ReDimandReDim 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
| Message | Cause | Fix |
|---|---|---|
BC30455: Argument not specified for parameter | Missing argument, or an Optional misunderstood | Check the signature |
BC32029: Option Strict On disallows narrowing in implicit type conversions in a ByRef call | Passing a different type by reference | Match the types exactly |
System.IndexOutOfRangeException | VB array bounds are 0 To upper, and Dim a(5) gives six elements | Read the declaration carefully |
| A value changes unexpectedly after a call | The parameter is ByRef | Check for ByRef in the signature |
ReDim loses all the data | Used ReDim without Preserve | ReDim 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) withLength(count) - Assigning to the function name and assuming it exits the procedure
- A
Functionwith a path that assigns nothing, silently returning0orNothing - Carrying VB6
ByRefdefaults into VB.NET without intent - Expecting
ByValon a reference type to prevent property changes ReDimwithoutPreserve, losing all dataReDim Preserveinside a loop instead of usingList(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
SubfromFunction, andByValfromByRef - Size and resize arrays without off-by-one errors
- Use
ReDim Preserveand say what plainReDimcosts - Recognise
OptionalandParamArrayparameters
Review questions
- How many elements does
Dim marks(9) As Integerhold? - What is returned by a
Functionthat reachesEnd Functionwithout assigning a value? - Why does assigning to the function name behave differently from
Return? - What is wrong with calling
ReDim Preserveinside a loop?
Next: Classes and OOP