Why I Dislike VB.NET
Contents
I have used Visual Basic .NET (VB.NET) at work for the past six years. I also strongly dislike it as a language. During this period as my software development skills and taste in language design have developed, so has my utter distaste for VB.NET.
For a while now I have been meaning to compile my thoughts on the subject so that when someone asks me why I don't like VB.NET I can point them somewhere that explains, in detail, the issues I have with the language.
The factors that have influenced by dislike for VB.NET range from major problems with the language to minor annoyances that have driven me crazy over time. While I try to maintain a somewhat objective position, there are definitely some factors that are purely my own opinion.
With all that said, let's get into it.
Lax Compiler Defaults
The VB.NET compiler has four different compiler options: Option Compare
, Option Explicit
, Option Infer
, and Option Strict
. Of these four, the latter three should all be set to On
in order to get the most safety and utility out of the compiler.
While Option Explicit
and Option Infer
both default to On
, Option Strict
does not.
According to the docs:
[
Option Strict
] [r]estricts implicit data type conversions to only widening conversions, disallows late binding, and disallows implicit typing that results in anObject
type.
That may sound like a lot of jargon, so let's take a closer look at why the default of Off
for Option Strict
is a bad choice.
Exhibit A
Here we have a simple program:
Imports System
Imports System.Collections.Generic
Imports Newtonsoft.Json
Module Program
<JsonObject>
Class ItemsContainer
Public Property Items As List(Of Integer)
End Class
Sub Main(args As String())
Dim items As New List(Of Integer) From {1, 2, 3, 4, 5}
Dim cart As New ItemsContainer With {
.Items = items.Select(Function(n) n + 1)
}
Console.WriteLine(JsonConvert.SerializeObject(cart))
End Sub
End Module
This program will compile with no errors or warnings. However, running this program will result in you being greeted with a lovely System.InvalidCastException
:
Unhandled exception. System.InvalidCastException: Unable to cast object of type 'SelectListIterator`2[System.Int32,System.Int32]' to type 'System.Collections.Generic.List`1[System.Int32]'.
at Sandbox.Program.Main(String[] args) in /Sandbox/Program.vb:line 14
If we modify the project options and set Option Strict
to On
we get a very different result at compile time:
/Sandbox/Program.vb(15,22): error BC30512: Option Strict On disallows implicit conversions from 'IEnumerable(Of Integer)' to 'List(Of Integer)'.
Now that the compiler has alerted us to this mistake, fixing it is just a matter of adding a .ToList
to materialize the list:
Dim cart As New ItemsContainer With {
- .Items = items.Select(Function(n) n + 1)
+ .Items = items.Select(Function(n) n + 1).ToList
}
Exhibit B
Let's turn Option Strict
back off and take a look at another example:
Imports System
Module Program
Class Employee
Public Property IdNumber As Integer
End Class
Class Customer
Public Property CustomerNumber As Integer
End Class
Sub Main(args As String())
Dim employeeOfTheMonth = GetEmployeeOfTheMonth()
Pay(employeeOfTheMonth)
End Sub
Function GetEmployeeOfTheMonth()
Dim alice As New Employee With {
.IdNumber = 64812
}
Dim bob As New Customer With {
.CustomerNumber = 97832
}
Return bob
End Function
Sub Pay(employee As Employee)
Console.WriteLine($"Paid employee #{employee.IdNumber}.")
End Sub
End Module
Did you notice the problem with this code snippet? If you didn't, don't feel bad; the compiler didn't catch this one either. If we build our program, we get no errors or warnings from the compiler.
Let's try running our program:
Unhandled exception. System.InvalidCastException: Unable to cast object of type 'Customer' to type 'Employee'.
at Sandbox.Program.Main(String[] args) in /Sandbox/Program.vb:line 15
Another System.InvalidCastException
!
Let's turn Option Strict
back on and see if the compiler could have saved us here:
/Sandbox/Program.vb(17,14): error BC30210: Option Strict On requires all Function, Prope
rty, and Operator declarations to have an 'As' clause.
The first thing our now-enlightened compiler points out is that we are missing a return type on our GetEmployeeOfTheMonth
function:
-Function GetEmployeeOfTheMonth()
+Function GetEmployeeOfTheMonth() As Employee
Dim alice As New Employee With {
.IdNumber = 64812
}
Dim bob As New Customer With {
.CustomerNumber = 97832
}
Return bob
GetEmployeeOfTheMonth
should clearly return an Employee
.
If we build again we'll see another error:
/Sandbox/Program.vb(26,16): error BC30311: Value of type 'Program.Customer' cannot be converted to 'Program.Employee'.
We've now determined the root cause of our troubles: we were trying to return a customer as the employee of the month!
One small tweak and we'll make sure that our employee, Alice, gets the compensation she deserves:
Function GetEmployeeOfTheMonth() As Employee
Dim alice As New Employee With {
.IdNumber = 64812
}
Dim bob As New Customer With {
.CustomerNumber = 97832
}
- Return bob
+ Return alice
End Function
If we run the program now we'll see that we correctly paid our employee of the month:
Paid employee #64812.
The Microsoft.VisualBasic Namespace
The Microsoft.VisualBasic namespace contains types that support the Visual Basic Runtime in Visual Basic.
While undoubtedly useful as a tool for transitioning legacy VB6 programs to VB.NET, the usefulness of the Microsoft.VisualBasic
namespace stops there.
Its existence serves as a crutch for those who are used to programming in VB6 and prevents the adoption of common .NET idioms. One good example of this is Microsoft.VisualBasic
's TriState
enum. This type is used to indicate True
, False
, or the absence of a value UseDefault
. This type, however, is entirely redundant with Boolean?
(aka Nullable(Of Boolean)
) that exists within .NET itself.
This namespace is also imported by default for .NET Framework projects. This is especially problematic, as it means that developers may unwittingly start relying it.
One other major problem is that the Microsoft.VisualBasic
namespace only exists on .NET Framework, which means that any VB.NET programs that rely on it cannot be ported to .NET Standard or .NET Core without first removing all usages of members from this namespace.
Functional Programming Woes
Trying to write VB.NET code in a functional style is an exercise wrought with pain. While possible, the end result is incredibly verbose and far uglier than what could be achieved in F# or even C#.
AddressOf
When passing functions as arguments VB.NET forces you to use the AddressOf
operator, which is needlessly verbose:
Imports System
Imports System.Linq
Module Program
Sub Main(args As String())
Dim numbers As New List(Of Integer) From {1, 2, 3, 4, 5}
Dim factorial = numbers.Aggregate(1, AddressOf Multiply)
Console.WriteLine(factorial)
End Sub
Function Multiply(a As Integer, b As Integer) As Integer
Return a * b
End Function
End Module
Bad Lambdas
VB.NET's lambdas are also far too verbose. The keywords themselves are far too long, especially when dealing with lambda-heavy APIs like LINQ.
Visual Studio also has this annoying habit of lining up lambdas like so:
Imports System
Module Program
Sub Main(args As String())
Dim message = AFunctionWithAReallyLongNameToProveAPoint(Function()
Return "Hello, world"
End Function)
Console.WriteLine(message)
End Sub
Function AFunctionWithAReallyLongNameToProveAPoint(f As Func(Of String)) As String
Return f()
End Function
End Module
This usually results in the more interesting parts of the code getting lost on the right side of the screen.
Separators
In VB.NET the separator character (:
) exists to put multiple statements on a single line, like so:
a = 3.2 : b = 7.6 : c = 2
Personally, I don't see why you'd want to do this. Having the statements on separate lines would be easier to read as well as play nicer with version control.
The real problem I have with separators is when they make their way into control flow:
If True Then Console.WriteLine("Foo") : If False Then Console.WriteLine("Bar") : Else Console.WriteLine("Baz")
What would this piece of code output if we were to run it?
The answer is:
Foo
Baz
Regardless of how trivial it might have been to figure out what the output would be, I maintain that it would be much easier were that code written like this:
If True Then
Console.WriteLine("Foo")
If False Then
Console.WriteLine("Bar")
Else
Console.WriteLine("Baz")
End If
End If
The second example makes it blatantly obvious what the result of running this code would be.
ByVal
The ByVal
keyword exists to indicate that an argument is passed by value.
Sub Greet(ByVal name As String)
Console.WriteLine($"Hello there, {name}")
End Sub
By default, all arguments in VB.NET are passed by value, so we can just drop the ByVal
entirely:
Sub Greet(name As String)
Console.WriteLine($"Hello there, {name}")
End Sub
The official guidance from the VB.NET docs is to not use ByVal
:
Because it is the default, you do not have to explicitly specify the
ByVal
keyword in method signatures. It tends to produce noisy code and often leads to the non-defaultByRef
keyword being overlooked.
This begs the question: why even bother having ByVal
in the first place?
I suspect this is yet another wart inherited from VB6. In VB6 every argument is ByRef
by default with ByVal
being opt-in behavior: the exact opposite of how it works in VB.NET.
It's Not F#
At the end of the day, the biggest reason I dislike VB.NET is because if I'm writing VB.NET it means I could be writing F# instead. Given that both languages run on the .NET platform and can interoperate with other .NET languages, F# can be used anywhere that VB.NET can.
The bottom line is that F# is my go-to choice for solving any problem on .NET, and if there is a particular area where F# doesn't quite fit then I'll use C# to plug in the gaps.