Skip to main content
Published / updated

Classes and Object-Oriented Programming

Before you start

You need: procedures and arrays (Article 03). Classes from Track 03 Article 04 make this much faster.

Time: about 45 minutes, plus the practice.

Learning objective

Read a VB.NET class hierarchy and state which member actually runs for a given object, including when inheritance and hiding are involved.

Topics

  • Class, New, and constructors
  • Auto-implemented and expanded properties
  • Module versus Class, and Shared members
  • Access modifiers
  • Inherits, MustInherit, NotInheritable
  • Overridable, Overrides, MustOverride, MyBase
  • Implements and interfaces
  • Shadows versus Overrides

Classes and constructors

Public Class Student

Public Property Id As Integer
Public Property PublicId As Guid
Public Property SchoolId As Integer
Public Property Name As String
Public Property RollNumber As String
Public Property ClassName As String
Public Property Section As String
Public Property Status As StudentStatus

Public Sub New()
PublicId = Guid.NewGuid()
Status = StudentStatus.Active
End Sub

Public Sub New(name As String, rollNumber As String)
Me.New()

If String.IsNullOrWhiteSpace(name) Then
Throw New ArgumentException("Name is required.", NameOf(name))
End If

Me.Name = name.Trim()
Me.RollNumber = rollNumber
End Sub

End Class

The constructor is always Sub New. Me is C#'s this. Me.New() chains to another constructor in the same class and must be the first statement — C# writes this as : this().

Instantiate with New:

Dim student As New Student("Ravi Kumar", "NCA-2024-0012")

Dim teacher As Teacher = New Teacher() With {
.Name = "Dr. Mehta",
.EmployeeCode = "NCA-T-014",
.Qualification = "M.Sc, B.Ed"
}

The With {} object initialiser prefixes each member with a dot.

Properties

Auto-implemented properties are the modern form:

Public Property MarksObtained As Integer
Public Property Remarks As String = String.Empty

An auto-property can carry an initialiser, which C# only gained later.

The expanded form is what you meet in older code, and it is verbose:

Private _marksObtained As Integer

Public Property MarksObtained As Integer
Get
Return _marksObtained
End Get
Set(value As Integer)
If value < 0 Then
Throw New ArgumentOutOfRangeException(NameOf(value))
End If

_marksObtained = value
End Set
End Property

Read-only and asymmetric access:

Public ReadOnly Property IsPassing As Boolean
Get
Return Not IsAbsent AndAlso MarksObtained >= _passingMarks
End Get
End Property

Public Property Section As String
Get
Return _section
End Get
Private Set(value As String)
_section = value
End Set
End Property

Default properties allow indexer-style access, which is why parentheses on an object are ambiguous:

Public Class StudentCollection
Default Public ReadOnly Property Item(index As Integer) As Student
Get
Return _items(index)
End Get
End Property
End Class
Dim first As Student = students(0) ' calls the Default property

A Default property must take at least one argument. When you see parentheses on something that is not obviously an array, check for a Default Property before assuming.

Module versus Class

A Module is a VB.NET-only construct: all its members are implicitly Shared, and they are callable without qualification from anywhere in the same project.

Public Module FeeCalculator

Public Function OutstandingBalance(account As FeeAccount) As Decimal
Return account.TotalFees - account.PaidAmount - account.DiscountAmount
End Function

End Module
' No type name needed — this is the point, and the problem
Dim due As Decimal = OutstandingBalance(feeAccount)

That unqualified call is why legacy VB.NET is hard to navigate: a function name with no receiver could be a local, a member of the current class, or a member of any Module in the project. "Go to definition" is the only reliable way to find it.

A Class with Shared members is the equivalent that keeps the qualification:

Public NotInheritable Class FeeCalculator
Public Shared Function OutstandingBalance(account As FeeAccount) As Decimal
Return account.TotalFees - account.PaidAmount - account.DiscountAmount
End Function
End Class
Dim due As Decimal = FeeCalculator.OutstandingBalance(feeAccount)

Prefer the class form in new code. Module compiles to a sealed static class, so the two are close relatives.

Access modifiers

VB.NETC#Visible to
PublicpublicEverything
PrivateprivateThe declaring type
ProtectedprotectedThe type and its derived types
FriendinternalThe same assembly
Protected Friendprotected internalSame assembly, or derived types

Friend is the one to remember — it means internal, not protected. Class members with no modifier default to Public, unlike C# where they default to private. A member someone forgot to mark is exposed, not hidden.

Inheritance

Public MustInherit Class Person

Public Property Id As Integer
Public Property SchoolId As Integer
Public Property Name As String

Public MustOverride Function GetDisplayLabel() As String

Public Overridable Function GetContactSummary() As String
Return Name
End Function

End Class
Public Class Teacher
Inherits Person

Public Property EmployeeCode As String
Public Property Qualification As String

Public Overrides Function GetDisplayLabel() As String
Return Name & " (" & EmployeeCode & ")"
End Function

Public Overrides Function GetContactSummary() As String
Return MyBase.GetContactSummary() & " · " & Qualification
End Function

End Class
VB.NETC#
Inherits: BaseClass
MustInheritabstract class
MustOverrideabstract member
Overridablevirtual
Overridesoverride
NotInheritablesealed class
NotOverridablesealed member
MyBasebase
MyClassno direct equivalent

Inherits goes on its own line immediately after the class declaration. Only one base class is allowed.

MyClass calls the version of a member declared in the current class even if a derived class has overridden it. It has no C# equivalent and is rare, but it changes behaviour when present — do not read it as a synonym for Me.

Interfaces

VB.NET requires each member to state which interface member it satisfies, with an Implements clause on the member itself.

Public Interface IStudentRepository
Function FindByRollNumber(schoolId As Integer, rollNumber As String) As Student
Sub Save(student As Student)
End Interface
Public Class SqlStudentRepository
Implements IStudentRepository

Public Function FindByRollNumber(schoolId As Integer, rollNumber As String) As Student _
Implements IStudentRepository.FindByRollNumber

' ...
End Function

Public Sub Save(student As Student) Implements IStudentRepository.Save
' ...
End Sub

End Class

This is more verbose than C# but more explicit: the member name does not have to match the interface member's name, and one method can satisfy several interface members at once by listing them comma-separated.

Because the link is explicit, renaming a method does not break the interface implementation — the Implements clause still points at the right member. The reverse also holds: removing an Implements clause silently turns an implementation into an ordinary method, and the class stops compiling only at the class declaration.

Shadows versus Overrides

This is the distinction most worth understanding, because it changes which code runs.

Public Class Person
Public Overridable Function GetLabel() As String
Return "Person"
End Function
End Class

Public Class Teacher
Inherits Person

Public Overrides Function GetLabel() As String
Return "Teacher"
End Function
End Class

Public Class Staff
Inherits Person

Public Shadows Function GetLabel() As String
Return "Staff"
End Function
End Class
Dim asPerson As Person = New Teacher()
Console.WriteLine(asPerson.GetLabel()) ' "Teacher" — Overrides follows the object

Dim alsoPerson As Person = New Staff()
Console.WriteLine(alsoPerson.GetLabel()) ' "Person" — Shadows follows the variable type

Overrides replaces the base implementation; the call is dispatched on the actual object. Shadows hides it; the call is dispatched on the declared type of the variable. Shadows is C#'s new modifier.

Shadows also hides every base member of that name, including overloads with different signatures — which is broader than C#'s new.

When behaviour differs depending on how a variable is typed, Shadows is the first thing to look for. It is almost always accidental in legacy code.

Errors you will hit

MessageCauseFix
BC30389: '<class>' is not accessible in this contextFriend or Private class used from outsideCheck the modifier
BC31411: must be declared 'MustInherit' because it contains methods declared 'MustOverride'Abstract member in a concrete classMark the class MustInherit
BC30284: cannot be declared 'Overrides' because it does not override a method in a base classSignature mismatch, or the base is not OverridableMatch the signature exactly
The wrong method runsShadows instead of OverridesShadows picks by the declared variable type, not the object
BC30002: Type is not definedMissing Imports, or a missing project referenceAdd the Imports

Shadows versus Overrides is the one to slow down on. With Shadows, which implementation runs depends on the type of the variable, not the object it holds — so the same object gives different answers through different references.

Common mistakes

  • Reading Friend as protected instead of internal
  • Assuming an unmarked member is private — VB.NET class members default to Public
  • Using Shadows where Overrides was meant, so the base version runs
  • Confusing MyClass with Me
  • Calling a Module function unqualified and being unable to locate its definition
  • Removing or renaming an Implements clause and breaking the interface contract
  • Overlooking a Default property and misreading parentheses as array access
  • Forgetting that Me.New() must be the first statement in a constructor

Practice

In an existing VB.NET project, find one class that inherits from another. List every member the derived class declares, and mark each as Overrides, Shadows, or new. For any Shadows, write out what happens when an instance is assigned to a base-typed variable and the member is called — then verify in the debugger. Separately, count the Module declarations in the project and pick one unqualified call to trace back to its definition.

You can now

  • Read a VB.NET class and its properties in both auto and expanded form
  • Tell Module from Class, and say when each is used
  • Predict which implementation runs for a given call
  • Explain what Shadows does differently from Overrides
  • Read Inherits and Implements and say what each obliges the class to do

Review questions

  1. What does Friend correspond to in C#?
  2. What is the default accessibility of a class member declared with no modifier?
  3. How does Shadows differ from Overrides when the object is held in a base-typed variable?
  4. Why is an unqualified call to a Module function harder to trace than a Shared method call?

Next: Collections and exceptions