Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Language Atlas (mini-courses for more languages)  Module 3 – Visual Basic .NET for maintainers (and the road to C#)

VB.NET for C# developers: the differences

Map C# to Visual Basic and catch the traps that compile in both, from array bounds and CInt rounding to And versus AndAlso, events and Option Compare.

  • Beginner
  • 15 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7 and .NET 10.0.401
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Map C# syntax to Visual Basic: Dim, Sub and Function, ByVal and ByRef, Nothing, AndAlso and OrElse
  • Predict Visual Basic array bounds, For loop end values, integer division, Mod and conversion rounding
  • Read event wiring with WithEvents, Handles and AddHandler, and modules and shared members
  • Check which string comparison a Visual Basic file uses before trusting = on strings

Before you start

What you’ll learn · Before you start

What you will learn

  • Map C# syntax to Visual Basic: Dim, Sub and Function, ByVal and ByRef, Nothing, AndAlso and OrElse
  • Predict Visual Basic array bounds, For loop end values, integer division, Mod and conversion rounding
  • Read event wiring with WithEvents, Handles and AddHandler, and modules and shared members
  • Check which string comparison a Visual Basic file uses before trusting = on strings

Before you start

On this page

A C# developer reads Dim slots(3) As Integer as an array of three, and Visual Basic makes four. In the same file 7 / 2 is 3.5, CInt(3.5) is 4 where C#’s (int)3.5 is 3, and If x IsNot Nothing And x.Count > 0 throws when x is Nothing, although each line compiles and looks like something you know.

This lesson maps C# to Visual Basic, one construct at a time, and pins down the places where the same-looking code means something else: declarations and ByRef, the logical operators, array bounds and For loops, division, rounding and overflow, events and modules, and the Option Compare that can change = in one file. Every claim comes with a run on the .NET SDK 10.0.401, and the numbers part runs the same lines in C# beside it.

Declarations read differently: Dim, Sub, Function and ByRef

Start with what this program prints:

declarations/Program.vb VB · declarations/Program.vb
' Declarations and calls that read differently from C#, each printed with its result.
Module Program
    ' A Sub returns nothing, like a void method. ByRef lets it change the caller's variable.
    Sub AddTax(ByRef amount As Integer)
        amount += amount * 18 \ 100
    End Sub

    ' A Function returns a value: the type after As is the return type.
    Function Greeting(name As String) As String
        Return $"Hello, {If(name, "guest")}"
    End Function

    Sub Main()
        Dim count As Integer = Nothing
        Console.WriteLine($"Dim count As Integer = Nothing gives {count}")
        Dim nickname As String = Nothing
        Console.WriteLine(Greeting(nickname))
        Console.WriteLine($"nickname Is Nothing: {nickname Is Nothing}, nickname = """": {nickname = ""}")
        Dim price As Integer = 1000
        AddTax(price)
        Console.WriteLine($"AddTax(price): price = {price}")
        AddTax((price))
        Console.WriteLine($"AddTax((price)): price = {price}")
    End Sub
End Module
Nothing, If() and ByRef on the .NET SDK 10.0.401 Python · declarations_session.py
from session import run

# Nothing as a default value, If() with two arguments, and a ByRef parameter called with and without extra
# parentheses.
run("""
dotnet run --project declarations
""")

Output

$ dotnet run --project declarations
Dim count As Integer = Nothing gives 0
Hello, guest
nickname Is Nothing: True, nickname = "": True
AddTax(price): price = 1180
AddTax((price)): price = 1180

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 declarations_session.py

Dim count As Integer = Nothing gave 0. Nothing is the default value of whatever type receives it: a null reference for an object or a string, 0 for an Integer, False for a Boolean. That is C#’s default, not its null, which an int does not even accept. If(nickname, "guest") is C#’s ??. The comparison line shows a difference C# developers trip over: for strings, nickname = "" is True while nickname holds Nothing, because Visual Basic treats the empty string as equal to Nothing, and only Is Nothing asks whether there is a string at all.

The ByRef lines matter most for reading old code. AddTax(price) changed the caller’s price to 1180, and nothing at the call site said it might: C# makes you write ref in the call, Visual Basic only in the declaration. Wrapping the argument in an extra pair of parentheses, AddTax((price)), passes a copy, so the second call left price at 1180 instead of raising it to 1392. A call that looks harmless can change a variable, and an extra pair of brackets can stop it.

The rest of the mapping, with C# first:

C# Visual Basic Watch for
int count = 5; Dim count As Integer = 5 Dim count = 5 takes the type Integer with Option Infer On
void Save() Sub Save() A Sub returns nothing
int Total() Function Total() As Integer The type after As is the return type
ref int amount ByRef amount As Integer No marker at the call site
a parameter by value ByVal, also the default Parentheses around an argument force a copy
null, default Nothing 0 for an Integer, null for an object
a ?? b and c ? x : y If(a, b) and If(c, x, y) Not IIf, as the next section shows
x == null, x != null x Is Nothing, x IsNot Nothing = compares values, Is compares references
s + t for strings s & t + on a string and a number can add, with Option Strict Off
static class, static member Module, Shared member Module members are called without the module’s name
this, base Me, MyBase

The row for + on strings is the trap for old code: with Option Strict Off, "34" + 6 converts the string to a number and gives 40, where C# would join them into “346”. Use & for text, always.

Checkpoint

Check: Declarations, Nothing and ByRef

  1. Question 1 of 2 What does Dim count As Integer = Nothing store in count?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 0, the default value of an Integer

    Nothing in Visual Basic is closer to C#'s default than to its null: a null reference for objects and strings, 0 for numbers, False for a Boolean.

  2. Question 2 of 2 Sub AddTax(ByRef amount As Integer) adds 18 % to amount. price is 1000, and the code calls AddTax((price)), with two pairs of parentheses. What is price afterwards?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 1000

    A ByRef parameter changes the caller's variable, and nothing at the call site shows it. Enclosing the argument in its own parentheses overrides ByRef and passes a copy, as the run showed when the second call left 1180 unchanged.

And and Or evaluate both sides; AndAlso, OrElse and If() stop early

C#’s && and || stop as soon as the left side decides the answer. Visual Basic’s And and Or do not: they are the operators for both logic and bits, and they always evaluate both sides. The short-circuit evaluation: Evaluating a logical expression only as far as needed for its result: if the left side of an and is False, the right side is skipped. Visual Basic's AndAlso and OrElse and C#'s && and || work this way; And and Or do not. versions are spelled AndAlso and OrElse. This program asks whether an order that is Nothing has lines, four ways:

logic/Program.vb VB · logic/Program.vb
' And and IIf evaluate both sides; AndAlso and If() stop as soon as the answer is known.
Module Program
    Class Order
        Public Lines As New List(Of String)
    End Class

    Sub Main()
        Dim missing As Order = Nothing
        Console.WriteLine($"AndAlso: {missing IsNot Nothing AndAlso missing.Lines.Count > 0}")
        Try
            Console.WriteLine($"And: {missing IsNot Nothing And missing.Lines.Count > 0}")
        Catch ex As NullReferenceException
            Console.WriteLine($"And: {ex.GetType().Name}")
        End Try
        Console.WriteLine($"If(): {If(missing Is Nothing, 0, missing.Lines.Count)}")
        Try
            Console.WriteLine($"IIf: {IIf(missing Is Nothing, 0, missing.Lines.Count)}")
        Catch ex As NullReferenceException
            Console.WriteLine($"IIf: {ex.GetType().Name}")
        End Try
    End Sub
End Module
AndAlso, And, If() and IIf on an order that is Nothing Python · logic_session.py
from session import run

# And and IIf evaluate both sides of a test whose left side is already decided; AndAlso and If() do not.
run("""
dotnet run --project logic
""")

Output

$ dotnet run --project logic
AndAlso: False
And: NullReferenceException
If(): 0
IIf: NullReferenceException

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 logic_session.py

Step through the first two tests with the order set to Nothing:

Interactive · stage by stage Why And fails where AndAlso does not, on an order that is Nothing Step through the test of logic/Program.vb with missing set to Nothing, first as AndAlso, then as And.
Why And fails where AndAlso does not, on an order that is Nothing, step 5 of 5. Showing Left side, The answer is known, Right side, Result (If(): 0; IIf: an exception). In focus: Right side, Result.AndAlso, OrElse, If()Left sidemissing IsNot NothingThe answer is knownFalse And anything is FalseRight sidemissing.Lines.Count > 0ResultIf(): 0; IIf: an exception

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The left side first. Both versions start with the left side. missing holds Nothing, so missing IsNot Nothing is False.
  2. The answer is already known. False And anything is False, so the right side cannot change the result. What happens next depends on the operator.
  3. AndAlso stops. AndAlso is short-circuiting: it skips the right side and gives False at once. The program printed AndAlso: False.
  4. And reads the right side anyway. And always evaluates both sides. missing.Lines reads a field of Nothing, which throws NullReferenceException before any result exists: the program printed And: NullReferenceException.
  5. IIf is a function too. IIf(missing Is Nothing, 0, missing.Lines.Count) is a function call, so all three arguments are evaluated before it runs, and the third throws. The If() operator evaluates only the branch it returns, and printed 0.

With AndAlso, the left side, missing IsNot Nothing, is False and decides the result, so the right side never runs. With And, Visual Basic evaluates missing.Lines.Count > 0 as well and throws NullReferenceException on a variable that holds Nothing. The same split runs through the two ways to choose a value: the If(c, x, y) operator evaluates only the branch it returns, while IIf(c, x, y) is an ordinary function, so all three arguments are evaluated before it is called, and the one that should have been skipped throws.

Porting && as And

If order IsNot Nothing And order.Lines.Count > 0 Then compiles and works in every test with a real order, then throws on the first Nothing. Write AndAlso and OrElse for every condition, and keep And and Or for combining bits, such as flags And 4.

Checkpoint

Check: And and Or against AndAlso, OrElse and If()

  1. Question 1 of 2 order holds Nothing. What does order IsNot Nothing AndAlso order.Lines.Count > 0 give?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: False, without reading order.Lines

    AndAlso and OrElse stop as soon as the left side decides the answer, like C#'s && and ||. The run printed AndAlso: False.

  2. Question 2 of 2 missing holds Nothing. Which of these throw NullReferenceException? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • missing IsNot Nothing And missing.Lines.Count > 0
    • IIf(missing Is Nothing, 0, missing.Lines.Count)

    And, Or and IIf evaluate everything; AndAlso, OrElse and the If() operator evaluate only what the result needs. The run printed NullReferenceException for And and IIf, and False and 0 for the others.

Arrays are declared by their last index, and For loops include their end

Visual Basic declares an array by its upper bound: The highest index of an array. Visual Basic declares an array by it, so Dim marks(4) As Integer has the indexes 0 to 4 and five elements, while C# declares new int[4] by its length, four elements., the last index, not by its length. The For ... To loop is inclusive in the same way: its end value is the last value the counter takes.

loops/Program.vb VB · loops/Program.vb
' Arrays are declared by their last index, and For ... To includes its end value.
Module Program
    Sub Main()
        Dim marks(4) As Integer
        Console.WriteLine($"Dim marks(4): {marks.Length} elements, last index {marks.GetUpperBound(0)}")
        Dim none(-1) As Integer
        Console.WriteLine($"Dim none(-1): {none.Length} elements")
        ReDim Preserve marks(6)
        Console.WriteLine($"ReDim Preserve marks(6): {marks.Length} elements")
        Dim upper As Integer = -2
        Try
            Dim broken(upper) As Integer
            Console.WriteLine($"Dim broken(-2): {broken.Length} elements")
        Catch ex As OverflowException
            Console.WriteLine($"Dim broken(-2): {ex.GetType().Name}")
        End Try
        Dim visited As New List(Of Integer)
        For i As Integer = 1 To 10 Step 3
            visited.Add(i)
        Next
        Console.WriteLine($"For i = 1 To 10 Step 3: {String.Join(", ", visited)}")
        visited.Clear()
        For i As Integer = 10 To 1 Step -4
            visited.Add(i)
        Next
        Console.WriteLine($"For i = 10 To 1 Step -4: {String.Join(", ", visited)}")
        visited.Clear()
        For i As Integer = 1 To 0
            visited.Add(i)
        Next
        Console.WriteLine($"For i = 1 To 0: {visited.Count} passes")
        Dim last As Integer = 3
        Dim passes As Integer = 0
        For i As Integer = 1 To last
            last = 10
            passes += 1
        Next
        Console.WriteLine($"For i = 1 To last, with last set to 10 inside: {passes} passes")
    End Sub
End Module
Array bounds and For loops on the .NET SDK 10.0.401 Python · loops_session.py
from session import run

# Arrays declared by their last index, ReDim Preserve, and the values For ... To ... Step visits.
run("""
dotnet run --project loops
""")

Output

$ dotnet run --project loops
Dim marks(4): 5 elements, last index 4
Dim none(-1): 0 elements
ReDim Preserve marks(6): 7 elements
Dim broken(-2): OverflowException
For i = 1 To 10 Step 3: 1, 4, 7, 10
For i = 10 To 1 Step -4: 10, 6, 2
For i = 1 To 0: 0 passes
For i = 1 To last, with last set to 10 inside: 3 passes

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 loops_session.py

Declaration or loop Visual Basic C# that looks the same
Dim marks(4) As Integer 5 elements, indexes 0 to 4 new int[4]: 4 elements, 0 to 3
Dim none(-1) As Integer 0 elements, an empty array new int[0]
For i = 1 To 10 Step 3 1, 4, 7, 10 for (i = 1; i < 10; i += 3): 1, 4, 7
For i = 10 To 1 Step -4 10, 6, 2 for (i = 10; i > 1; i -= 4): 10, 6, 2

So a port of Dim marks(n) to C# is new int[n + 1], and a port of For i = a To b is a loop with <=. Three more facts from the run. ReDim Preserve marks(6) gave 7 elements and kept the old values, and an upper bound of -2 is not an empty array but an OverflowException. And the end of a For loop is read once, before the first pass: setting last to 10 inside the loop still gave 3 passes, as Microsoft’s reference for the statement describes.

Key idea

In Visual Basic the number in Dim a(n) and the end of For i = a To n are both included: n is the last index and the last value, so the array has n + 1 elements.

Checkpoint

Check: Array bounds and For loops

  1. Question 1 of 2 How many elements does Dim marks(4) As Integer create?

    Type a number.

    A hint

    The 4 is the last index, and the first index is 0.

    Show the answer: Question 1 of 2

    Answer: 5 elements

    The number in the declaration is the upper bound, the last index, so the indexes run from 0 to 4: five elements, as the run printed. The same array in C# is new int[5].

  2. Question 2 of 2 Which values does For i As Integer = 2 To 11 Step 3 give i?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 2, 5, 8, 11

    For ... To runs while the counter is at most the end (at least, with a negative step), so the end is the last value when the steps land on it: the run gave 1, 4, 7, 10 for 1 To 10 Step 3.

Division gives a Double, CInt rounds, and overflow is checked

Numbers are where a port changes results without a single error. Here are the same lines in Visual Basic and in C#, each run on the .NET SDK 10.0.401:

numbers/Program.vb VB · numbers/Program.vb
' Division, rounding and overflow: the lines a C# developer reads differently.
Module Program
    Sub Main()
        Console.WriteLine($"7 / 2 = {7 / 2}, 7 \ 2 = {7 \ 2}, -7 \ 2 = {-7 \ 2}, -7 Mod 2 = {-7 Mod 2}")
        Console.WriteLine($"CInt(2.5) = {CInt(2.5)}, CInt(3.5) = {CInt(3.5)}, CInt(-2.5) = {CInt(-2.5)}, CInt(2.7) = {CInt(2.7)}")
        Console.WriteLine($"Fix(2.7) = {Fix(2.7)}, Fix(-2.7) = {Fix(-2.7)}, Int(-2.7) = {Int(-2.7)}")
        Dim big As Integer = Integer.MaxValue
        Try
            big += 1
            Console.WriteLine($"Integer.MaxValue + 1 = {big}")
        Catch ex As OverflowException
            Console.WriteLine($"Integer.MaxValue + 1: {ex.GetType().Name}")
        End Try
        Dim nearMax As Double = 2147483647.5
        Try
            Console.WriteLine($"CInt(2147483647.5) = {CInt(nearMax)}")
        Catch ex As OverflowException
            Console.WriteLine($"CInt(2147483647.5): {ex.GetType().Name}")
        End Try
        Dim nearMin As Double = -2147483648.5
        Console.WriteLine($"CInt(-2147483648.5) = {CInt(nearMin)}")
    End Sub
End Module
The Visual Basic version Python · numbers_session.py
from session import run

# Division, conversion rounding and integer overflow as Visual Basic does them, with every Option of the project On.
run("""
dotnet run --project numbers
""")

Output

$ dotnet run --project numbers
7 / 2 = 3.5, 7 \ 2 = 3, -7 \ 2 = -3, -7 Mod 2 = -1
CInt(2.5) = 2, CInt(3.5) = 4, CInt(-2.5) = -2, CInt(2.7) = 3
Fix(2.7) = 2, Fix(-2.7) = -2, Int(-2.7) = -3
Integer.MaxValue + 1: OverflowException
CInt(2147483647.5): OverflowException
CInt(-2147483648.5) = -2147483648

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 numbers_session.py

The same lines in C# C# · csharp/numbers.cs
// The lines of numbers/Program.vb in C#, written the way a C# developer would first write them.
int seven = 7;
Console.WriteLine($"7 / 2 = {seven / 2}, 7 / 2.0 = {seven / 2.0}, -7 / 2 = {-seven / 2}, -7 % 2 = {-seven % 2}");

double half = 2.5;
double threeHalves = 3.5;
double almost = 2.7;
Console.WriteLine($"(int)2.5 = {(int)half}, (int)3.5 = {(int)threeHalves}, (int)-2.5 = {(int)-half}, (int)2.7 = {(int)almost}");
Console.WriteLine($"Convert.ToInt32(2.5) = {Convert.ToInt32(half)}, Convert.ToInt32(3.5) = {Convert.ToInt32(threeHalves)}, Math.Floor(-2.7) = {Math.Floor(-almost)}");

int big = int.MaxValue;
big += 1;
Console.WriteLine($"int.MaxValue + 1 = {big}");

try
{
    checked
    {
        big = int.MaxValue;
        big += 1;
    }
}
catch (OverflowException ex)
{
    Console.WriteLine($"checked: {ex.GetType().Name}");
}

Output

7 / 2 = 3, 7 / 2.0 = 3.5, -7 / 2 = -3, -7 % 2 = -1
(int)2.5 = 2, (int)3.5 = 3, (int)-2.5 = -2, (int)2.7 = 2
Convert.ToInt32(2.5) = 2, Convert.ToInt32(3.5) = 4, Math.Floor(-2.7) = -3
int.MaxValue + 1 = -2147483648
checked: OverflowException

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run numbers.cs

Expression Visual Basic C#
7 divided by 2 7 / 2 is 3.5; 7 \ 2 is 3 7 / 2 is 3 for integers
-7 divided by 2, remainder -7 \ 2 is -3, -7 Mod 2 is -1 -7 / 2 is -3, -7 % 2 is -1
2.5 and 3.5 to an integer CInt: 2 and 4 (int): 2 and 3; Convert.ToInt32: 2 and 4
2.7 to an integer CInt(2.7) is 3, Fix(2.7) is 2 (int)2.7 is 2
-2.7 down to an integer Int(-2.7) is -3, Fix(-2.7) is -2 Math.Floor(-2.7) is -3
Integer.MaxValue + 1 OverflowException -2147483648, unless checked

Three rows decide most ports. / in Visual Basic always divides into a Double, so C#’s integer / becomes \, and \ and Mod truncate towards zero just as C#’s / and % do. CInt rounds, and a half goes to the even neighbour, banker's rounding: Rounding in which a value exactly halfway between two whole numbers goes to the even one: 2.5 becomes 2 and 3.5 becomes 4. Visual Basic's CInt and Math.Round and Python's round() use it. More in “A one-week plan for learning any language”; C#’s cast (int) drops the fraction, so CInt(3.5) is 4 where (int)3.5 is 3. Convert.ToInt32 rounds the way CInt does, which makes it the safe translation. And Visual Basic checks integer arithmetic for overflow unless a project removes the checks, while C# arithmetic is unchecked unless the code or the project asks for checked arithmetic, so a counter that overflows throws in one language and wraps to -2147483648 in the other.

Rounding Calculator Round 2.5, 3.5 and -2.5 half to even and half away from zero, and compare the results with CInt and C#'s cast.

Checkpoint

Check: Division, conversion rounding and overflow

  1. Question 1 of 2 Which C# expression gives the same result as Visual Basic's CInt(x) for every double x that fits in an int?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Convert.ToInt32(x)

    CInt rounds half to even. In C#, Convert.ToInt32 does the same, while the cast truncates, which is the change a quick port makes without any warning.

  2. Question 2 of 2 A Visual Basic counter of type Integer is incremented once past 2147483647. What happens, and what does a line-by-line C# port do?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Visual Basic throws OverflowException; the C# port wraps to -2147483648 unless the code is checked

    A counter that overflows fails loudly in Visual Basic and silently in C#, so a port either adds checked arithmetic or a check of its own where overflow is possible.

Handles wires events when the code compiles, AddHandler while it runs

A C# developer subscribes to an event with += and unsubscribes with -=. Visual Basic has that too, as AddHandler and RemoveHandler, and a second way, which the Windows Forms designer writes for every control you double-click: a field declared WithEvents and a Handles clause on the procedure that handles its event. In this program a till raises an event for every sale:

events/Program.vb VB · events/Program.vb
' A till raises an event for every sale. One handler is wired with WithEvents and Handles, one with AddHandler.
Public Class Till
    Public Shared Created As Integer
    Public Event SaleRecorded(amount As Integer)

    Public Sub New()
        Created += 1
    End Sub

    Public Sub Record(amount As Integer)
        RaiseEvent SaleRecorded(amount)
    End Sub
End Class

Module Shop
    Private WithEvents counter As New Till
    Private total As Integer

    Private Sub AddToTotal(amount As Integer) Handles counter.SaleRecorded
        total += amount
        Console.WriteLine($"AddToTotal: total is {total}")
    End Sub

    Private Sub PrintReceipt(amount As Integer)
        Console.WriteLine($"PrintReceipt: receipt for {amount}")
    End Sub

    Sub Main()
        counter.Record(120)
        AddHandler counter.SaleRecorded, AddressOf PrintReceipt
        counter.Record(80)
        RemoveHandler counter.SaleRecorded, AddressOf PrintReceipt
        counter.Record(50)
        counter = New Till()
        counter.Record(10)
        AddHandler counter.SaleRecorded, AddressOf AddToTotal
        counter.Record(5)
        Console.WriteLine($"Till.Created = {Till.Created}")
    End Sub
End Module
WithEvents, Handles, AddHandler and RemoveHandler Python · events_session.py
from session import run

# One event, a handler wired with WithEvents and Handles, one added and removed with AddHandler and RemoveHandler.
run("""
dotnet run --project events
""")

Output

$ dotnet run --project events
AddToTotal: total is 120
AddToTotal: total is 200
PrintReceipt: receipt for 80
AddToTotal: total is 250
AddToTotal: total is 260
AddToTotal: total is 265
AddToTotal: total is 270
Till.Created = 2

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 events_session.py

Step through Shop.Main and watch which handler each sale reaches:

Interactive · message by message One till, two handlers, and the wiring that changes on the way Step through Shop.Main. Watch which handler each sale reaches and why the last sale is counted twice.
One till, two handlers, and the wiring that changes on the way, step 6 of 6. Messages so far: Shop.Main to counter As Till: Record(120); counter As Till to Handlers: AddToTotal: 120; counter As Till to Handlers: both handlers: 80; counter As Till to Handlers: AddToTotal: 50; counter As Till to Handlers: AddToTotal: 10; counter As Till to Handlers: AddToTotal twice: 5.Shop.Mainrecords the salescounter As Tilla second TillHandlersAddToTotal, PrintReceiptRecord(120)AddToTotal: 120both handlers: 80AddToTotal: 50AddToTotal: 10AddToTotal twice: 5total off by 5

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. A sale of 120. Main calls counter.Record(120), and the till raises SaleRecorded. AddToTotal has a Handles counter.SaleRecorded clause, so it runs and prints total is 120.
  2. Handles at work. The Handles clause was wired when the code was compiled, for whatever object the WithEvents field counter holds. Nothing in Main had to subscribe.
  3. AddHandler adds a second handler. AddHandler counter.SaleRecorded, AddressOf PrintReceipt subscribes another procedure while the program runs. The sale of 80 reaches both: total is 200, then receipt for 80.
  4. RemoveHandler takes it away. After RemoveHandler, the sale of 50 reaches AddToTotal only, which prints total is 250. A handler added with AddHandler stays until RemoveHandler or the end of the object.
  5. A new till in the same field. Main puts a new Till into counter. The Handles wiring moves with the field to the new object, so its sale of 10 still reaches AddToTotal: total is 260. The constructor counted a second till in the Shared field Created.
  6. Wired twice, counted twice. Then Main also adds AddToTotal with AddHandler, while its Handles clause still applies. The sale of 5 calls it twice, 265 and then 270: a double count that one added line can bring into Windows Forms code, where the designer already wrote the Handles clause.

Handles counter.SaleRecorded wires AddToTotal to whatever object the WithEvents field holds, when the code is compiled; Microsoft’s guide to events calls it the declarative way. That is why the sale of 10 reached AddToTotal after a new till was put into counter: the wiring follows the field. AddHandler and RemoveHandler subscribe while the program runs, as C#’s += and -= do. Mixing the two on one procedure counted the sale of 5 twice.

Two words in the program map to C#’s static. Shop is a Module, Visual Basic’s form of a static class: all its members are Shared, nobody creates it, and code anywhere in its namespace may call its members without the module’s name, unless two modules have members of the same name, as BinaryRules.Report and TextRules.Report in the next section do. Till.Created is a Shared field of a class, one value for all tills, which the constructor counted to 2.

A handler wired twice

A procedure with a Handles Button1.Click clause runs on every click already. Adding AddHandler Button1.Click, AddressOf Button1_Click as well, say in Form_Load, makes it run twice per click. Use one wiring per handler: keep Handles for controls the designer created, and AddHandler for objects you create while the program runs.

Checkpoint

Check: WithEvents, Handles and AddHandler; modules and Shared

  1. Question 1 of 2 After counter = New Till(), the new till records a sale of 10. Which handler runs?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: AddToTotal, because the Handles wiring follows the WithEvents field to the new object

    Assigning a new object to a WithEvents field moves every Handles wiring to it. Handlers added with AddHandler stay with the object they were added to.

  2. Question 2 of 2 The last sale, of 5, raised the total from 260 to 270. Why?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: AddToTotal was wired twice, by its Handles clause and by AddHandler, so it ran twice

    A procedure with a Handles clause is already subscribed. AddHandler adds a second subscription, so use one way of wiring per handler.

Option Compare Text changes string comparisons, one file at a time

Option Compare decides how Visual Basic’s own string operators compare: =, <, >, Like, and Select Case on strings. Binary, the default, compares character codes, so case matters, as it does in C#. Text compares as text in the current culture and ignores case. Like the other Options, a statement at the top of a file overrides the project for that file. This project makes the same four comparisons in two files:

One project, two files, two string comparisons

compare/Binary.vb

' This file has no Option Compare statement, so the project's Option Compare Binary applies.
Module BinaryRules
    Function Report() As String
        Return $"Binary file: ""Pune"" = ""PUNE"" is {"Pune" = "PUNE"}, ""apple"" < ""Banana"" is {"apple" < "Banana"}, " &
               $"""INV-42"" Like ""inv-*"" is {"INV-42" Like "inv-*"}, ""Pune"".Equals(""PUNE"") is {"Pune".Equals("PUNE")}"
    End Function
End Module

compare/Text.vb

Option Compare Text
' This file compares strings as text, ignoring case, in the current culture.
Module TextRules
    Function Report() As String
        Return $"Text file:   ""Pune"" = ""PUNE"" is {"Pune" = "PUNE"}, ""apple"" < ""Banana"" is {"apple" < "Banana"}, " &
               $"""INV-42"" Like ""inv-*"" is {"INV-42" Like "inv-*"}, ""Pune"".Equals(""PUNE"") is {"Pune".Equals("PUNE")}"
    End Function
End Module

compare/Program.vb

' The same four comparisons, made in two files of one project.
Module Program
    Sub Main()
        Console.WriteLine(BinaryRules.Report())
        Console.WriteLine(TextRules.Report())
    End Sub
End Module
The same comparisons in a Binary file and a Text file Python · compare_session.py
from session import run

# The same comparisons in a file that keeps the project's Option Compare Binary and in a file that says Option Compare
# Text.
run("""
dotnet run --project compare
""")

Output

$ dotnet run --project compare
Binary file: "Pune" = "PUNE" is False, "apple" < "Banana" is False, "INV-42" Like "inv-*" is False, "Pune".Equals("PUNE") is False
Text file:   "Pune" = "PUNE" is True, "apple" < "Banana" is True, "INV-42" Like "inv-*" is True, "Pune".Equals("PUNE") is False

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 compare_session.py

In Text.vb, "Pune" = "PUNE" is True, "apple" < "Banana" is True because case no longer puts every capital first, and Like "inv-*" matches "INV-42". In Binary.vb all three are False. C#’s == gives the Binary answer; C# has no < for strings, and its string.CompareOrdinal, which compares character codes, also puts “Banana” before “apple”. The last comparison is the same in both files: String.Equals is a .NET method, not a Visual Basic operator, so Option Compare does not reach it. Before you trust = on strings in an old file, read its first lines; and when you port such a file to C#, write the comparison out, for example string.Equals(a, b, StringComparison.CurrentCultureIgnoreCase).

Checkpoint

Check: Option Compare and string comparisons

  1. Question 1 of 2 The project sets Option Compare Binary, and Text.vb starts with Option Compare Text. What is "Pune" = "PUNE" in Text.vb?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: True, because the file compares as text and ignores case

    Option Compare is set per file: the same expression is False in Binary.vb and True in Text.vb of one project.

  2. Question 2 of 2 In Text.vb, which comparison still treats "Pune" and "PUNE" as different?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: "Pune".Equals("PUNE")

    Option Compare reaches =, <, >, Like and Select Case, but not .NET methods such as String.Equals or String.Compare, which keep their own rules. That difference survives into a C# port, where == is always ordinal.

Practise: port loops and conversions without changing a result

The exercise turns three of this lesson’s rules into Python functions that say what the Visual Basic code did: how many elements Dim a(upper) creates, which values a For ... To ... Step loop visits, and what CInt returns, overflow included. Its sample tests are the recorded runs of loops/Program.vb and numbers/Program.vb, so a port that passes them keeps the results.

Exercise · Easy · Python

Port Visual Basic array bounds, For loops and CInt to Python

Code ported from Visual Basic goes wrong most often in three places that compile without a word in the new language. Write three functions in vb_rules.py that answer what the Visual Basic code did, so that a port can be checked against them:

  • dim_length(upper): the number of elements that Dim a(upper) As Integer creates. upper is the last index, so Dim a(4) has 5 elements and Dim a(-1) none. For upper below -1, Visual Basic throws OverflowException: raise OverflowError.
  • for_values(start, end, step=1): the list of values that For i = start To end Step step gives i, in order. The end value is included when the loop reaches it, a negative step counts down, and a loop whose start is already past its end runs no pass. A step of 0 never moves the counter, so in Visual Basic such a loop never ends once its first pass runs: raise ValueError for any step of 0 instead.
  • cint(x): what CInt(x) returns for a floating-point number x: the nearest whole number, a half going to the even neighbour (CInt(2.5) is 2, CInt(3.5) is 4). When that whole number does not fit in an Integer, from -2,147,483,648 to 2,147,483,647, Visual Basic throws OverflowException: raise OverflowError.

The sample tests are the lesson's recorded runs of loops/Program.vb and numbers/Program.vb.

Starter code · vb_rules.py

INT_MIN = -2_147_483_648
INT_MAX = 2_147_483_647


def dim_length(upper):
    """The number of elements of Dim a(upper) As Integer: upper is the last index."""
    return upper  # Replace this line: upper is the last index, not the length.


def for_values(start, end, step=1):
    """The values For i = start To end Step step gives i, in order."""
    return list(range(start, end, step))  # Replace this line: range() stops before its end.


def cint(x):
    """CInt(x) for a floating-point x: round half to even, then check that it fits in an Integer."""
    return int(x)  # Replace this line: int() drops the fraction instead of rounding.
The sample tests · test_vb_rules.py
from vb_rules import cint, dim_length, for_values


def test_array_lengths_from_the_loops_run():
    """counts the elements of Dim marks(4), Dim none(-1) and ReDim Preserve marks(6)"""
    assert dim_length(4) == 5
    assert dim_length(-1) == 0
    assert dim_length(6) == 7


def test_an_upper_bound_below_minus_one_overflows():
    """raises OverflowError for Dim broken(-2), as the loops run threw OverflowException"""
    try:
        dim_length(-2)
    except OverflowError:
        pass
    else:
        assert False, "Dim a(-2) must raise OverflowError"


def test_for_loops_include_their_end():
    """visits 1, 4, 7, 10 for For i = 1 To 10 Step 3, and 10, 6, 2 for Step -4"""
    assert for_values(1, 10, 3) == [1, 4, 7, 10]
    assert for_values(10, 1, -4) == [10, 6, 2]
    assert for_values(1, 4) == [1, 2, 3, 4]


def test_a_loop_past_its_end_runs_no_pass():
    """runs no pass for For i = 1 To 0, and refuses a step of 0"""
    assert for_values(1, 0) == []
    try:
        for_values(1, 5, 0)
    except ValueError:
        pass
    else:
        assert False, "Step 0 must raise ValueError"


def test_cint_rounds_halves_to_even():
    """gives 2, 4, -2 and 3 for CInt(2.5), CInt(3.5), CInt(-2.5) and CInt(2.7), as the numbers run printed"""
    assert [cint(2.5), cint(3.5), cint(-2.5), cint(2.7)] == [2, 4, -2, 3]


def test_cint_overflows_only_past_the_integer_range():
    """raises OverflowError for CInt(2147483647.5), and keeps -2147483648.5, which rounds to the even -2147483648"""
    try:
        cint(2147483647.5)
    except OverflowError:
        pass
    else:
        assert False, "CInt(2147483647.5) must raise OverflowError"
    assert cint(-2147483648.5) == -2147483648
A hint

For for_values, keep adding step to a counter while it has not passed end: with a positive step the loop runs while the counter is at most end, with a negative step while it is at least end. Python's own round() already takes a half to the even neighbour, so cint only needs the range check after rounding.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Six differences, six groups of questions: the self-check opens the group where your answers have been shakiest and moves on from there.

Practice that adapts to you

Practise this lesson

18 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 18 Which Visual Basic declaration matches the C# method int Total(int[] items)?

    Choose one answer.

    Show the answer: Question 1 of 18

    Answer: Function Total(items As Integer()) As Integer

    Sub is a procedure that returns nothing, like a void method; Function returns the type after As. Parameters are ByVal unless they say ByRef.

  2. Question 2 of 18 In a file with Option Strict Off, what does Dim myAge As Integer = "34" + 6 store?

    Choose one answer.

    Show the answer: Question 2 of 18

    Answer: 40, because the string is converted to a number and added

    Use & to join text in Visual Basic. + on strings works too, but when one side is a number it can add instead, which is why old code with Option Strict Off can turn "34" into 40.

  3. Question 3 of 18 You read Recalculate(total) in old code, and total has changed afterwards. Where do you look to find out why?

    Choose one answer.

    Show the answer: Question 3 of 18

    Answer: At the declaration of Recalculate, for a ByRef parameter

    In Visual Basic, the passing mechanism is visible only where the procedure is declared. When porting to C#, every ByRef parameter needs ref at every call site.

  4. Question 4 of 18 Which Visual Basic operator is C#'s ||?

    Choose one answer.

    Show the answer: Question 4 of 18

    Answer: OrElse

    AndAlso is &&, OrElse is ||. And and Or evaluate both sides, like C#'s & and | on Boolean values.

  5. Question 5 of 18 Why is If(divisor <> 0, total \ divisor, 0) safe when divisor is 0, while IIf(divisor <> 0, total \ divisor, 0) is not?

    Choose one answer.

    Show the answer: Question 5 of 18

    Answer: If() evaluates only the branch it returns, while IIf is a function whose arguments are all evaluated first

    The If() operator short-circuits, and IIf, an ordinary function, cannot: Visual Basic evaluates every argument of a function before calling it.

  6. Question 6 of 18 A colleague suggests replacing every And with AndAlso. Where is And still the right operator?

    Choose one answer.

    Show the answer: Question 6 of 18

    Answer: In bit arithmetic on numbers, such as flags And 4

    And and Or are both logical and bitwise operators. Use AndAlso and OrElse for conditions, and keep And and Or for bits.

  7. Question 7 of 18 Which C# line ports Dim totals(months) As Decimal?

    Choose one answer.

    Show the answer: Question 7 of 18

    Answer: decimal[] totals = new decimal[months + 1];

    A Visual Basic array declaration names its upper bound. Add one for the C# length, and loop with <= where the Visual Basic code used For ... To.

  8. Question 8 of 18 For i As Integer = 1 To items.Count removes items inside the loop. What does Visual Basic do with items.Count?

    Choose one answer.

    Show the answer: Question 8 of 18

    Answer: It reads items.Count once, before the first pass, so the loop still runs to the old count

    Because the end is fixed when the loop starts, removing items inside a For loop that counts up runs past the end of the list. Count down instead, or loop over a copy.

  9. Question 9 of 18 What happened in the recorded run for Dim broken(upper) As Integer with upper at -2?

    Choose one answer.

    Show the answer: Question 9 of 18

    Answer: It threw OverflowException

    -1 is the upper bound of an empty array. Anything lower cannot be an array, and the run threw OverflowException.

  10. Question 10 of 18 What does Visual Basic give for 7 / 2?

    Type a number.

    Show the answer: Question 10 of 18

    Answer: 3.5 result

    In Visual Basic, / always divides into a floating-point result, so 7 / 2 is 3.5, as the run printed. Integer division, which C#'s / does on two ints, is \ in Visual Basic: 7 \ 2 is 3.

  11. Question 11 of 18 What did the run print for Int(-2.7) and Fix(-2.7)?

    Choose one answer.

    Show the answer: Question 11 of 18

    Answer: Int(-2.7) = -3 and Fix(-2.7) = -2

    For positive numbers Int and Fix agree; for negative ones Int is Math.Floor and Fix is the truncating cast. CInt is different again: it rounds to the nearest, halves to even.

  12. Question 12 of 18 You port the C# line int perPerson = total / people; with two ints to Visual Basic. Which line keeps its result for every value?

    Choose one answer.

    Show the answer: Question 12 of 18

    Answer: Dim perPerson As Integer = total \ people

    Integer division is \ in Visual Basic and / on integers in C#, and both truncate towards zero; Mod and % both keep the sign of the dividend.

  13. Question 13 of 18 When does a Handles clause connect its procedure to an event?

    Choose one answer.

    Show the answer: Question 13 of 18

    Answer: When the code is compiled, for whatever object the WithEvents field holds

    Handles is fixed when the code is compiled; AddHandler and RemoveHandler change the wiring while the program runs, like C#'s += and -=.

  14. Question 14 of 18 Which C# construct is a Visual Basic Module?

    Choose one answer.

    Show the answer: Question 14 of 18

    Answer: A static class

    A Module is Visual Basic's static class: its members are implicitly Shared, and they can be called from anywhere in the namespace without the module's name.

  15. Question 15 of 18 A form creates one button for each customer while it runs. How do you wire their Click events in Visual Basic?

    Choose one answer.

    Show the answer: Question 15 of 18

    Answer: With AddHandler for each button as it is created

    Handles fits controls known when the code is compiled, such as those the designer creates. Objects created while the program runs are wired with AddHandler and unwired with RemoveHandler.

  16. Question 16 of 18 A Visual Basic file has no Option Compare statement, and its project file says nothing either. How does = compare strings there?

    Choose one answer.

    Show the answer: Question 16 of 18

    Answer: By character code, so case matters (Binary)

    The default comparison method is Binary. Text has to be asked for, and once it is, = ignores case in that file.

  17. Question 17 of 18 In a file with Option Compare Text, Select Case code has a Case "ADMIN". Does it match the value "admin"?

    Choose one answer.

    Show the answer: Question 17 of 18

    Answer: Yes, because Select Case compares strings by Option Compare

    Option Compare reaches every string comparison of the language in that file: =, <, >, Like and Select Case.

  18. Question 18 of 18 You port a function from a file with Option Compare Text to C#. It compares customer codes with =. What do you write in C#?

    Choose one answer.

    Show the answer: Question 18 of 18

    Answer: string.Equals(a, b, StringComparison.CurrentCultureIgnoreCase)

    The setting of the file is part of the code's meaning. In C#, write the comparison out, so the next reader sees that case is ignored.

Interview questions

Warm-up (fresher to mid-level): What is the difference between And and AndAlso in Visual Basic, and which one is C#’s &&?

And always evaluates both of its operands, while AndAlso stops when the left operand is False, because the result is then known to be False. C#’s && short-circuits, so it corresponds to AndAlso, and || to OrElse. The difference matters whenever the right side depends on the left: x IsNot Nothing And x.Count > 0 throws NullReferenceException when x is Nothing, because And still reads x.Count, while the AndAlso version gives False. And and Or are also Visual Basic’s bitwise operators, as in flags And 4, which is why they evaluate both sides. The same split holds for choosing a value: the If(c, x, y) operator evaluates only the branch it returns, while the IIf function evaluates every argument before it runs.

Two harder questions, both about code that compiles in either language and behaves differently:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    After a refactor of a Visual Basic Windows Forms app, saving a form writes every record twice. The Save button's Click handler has a Handles clause, and new code in Form_Load also calls AddHandler for it. What happens, how do you fix it, and how do you find other handlers wired twice?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to staff

    In one Visual Basic project, looking up the customer code "acme-01" finds "ACME-01" in the billing module but not in the shipping module, although both compare with =. How can that be, and what do you do before porting both modules to C#?

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. How many elements does Dim marks(4) As Integer have, and which values does For i = 1 To 10 Step 3 visit?

    Five, indexes 0 to 4, because the number is the last index; the loop visits 1, 4, 7 and 10, its end included. A C# port needs new int[n + 1] and a loop with <=.

  2. What does Dim count As Integer = Nothing give, and what is "" = Nothing for strings?

    0: Nothing is the default of whatever type receives it, like C#'s default. For strings, an empty string equals Nothing, so only Is Nothing asks whether there is a string at all.

  3. A Sub declares ByRef amount. What does the caller's code show, and how can it pass a copy?

    Nothing at the call site shows that the variable may change; C# would make you write ref. Wrapping the argument in an extra pair of parentheses, AddTax((price)), passes a copy.

  4. Which Visual Basic operators are C#'s && and ||?

    AndAlso and OrElse. And and Or evaluate both sides, so x IsNot Nothing And x.Count > 0 throws NullReferenceException when x is Nothing; IIf also evaluates every argument.

  5. What are CInt(3.5) and C#'s (int)3.5, and which C# call matches CInt?

    4 and 3. CInt rounds a half to the even neighbour, the cast drops the fraction, and Convert.ToInt32 rounds the way CInt does.

  6. What happens at Integer.MaxValue + 1 in Visual Basic, and at int.MaxValue + 1 in C#?

    Visual Basic throws OverflowException, because it checks integer arithmetic unless the project turns that off. C# wraps to -2147483648 unless the code or the project asks for checked arithmetic.

  7. A procedure has Handles Button1.Click and Form_Load also adds it with AddHandler. What happens on a click?

    It runs twice, once per wiring. Keep Handles for controls the designer made and AddHandler for objects created while the program runs, one wiring per handler.

  8. Option Compare Text is the first line of a file. What is "Pune" = "PUNE" there?

    True: in that file, Visual Basic's own string operators ignore case and follow the current culture. String.Equals is a .NET method, so it still tells the two apart.

Key takeaways

  • Nothing is the default of any type, 0 for an Integer; ByRef changes the caller’s variable with no marker at the call site, and an extra pair of parentheses passes a copy.
  • And and Or evaluate both sides and IIf evaluates every argument, so conditions use AndAlso, OrElse and the If() operator.
  • Dim a(n) has n + 1 elements and For i = a To n includes n, so ports need new T[n + 1] and <=.
  • / divides into a Double, CInt rounds half to even where C#’s (int) truncates, and Visual Basic throws on integer overflow where C# wraps unless code is checked.
  • Handles wires a handler to a WithEvents field when the code compiles, AddHandler while it runs, and using both on one procedure runs it twice; a Module is a static class and Shared means static.
  • Option Compare Text at the top of a file makes =, < and Like ignore case in that file only, and leaves .NET methods such as String.Equals unchanged.

Terms in this lesson

short-circuit evaluation
Evaluating a logical expression only as far as needed for its result: if the left side of an and is False, the right side is skipped. Visual Basic's AndAlso and OrElse and C#'s && and || work this way; And and Or do not.
upper bound
The highest index of an array. Visual Basic declares an array by it, so Dim marks(4) As Integer has the indexes 0 to 4 and five elements, while C# declares new int[4] by its length, four elements.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.