Type Conversions

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. A conversion changes how a value is represented, so that it can be stored in a variable or passed to a parameter of another type. Visual Basic has more conversion syntax than most languages, and each form has different rules for what it accepts, what it rounds, which culture it reads and which exception it throws: the sixteen conversion functions (CInt and its relatives), the operators CType, DirectCast and TryCast, the type test TypeOf ... Is, the Convert class, Parse and TryParse, and conversion operators that a type defines for itself. This page puts them side by side. The rule that decides how many of them are allowed implicitly is Option Strict, described on Option Statements; the types themselves on Data Types and Literals.

The page covers the following, in this order: widening and narrowing with the complete matrix produced by the compiler; the conversion functions; the cast operators; text and culture; user-defined conversions; variance; enumerations and Boolean; a decision flowchart; the exceptions; and the differences from C#. Operators and their result types are on Operators and Expressions and string formatting on Strings and Text.

Every example is a complete program that was built with Option Strict On and warnings as errors on the .NET 10 SDK and run; the output is the real one. The programs are small by design, and use the shelf catalogue’s types (CatalogItem, Book, Magazine, Isbn, LoanState). Examples that are meant not to compile are labelled and show the compiler’s messages.

Widening and narrowing

A conversion is widening when every value of the source type can be represented in the target type’s range, and narrowing when some cannot. Learn’s definition adds that a widening conversion can still lose precision (Long to Double keeps the magnitude but not all the digits), so widening is a statement about range and not about exactness. A conversion is implicit when the compiler applies it without being asked, and explicit when the source code asks for it with a function or an operator. The rules:

  • Widening conversions are always allowed implicitly, and never throw.

  • Narrowing conversions are allowed implicitly only with Option Strict Off, where a failure appears at run time. With Option Strict On every narrowing conversion needs an explicit conversion (BC30512 otherwise).

  • A constant that fits the target type converts implicitly in either direction (Dim b As Byte = 200).

  • The For Each loop variable is the documented exception: its narrowing conversion from the element type is not checked at compile time.

The compiler can tell which pair is which, so the matrix below was generated: a program of 240 one-line assignments, Dim t As Target = v for every ordered pair of the sixteen elementary types with v of the source type, was built with Option Strict On, and the pairs that produced no error are widening. It was built only; the 179 diagnostics are the pairs that are not.

From Implicit widening conversions that Option Strict On accepts

Boolean

Object

Byte

Short, UShort, Integer, UInteger, Long, ULong, Decimal, Single, Double, Object

SByte

Short, Integer, Long, Decimal, Single, Double, Object

Short

Integer, Long, Decimal, Single, Double, Object

UShort

Integer, UInteger, Long, ULong, Decimal, Single, Double, Object

Integer

Long, Decimal, Single, Double, Object

UInteger

Long, ULong, Decimal, Single, Double, Object

Long

Decimal, Single, Double, Object

ULong

Decimal, Single, Double, Object

Decimal

Single, Double, Object

Single

Double, Object

Double

Object

Char

String, Object

String

Object

Date

Object

Read the table for four things:

  • The integer chains: Byte widens to every other numeric type except SByte; Short does not widen to UShort; Integer does not widen to UInteger; a signed type never widens to an unsigned one and the reverse needs a larger signed type (UInteger to Long).

  • Long and ULong widen to Single and Double, and Decimal widens to both. The value stays in range; the low digits may not survive.

  • Char widens to String, and a String does not narrow to Char implicitly. Date does not widen to anything but Object.

  • Everything widens to Object (the last column of each row), and Object narrows to everything.

The pairs that are missing are of three kinds, with different messages. The compiler’s wording is part of the diagnosis:

Code Message Meaning

BC30512

Option Strict On disallows implicit conversions from 'Double' to 'Integer'.

A narrowing conversion exists. Write the conversion.

BC30311

Value of type 'Byte' cannot be converted to 'Date'.

No conversion exists at all between the types, with any syntax. Boolean and Date against numbers and Char are the main cases.

BC32006 and BC32007

'Char' values cannot be converted to 'Byte'. Use 'Microsoft.VisualBasic.AscW' to interpret a character as a Unicode value or 'Microsoft.VisualBasic.Val' to interpret it as a digit. And the reverse: 'Byte' values cannot be converted to 'Char'. Use 'Microsoft.VisualBasic.ChrW'.

Char and the numeric types are not convertible: ask for the code point or the digit explicitly.

BC30532 and BC30533

Conversion from 'Date' to 'Double' requires calling the 'Date.ToOADate' method. And: Conversion from 'Double' to 'Date' requires calling the 'Date.FromOADate' method.

The Visual Basic 6 habit of treating a date as a number of days is not available implicitly.

Everything else the compiler reports is BC30512 (129 pairs) or BC30311 (32 pairs); the remaining pairs split between BC32006 or BC32007 (16 pairs) and BC30532 or BC30533 (2 pairs).

Plus is not concatenation

Visual Basic converts the operands of + toward numbers when one of them is a string, which is the origin of the best-known conversion bug of the language. With Option Strict On the compiler stops it, and the messages show the two conversions that it refused:

Option Strict On

Module Program
    Sub Main()
        Dim copies As Integer = 3
        Dim label As String = "Copies: " + copies
        Dim total As Integer = "5" + 1
        Dim joined As String = "Copies: " & copies
        Dim sum As String = "5" & 1 & 2
        Dim d As Date = #10/19/2026# + 1
    End Sub
End Module
Program.vb(10,25): error BC30452: Operator '+' is not defined for types 'Date' and 'Integer'.
Program.vb(6,31): error BC30512: Option Strict On disallows implicit conversions from 'Double' to 'String'.
Program.vb(6,31): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Double'.
Program.vb(7,32): error BC30512: Option Strict On disallows implicit conversions from 'Double' to 'Integer'.
Program.vb(7,32): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Double'.

Each of lines 6 and 7 reports two BC30512 errors: the String operand would be converted to Double and the Double result converted back. The two & lines compile, because every conversion to a string by the concatenation operator is considered widening. The last line shows that Date + Integer has no meaning at all (BC30452, "Operator '+' is not defined"); Date arithmetic uses AddDays and its relatives. Quickstart Guide pp. 18 and 134 concatenate with +; the correct operator is &, or an interpolated string.

The conversion functions

The sixteen conversion functions are keywords of the language, one per elementary type. Each converts its argument to the target type, and the compiler inlines the common cases. They are the usual way to write an explicit conversion between built-in types.

Function Returns Notes

CBool

Boolean

Zero is False, any other number True. A string must be "True", "False" or a number ("1" gives True); anything else is InvalidCastException.

CByte

Byte

Rounds, then range-checks 0 to 255.

CChar

Char

The first character of a string. An empty string gives ChrW(0). Numbers do not convert (BC32007).

CDate

Date

Parses the string with the current culture; "2026-10-19" works everywhere, "31/12/2026" only where day-first is the culture.

CDbl

Double

Accepts exponents ("1.5e2"), whitespace, culture-specific separators.

CDec

Decimal

From Double it rounds to 15 significant digits first, so CDec(0.1) is 0.1, unlike the exact binary value of the double.

CInt

Integer

Rounds half to even. Strings may be decimal or carry a &H or &O prefix.

CLng

Long

As CInt.

CObj

Object

Boxes a value type; never fails.

CSByte

SByte

Range-checks -128 to 127.

CShort

Short

As CInt.

CSng

Single

Rounds to the nearest Single.

CStr

String

True gives "True", numbers and dates are formatted with the current culture, Nothing gives Nothing. Return values are tabulated in Learn’s article on CStr.

CUInt

UInteger

Negative values throw OverflowException.

CULng

ULong

As CUInt.

CUShort

UShort

As CUInt.

The program below applies the functions to values from the shelf scenario (a quantity typed as text, a price, a due date) and prints the result or the exception. Every Attempt call runs the conversion on a variable, not on a constant, because the compiler folds constant conversions and reports their errors at build time:

Option Strict On

Imports System.Globalization

Module Program
    Private Sub Attempt(label As String, action As Func(Of Object))
        Dim shown As String
        Try
            Dim result As Object = action()
            shown = TypeName(result) & " " & Convert.ToString(result, CultureInfo.InvariantCulture)
        Catch ex As Exception
            shown = ex.GetType().Name & ": " & ex.Message
        End Try
        Console.WriteLine(label.PadRight(26) & shown)
    End Sub

    Sub Main()
        Dim text As String = "12"
        Dim blank As String = ""
        Dim maybe As Object = Nothing
        Dim threeHundred As Integer = 300
        Dim minusOne As Integer = -1
        Dim big As Long = 3000000000L
        Dim fraction As Double = 3.99
        Dim notANumber As Double = Double.NaN
        Dim twoPointFive As Double = 2.5

        ' To Integer, from strings, Nothing, Boolean and floating point.
        Attempt("CInt(""12"")", Function() CInt(text))
        Attempt("CInt(""&H1F"")", Function() CInt("&H1F"))
        Attempt("CInt("" 12 "")", Function() CInt(" 12 "))
        Attempt("CInt("""")", Function() CInt(blank))
        Attempt("CInt(""abc"")", Function() CInt("abc"))
        Attempt("CInt(Nothing)", Function() CInt(maybe))
        Attempt("CInt(3.99)", Function() CInt(fraction))
        Attempt("CInt(True)", Function() CInt(True))
        Attempt("CInt(3000000000L)", Function() CInt(big))
        Attempt("CInt(NaN variable)", Function() CInt(notANumber))
        Attempt("CByte(300)", Function() CByte(threeHundred))
        Attempt("CByte(-1)", Function() CByte(minusOne))
        Attempt("CUInt(-1)", Function() CUInt(minusOne))
        Attempt("CSByte(300)", Function() CSByte(threeHundred))

        ' Boolean, Char and String.
        Attempt("CBool(0) CBool(5)", Function() CBool(0).ToString() & " " & CBool(threeHundred).ToString())
        Attempt("CBool(""true"")", Function() CBool("true"))
        Attempt("CBool(""1"")", Function() CBool("1"))
        Attempt("CBool(""yes"")", Function() CBool("yes"))
        Attempt("CChar(""hello"")", Function() CChar("hello"))
        Attempt("AscW(CChar(""""))", Function() AscW(CChar(blank)))
        Attempt("CStr(True)", Function() CStr(True))
        Attempt("CStr(1.5)", Function() CStr(1.5))
        Attempt("CStr(Nothing)", Function() CStr(maybe) Is Nothing)

        ' Floating point, Decimal and Date.
        Attempt("CDbl(""1.5e2"")", Function() CDbl("1.5e2"))
        Attempt("CDec(""19.99"")", Function() CDec("19.99"))
        Attempt("CDec(0.1)", Function() CDec(0.1))
        Attempt("CSng(1 / 3)", Function() CSng(1 / 3).ToString("R", CultureInfo.InvariantCulture))
        Attempt("CDate(""2026-10-19"")", Function() CDate("2026-10-19"))
        Attempt("CDate(""31/12/2026"")", Function() CDate("31/12/2026"))
        Attempt("CObj(5)", Function() CObj(5).GetType().Name)
        Attempt("CULng(2.5)", Function() CULng(twoPointFive))
    End Sub
End Module
CInt("12")                Integer 12
CInt("&H1F")              Integer 31
CInt(" 12 ")              Integer 12
CInt("")                  InvalidCastException: Conversion from string "" to type 'Integer' is not valid.
CInt("abc")               InvalidCastException: Conversion from string "abc" to type 'Integer' is not valid.
CInt(Nothing)             Integer 0
CInt(3.99)                Integer 4
CInt(True)                Integer -1
CInt(3000000000L)         OverflowException: Arithmetic operation resulted in an overflow.
CInt(NaN variable)        OverflowException: Arithmetic operation resulted in an overflow.
CByte(300)                OverflowException: Arithmetic operation resulted in an overflow.
CByte(-1)                 OverflowException: Arithmetic operation resulted in an overflow.
CUInt(-1)                 OverflowException: Arithmetic operation resulted in an overflow.
CSByte(300)               OverflowException: Arithmetic operation resulted in an overflow.
CBool(0) CBool(5)         String False True
CBool("true")             Boolean True
CBool("1")                Boolean True
CBool("yes")              InvalidCastException: Conversion from string "yes" to type 'Boolean' is not valid.
CChar("hello")            Char h
AscW(CChar(""))           Integer 0
CStr(True)                String True
CStr(1.5)                 String 1.5
CStr(Nothing)             Boolean True
CDbl("1.5e2")             Double 150
CDec("19.99")             Decimal 19.99
CDec(0.1)                 Decimal 0.1
CSng(1 / 3)               String 0.33333334
CDate("2026-10-19")       Date 10/19/2026 00:00:00
CDate("31/12/2026")       InvalidCastException: Conversion from string "31/12/2026" to type 'Date' is not valid.
CObj(5)                   String Int32
CULng(2.5)                ULong 2

What the output teaches:

  • CInt("&H1F") is 31: a string with a radix prefix is parsed as hexadecimal, which neither Integer.Parse nor C# does.

  • CInt(Nothing) is 0, but CInt("") is InvalidCastException. A null String or Object reads as zero; an empty string is not a number.

  • CInt(3.99) is 4: the conversion functions round to the nearest, they do not truncate (next section).

  • CInt(True) is -1 (see the section on Boolean).

  • CInt(3000000000L), CByte(300), CByte(-1), CUInt(-1) and CSByte(300) throw OverflowException, "Arithmetic operation resulted in an overflow.", and so does a floating-point NaN stored in a variable. The compiler folds conversions of constants: CInt(Double.NaN) written with the constant compiled and was folded to 0 by the compiler with no diagnostic in an earlier build of this program, whereas the same conversion of a variable throws. Do not use a conversion of a constant to test the run-time behaviour.

  • CBool("yes") throws InvalidCastException, "Conversion from string "yes" to type 'Boolean' is not valid."

  • CChar("") is the NUL character (code 0), and CChar("hello") is h.

  • CDbl("1.5e2") is 150 and CDec(0.1) is exactly 0.1.

  • CDate("31/12/2026") fails here because the process runs with the invariant culture, whose short date is month first.

Banker’s rounding

When the argument is not integral, the integer conversion functions round to the nearest integer, and a value exactly halfway rounds to the even neighbour (banker’s rounding, IEEE 754 round half to even): 0.5 becomes 0, 1.5 becomes 2, 2.5 becomes 2 and 3.5 becomes 4. This is a difference from C#, whose cast (int)x truncates toward zero. The next program prints the rounding family together, so you can pick the one you mean:

Option Strict On

Imports System.Globalization

Module Program
    Sub Main()
        Dim inv As CultureInfo = CultureInfo.InvariantCulture
        Dim values As Double() = {-2.5, -1.5, -0.7, -0.5, 0.5, 1.5, 2.5, 2.7, 3.5}
        Console.WriteLine("value  CInt  Math.Round  AwayFromZero  Fix  Int  Floor  Ceiling  Truncate")
        For Each v In values
            Console.WriteLine(v.ToString("0.0", inv).PadLeft(5) & "  " &
                              CInt(v).ToString().PadLeft(4) & "  " &
                              Math.Round(v).ToString("0", inv).PadLeft(10) & "  " &
                              Math.Round(v, MidpointRounding.AwayFromZero).ToString("0", inv).PadLeft(12) & "  " &
                              Fix(v).ToString("0", inv).PadLeft(3) & "  " &
                              Int(v).ToString("0", inv).PadLeft(3) & "  " &
                              Math.Floor(v).ToString("0", inv).PadLeft(5) & "  " &
                              Math.Ceiling(v).ToString("0", inv).PadLeft(7) & "  " &
                              Math.Truncate(v).ToString("0", inv).PadLeft(8))
        Next

        ' Integer division truncates toward zero and works on integers; / returns a Double first.
        Dim total As Integer = 7
        Dim count As Integer = 2
        Console.WriteLine("7 \ 2 = " & (total \ count).ToString() & ", -7 \ 2 = " & (-total \ count).ToString() &
                          ", CInt(7 / 2) = " & CInt(total / count).ToString() & ", CInt(-7 / 2) = " & CInt(-total / count).ToString())

        ' Decimal rounds the same way: banker's rounding to even.
        Console.WriteLine("CInt(2.5D) = " & CInt(2.5D).ToString() & ", CInt(3.5D) = " & CInt(3.5D).ToString() & ", Math.Round(2.675D, 2) = " & Math.Round(2.675D, 2).ToString(inv))

        ' The optimized pattern of Visual Basic 16: the argument is already integral.
        Dim s As Single = 173.7619F
        Dim d As Double = s
        Console.WriteLine(CInt(Fix(s)).ToString() & " " & CByte(Int(d)).ToString() & " " & CShort(Math.Truncate(s)).ToString() & " " &
                          CInt(Math.Ceiling(d)).ToString() & " " & CInt(Math.Round(s)).ToString())
    End Sub
End Module
value  CInt  Math.Round  AwayFromZero  Fix  Int  Floor  Ceiling  Truncate
 -2.5    -2          -2            -3   -2   -3     -3       -2        -2
 -1.5    -2          -2            -2   -1   -2     -2       -1        -1
 -0.7    -1          -1            -1   -0   -1     -1       -0        -0
 -0.5     0          -0            -1   -0   -1     -1       -0        -0
  0.5     0           0             1    0    0      0        1         0
  1.5     2           2             2    1    1      1        2         1
  2.5     2           2             3    2    2      2        3         2
  2.7     3           3             3    2    2      2        3         2
  3.5     4           4             4    3    3      3        4         3
7 \ 2 = 3, -7 \ 2 = -3, CInt(7 / 2) = 4, CInt(-7 / 2) = -4
CInt(2.5D) = 2, CInt(3.5D) = 4, Math.Round(2.675D, 2) = 2.68
173 173 173 174 174

Source: Math.Round

Compare the columns row by row. CInt and Math.Round(v) agree: both round half to even, -2.5 to -2 and 2.5 to 2. MidpointRounding.AwayFromZero is the school rule (2.5 to 3, -2.5 to -3) and the one to use for money when the business says "round half up". Fix and Math.Truncate drop the fraction toward zero; Int and Math.Floor go toward negative infinity (-2.5 gives -3); Math.Ceiling toward positive infinity. Several cells print -0: Fix, Math.Ceiling, Math.Truncate and Math.Round of values between -1 and 0 return the Double negative zero, and the formatting of .NET Core 3.0 and later shows its sign. The last lines show that the integer division \ truncates toward zero (-7 \ 2 is -3) and that CInt(7 / 2) rounds the Double 3.5 to 4: the same two numbers, two different answers. Decimal rounds the same way as Double: CInt(2.5D) is 2 and CInt(3.5D) is 4.

Fix, Int and the Visual Basic 16 optimization

Fix and Int are runtime-library functions that return the integral part of a number: Fix drops the fraction (toward zero), Int rounds toward negative infinity. Both return the same type as the argument, so Fix(173.7619) is the Double 173 and a conversion is needed to store it in an Integer. Visual Basic 16.0 (2019) made the combination fast: when the argument of CByte, CShort, CInt, CLng, CSByte, CUShort, CUInt or CULng is the result of Fix, Int, Math.Ceiling, Math.Floor, Math.Round or Math.Truncate on a Double or Single, the conversion is much faster; Learn says up to twice as fast for code with many conversions, and that the optimization truncates rather than rounds. The argument is already a whole number in these cases, so the value is the same either way. The last output line of the program above applies the five pairs of the documentation example to 173.7619 and prints 173 173 173 174 174.

CType, DirectCast and TryCast

The three operators convert between any types, not only built-in ones, and differ in what they are allowed to do:

Operator What it does Fails with Typical use

CType(x, T)

Chooses the conversion at compile time from the static types: a built-in conversion, a user-defined one, a reference conversion or unboxing; with an Object source, a run-time conversion through the Visual Basic helpers (CType(obj, Integer) converts a Short, a String or a Boolean).

InvalidCastException, OverflowException.

The general explicit conversion, and the only operator that uses user-defined conversions.

DirectCast(x, T)

A reference cast or an unboxing. No helper is called and nothing is converted: the run-time type of the object must be T or derive from or implement it. For value types, exactly T.

InvalidCastException when the type is wrong. Nothing is cast to Nothing for a reference type.

Downcasts that you know are right, and unboxing. Learn notes that it has a small performance advantage over CType when converting to and from Object.

TryCast(x, T)

As DirectCast for reference types, but returns Nothing instead of throwing.

Nothing; but T must be a reference type (BC30792 otherwise, Integer? included).

A downcast that may legitimately be wrong, followed by If result IsNot Nothing.

TypeOf x Is T

True when x is not Nothing and its run-time type is T, derives from it or implements it. IsNot since Visual Basic 14.

Nothing; the left operand must have a reference type (BC30021 for an Integer expression).

The guard before a DirectCast.

The program (built in a project whose root namespace is Shelf, which is why the exception messages say Shelf.Book) uses the shelf’s CatalogItem, Book and Magazine. item is statically a CatalogItem that holds a Book, magazine holds a Magazine, anything is a boxed Integer and shortValue a boxed Short. Each line shows what the expression gives or which exception it throws, with the message of the exception:

Option Strict On

Public MustInherit Class CatalogItem
    Public ReadOnly Property Title As String

    Protected Sub New(title As String)
        Me.Title = title
    End Sub
End Class

Public Class Book
    Inherits CatalogItem
    Implements IComparable

    Public Sub New(title As String)
        MyBase.New(title)
    End Sub

    Public Function CompareTo(obj As Object) As Integer Implements IComparable.CompareTo
        Return String.CompareOrdinal(Title, DirectCast(obj, Book).Title)
    End Function
End Class

Public Class Magazine
    Inherits CatalogItem

    Public Sub New(title As String)
        MyBase.New(title)
    End Sub
End Class

Module Program
    Private Sub Attempt(label As String, action As Func(Of Object))
        Try
            Dim result As Object = action()
            Console.WriteLine(label.PadRight(40) & If(result Is Nothing, "Nothing", result.ToString()))
        Catch ex As Exception
            Console.WriteLine(label.PadRight(40) & ex.GetType().Name & ": " & ex.Message)
        End Try
    End Sub

    Sub Main()
        Dim dune As New Book("Dune")
        Dim item As CatalogItem = dune
        Dim anything As Object = 42
        Dim shortValue As Object = CShort(42)
        Dim magazine As CatalogItem = New Magazine("Wired")
        Dim nothingItem As CatalogItem = Nothing

        ' Widening reference conversions are implicit; narrowing ones need a cast.
        Attempt("DirectCast(item, Book).Title", Function() DirectCast(item, Book).Title)
        Attempt("DirectCast(magazine, Book)", Function() DirectCast(magazine, Book))
        Attempt("DirectCast(nothingItem, Book)", Function() DirectCast(nothingItem, Book))
        Attempt("DirectCast(item, IComparable)", Function() DirectCast(item, IComparable) IsNot Nothing)
        Attempt("TryCast(item, Book) Is Nothing", Function() TryCast(item, Book) Is Nothing)
        Attempt("TryCast(magazine, Book) Is Nothing", Function() TryCast(magazine, Book) Is Nothing)
        Attempt("CType(item, Book).Title", Function() CType(item, Book).Title)

        ' Unboxing needs the exact type with DirectCast; CType and CInt convert.
        Attempt("DirectCast(anything, Integer)", Function() DirectCast(anything, Integer))
        Attempt("DirectCast(shortValue, Integer)", Function() DirectCast(shortValue, Integer))
        Attempt("CType(shortValue, Integer)", Function() CType(shortValue, Integer))
        Attempt("CInt(shortValue)", Function() CInt(shortValue))
        Attempt("DirectCast(anything, Long)", Function() DirectCast(anything, Long))
        Attempt("CLng(anything)", Function() CLng(anything))
        Attempt("CInt(CObj(item))", Function() CInt(CObj(item)))

        ' Testing the type first, then casting.
        If TypeOf item Is Book Then
            Console.WriteLine("TypeOf item Is Book -> " & DirectCast(item, Book).Title)
        End If
        If TypeOf magazine IsNot Book Then
            Console.WriteLine("TypeOf magazine IsNot Book -> " & magazine.GetType().Name)
        End If
        Dim asBook As Book = TryCast(magazine, Book)
        If asBook Is Nothing Then Console.WriteLine("TryCast gave Nothing, no exception")
    End Sub
End Module
DirectCast(item, Book).Title            Dune
DirectCast(magazine, Book)              InvalidCastException: Unable to cast object of type 'Shelf.Magazine' to type 'Shelf.Book'.
DirectCast(nothingItem, Book)           Nothing
DirectCast(item, IComparable)           True
TryCast(item, Book) Is Nothing          False
TryCast(magazine, Book) Is Nothing      True
CType(item, Book).Title                 Dune
DirectCast(anything, Integer)           42
DirectCast(shortValue, Integer)         InvalidCastException: Unable to cast object of type 'System.Int16' to type 'System.Int32'.
CType(shortValue, Integer)              42
CInt(shortValue)                        42
DirectCast(anything, Long)              InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.Int64'.
CLng(anything)                          42
CInt(CObj(item))                        InvalidCastException: Conversion from type 'Book' to type 'Integer' is not valid.
TypeOf item Is Book -> Dune
TypeOf magazine IsNot Book -> Magazine
TryCast gave Nothing, no exception

Notes on the output:

  • DirectCast(magazine, Book) throws InvalidCastException with the CLR’s wording: "Unable to cast object of type 'Shelf.Magazine' to type 'Shelf.Book'."

  • DirectCast(nothingItem, Book) is Nothing: casting a null reference succeeds.

  • DirectCast(anything, Integer) unboxes the Integer that is in the box, but DirectCast(shortValue, Integer) fails: "Unable to cast object of type 'System.Int16' to type 'System.Int32'." The box holds a Short, and an unbox has to name the exact type. CType(shortValue, Integer) and CInt(shortValue) succeed because they go through the conversion helpers, and CLng(anything) converts an Integer box to Long where DirectCast(anything, Long) fails.

  • CInt(CObj(item)) fails with "Conversion from type 'Book' to type 'Integer' is not valid.", a message from the Visual Basic helpers. The same call written without CObj is rejected at compile time (next listing).

  • TypeOf item Is Book, TypeOf magazine IsNot Book and TryCast are three ways of the same decision; TryCast does the test and the cast in one step and is the one to prefer when the cast is needed afterwards.

Conversions the compiler refuses

The operators also reject the conversions that cannot possibly work. The next program is deliberately wrong and was built to show the compiler messages:

Option Strict On

Class Book
End Class

Class Magazine
End Class

Module Program
    Sub Main()
        Dim dune As New Book()
        Dim viaFunction As Integer = CInt(dune)
        Dim unrelated As Magazine = DirectCast(dune, Magazine)
        Dim fromString As Book = CType("text", Book)
        Dim valueTryCast As Integer = TryCast(5, Integer)
        Dim nullableTryCast As Integer? = TryCast(dune, Integer?)
        Dim directLong As Long = DirectCast(5, Long)
        Dim asDate As Date = CDate(5)
        Dim isInt As Boolean = TypeOf 5 Is Integer
    End Sub
End Module
Program.vb(12,43): error BC30311: Value of type 'Book' cannot be converted to 'Integer'.
Program.vb(13,48): error BC30311: Value of type 'Book' cannot be converted to 'Magazine'.
Program.vb(14,40): error BC30311: Value of type 'String' cannot be converted to 'Book'.
Program.vb(15,50): error BC30792: 'TryCast' operand must be reference type, but 'Integer' is a value type.
Program.vb(16,57): error BC30792: 'TryCast' operand must be reference type, but 'Integer?' is a value type.
Program.vb(17,45): error BC30311: Value of type 'Integer' cannot be converted to 'Long'.
Program.vb(18,36): error BC30311: Value of type 'Integer' cannot be converted to 'Date'.
Program.vb(19,39): error BC30021: 'TypeOf ... Is' requires its left operand to have a reference type, but this operand has the value type 'Integer'.

Source: CType Operator

Two of these are surprising. DirectCast(5, Long) is BC30311 even though the same Integer widens to Long implicitly: DirectCast accepts only reference conversions and unboxing, so the argument has to be an Object for the Long request to be legal (and then it fails unless the box holds a Long). And TryCast is limited to reference types: for a nullable or a number, use TypeOf ... Is followed by DirectCast, or a parse method.

The CType of a numeric literal to an enumeration, to a type with a user-defined conversion and between related classes is where CType is the right tool; for numbers, prefer the conversion function whose name states the target, as it is shorter and the rounding is the same.

Text, culture and Parse

Converting between text and values is where conversion bugs live, because the same text means different things in different cultures and because users type incorrect text. There are three families, with different defaults:

Family Culture On bad input Notes

CInt, CDbl, CDate, CStr, Val

The current culture, except Val, which is always invariant.

InvalidCastException (and OverflowException for range).

Short, forgiving (&H prefix, whitespace). Right for text that came from the same user’s culture.

Convert.ToInt32, Convert.ToBoolean, …​

The current culture, with an IFormatProvider overload.

FormatException, OverflowException. Nothing gives 0 for numbers.

Rounds like CInt; Convert.ToInt32(True) is 1, not -1.

Integer.Parse, Double.Parse, Date.ParseExact

The current culture unless you pass one.

FormatException, OverflowException, ArgumentNullException.

Explicit NumberStyles and CultureInfo. The right choice for data formats.

Integer.TryParse, Date.TryParseExact, …​

As Parse.

Return False; the ByRef result is 0 or the default.

The right choice for user input.

The same code produces different results on two machines, which the next program shows by switching CultureInfo.CurrentCulture between en-US and de-DE and printing what each conversion does with the same text:

Option Strict On

Imports System.Globalization

Module Program
    Private Function Safe(action As Func(Of String)) As String
        Try
            Return action()
        Catch ex As InvalidCastException
            Return ex.GetType().Name
        End Try
    End Function

    Private Sub Report(cultureName As String)
        CultureInfo.CurrentCulture = New CultureInfo(cultureName)
        Dim invariant As CultureInfo = CultureInfo.InvariantCulture
        Dim price As Double = 1234.5
        Dim parsed As Double
        Console.WriteLine("[" & cultureName & "]")
        Console.WriteLine("  CStr(1234.5)                  = " & CStr(price))
        Console.WriteLine("  price.ToString(Invariant)     = " & price.ToString(invariant))
        Console.WriteLine("  CDbl(""1,234.5"")               = " & Safe(Function() CDbl("1,234.5").ToString(invariant)))
        Console.WriteLine("  CDbl(""1.234,5"")               = " & Safe(Function() CDbl("1.234,5").ToString(invariant)))
        Console.WriteLine("  Val(""1234.5abc"")              = " & Val("1234.5abc").ToString(invariant))
        Console.WriteLine("  CDate(""10/11/2026"")           = " & CDate("10/11/2026").ToString("yyyy-MM-dd", invariant))
        Console.WriteLine("  TryParse(""1,5"", Invariant)    = " & Double.TryParse("1,5", NumberStyles.Float, invariant, parsed).ToString())
        Console.WriteLine("  TryParse(""1,5"", current)      = " & Double.TryParse("1,5", NumberStyles.Float, CultureInfo.CurrentCulture, parsed).ToString() & " " & parsed.ToString(invariant))
    End Sub

    Sub Main()
        Report("en-US")
        Report("de-DE")
    End Sub
End Module
[en-US]
  CStr(1234.5)                  = 1234.5
  price.ToString(Invariant)     = 1234.5
  CDbl("1,234.5")               = 1234.5
  CDbl("1.234,5")               = InvalidCastException
  Val("1234.5abc")              = 1234.5
  CDate("10/11/2026")           = 2026-10-11
  TryParse("1,5", Invariant)    = False
  TryParse("1,5", current)      = False 0
[de-DE]
  CStr(1234.5)                  = 1234,5
  price.ToString(Invariant)     = 1234.5
  CDbl("1,234.5")               = InvalidCastException
  CDbl("1.234,5")               = 1234.5
  Val("1234.5abc")              = 1234.5
  CDate("10/11/2026")           = 2026-11-10
  TryParse("1,5", Invariant)    = False
  TryParse("1,5", current)      = True 1.5

Reading it:

  • CStr(1234.5) is 1234.5 in en-US and 1234,5 in de-DE. Text that is stored in a file or sent over a network must use ToString(CultureInfo.InvariantCulture).

  • CDbl("1,234.5") is accepted in en-US (the comma is a thousands separator) and rejected in de-DE; CDbl("1.234,5") is the mirror image.

  • Val does not look at the culture: it reads digits and the period, stops at the first other character and returns a Double, so Val("1234.5abc") is 1234.5 everywhere. It is also silent about bad input (Val("abc") is 0), which is why it has no place in validation.

  • CDate("10/11/2026") is 11 October in en-US and 10 November in de-DE. Date text without a year-first format is ambiguous: store yyyy-MM-dd, parse with Date.TryParseExact, and never rely on CDate.

  • Double.TryParse("1,5", NumberStyles.Float, ...) is False with the invariant culture and True (value 1.5) with de-DE.

The parse and Convert methods have more detailed behaviour, and the next program lists it for integers, Boolean, dates and enumerations:

Option Strict On

Imports System.Globalization

Enum LoanState
    Available = 1
    OnLoan = 2
    Overdue = 3
End Enum

Module Program
    Private Sub Attempt(label As String, action As Func(Of Object))
        Try
            Dim result As Object = action()
            Console.WriteLine(label.PadRight(44) & TypeName(result) & " " & Convert.ToString(result, CultureInfo.InvariantCulture))
        Catch ex As Exception
            Console.WriteLine(label.PadRight(44) & ex.GetType().Name)
        End Try
    End Sub

    Sub Main()
        Dim invariant As CultureInfo = CultureInfo.InvariantCulture
        Dim twoAndHalf As Double = 2.5
        Dim threeAndHalf As Double = 3.5
        Dim flag As Boolean = True
        Dim missing As String = Nothing
        Dim input As String = "1,234"

        ' Convert rounds like CInt; Boolean converts to 1, not -1.
        Attempt("Convert.ToInt32(2.5) / (3.5)", Function() Convert.ToInt32(twoAndHalf).ToString() & " / " & Convert.ToInt32(threeAndHalf).ToString())
        Attempt("CInt(True) and Convert.ToInt32(True)", Function() CInt(flag).ToString() & " and " & Convert.ToInt32(flag).ToString())
        Attempt("Convert.ToBoolean(""True"")", Function() Convert.ToBoolean("True"))
        Attempt("Convert.ToInt32(Nothing As String)", Function() Convert.ToInt32(missing))
        Attempt("Integer.Parse(Nothing As String)", Function() Integer.Parse(missing))
        Attempt("Convert.ToInt32(""12"", 16)", Function() Convert.ToInt32("12", 16))

        ' Parse and TryParse: culture and styles are explicit.
        Attempt("Integer.Parse(""1,234"")", Function() Integer.Parse(input))
        Attempt("Integer.Parse(""1,234"", AllowThousands)", Function() Integer.Parse(input, NumberStyles.AllowThousands, invariant))
        Attempt("Integer.Parse(""abc"")", Function() Integer.Parse("abc", invariant))
        Attempt("Integer.Parse(""3000000000"")", Function() Integer.Parse("3000000000", invariant))
        Attempt("Integer.Parse("" 42 "")", Function() Integer.Parse(" 42 ", invariant))

        Dim parsed As Integer
        Dim ok As Boolean = Integer.TryParse("42abc", NumberStyles.Integer, invariant, parsed)
        Console.WriteLine("TryParse(""42abc"")".PadRight(44) & ok.ToString() & ", parsed = " & parsed.ToString())
        ok = Integer.TryParse("42", NumberStyles.Integer, invariant, parsed)
        Console.WriteLine("TryParse(""42"")".PadRight(44) & ok.ToString() & ", parsed = " & parsed.ToString())
        Dim moment As Date
        ok = Date.TryParseExact("2026-10-19", "yyyy-MM-dd", invariant, DateTimeStyles.None, moment)
        Console.WriteLine("Date.TryParseExact(yyyy-MM-dd)".PadRight(44) & ok.ToString() & ", " & moment.ToString("dddd d MMMM yyyy", invariant))

        ' Enumerations: a number converts without any check that it names a member.
        Dim state As LoanState = CType(2, LoanState)
        Dim unknown As LoanState = CType(9, LoanState)
        Console.WriteLine("CType(2, LoanState) / CType(9, ...)".PadRight(44) & state.ToString() & " / " & unknown.ToString() & ", IsDefined(9) = " & [Enum].IsDefined(GetType(LoanState), 9).ToString())
        Dim byName As LoanState
        ok = [Enum].TryParse("overdue", True, byName)
        Console.WriteLine("Enum.TryParse(""overdue"", ignoreCase)".PadRight(44) & ok.ToString() & ", " & byName.ToString() & " = " & CInt(byName).ToString())
        Attempt("[Enum].Parse(GetType(LoanState), ""Lost"")", Function() [Enum].Parse(GetType(LoanState), "Lost"))
    End Sub
End Module
Convert.ToInt32(2.5) / (3.5)                String 2 / 4
CInt(True) and Convert.ToInt32(True)        String -1 and 1
Convert.ToBoolean("True")                   Boolean True
Convert.ToInt32(Nothing As String)          Integer 0
Integer.Parse(Nothing As String)            ArgumentNullException
Convert.ToInt32("12", 16)                   Integer 18
Integer.Parse("1,234")                      FormatException
Integer.Parse("1,234", AllowThousands)      Integer 1234
Integer.Parse("abc")                        FormatException
Integer.Parse("3000000000")                 OverflowException
Integer.Parse(" 42 ")                       Integer 42
TryParse("42abc")                           False, parsed = 0
TryParse("42")                              True, parsed = 42
Date.TryParseExact(yyyy-MM-dd)              True, Monday 19 October 2026
CType(2, LoanState) / CType(9, ...)         OnLoan / 9, IsDefined(9) = False
Enum.TryParse("overdue", ignoreCase)        True, Overdue = 3
[Enum].Parse(GetType(LoanState), "Lost")    ArgumentException

Source: Int32.TryParse

  • Convert.ToInt32(2.5) is 2 and Convert.ToInt32(3.5) is 4, the same banker’s rounding as CInt, but Convert.ToInt32(True) is 1 where CInt(True) is -1.

  • Convert.ToInt32(Nothing As String) is 0 and Integer.Parse(Nothing As String) throws ArgumentNullException. Convert.ToInt32("12", 16) is 18: the second argument is the base.

  • Integer.Parse("1,234") fails with FormatException because the default style NumberStyles.Integer has no thousands separator; with NumberStyles.AllowThousands it gives 1234. The default style does allow leading and trailing white space (" 42 " is 42).

  • Integer.Parse("3000000000") is OverflowException, not FormatException: the text is a valid number that does not fit.

  • TryParse("42abc") is False with the result 0, and TryParse("42") is True. Always test the returned Boolean.

  • Date.TryParseExact with an explicit format and CultureInfo.InvariantCulture is the one safe way to read a date from a file.

  • An enumeration accepts any integer: CType(9, LoanState) is a LoanState that prints as 9, because no member has that value. [Enum].IsDefined tells you; [Enum].TryParse reads names and numbers, optionally ignoring case, and [Enum].Parse throws ArgumentException for a name that does not exist.

User-defined conversions

A Structure or a Class can define conversions to and from other types with the Widening and Narrowing operator forms. A Widening conversion is implicit and must never fail or lose information; a Narrowing conversion has to be written with CType (or a conversion function, with Option Strict On) and may throw. The following structure is the shelf’s ISBN, with the two forms. Every Isbn is a valid String, so conversion to String is widening. Not every String is an Isbn, so the conversion from String validates, throws and is narrowing:

Option Strict On

Public Structure Isbn
    Implements IEquatable(Of Isbn)

    Public ReadOnly Property Value As String

    Private Sub New(value As String)
        Me.Value = value
    End Sub

    Public Shared Function TryParse(text As String, ByRef result As Isbn) As Boolean
        Dim digits As String = If(text, "").Replace("-", "")
        If digits.Length = 13 AndAlso digits.All(AddressOf Char.IsDigit) AndAlso HasValidCheckDigit(digits) Then
            result = New Isbn(digits)
            Return True
        End If
        Return False
    End Function

    Private Shared Function HasValidCheckDigit(digits As String) As Boolean
        Dim sum As Integer = 0
        For i As Integer = 0 To 12
            Dim digit As Integer = AscW(digits(i)) - AscW("0"c)
            sum += If(i Mod 2 = 0, digit, digit * 3)
        Next
        Return sum Mod 10 = 0
    End Function

    ' Every Isbn is a valid String, so this conversion can never fail and may be implicit.
    Public Shared Widening Operator CType(isbn As Isbn) As String
        Return isbn.Value
    End Operator

    ' Not every String is an Isbn: the conversion validates, throws, and must be written explicitly.
    Public Shared Narrowing Operator CType(text As String) As Isbn
        Dim result As Isbn = Nothing
        If Not TryParse(text, result) Then
            Throw New InvalidCastException("'" & text & "' is not a valid ISBN-13.")
        End If
        Return result
    End Operator

    Public Overloads Function Equals(other As Isbn) As Boolean Implements IEquatable(Of Isbn).Equals
        Return Value = other.Value
    End Function

    Public Overrides Function ToString() As String
        Return Value
    End Function
End Structure

Module Program
    Private Function Shout(text As String) As String
        Return "[" & text.ToUpperInvariant() & "]"
    End Function

    Sub Main()
        Dim isbn As Isbn = CType("978-0-441-01359-3", Isbn)
        Dim asText As String = isbn
        Console.WriteLine("widening, no cast needed : " & asText & " (" & asText.Length.ToString() & " characters)")
        Console.WriteLine("as a String argument     : " & Shout(isbn))
        Try
            Dim bad As Isbn = CType("978-0-441-01359-4", Isbn)
            Console.WriteLine(bad)
        Catch ex As InvalidCastException
            Console.WriteLine("narrowing, bad check digit: " & ex.Message)
        End Try

        ' The conversion operator is chosen at compile time from the static types. DirectCast never uses it,
        ' and CType on an Object only unboxes: a String inside an Object is not turned into an Isbn.
        Dim boxed As Object = isbn
        Console.WriteLine("DirectCast(boxed, Isbn)  : " & DirectCast(boxed, Isbn).Value)
        Dim hidden As Object = "9780441013593"
        Try
            Console.WriteLine(DirectCast(hidden, Isbn))
        Catch ex As InvalidCastException
            Console.WriteLine("DirectCast of an Object String: " & ex.GetType().Name)
        End Try
        Try
            Console.WriteLine(CType(hidden, Isbn))
        Catch ex As InvalidCastException
            Console.WriteLine("CType of an Object String     : " & ex.GetType().Name)
        End Try
        Console.WriteLine("CType of a typed String       : " & CType(DirectCast(hidden, String), Isbn).Value)
    End Sub
End Module
widening, no cast needed : 9780441013593 (13 characters)
as a String argument     : [9780441013593]
narrowing, bad check digit: '978-0-441-01359-4' is not a valid ISBN-13.
DirectCast(boxed, Isbn)  : 9780441013593
DirectCast of an Object String: InvalidCastException
CType of an Object String     : InvalidCastException
CType of a typed String       : 9780441013593

The rules that follow:

  • The conversion operator is Public Shared Widening|Narrowing Operator CType(parameter As From) As To, and either From or To has to be the type that declares it.

  • A conversion cannot be defined between a type and its base type or an interface it implements, nor from or to Object; those exist already as reference conversions.

  • The compiler chooses the operator from the static types at compile time. A String that arrives inside an Object is not converted: the last two attempts of the program, DirectCast(hidden, Isbn) and CType(hidden, Isbn), both throw InvalidCastException although CType of a typed String works. DirectCast and TryCast never use user-defined conversions.

  • An implicit widening conversion is applied to assignments and arguments (Dim asText As String = isbn and Shout(isbn)). It was not applied to the operand of & in the experiment: "ISBN " & isbn fails, as the next listing shows, and a member of the target type is not visible on the source (isbn.Length).

  • A narrowing conversion whose failure is an exceptional case should throw InvalidCastException; callers that expect failure need a TryParse method next to it, as Isbn.TryParse is.

The next file is deliberately wrong and shows the compiler’s answer for each misuse:

Option Strict On

Public Structure Isbn
    Implements IEquatable(Of Isbn)

    Public ReadOnly Property Value As String

    Private Sub New(value As String)
        Me.Value = value
    End Sub

    Public Shared Function TryParse(text As String, ByRef result As Isbn) As Boolean
        Dim digits As String = If(text, "").Replace("-", "")
        If digits.Length = 13 AndAlso digits.All(AddressOf Char.IsDigit) AndAlso HasValidCheckDigit(digits) Then
            result = New Isbn(digits)
            Return True
        End If
        Return False
    End Function

    Private Shared Function HasValidCheckDigit(digits As String) As Boolean
        Dim sum As Integer = 0
        For i As Integer = 0 To 12
            Dim digit As Integer = AscW(digits(i)) - AscW("0"c)
            sum += If(i Mod 2 = 0, digit, digit * 3)
        Next
        Return sum Mod 10 = 0
    End Function

    ' Every Isbn is a valid String, so this conversion can never fail and may be implicit.
    Public Shared Widening Operator CType(isbn As Isbn) As String
        Return isbn.Value
    End Operator

    ' Not every String is an Isbn: the conversion validates, throws, and must be written explicitly.
    Public Shared Narrowing Operator CType(text As String) As Isbn
        Dim result As Isbn = Nothing
        If Not TryParse(text, result) Then
            Throw New InvalidCastException("'" & text & "' is not a valid ISBN-13.")
        End If
        Return result
    End Operator

    Public Overloads Function Equals(other As Isbn) As Boolean Implements IEquatable(Of Isbn).Equals
        Return Value = other.Value
    End Function

    Public Overrides Function ToString() As String
        Return Value
    End Function
End Structure

Module Program
    Sub Main()
        Dim isbn As Isbn = CType("978-0-441-01359-3", Isbn)
        Dim implicitNarrowing As Isbn = "9780441013593"
        Dim concatenated As String = "ISBN " & isbn
        Dim direct As Isbn = DirectCast("9780441013593", Isbn)
        Dim tried As Isbn = TryCast("9780441013593", Isbn)
        Dim length As Integer = isbn.Length
    End Sub
End Module
Program.vb(56,41): error BC30512: Option Strict On disallows implicit conversions from 'String' to 'Isbn'.
Program.vb(57,38): error BC30452: Operator '&' is not defined for types 'String' and 'Isbn'.
Program.vb(58,41): error BC30311: Value of type 'String' cannot be converted to 'Isbn'.
Program.vb(59,54): error BC30792: 'TryCast' operand must be reference type, but 'Isbn' is a value type.
Program.vb(60,33): error BC30456: 'Length' is not a member of 'Isbn'.

Source: Narrowing

The listing shows Option Strict On disallowing the implicit narrowing at line 56 (BC30512), the missing operator & for String and Isbn at line 57 (BC30452), the refusal of DirectCast at line 58 (BC30311), TryCast on a value type at line 59 (BC30792) and Length which Isbn does not have at line 60 (BC30456). Learn describes Widening as the modifier of an operator that converts to a type that can always hold the value, and the rules for the combinations in the page for the Operator statement.

Arrays, generics and delegates: variance

Variance is the rule that lets a Book collection stand in for a CatalogItem collection. Visual Basic supports it for arrays, for generic interfaces and delegates that declare it (since Visual Basic 2010), and for delegate parameter types.

Option Strict On

Imports System.Collections.Generic

Public MustInherit Class CatalogItem
    Public ReadOnly Property Title As String

    Protected Sub New(title As String)
        Me.Title = title
    End Sub
End Class

Public Class Book
    Inherits CatalogItem

    Public Sub New(title As String)
        MyBase.New(title)
    End Sub
End Class

Public Class Magazine
    Inherits CatalogItem

    Public Sub New(title As String)
        MyBase.New(title)
    End Sub
End Class

Module Program
    Sub Main()
        Dim books As Book() = {New Book("Dune"), New Book("Emma")}

        ' Array covariance: an array of Book is an array of CatalogItem, checked at run time on store.
        Dim items As CatalogItem() = books
        Console.WriteLine("array covariance: " & items.Length.ToString() & " items, runtime type " & items.GetType().Name)
        Try
            items(0) = New Magazine("Wired")
        Catch ex As ArrayTypeMismatchException
            Console.WriteLine("storing a Magazine in the Book array: " & ex.GetType().Name)
        End Try

        ' Generic covariance: IEnumerable(Of Out T) and read-only interfaces allow the conversion implicitly.
        Dim readOnlyItems As IEnumerable(Of CatalogItem) = books
        Console.WriteLine("IEnumerable(Of Book) -> IEnumerable(Of CatalogItem): " & readOnlyItems.Count().ToString())
        Dim list As New List(Of Book)(books)
        Dim readOnlyList As IReadOnlyList(Of CatalogItem) = list
        Console.WriteLine("List(Of Book) -> IReadOnlyList(Of CatalogItem): " & readOnlyList.Count.ToString())
        ' Dim itemList As List(Of CatalogItem) = list   ' error BC30512: List(Of T) is invariant

        ' Delegate contravariance: a method that accepts any CatalogItem can serve where a Book is supplied.
        Dim describeItem As Func(Of CatalogItem, String) = Function(i) "item " & i.Title
        Dim describeBook As Func(Of Book, String) = describeItem
        Console.WriteLine("Func(Of CatalogItem, String) -> Func(Of Book, String): " & describeBook(books(1)))
        Dim makeBook As Func(Of Book) = Function() New Book("Ulysses")
        Dim makeItem As Func(Of CatalogItem) = makeBook
        Console.WriteLine("Func(Of Book) -> Func(Of CatalogItem): " & makeItem().Title)

        ' Booleans and numbers.
        Dim hasCopies As Boolean = CBool(3)
        Console.WriteLine("CBool(3) = " & hasCopies.ToString() & ", CInt(True) = " & CInt(hasCopies).ToString() & ", Math.Abs(CInt(True)) = " & Math.Abs(CInt(hasCopies)).ToString())

        ' An array of one type is not an array of another: element conversion needs a loop or LINQ.
        Dim numbers As Integer() = {1, 2, 3}
        Dim doubles As Double() = Array.ConvertAll(numbers, Function(n) CDbl(n))
        Console.WriteLine("Array.ConvertAll: " & String.Join(", ", doubles))
    End Sub
End Module
array covariance: 2 items, runtime type Book[]
storing a Magazine in the Book array: ArrayTypeMismatchException
IEnumerable(Of Book) -> IEnumerable(Of CatalogItem): 2
List(Of Book) -> IReadOnlyList(Of CatalogItem): 2
Func(Of CatalogItem, String) -> Func(Of Book, String): item Emma
Func(Of Book) -> Func(Of CatalogItem): Ulysses
CBool(3) = True, CInt(True) = -1, Math.Abs(CInt(True)) = 1
Array.ConvertAll: 1, 2, 3
  • Arrays are covariant: a Book() converts implicitly to CatalogItem(). The conversion is unsafe, because the array keeps its real type and a store of the wrong element throws ArrayTypeMismatchException, as the second line shows. Arrays of value types are not covariant (see the errors below).

  • Generic interfaces declare variance with Out (the type only comes out, so IEnumerable(Of Book) is an IEnumerable(Of CatalogItem)) and In (the type only goes in). IReadOnlyList(Of Out T) and IEnumerable(Of Out T) are covariant; List(Of T), ICollection(Of T) and IList(Of T) are invariant, because they both read and write.

  • Delegates are contravariant in their parameters and covariant in their result: a Func(Of CatalogItem, String) is a Func(Of Book, String), and a Func(Of Book) is a Func(Of CatalogItem). The In and Out of Func(Of In T, Out TResult) appear in the error message below.

  • There is no conversion between arrays of unrelated element types. Array.ConvertAll (or Select…​ToArray) creates the new array.

The next file is wrong on purpose, and the messages show where variance stops:

Option Strict On

Imports System.Collections.Generic

Class CatalogItem
    Public Title As String = ""
End Class

Class Book
    Inherits CatalogItem
End Class

Class Magazine
    Inherits CatalogItem
End Class

Module Program
    Sub Main()
        Dim list As New List(Of Book)()
        Dim itemList As List(Of CatalogItem) = list
        Dim numbers As Integer() = {1, 2, 3}
        Dim doubles As Double() = numbers
        Dim bookFromItem As Book = New Magazine()
        Dim describeBook As Func(Of Book, String) = Function(b) b.Title
        Dim describeItem As Func(Of CatalogItem, String) = describeBook
    End Sub
End Module
Program.vb(20,48): error BC36756: 'List(Of Book)' cannot be converted to 'List(Of CatalogItem)'. Consider using 'IEnumerable(Of CatalogItem)' instead.
Program.vb(22,35): error BC30332: Value of type 'Integer()' cannot be converted to 'Double()' because 'Integer' is not derived from 'Double'.
Program.vb(23,36): error BC30311: Value of type 'Magazine' cannot be converted to 'Book'.
Program.vb(25,60): error BC36755: 'Func(Of Book, String)' cannot be converted to 'Func(Of CatalogItem, String)' because 'CatalogItem' is not derived from 'Book', as required for the 'In' generic parameter 'T' in 'Delegate Function Func(Of In T, Out TResult)(arg As T) As TResult'.

BC36756 has the helpful hint "Consider using 'IEnumerable(Of CatalogItem)' instead": List(Of Book) is not a List(Of CatalogItem). BC30332 is the array of value types, where Integer() to Double() does not exist even though each element converts. BC36755 is the delegate that would be unsafe, a Func(Of Book, String) offered where it must accept every CatalogItem. Delegate conversion without arguments (AddressOf of a method with fewer parameters) is relaxed only in some contexts; with Option Strict On, see Program Structure and Code Conventions for the Handles case and BC36663.

Boolean and enumerations

  • CInt(True) is -1 and CInt(False) is 0 because Visual Basic represents True as all bits set. Every non-zero number converts to True. Convert.ToInt32(True) is 1, and True stored in a database or sent to another language is a source of off-by-sign errors. Write If(flag, 1, 0) when you need the number 1.

  • A Boolean widens only to Object. To a number or to String it is a narrowing conversion (and the reverse), so Option Strict On requires CInt, CBool, CStr and the like; to Char and Date no conversion exists (BC30311). In the shelf program, CBool(3) is True and Math.Abs(CInt(True)) is 1.

  • An Enum converts to and from its underlying integral type explicitly (CInt(state), CType(2, LoanState)) and never checks that the number names a member. Enum values also convert to String with ToString and from String with [Enum].Parse or TryParse.

Which conversion should I use?

The diagram follows the decisions of the previous sections:

flowchart TD start(["I have a value and a target type"]) --> q1{"Is the source text
to be read as a number,
date or Boolean?"} q1 -- yes --> q2{"Can the text be wrong,
for example user input?"} q2 -- yes --> tp["TryParse or TryParseExact
with an explicit CultureInfo
and NumberStyles"] q2 -- no --> q3{"Is the format fixed
and the same on every machine?"} q3 -- yes --> pa["Integer.Parse or Double.Parse
with CultureInfo.InvariantCulture"] q3 -- no --> cf["CInt, CDbl or CDate
read the current culture:
only for text of that culture"] q1 -- no --> q4{"Are both types
numeric or Boolean or Char
or Date?"} q4 -- yes --> q5{"Is it a widening conversion
in the matrix?"} q5 -- yes --> im["Assign directly:
the conversion is implicit"] q5 -- no --> fn["Conversion function CInt, CLng, CDbl...
round first with Math.Round or Fix
if you need another rule"] q4 -- no --> q6{"Are the types related by
inheritance or an interface?"} q6 -- "up to base or interface" --> im q6 -- "down to a subtype" --> q7{"Might the object
be another type?"} q7 -- yes --> tc["TryCast and test for Nothing,
or TypeOf ... Is before DirectCast"] q7 -- no --> dc["DirectCast: exact,
throws InvalidCastException
when it is wrong"] q6 -- no --> q8{"Does one of the types define
a Widening or Narrowing operator?"} q8 -- yes --> ct["CType with both static types known"] q8 -- no --> q9{"Is the source an Object
that holds a boxed value?"} q9 -- "exact type known" --> dc q9 -- "any convertible type" --> co["CInt, CType or Convert
convert at run time"] q9 -- no --> none["No conversion exists:
write a method or a mapping"]

The exceptions

Exception Raised by Remedy

InvalidCastException

DirectCast or CType with the wrong run-time type; CInt, CBool, CDate and friends with text that is not a value; a Narrowing operator that rejects its argument.

Test with TypeOf ... Is or TryCast; use TryParse; validate before converting.

OverflowException

A conversion or arithmetic result outside the target range; CByte(300), CInt of a large Long, Integer.Parse("3000000000"), floating-point NaN or infinity to an integer.

Check the range, widen first (CLng), or use a larger type. Do not switch the project’s overflow checking off to hide it.

FormatException

Parse, Convert.ToInt32 and Convert.ToDateTime with text in the wrong format. (CInt reports the same problem as InvalidCastException; the FormatException is its inner exception.)

TryParse with an explicit culture and styles.

ArgumentNullException

Integer.Parse(Nothing) and the other Parse methods.

Test for Nothing, or use Convert.ToInt32, which treats Nothing as 0.

ArrayTypeMismatchException

Storing an element into an array that is covariantly typed as a base array.

Prefer IReadOnlyList(Of T) or IEnumerable(Of T) for the covariant view.

Differences from C#

Concern Visual Basic C#

Implicit narrowing

Error under Option Strict On; allowed (and checked at run time) under Off.

Always an error.

Cast of a floating-point number to an integer

CInt(2.5) rounds half to even (2); Fix(2.5) truncates.

(int)2.5 truncates (2); Math.Round rounds.

Overflow

Checked by default: OverflowException.

unchecked by default; checked to throw.

Reference downcast

DirectCast(x, T) (throws) and TryCast(x, T) (returns Nothing).

(T)x and x as T.

Type test

TypeOf x Is T, TypeOf x IsNot T (VB 14).

x is T, x is not T, pattern matching.

Conversion of an Object to a number

CInt(obj) converts a Short, a String or a Boolean; DirectCast(obj, Integer) unboxes exactly.

(int)obj unboxes exactly; no helper converts.

String to number

CInt("12"), Integer.Parse, Val.

int.Parse, Convert.ToInt32; no keyword conversions.

Boolean to number

CInt(True) is -1.

No implicit or explicit conversion.

User-defined conversions

Widening Operator CType and Narrowing Operator CType.

implicit operator and explicit operator.

Type of x / y

Double for integers; \ is integer division.

Integer division for integers.

Generic variance

Out and In on interfaces and delegates, from VB 10.

out and in, from C# 4.

Where the books are outdated or wrong

  • Mastering VB.NET p. 43 assigns a / b and a ^ b to Integer variables: both operators return Double, and the assignments do not compile with Option Strict On (BC30512, as the matrix section shows). Write a \ b, or CInt(a / b) when rounding is the intent, and CInt(a ^ b).

  • Mastering VB.NET pp. 54-55, 119-121 and 128 say that 10 / 0 throws DivideByZeroException. / on integer operands converts to Double and gives Infinity; only \, Mod and Decimal division throw (the numbers section of Data Types and Literals runs them).

  • Quickstart Guide pp. 18 and 134 use + to concatenate strings, which the first section of this page shows is an Option Strict On error and a numeric addition in disguise otherwise. Use &.

  • Quickstart Guide p. 80 presents DateValue as if its result did not depend on the culture. It does, like CDate, as the culture example shows; use Date.TryParseExact with the invariant culture for stored dates.

  • Quickstart Guide pp. 83-84 give FormatDateTime custom format strings. It takes only the named formats; custom formats belong to ToString(format). The page on Dates, Times and Formatting covers both.