Marshall Bowers

Conjurer of code. Devourer of art. Pursuer of æsthetics.

Why I Dislike VB.NET

Friday, January 31, 2020
1646 words
9 minute read

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 an Object 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-default ByRef 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.