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
ModuleversusClass, andSharedmembers- Access modifiers
Inherits,MustInherit,NotInheritableOverridable,Overrides,MustOverride,MyBaseImplementsand interfacesShadowsversusOverrides
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.NET | C# | Visible to |
|---|---|---|
Public | public | Everything |
Private | private | The declaring type |
Protected | protected | The type and its derived types |
Friend | internal | The same assembly |
Protected Friend | protected internal | Same 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.NET | C# |
|---|---|
Inherits | : BaseClass |
MustInherit | abstract class |
MustOverride | abstract member |
Overridable | virtual |
Overrides | override |
NotInheritable | sealed class |
NotOverridable | sealed member |
MyBase | base |
MyClass | no 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
| Message | Cause | Fix |
|---|---|---|
BC30389: '<class>' is not accessible in this context | Friend or Private class used from outside | Check the modifier |
BC31411: must be declared 'MustInherit' because it contains methods declared 'MustOverride' | Abstract member in a concrete class | Mark the class MustInherit |
BC30284: cannot be declared 'Overrides' because it does not override a method in a base class | Signature mismatch, or the base is not Overridable | Match the signature exactly |
| The wrong method runs | Shadows instead of Overrides | Shadows picks by the declared variable type, not the object |
BC30002: Type is not defined | Missing Imports, or a missing project reference | Add 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
Friendasprotectedinstead ofinternal - Assuming an unmarked member is private — VB.NET class members default to
Public - Using
ShadowswhereOverrideswas meant, so the base version runs - Confusing
MyClasswithMe - Calling a
Modulefunction unqualified and being unable to locate its definition - Removing or renaming an
Implementsclause and breaking the interface contract - Overlooking a
Defaultproperty 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
ModulefromClass, and say when each is used - Predict which implementation runs for a given call
- Explain what
Shadowsdoes differently fromOverrides - Read
InheritsandImplementsand say what each obliges the class to do
Review questions
- What does
Friendcorrespond to in C#? - What is the default accessibility of a class member declared with no modifier?
- How does
Shadowsdiffer fromOverrideswhen the object is held in a base-typed variable? - Why is an unqualified call to a
Modulefunction harder to trace than aSharedmethod call?