Strings and Text

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.

Text is where Visual Basic shows its two layers most clearly. The first layer is the .NET String type: an immutable sequence of UTF-16 code units, with instance methods that count from zero, exactly as in C#. The second layer is the Microsoft.VisualBasic.Strings module, inherited from Visual Basic 6 and imported into every file by default: functions such as Mid, Left, InStr and Trim that count from one, treat Nothing as the empty string and, for some of them, take their comparison mode from Option Compare. Both layers are always available, and a large part of writing good VB text code is knowing which one to use and where their rules differ. This page covers the types, the literals, the two sets of operations, comparison and culture, building strings, regular expressions, encodings and the & versus + question. It was written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and checked on 2026-10-19. Every program was built with Option Strict On and TreatWarningsAsErrors, and run in a Linux container, where the current culture is the invariant culture; a program that depends on the current culture sets the culture it needs explicitly. For C#'s view of the same material see Strings and Text in the C# reference; the formatting of numbers and dates is on Dates, Times and Formatting, and the numeric conversions on Type Conversions.

The String and Char types

String is the keyword for System.String: a reference type whose value is a sequence of UTF-16 code units (Char values), not of "characters" as a person counts them. A String is immutable, so every operation that seems to change one returns a new string, and a string literal is the only form of constant that can be a reference type. Nothing is a valid value of type String (see Nothing and Nullable Value Types), and the VB operators and Strings functions treat it like "".

Char is the keyword for System.Char, a 16-bit UTF-16 code unit. There is no implicit conversion from a one-character String to Char: the declaration Dim c As Char = "A" is BC30512 under Option Strict On ("disallows implicit conversions from String to Char`"). A `Char literal is a string literal with the suffix c: "A"c. A string is indexed from zero: s(0) and s.Chars(0) are the same read-only indexer, and the compiler rejects an assignment to it with BC30526 ("Property Chars is `ReadOnly`").

Unicode: code units, runes and text elements

Because the unit is the UTF-16 code unit, three notions of "length" coexist. Length counts code units. Rune (System.Text, .NET Core 3.0 and later) is a Unicode scalar value, and one rune takes one or two code units (a surrogate pair). A grapheme cluster, what a reader sees as one character, may consist of several runes, for example a letter followed by a combining accent, and System.Globalization.StringInfo enumerates those "text elements".

Option Strict On
Imports System.Globalization
Imports System.Text

Module Ex02
    Sub Main()
        Console.OutputEncoding = Encoding.UTF8

        Dim s = "Shelf"
        Dim t = s.ToUpperInvariant()
        Console.WriteLine($"{s} -> {t}; the original is unchanged: {s}")

        Dim thumbsUp = ChrW(&HD83D) & ChrW(&HDC4D)
        Dim accented = "e" & ChrW(&H301)
        Console.WriteLine($"thumbs-up: Length = {thumbsUp.Length}, runes = {thumbsUp.EnumerateRunes().Count()}, first rune = U+{thumbsUp.EnumerateRunes().First().Value:X}")
        Console.WriteLine($"surrogates: {Char.IsHighSurrogate(thumbsUp(0))} {Char.IsLowSurrogate(thumbsUp(1))}")
        Console.WriteLine($"e + combining acute: Length = {accented.Length}, text elements = {New StringInfo(accented).LengthInTextElements}")
        Console.WriteLine($"Normalized to FC: Length = {accented.Normalize(NormalizationForm.FormC).Length}")
        Console.WriteLine($"Equal without normalization: {accented = ChrW(&HE9)}")
        Console.WriteLine($"Equal after normalization:   {accented.Normalize() = ChrW(&HE9)}")

        Dim c As Char = "7"c
        Console.WriteLine($"IsDigit={Char.IsDigit(c)}, IsLetter={Char.IsLetter(c)}, GetNumericValue={Char.GetNumericValue(c)}")
        Console.WriteLine($"ToUpperInvariant(""ß"") = {"ß".ToUpperInvariant()}, Char.ToUpperInvariant('ß') = {Char.ToUpperInvariant("ß"c)}")

        Dim reversed = New String(s.Reverse().ToArray())
        Console.WriteLine($"Reversed with LINQ: {reversed}; with StrReverse: {StrReverse(s)}")
        Dim mixed = "a" & thumbsUp & accented
        Dim byUnits = New String(mixed.Reverse().ToArray())
        Console.WriteLine($"Reverse by code units : {String.Join(" ", byUnits.Select(Function(ch) AscW(ch).ToString("X4")))}")
        Console.WriteLine($"StrReverse            : {String.Join(" ", StrReverse(mixed).Select(Function(ch) AscW(ch).ToString("X4")))}")
        Console.WriteLine($"Chars: {String.Join(" ", s.Select(Function(ch) AscW(ch)))}")
        Console.WriteLine($"s(0) = {s(0)}, s.Chars(1) = {s.Chars(1)}")
    End Sub
End Module
Shelf -> SHELF; the original is unchanged: Shelf
thumbs-up: Length = 2, runes = 1, first rune = U+1F44D
surrogates: True True
e + combining acute: Length = 2, text elements = 1
Normalized to FC: Length = 1
Equal without normalization: False
Equal after normalization:   True
IsDigit=True, IsLetter=False, GetNumericValue=7
ToUpperInvariant("ß") = ß, Char.ToUpperInvariant('ß') = ß
Reversed with LINQ: flehS; with StrReverse: flehS
Reverse by code units : 0301 0065 DC4D D83D 0061
StrReverse            : 0065 0301 D83D DC4D 0061
Chars: 83 104 101 108 102
s(0) = S, s.Chars(1) = h

Consequences you should plan for: Length and Mid/Substring can cut a surrogate pair in half; LINQ’s Reverse() reverses code units and breaks pairs and combining sequences (StrReverse, in contrast, reverses by text element and keeps them intact); Equals and = compare code units, so a precomposed é and e + U+0301 are different strings until both are normalised with Normalize(); ToUpperInvariant() and Char.ToUpperInvariant apply simple one-to-one mappings (the ß stays ß). For text that users see, process runes or text elements, or leave the splitting to a library.

String literals and special characters

A string literal is text between double quotes. There are no backslash escape sequences (so Windows paths and regular expressions can be written as is), a double quote inside the literal is written twice (""), and nothing else is special. Visual Basic 14 (Visual Studio 2015) allows a string literal to continue over several source lines, and the literal then contains the line break that is in the source file: LF in a file saved with Unix line endings (length 12 in the program below), CRLF in a file saved with Windows line endings. A literal whose content depends on how the file is saved is a portability bug in disguise; build line breaks explicitly with vbCrLf, vbLf or Environment.NewLine unless the line ending is irrelevant.

Characters that cannot be typed are named with constants and functions: ControlChars.Tab, ControlChars.Cr, ControlChars.Lf, ControlChars.CrLf, ControlChars.NewLine (always CR LF when measured on Linux with .NET 10, unlike Environment.NewLine, which is LF there), ControlChars.Quote, ControlChars.NullChar, ControlChars.Back, ControlChars.FormFeed, ControlChars.VerticalTab, the VB6-era constants vbTab, vbCr, vbLf, vbCrLf (always CR LF, two characters), vbNewLine (the platform’s), vbNullChar, vbBack, vbFormFeed, vbVerticalTab, vbNullString (which is simply Nothing), and ChrW(code) / AscW(text) for any UTF-16 code unit. Source files may contain any Unicode text, so a literal "€" is fine. The compiler also accepts the typographic quotes U+201C and U+201D as string delimiters (a program that wrote “smart” compiled and printed smart), a behaviour that surprises people who paste code from word processors: an unnoticed pair of curly quotes is a legal string literal.

Option Strict On

Module Ex01
    Sub Main()
        ' Quotes are doubled; there are no backslash escapes.
        Dim quoted = "She said ""Hello"" and left"
        Dim windowsPath = "C:\Shelf\catalog.json"
        Console.WriteLine(quoted)
        Console.WriteLine(windowsPath)

        ' Special characters come from constants, ControlChars or ChrW.
        Console.WriteLine("Tab:" & vbTab & "|" & ControlChars.Tab & "|")
        Console.WriteLine("Two lines:" & Environment.NewLine & "second line")
        Console.WriteLine("Euro sign: " & ChrW(&H20AC) & ", code " & AscW("€"c))
        Console.WriteLine("vbCrLf length = " & vbCrLf.Length & ", vbLf length = " & vbLf.Length)

        ' A string literal may contain line breaks (VB 14): the text includes the file's own line endings.
        Dim poem = "first
second"
        Console.WriteLine("Literal with a line break has length " & poem.Length & " and contains LF: " & poem.Contains(vbLf))

        ' Character literals use the c suffix.
        Dim letter As Char = "A"c
        Console.WriteLine(letter & " has code " & AscW(letter) & ", next is " & ChrW(AscW(letter) + 1))

        ' Interpolation: {expression,alignment:format}
        Dim title = "Dune"
        Dim year = 1965
        Dim price = 12.5D
        Console.WriteLine($"[{title,-8}] [{title,8}] {year} {price:N2}")
        Console.WriteLine($"Braces are doubled: {{{title}}}")
        Console.WriteLine($"Nested quotes are fine: {If(year > 1900, "modern", "old")}")
        Console.WriteLine($"Inside {title.ToUpperInvariant()} the calculation is {year + 1}")
    End Sub
End Module
She said "Hello" and left
C:\Shelf\catalog.json
Tab:	|	|
Two lines:
second line
Euro sign: €, code 8364
vbCrLf length = 2, vbLf length = 1
Literal with a line break has length 12 and contains LF: True
A has code 65, next is B
[Dune    ] [    Dune] 1965 12.50
Braces are doubled: {Dune}
Nested quotes are fine: modern
Inside DUNE the calculation is 1966

What VB does not have, unlike C#: no \n or € escape sequences, no raw string literals (C# 11), no UTF-8 string literals ("abc"u8, C# 11: BC30205 "End of statement expected"), no verbatim prefix (everything is already verbatim). Use ChrW, ControlChars and &.

Interpolated strings

Visual Basic 14 added interpolated strings: $"Hello {name}". A hole is {expression}, {expression,alignment}, {expression:format} or {expression,alignment:format}; the alignment is a signed integer (negative left-aligns) and the format is any format string that the value’s type understands (see Dates, Times and Formatting). {{ and }} produce literal braces. The expression is ordinary VB, including string literals with plain quotes and calls ({If(year > 1900, "modern", "old")}), and the compiler turns the string into a call to String.Format, so the current culture applies to every formatted value. Assign it to FormattableString to defer the formatting and pass a culture later: FormattableString.Invariant($"{price:N2}") is the idiom for stable, culture-independent output. VB 17.13 has no interpolated string handlers (a C# 10 feature): String.Create(CultureInfo.InvariantCulture, $"{x}") fails with BC30311 ("Value of type String cannot be converted to DefaultInterpolatedStringHandler`"), so use `FormattableString.Invariant. VB has no $@ prefix and no multi-quote variants, and a format specifier cannot contain }.

String methods and the Strings module

Every VB program can call both text.Substring(6, 5) and Mid(text, 7, 5). The module Microsoft.VisualBasic.Strings (documented in the Language Reference as the "string functions") is in scope without an Imports, because Visual Basic projects import Microsoft.VisualBasic implicitly. The functions are thin wrappers, but they carry the VB6 semantics, and the differences are what matter.

Option Strict On

Module Ex03
    Sub Main()
        Dim text = "Hello World!"

        Console.WriteLine("-- Mid is 1-based, Substring is 0-based")
        Console.WriteLine($"Mid(text, 6, 5)       = '{Mid(text, 6, 5)}'")
        Console.WriteLine($"text.Substring(6, 5)  = '{text.Substring(6, 5)}'")
        Console.WriteLine($"Mid(text, 7, 5)       = '{Mid(text, 7, 5)}'")
        Console.WriteLine($"Mid(text, 7)          = '{Mid(text, 7)}'")
        Console.WriteLine($"Left(text, 5)         = '{Left(text, 5)}'")
        Console.WriteLine($"Right(text, 6)        = '{Right(text, 6)}'")
        Console.WriteLine($"Left(text, 99)        = '{Left(text, 99)}'")
        Console.WriteLine($"Mid(text, 99)         = '{Mid(text, 99)}'")

        Try
            Console.WriteLine(text.Substring(99))
        Catch ex As ArgumentOutOfRangeException
            Console.WriteLine("text.Substring(99) throws " & ex.GetType().Name)
        End Try
        Try
            Console.WriteLine(Mid(text, 0, 2))
        Catch ex As ArgumentException
            Console.WriteLine("Mid(text, 0, 2) throws " & ex.GetType().Name)
        End Try

        Console.WriteLine("-- Searching: 0 means not found for InStr, -1 for IndexOf")
        Console.WriteLine($"InStr(text, ""o"")        = {InStr(text, "o")}")
        Console.WriteLine($"InStr(6, text, ""o"")     = {InStr(6, text, "o")}")
        Console.WriteLine($"InStrRev(text, ""o"")     = {InStrRev(text, "o")}")
        Console.WriteLine($"InStr(text, ""z"")        = {InStr(text, "z")}")
        Console.WriteLine($"text.IndexOf(""o"")       = {text.IndexOf("o")}")
        Console.WriteLine($"text.IndexOf(""z"")       = {text.IndexOf("z")}")
        Console.WriteLine($"text.LastIndexOf(""o"")   = {text.LastIndexOf("o")}")
        Console.WriteLine($"text.Contains(""World"")  = {text.Contains("World")}")

        Console.WriteLine("-- Trim: the VB function removes only spaces")
        Dim padded = vbTab & "  Shelf  " & vbTab
        Console.WriteLine($"Trim(padded).Length   = {Trim(padded).Length}")
        Console.WriteLine($"padded.Trim().Length  = {padded.Trim().Length}")
        Console.WriteLine($"LTrim / RTrim lengths = {LTrim(padded).Length} / {RTrim(padded).Length}")

        Console.WriteLine("-- Case, length, repeat")
        Console.WriteLine($"UCase / LCase         = {UCase(text)} / {LCase(text)}")
        Console.WriteLine($"Len(text)             = {Len(text)}")
        Console.WriteLine($"Space(3) & StrDup     = '{Space(3)}' '{StrDup(3, "ab")}'")
        Console.WriteLine($"ToTitleCase           = {System.Globalization.CultureInfo.InvariantCulture.TextInfo.ToTitleCase("the lord of the rings")}")

        Console.WriteLine("-- Replace, Split, Join")
        Console.WriteLine($"Replace(text, ""l"", ""L"")          = {Replace(text, "l", "L")}")
        Console.WriteLine($"Replace(text, ""l"", ""L"", 5)       = {Replace(text, "l", "L", 5)}")
        Console.WriteLine($"Replace(text, ""l"", ""L"", 1, 2)    = {Replace(text, "l", "L", 1, 2)}")
        Console.WriteLine($"text.Replace(""l"", ""L"")           = {text.Replace("l", "L")}")
        Dim parts = Split("a,b,,c", ",")
        Console.WriteLine($"Split keeps empty entries: {parts.Length} -> {Join(parts, "|")}")
        Dim parts2 = "a,b,,c".Split(","c, StringSplitOptions.RemoveEmptyEntries)
        Console.WriteLine($"String.Split with options: {parts2.Length} -> {String.Join("|", parts2)}")
        Console.WriteLine($"Split(""a b c"") default delimiter is a space: {Split("a b c").Length}")
        Console.WriteLine($"Split with limit: {Join(Split("a,b,c,d", ",", 2), "|")}")
        Console.WriteLine($"Filter: {Join(Filter({"ada", "alan", "grace"}, "al"), ",")}")
    End Sub
End Module
-- Mid is 1-based, Substring is 0-based
Mid(text, 6, 5)       = ' Worl'
text.Substring(6, 5)  = 'World'
Mid(text, 7, 5)       = 'World'
Mid(text, 7)          = 'World!'
Left(text, 5)         = 'Hello'
Right(text, 6)        = 'World!'
Left(text, 99)        = 'Hello World!'
Mid(text, 99)         = ''
text.Substring(99) throws ArgumentOutOfRangeException
Mid(text, 0, 2) throws ArgumentException
-- Searching: 0 means not found for InStr, -1 for IndexOf
InStr(text, "o")        = 5
InStr(6, text, "o")     = 8
InStrRev(text, "o")     = 8
InStr(text, "z")        = 0
text.IndexOf("o")       = 4
text.IndexOf("z")       = -1
text.LastIndexOf("o")   = 7
text.Contains("World")  = True
-- Trim: the VB function removes only spaces
Trim(padded).Length   = 11
padded.Trim().Length  = 5
LTrim / RTrim lengths = 11 / 11
-- Case, length, repeat
UCase / LCase         = HELLO WORLD! / hello world!
Len(text)             = 12
Space(3) & StrDup     = '   ' 'aaa'
ToTitleCase           = The Lord Of The Rings
-- Replace, Split, Join
Replace(text, "l", "L")          = HeLLo WorLd!
Replace(text, "l", "L", 5)       = o WorLd!
Replace(text, "l", "L", 1, 2)    = HeLLo World!
text.Replace("l", "L")           = HeLLo WorLd!
Split keeps empty entries: 4 -> a|b||c
String.Split with options: 3 -> a|b|c
Split("a b c") default delimiter is a space: 3
Split with limit: a|b,c,d
Filter: alan

Source: Strings Class

VB function Counterpart on String What differs

Mid(s, start, length)

s.Substring(start - 1, length)

start is 1-based. start greater than the length gives "", not an exception, and a length that is too long is shortened. start less than 1 throws ArgumentException. Mid(s, 6, 5) of "Hello World!" is " Worl", not "World": the word starts at position 7

Left(s, n), Right(s, n)

s.Substring(0, n), s.Substring(s.Length - n)

n larger than the length returns the whole string; Substring throws ArgumentOutOfRangeException. A negative n throws

Len(s)

s.Length

Len(Nothing) is 0; Nothing.Length throws NullReferenceException

InStr(s, find), InStr(start, s, find, compare)

s.IndexOf(find)

1-based and 0 for not found (IndexOf is 0-based and -1). The compare mode defaults to Option Compare; InStrRev(s, find) searches from the end and also returns 0

Trim(s), LTrim(s), RTrim(s)

s.Trim(), s.TrimStart(), s.TrimEnd()

The VB functions remove only the space character U+0020: tabs and line breaks stay. String.Trim() removes every white-space character

UCase(s), LCase(s)

s.ToUpper(), s.ToLower()

The same current-culture conversion; use ToUpperInvariant() for identifiers (see culture, below)

Replace(s, find, repl, start, count, compare)

s.Replace(find, repl)

start (default 1) is 1-based, and the result starts at start: Replace(text, "l", "L", 5) returns "o WorLd!", with the first four characters discarded. count -1 means all. The compare mode comes from Option Compare

Split(s, delimiter, limit, compare)

s.Split(delimiter)

The delimiter defaults to a single space; empty entries are kept; limit stops splitting. String.Split takes StringSplitOptions (RemoveEmptyEntries, TrimEntries)

Join(array, delimiter)

String.Join(delimiter, values)

Argument order differs. Join takes an array of String or Object; String.Join takes any IEnumerable

StrComp(a, b, compare)

String.Compare(a, b, comparison)

Returns -1, 0 or 1, and the compare mode comes from Option Compare. String.Compare uses the current culture and returns any negative or positive value

Space(n), StrDup(n, text)

New String(" "c, n), New String(c, n)

StrDup(3, "ab") repeats only the first character of the text ("aaa"); use String.Concat(Enumerable.Repeat("ab", 3)) or the String.PadLeft/PadRight methods to repeat longer pieces

StrReverse(s)

New String(s.Reverse().ToArray())

StrReverse keeps surrogate pairs and combining sequences together (it reverses text elements); the LINQ version reverses code units and breaks them

LSet(s, n), RSet(s, n)

s.PadRight(n), s.PadLeft(n)

They also truncate to n characters

Filter(array, text)

array.Where(Function(x) x.Contains(text))

Returns the elements that contain the text, as an array, with a Include flag and a compare mode

Format(value, style)

value.ToString(format)

VB6-style named and custom formats, see the dates page. Format and the FormatNumber, FormatCurrency, FormatPercent and FormatDateTime functions take their culture from the thread

StrConv(s, VbStrConv...)

TextInfo.ToTitleCase, ToUpperInvariant, …​

Windows only: the analyzer reports CA1416, "This call site is reachable on all platforms …​ only supported on: `windows`", and so the example avoids it. The narrow/wide, katakana/hiragana and Chinese conversions have no .NET equivalent outside Windows

Asc(c), Chr(code)

AscW(c), ChrW(code)

Asc and Chr convert through the system ANSI code page (code 128 to 255), which on .NET needs the code pages provider registered, otherwise NotSupportedException. AscW and ChrW are plain UTF-16

Which one should you use? There is no single rule, but these defaults are close to universal:

  • For new code prefer the String members (Substring, IndexOf, Contains, StartsWith, Trim, Replace, Split, PadLeft, String.Join): they are the same members C# developers use, they take StringComparison, and they do not depend on Option Compare.

  • The Strings functions that have no good counterpart are fine: Mid and Left/Right when you want "cut politely, never throw", StrDup, Space, InStrRev and the Like operator.

  • When you call Left, Right, Mid or Len inside a class that has a member with the same name (a WinForms Form has a Left property, and a Control has Right), the member wins: qualify the call as Strings.Left(...).

  • A Strings function accepts Nothing and returns a sensible value; the String member needs a non-Nothing receiver.

The Mid statement

Mid is also a statement: Mid(target, start, length) = replacement overwrites characters of the string variable target in place, from the 1-based position start. The string is still immutable, so the statement assigns a new string to the variable; it never makes the string longer or shorter, uses at most length characters (and at most the characters left in target), and with the replacement shorter than the field, keeps the remaining characters. The first argument must be a writable variable or property, not an expression or a literal.

Option Strict On

Module Ex04
    Sub Main()
        ' The Mid statement overwrites characters in place; it never changes the length.
        Dim label = "Title: Dune     "
        Mid(label, 8) = "Emma"
        Console.WriteLine($"[{label}]")

        Dim code = "ABCDEFGH"
        Mid(code, 3, 2) = "xyz"
        Console.WriteLine($"Mid(code, 3, 2) = ""xyz""  -> {code}")
        Mid(code, 7) = "123456"
        Console.WriteLine($"Mid(code, 7) = ""123456"" -> {code}")

        ' The LSet and RSet functions pad or truncate to a fixed width.
        Console.WriteLine("[" & LSet("left", 10) & "] [" & RSet("right", 10) & "] [" & LSet("truncated", 4) & "]")

        ' Strings are immutable, so the usual replacement is to build a new string.
        Dim original = "ISBN 978-0-441-17271-9"
        Dim masked = original.Substring(0, 9) & New String("*"c, original.Length - 9)
        Console.WriteLine(masked)

        Dim chars = original.ToCharArray()
        chars(0) = "i"c
        Console.WriteLine(New String(chars))
        Console.WriteLine(original.Insert(4, ":").Remove(0, 1).PadLeft(25, "_"c))
    End Sub
End Module
[Title: Emma     ]
Mid(code, 3, 2) = "xyz"  -> ABxyEFGH
Mid(code, 7) = "123456" -> ABxyEF12
[left      ] [     right] [trun]
ISBN 978-*************
iSBN 978-0-441-17271-9
___SBN: 978-0-441-17271-9

Comparing strings

The question "are these two strings equal?" has at least four answers, depending on what "equal" means for your data. Visual Basic hides some of them behind the = operator, and the table shows what each way of asking does.

How you ask Rule Notes

a = b, a <> b, a < b, Select Case a

Option Compare of the file (default Binary)

Binary compares UTF-16 code units; Text compares case-insensitively using the current culture’s CompareInfo (the runtime also ignores width and kana differences). Nothing and "" are equal

a.Equals(b), String.Equals(a, b)

Ordinal

Never reads Option Compare; null-safe in the static form

String.Equals(a, b, StringComparison.OrdinalIgnoreCase)

Ordinal, ignoring simple case differences

The right test for identifiers, file extensions and protocol keywords

String.Compare(a, b), a.CompareTo(b)

Current culture

Sorting for people: "a" sorts before "B" even though 97 > 66

String.CompareOrdinal(a, b)

UTF-16 code unit order

Fast and culture-independent; the order is not alphabetical

StrComp(a, b)

Option Compare unless a mode is given

Returns -1, 0 or 1

a Like pattern

Option Compare

Wildcards, not a regular expression (see below)

a.StartsWith(b), a.IndexOf(b), a.EndsWith(b)

Current culture for String arguments, ordinal for Char arguments

A common surprise; always pass a StringComparison

a.Contains(b), a.Replace(b, c)

Ordinal

There are overloads that take StringComparison

Option Compare

Option Compare Binary (the default of the SDK, and of VB itself) and Option Compare Text are file-level statements that appear before any declaration, like Option Strict. They determine the comparison used by the string operators, Select Case, and, as the default for their Compare argument, by InStr, StrComp, Replace, Split and Filter. They have no effect on String members, Dictionary keys, List.Contains, Sort or LINQ: those use the comparer you pass or the type’s default. The project property <OptionCompare>Text</OptionCompare> changes the default for the whole project; do not do that, because it silently changes what = means in code that was written assuming Binary. The page Option Statements covers the statements and the project properties in full.

Option Strict On
Option Compare Binary

Module Ex05
    Sub Main()
        Dim a = "apple"
        Dim b = "Banana"
        Dim c = "APPLE"

        Console.WriteLine("-- Operators follow Option Compare (Binary: UTF-16 code unit order)")
        Console.WriteLine($"a = c         : {a = c}")
        Console.WriteLine($"a < b         : {a < b}   (97 is greater than 66)")
        Console.WriteLine($"a Like ""APP*"" : {a Like "APP*"}")

        Console.WriteLine("-- StrComp, InStr and Replace take an explicit CompareMethod")
        Console.WriteLine($"StrComp(a, c)                      = {StrComp(a, c)}")
        Console.WriteLine($"StrComp(a, c, CompareMethod.Text)  = {StrComp(a, c, CompareMethod.Text)}")
        Console.WriteLine($"InStr(""Shelf"", ""HE"")             = {InStr("Shelf", "HE")}")
        Console.WriteLine($"InStr(1, ""Shelf"", ""HE"", CompareMethod.Text) = {InStr(1, "Shelf", "HE", CompareMethod.Text)}")
        Console.WriteLine($"Replace(""Shelf"", ""SH"", ""-"", Compare:=CompareMethod.Text) = {Replace("Shelf", "SH", "-", Compare:=CompareMethod.Text)}")

        Console.WriteLine("-- The .NET methods never read Option Compare")
        Console.WriteLine($"a.Equals(c)                                  = {a.Equals(c)}")
        Console.WriteLine($"String.Equals(a, c, OrdinalIgnoreCase)       = {String.Equals(a, c, StringComparison.OrdinalIgnoreCase)}")
        Console.WriteLine($"String.Compare(a, b)                         = {String.Compare(a, b)} (current culture, here invariant)")
        Console.WriteLine($"String.CompareOrdinal(a, b)                  = {String.CompareOrdinal(a, b)}")
        Console.WriteLine($"a.StartsWith(""AP"", StringComparison.OrdinalIgnoreCase) = {a.StartsWith("AP", StringComparison.OrdinalIgnoreCase)}")
        Console.WriteLine($"a.Contains(""PPL"", StringComparison.OrdinalIgnoreCase)   = {a.Contains("PPL", StringComparison.OrdinalIgnoreCase)}")

        Console.WriteLine("-- Sorting with comparers")
        Dim titles As String() = {"banana", "Cherry", "apple", "Apple", "cherry"}
        Dim ordinal = titles.OrderBy(Function(t) t, StringComparer.Ordinal).ToArray()
        Dim ignoreCase = titles.OrderBy(Function(t) t, StringComparer.OrdinalIgnoreCase).ThenBy(Function(t) t, StringComparer.Ordinal).ToArray()
        Dim cultural = titles.OrderBy(Function(t) t, StringComparer.InvariantCulture).ToArray()
        Console.WriteLine("Ordinal:           " & String.Join(" ", ordinal))
        Console.WriteLine("OrdinalIgnoreCase: " & String.Join(" ", ignoreCase))
        Console.WriteLine("InvariantCulture:  " & String.Join(" ", cultural))
    End Sub
End Module
-- Operators follow Option Compare (Binary: UTF-16 code unit order)
a = c         : False
a < b         : False   (97 is greater than 66)
a Like "APP*" : False
-- StrComp, InStr and Replace take an explicit CompareMethod
StrComp(a, c)                      = 1
StrComp(a, c, CompareMethod.Text)  = 0
InStr("Shelf", "HE")             = 0
InStr(1, "Shelf", "HE", CompareMethod.Text) = 2
Replace("Shelf", "SH", "-", Compare:=CompareMethod.Text) = -elf
-- The .NET methods never read Option Compare
a.Equals(c)                                  = False
String.Equals(a, c, OrdinalIgnoreCase)       = True
String.Compare(a, b)                         = -1 (current culture, here invariant)
String.CompareOrdinal(a, b)                  = 31
a.StartsWith("AP", StringComparison.OrdinalIgnoreCase) = True
a.Contains("PPL", StringComparison.OrdinalIgnoreCase)   = True
-- Sorting with comparers
Ordinal:           Apple Cherry apple banana cherry
OrdinalIgnoreCase: Apple apple banana Cherry cherry
InvariantCulture:  apple Apple banana cherry Cherry

The same questions in a file that says Option Compare Text:

Option Strict On
Option Compare Text

Module Ex06
    Sub Main()
        Dim a = "apple"
        Dim c = "APPLE"

        Console.WriteLine($"a = c           : {a = c}")
        Console.WriteLine($"a < ""Banana""    : {a < "Banana"}")
        Console.WriteLine($"a Like ""APP*""   : {a Like "APP*"}")
        Console.WriteLine($"InStr(""Shelf"", ""HE"") = {InStr("Shelf", "HE")}")
        Console.WriteLine($"StrComp(a, c)   = {StrComp(a, c)}")
        Select Case "YES"
            Case "yes", "y"
                Console.WriteLine("Select Case matched without regard to case")
            Case Else
                Console.WriteLine("no match")
        End Select

        ' The .NET members still ignore Option Compare.
        Console.WriteLine($"a.Equals(c)     : {a.Equals(c)}")
        Console.WriteLine($"a.Contains(""PPL"") : {a.Contains("PPL")}")

        ' Dictionary keys use the comparer given to the constructor, never Option Compare.
        Dim byName As New Dictionary(Of String, Integer)()
        byName("Ada") = 1
        Console.WriteLine($"Dictionary lookup of ""ADA"": {byName.ContainsKey("ADA")}")
        Dim relaxed As New Dictionary(Of String, Integer)(StringComparer.OrdinalIgnoreCase)
        relaxed("Ada") = 1
        Console.WriteLine($"OrdinalIgnoreCase lookup of ""ADA"": {relaxed.ContainsKey("ADA")}")
    End Sub
End Module
a = c           : True
a < "Banana"    : True
a Like "APP*"   : True
InStr("Shelf", "HE") = 2
StrComp(a, c)   = 0
Select Case matched without regard to case
a.Equals(c)     : False
a.Contains("PPL") : False
Dictionary lookup of "ADA": False
OrdinalIgnoreCase lookup of "ADA": True

Notice what the second program shows: a = c and the Like operator turn case-insensitive, a.Equals(c) does not, and a Dictionary(Of String, Integer) created with the default comparer does not either. Using Option Compare Text to get case-insensitive keys is therefore a mistake that works in one place and fails in the next; pass StringComparer.OrdinalIgnoreCase to the collection.

Culture, ordinal and invariant

NET comparison and casing have three families. Ordinal compares the numeric value of the UTF-16 code units: fast, stable and independent of the machine. Culture-sensitive uses the collation rules of a culture

(CultureInfo.CurrentCulture unless one is passed): correct for text shown to people, but the result can differ between users and between operating systems, because since .NET 5 the rules come from ICU on Linux, macOS and recent Windows, and from NLS on older Windows. Invariant culture applies the same culture-sensitive rules for an English-like "no particular culture", intended for persisted data. The guidance of the .NET best-practices for strings applies unchanged to VB:

  • Use ordinal (StringComparison.Ordinal, OrdinalIgnoreCase) for identifiers, keys, file names, paths, URLs, protocol tokens and any other data that a program, not a person, reads.

  • Use culture-sensitive comparison for text that you sort or search for people, passing the culture explicitly (String.Compare(a, b, culture, CompareOptions.IgnoreCase)).

  • Convert case with ToUpperInvariant or ToLowerInvariant before comparing machine data, never with ToLower() (the Turkish I, below), and prefer OrdinalIgnoreCase to converting at all.

Option Strict On
Imports System.Globalization

Module Ex07
    Sub Main()
        Dim turkish = CultureInfo.GetCultureInfo("tr-TR")
        Dim english = CultureInfo.GetCultureInfo("en-US")
        Dim word = "TITLE"
        Dim file = "i"

        Console.WriteLine($"ToLower(en-US)     : {word.ToLower(english)}")
        Console.WriteLine($"ToLower(tr-TR)     : {word.ToLower(turkish)}")
        Console.WriteLine($"ToLowerInvariant   : {word.ToLowerInvariant()}")
        Console.WriteLine($"ToUpper(tr-TR) of i: {file.ToUpper(turkish)}  (U+{AscW(file.ToUpper(turkish)(0)):X4})")
        Console.WriteLine($"ToUpperInvariant   : {file.ToUpperInvariant()}")

        ' A classic bug: a protocol keyword compared after a culture-sensitive case change.
        Dim command = "TITLE"
        Console.WriteLine($"command.ToLower(tr-TR) = ""title"": {command.ToLower(turkish) = "title"}")
        Console.WriteLine($"String.Equals ordinal ignore case: {String.Equals(command, "title", StringComparison.OrdinalIgnoreCase)}")

        ' Culture-aware searching and comparison
        Dim cafe = "caf" & ChrW(&HE9)
        Dim cafeDecomposed = "cafe" & ChrW(&H301)
        Console.WriteLine($"Ordinal equal: {String.Equals(cafe, cafeDecomposed, StringComparison.Ordinal)}")
        Console.WriteLine($"InvariantCulture equal: {String.Equals(cafe, cafeDecomposed, StringComparison.InvariantCulture)}")
        Console.WriteLine($"IndexOf(""e"") ordinal: {cafeDecomposed.IndexOf("e", StringComparison.Ordinal)}, culture: {cafeDecomposed.IndexOf("e", StringComparison.InvariantCulture)}")

        Dim spanish = CultureInfo.GetCultureInfo("es-ES")
        Dim swedish = CultureInfo.GetCultureInfo("sv-SE")
        Console.WriteLine($"Compare(""z"", ""a-umlaut"", sv-SE) = {String.Compare("z", "ä", swedish, CompareOptions.None)}")
        Console.WriteLine($"Compare(""z"", ""a-umlaut"", es-ES) = {String.Compare("z", "ä", spanish, CompareOptions.None)}")

        Dim options = CompareOptions.IgnoreCase Or CompareOptions.IgnoreNonSpace
        Console.WriteLine($"Ignoring case and accents: {String.Compare(cafe.ToUpperInvariant(), "cafe", english, options)}")

        Console.WriteLine($"Current culture while this ran: '{CultureInfo.CurrentCulture.Name}' ({CultureInfo.CurrentCulture.EnglishName})")
    End Sub
End Module
ToLower(en-US)     : title
ToLower(tr-TR)     : tıtle
ToLowerInvariant   : title
ToUpper(tr-TR) of i: İ  (U+0130)
ToUpperInvariant   : I
command.ToLower(tr-TR) = "title": False
String.Equals ordinal ignore case: True
Ordinal equal: False
InvariantCulture equal: True
IndexOf("e") ordinal: 3, culture: -1
Compare("z", "a-umlaut", sv-SE) = -1
Compare("z", "a-umlaut", es-ES) = 1
Ignoring case and accents: 0
Current culture while this ran: '' (Invariant Language (Invariant Country))

The Turkish line is the classic failure: with the culture tr-TR the lower-case form of I is the dotless ı (U+0131), so "TITLE".ToLower() is not "title" and a keyword comparison written that way fails on a Turkish machine. In the same way, the Swedish alphabet sorts ä after z and the Spanish one does not, and the decomposed form of café is culture-equal but ordinal-different from the precomposed one. A program that only ever runs with a fixed English culture does not see any of this, which is why the problem stays hidden until a customer reports it. The project property <InvariantGlobalization>true</InvariantGlobalization> removes ICU entirely and makes every culture behave like the invariant culture, which is the usual setting for container images but disables the culture-specific results above.

Building strings

Concatenating a few values with &, String.Concat or an interpolated string is fine and the compiler turns several operands into a single String.Concat call. The pattern to avoid is a loop that appends to a string with &=: each step creates a new string and copies the old one, so n appends cost O(n2) time and produce n short-lived garbage strings. Use System.Text.StringBuilder, or String.Join when you already have a sequence, or a TextWriter when the destination is a stream. StringBuilder is mutable, grows by chunks, has chainable Append, AppendLine, AppendFormat, AppendJoin, Insert, Remove and Replace, and is not thread-safe.

Option Strict On
Imports System.Text

Module Ex08
    Sub Main()
        Dim titles As String() = {"Dune", "Emma", "Ulysses"}

        Dim csv = String.Join(",", titles)
        Console.WriteLine(csv)
        Console.WriteLine(String.Concat("a", "b", "c") & String.Concat(titles))
        Console.WriteLine(String.Join(", ", titles.Select(Function(t) t.Length)))

        ' Repeated & in a loop copies the whole string each time: O(n squared).
        Dim slow = ""
        For i = 1 To 5
            slow &= i & ";"
        Next
        Console.WriteLine(slow)

        Dim builder As New StringBuilder()
        For Each t In titles
            If builder.Length > 0 Then builder.Append(" | ")
            builder.Append(t).Append(" (").Append(t.Length).Append(")"c)
        Next
        builder.AppendLine().AppendFormat("{0} titles", titles.Length)
        builder.Insert(0, ">> ")
        Console.WriteLine(builder.ToString())
        Console.WriteLine($"Length = {builder.Length}, capacity >= length: {builder.Capacity >= builder.Length}")
        builder.Clear()
        Console.WriteLine($"After Clear: Length = {builder.Length}")

        ' Spans can be used inside one expression, but a local variable cannot have a span type in VB.
        Dim isbn = "978-0-441-17271-9"
        Console.WriteLine($"Span slice: {isbn.AsSpan(4, 1).ToString()}, length {isbn.AsSpan(4, 1).Length}")
        Console.WriteLine($"Span search: {isbn.AsSpan().IndexOf("-"c)}")
        Dim digits = isbn.Where(AddressOf Char.IsDigit).Count()
        Console.WriteLine($"Digits via LINQ: {digits}")

        Console.WriteLine($"String.Format: {String.Format("{0,-6}|{1,6:N1}|{2:P0}", "Dune", 12.34, 0.256)}")
        Dim invariantText = FormattableString.Invariant($"{1234.5:N2} on {#2026-10-19#:yyyy-MM-dd}")
        Console.WriteLine(invariantText)
        Dim f As FormattableString = $"{titles(0)} and {titles(1)}"
        Console.WriteLine($"Format = '{f.Format}', ArgumentCount = {f.ArgumentCount}")
    End Sub
End Module
Dune,Emma,Ulysses
abcDuneEmmaUlysses
4, 4, 7
1;2;3;4;5;
>> Dune (4) | Emma (4) | Ulysses (7)
3 titles
Length = 45, capacity >= length: True
After Clear: Length = 0
Span slice: 0, length 1
Span search: 3
Digits via LINQ: 13
String.Format: Dune  |  12.3|26 %
1,234.50 on 2026-10-19
Format = '{0} and {1}', ArgumentCount = 2

Spans. ReadOnlySpan(Of Char) and Span(Of Char) are ref structs. Visual Basic 17.13 can pass them to methods and call their members in an expression (isbn.AsSpan(4, 1).ToString()), but it cannot declare a local variable, field or parameter of a span type: the line Dim part As ReadOnlySpan(Of Char) = ... fails with BC30668 ("`ReadOnlySpan(Of Char)` is obsolete: Types with embedded references are not supported in this version of your compiler"). VB also has no stackalloc. For allocation-free text work, write the hot path in a C# library or use the String and StringBuilder APIs, and see the memory and spans notes in Unsafe Code, Spans and Performance.

Pattern matching: Like and regular expressions

The Like operator compares a string with a pattern that has five metacharacters: ? (one character), * (any number of characters, also none), # (one digit), [list] (one character from the list or a range such as [A-C]), and [!list] (one character not in the list). Put a metacharacter in brackets to match it literally: a[*]b. The case sensitivity follows Option Compare. Like works on .NET: it is implemented by LikeOperator in Microsoft.VisualBasic.Core, and the following program, built and run on .NET 10, shows that it does. The Language Reference page for the operator still carries a note that it is not supported on .NET Core, which is out of date (checked 2026-10-19).

Option Strict On

Module Ex12
    Sub Check(text As String, pattern As String)
        Console.WriteLine($"{text,-12} Like {pattern,-12} -> {text Like pattern}")
    End Sub

    Sub Main()
        Check("Dune", "D*")
        Check("Dune", "D??e")
        Check("Dune", "d*")
        Check("2026", "####")
        Check("A12", "[A-C]##")
        Check("D12", "[!A-C]##")
        Check("a*b", "a[*]b")
        Check("978-0-441", "978-#-###")
        Check("ISBN", "")

        Dim padded = "Dune".PadRight(8, "."c) & "1965".PadLeft(6)
        Console.WriteLine(padded)
        Dim catalog As (Title As String, Year As Integer)() = {("Dune", 1965), ("Emma", 1815), ("Ulysses", 1922)}
        For Each row In catalog
            Console.WriteLine($"{row.Title,-10}{row.Year,6}")
        Next
        Console.WriteLine(New String("="c, 16))
    End Sub
End Module
Dune         Like D*           -> True
Dune         Like D??e         -> True
Dune         Like d*           -> False
2026         Like ####         -> True
A12          Like [A-C]##      -> True
D12          Like [!A-C]##     -> True
a*b          Like a[*]b        -> True
978-0-441    Like 978-#-###    -> True
ISBN         Like              -> False
Dune....  1965
Dune        1965
Emma        1815
Ulysses     1922
================

For anything beyond that, use System.Text.RegularExpressions.Regex. VB string literals need no escaping of backslashes, which makes patterns easier to read than in C# without the verbatim prefix. Points specific to VB and to current .NET:

  • Named groups are read with m.Groups("year"); test .Success on a group for an optional part.

  • Always give a match timeout on patterns that process untrusted input (New Regex(pattern, options, TimeSpan.FromSeconds(1))): nested quantifiers such as (a+)+$ have exponential backtracking and raise RegexMatchTimeoutException when the limit is reached. RegexOptions.NonBacktracking (.NET 7 and later) avoids backtracking altogether, with a smaller feature set.

  • RegexOptions.CultureInvariant together with IgnoreCase avoids the Turkish I problem; RegexOptions.Compiled trades start-up time for speed.

  • Escape user input before embedding it in a pattern with Regex.Escape.

  • The GeneratedRegex source generator of .NET 7 is not available to VB: its attribute goes on a partial method with a return value, and VB’s partial methods must be Sub (BC31437, "partial methods must be Subs"). A Shared ReadOnly field holding a New Regex(...) is the VB replacement.

  • The static methods (Regex.IsMatch(text, pattern)) cache recently used patterns, which is fine for a few patterns and a poor choice in a hot loop with many.

Option Strict On
Imports System.Text.RegularExpressions

Module Ex09
    Sub Main()
        Dim lines As String() = {
            "Dune (1965) by Frank Herbert, ISBN 978-0-441-17271-9",
            "Emma (1815) by Jane Austen",
            "no year here"
        }

        ' Verbatim by nature: backslashes in VB string literals are ordinary characters.
        Dim pattern As New Regex("^(?<title>.+?) \((?<year>\d{4})\)(?: by (?<author>[^,]+))?", RegexOptions.CultureInvariant, TimeSpan.FromSeconds(1))

        For Each line In lines
            Dim m = pattern.Match(line)
            If m.Success Then
                Dim author = m.Groups("author").Value
                Console.WriteLine($"{m.Groups("title").Value} | {CInt(m.Groups("year").Value) + 1} | {If(m.Groups("author").Success, author, "(none)")}")
            Else
                Console.WriteLine($"No match: {line}")
            End If
        Next

        Dim isbn = New Regex("\b97[89](?:-?\d){10}\b")
        Console.WriteLine($"ISBN found: {isbn.Match(lines(0)).Value}")

        Dim spaced = Regex.Replace("a    b   c", "\s+", " ")
        Console.WriteLine(spaced)
        Dim shouted = Regex.Replace("shelf catalog", "\b[a-z]", Function(m As Match) m.Value.ToUpperInvariant())
        Console.WriteLine(shouted)
        Console.WriteLine(String.Join("|", Regex.Split("one, two;three", "[,;]\s*")))
        Console.WriteLine($"IsMatch ignoring case: {Regex.IsMatch("ISBN", "isbn", RegexOptions.IgnoreCase)}")
        Console.WriteLine($"Escaped user input: {Regex.Escape("1+1=2 (really?)")}")
        Console.WriteLine($"Count of digits: {Regex.Matches("a1b22c333", "\d+").Count}")

        Try
            Dim slow As New Regex("^(a+)+$", RegexOptions.None, TimeSpan.FromMilliseconds(50))
            slow.IsMatch(New String("a"c, 40) & "!")
        Catch ex As RegexMatchTimeoutException
            Console.WriteLine("RegexMatchTimeoutException after " & ex.MatchTimeout.TotalMilliseconds & " ms")
        End Try
    End Sub
End Module
Dune | 1966 | Frank Herbert
Emma | 1816 | Jane Austen
No match: no year here
ISBN found: 978-0-441-17271-9
a b c
Shelf Catalog
one|two|three
IsMatch ignoring case: True
Escaped user input: 1\+1=2\ \(really\?\)
Count of digits: 3
RegexMatchTimeoutException after 50 ms

Encodings and bytes

A String has no encoding; a byte sequence does. System.Text.Encoding converts between them. .NET defaults to UTF-8 for files and streams (File.ReadAllText, StreamReader, Encoding.UTF8), which is not the same as the console or the Windows ANSI code page. The program shows the byte counts for Café €, what happens with characters that the target cannot represent (? by default, or an exception if you ask for one), the byte order mark, the replacement character for invalid input, and the code pages that .NET Core does not ship.

Option Strict On
Imports System.Text

Module Ex10
    Sub Main()
        Dim text = "Caf" & ChrW(&HE9) & " " & ChrW(&H20AC)

        Console.WriteLine($"UTF-16 code units: {text.Length}")
        For Each name In {"utf-8", "utf-16", "utf-16BE", "utf-32", "us-ascii", "iso-8859-1"}
            Dim enc = Encoding.GetEncoding(name)
            Dim bytes = enc.GetBytes(text)
            Console.WriteLine($"{enc.WebName,-12} {bytes.Length,2} bytes: {Convert.ToHexString(bytes)}")
        Next

        ' Bytes that are not valid in the encoding are replaced, silently by default.
        Dim latin = Encoding.Latin1.GetBytes(text)
        Console.WriteLine($"Latin-1 round trip: {Encoding.Latin1.GetString(latin)}")
        Dim strict = Encoding.GetEncoding("us-ascii", New EncoderExceptionFallback(), New DecoderExceptionFallback())
        Try
            strict.GetBytes(text)
        Catch ex As EncoderFallbackException
            Console.WriteLine("EncoderFallbackException for U+" & AscW(ex.CharUnknown).ToString("X4"))
        End Try

        Dim utf8Bom = New UTF8Encoding(encoderShouldEmitUTF8Identifier:=True).GetPreamble()
        Console.WriteLine($"UTF-8 BOM: {Convert.ToHexString(utf8Bom)}; Encoding.UTF8 preamble: {Convert.ToHexString(Encoding.UTF8.GetPreamble())}")

        Dim broken As Byte() = {&H43, &HFF, &H44}
        Console.WriteLine($"Invalid UTF-8 decodes with a replacement character: {Encoding.UTF8.GetString(broken)} (U+{AscW(Encoding.UTF8.GetString(broken)(1)):X4})")

        Try
            Encoding.GetEncoding(1252)
        Catch ex As NotSupportedException
            Console.WriteLine("Code page 1252 needs the CodePages provider: " & ex.GetType().Name)
        End Try
        Try
            Console.WriteLine("Chr(200) = " & Chr(200))
        Catch ex As NotSupportedException
            Console.WriteLine("Chr(200) uses the ANSI code page too: " & ex.GetType().Name)
        End Try
        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)
        Console.WriteLine($"After RegisterProvider: {Encoding.GetEncoding(1252).WebName}, euro = {Convert.ToHexString(Encoding.GetEncoding(1252).GetBytes(ChrW(&H20AC)))}")

        Console.WriteLine($"Asc(""A"") = {Asc("A")}, Chr(65) = {Chr(65)}, AscW(""€"") = {AscW("€")}")
        Console.WriteLine($"After the provider is registered Chr(200) = {Chr(200)}; ChrW(200) = {ChrW(200)} regardless")
        Console.WriteLine($"Base64: {Convert.ToBase64String(Encoding.UTF8.GetBytes("Shelf"))}")
    End Sub
End Module
UTF-16 code units: 6
utf-8         9 bytes: 436166C3A920E282AC
utf-16       12 bytes: 430061006600E9002000AC20
utf-16BE     12 bytes: 00430061006600E9002020AC
utf-32       24 bytes: 430000006100000066000000E900000020000000AC200000
us-ascii      6 bytes: 4361663F203F
iso-8859-1    6 bytes: 436166E9203F
Latin-1 round trip: Café ?
EncoderFallbackException for U+00E9
UTF-8 BOM: EFBBBF; Encoding.UTF8 preamble: EFBBBF
Invalid UTF-8 decodes with a replacement character: C�D (U+FFFD)
Code page 1252 needs the CodePages provider: NotSupportedException
Chr(200) uses the ANSI code page too: NotSupportedException
After RegisterProvider: windows-1252, euro = 80
Asc("A") = 65, Chr(65) = A, AscW("€") = 8364
After the provider is registered Chr(200) = È; ChrW(200) = È regardless
Base64: U2hlbGY=

Source: Encoding Class

Rules of thumb: write UTF-8 (without a BOM unless a consumer needs it); use Encoding.Latin1 for ISO-8859-1; register CodePagesEncodingProvider.Instance once at start-up when you must read Windows-1252 or other legacy code pages; never rely on Chr and Asc for non-ASCII text, use ChrW and AscW; and for a console program that prints non-ASCII text set Console.OutputEncoding = Encoding.UTF8 yourself. The same encoding rules apply to File.ReadAllText/WriteAllText (see Files, Directories and Streams).

Concatenation: & versus

& is the concatenation operator: it always converts both operands to String and joins them, so "Copies: " & 2 is "Copies: 2" and 1 & 2 is "12". + is the addition operator that, when both operands are strings, also concatenates for compatibility with early BASIC. The two are not interchangeable:

  • With one string and one number, + tries to convert the string to a number: "1" + 2 and "x" + 2 are BC30512 under Option Strict On ("disallows implicit conversions from String to Double`"); with strict typing switched off (checked in a scratch project) the first quietly gives the `Double 3 and the second throws InvalidCastException ("Conversion from string "x" to type 'Double' is not valid") at run time.

  • The precedence is different: arithmetic binds tighter than &, so "Total: " & 1 + 2 is "Total: 3" while the addition-only reading would first apply + to a string and a number.

  • Nothing is an empty string for &, and Nothing + "a" also works but "a" + 1 does not.

  • Two strings that happen to contain digits (left + right in the program) are concatenated by +, which hides a missing conversion: someone later changes one variable to Integer and the meaning of the line changes.

Always write & for concatenation (and &= for appending), interpolation for formatting, and keep + for numbers.

Option Strict On

Module Ex11
    Sub Main()
        Dim count As Integer = 2
        Dim title As String = "Dune"

        ' & always concatenates and converts both sides to String.
        Console.WriteLine("Copies: " & count)
        Console.WriteLine(1 & 2)
        Console.WriteLine("Total: " & 1 + 2)
        Console.WriteLine("Total: " & (1 + 2))
        Console.WriteLine(title & Nothing & "!")

        ' Concatenation has lower precedence than arithmetic and higher than comparison.
        Console.WriteLine("a" & "b" = "ab")

        ' + on two strings concatenates as well, but it is a numeric operator that falls back to concatenation.
        Dim left As String = "10"
        Dim right As String = "5"
        Console.WriteLine(left + right)
        Console.WriteLine(left & right)
        Console.WriteLine(CInt(left) + CInt(right))

        ' Interpolation and String.Format never ask whether + was meant.
        Console.WriteLine($"{left}{right} / {count + 1}")
    End Sub
End Module
Copies: 2
12
Total: 3
Total: 3
Dune!
True
105
105
15
105 / 3

Comparison with C#

Topic Visual Basic C#

Indexing

s(0), s.Chars(0); functions Mid, Left, Right are 1-based

s[0], s.Substring; always 0-based; ranges s[1..3]

Escape sequences

none; "" for a quote, vbTab, ChrW(n), ControlChars.*

\n, \t, €, verbatim @"" and raw """..."""

Interpolation

$"{x,10:N2}" since VB 14; FormattableString; no handlers

$"..." since C# 6, handlers (C# 10), raw interpolated strings

UTF-8 literal

none

"abc"u8 (C# 11)

== on strings

=; ordinal by default, culture-insensitive text under Option Compare Text; Nothing = "" is true

== always ordinal; null == "" is false

Concatenation

& (and &=); + for numbers

+

Case conversion

UCase, LCase, ToUpper, ToUpperInvariant

ToUpper, ToUpperInvariant

Spans in locals

not supported (BC30668)

Span<char>, stackalloc

Regex source generator

not available (BC31437)

[GeneratedRegex]

String patterns

Like operator

is patterns, Regex

Smart quotes

accepted as string delimiters

rejected

Where the books are outdated. The two books that this section was planned from draw on VB6 and early VB.NET habits in text handling: they use ` for concatenation, they present `Mid("Hello World!", 6, 5)` as "World" when it is `" Worl"` (`Mid` is 1-based, so the word starts at position 7), and they treat `FormatDateTime` and `Format` as the usual way to format, where interpolation and `ToString(format)` are the current choice. The corrections are all in this page: `&` and interpolation instead of `, the 1-based Mid, Left, Right and InStr against the 0-based Substring and IndexOf, and the culture rules of ToLower and String.Compare. Their 8-bit view of text (Chr, Asc, ANSI) is also outdated: .NET text is UTF-16 and files default to UTF-8.

Pitfalls checklist

  • The positions in Mid, InStr and Left, and the start argument of Replace, are 1-based; Substring and IndexOf are 0-based; Replace with a start position drops the beginning.

  • Trim (the function) removes only spaces; s.Trim() removes all white space.

  • StrDup(n, "ab") repeats the first character only.

  • Nothing = "" is True, while Nothing Is "" is False (the second is an empty string, an object); use String.IsNullOrEmpty for "missing or empty".

  • Option Compare Text affects operators, Select Case, Like, InStr and StrComp only; dictionaries, List.Contains and String.Equals ignore it.

  • ToLower()/ToUpper() use the current culture: use the Invariant variants or OrdinalIgnoreCase for machine data.

  • StartsWith(String), IndexOf(String) and Compare are culture-sensitive by default; Contains, Replace and Split are ordinal.

  • A multiline string literal contains the line endings of the source file.

  • Curly quotes pasted into code are valid string delimiters.

  • The Asian conversions of StrConv are Windows-only; Chr and Asc above 127 depend on a code page.

  • s &= x in a loop is quadratic; use StringBuilder or String.Join.

  • + between a number and a string is a conversion error under Option Strict On; & never is.

  • Length counts UTF-16 code units, not characters; Mid and Substring can cut surrogate pairs.

  • No VB local can have a span type, and GeneratedRegex cannot be used.