Output & Running
Hello, World
The smallest complete program in each language, side by side — before anything else, notice how much of the Visual Basic column has no counterpart at all.
Option Strict On
Imports System
Module HelloWorld
Sub Main()
Console.WriteLine("Hello, World!")
End Sub
End ModuleConsole.WriteLine("Hello, World!");Both lines call the same
Console.WriteLine from the same .NET base class library; only the packaging differs. C# still has Main — the compiler writes one for you and puts these statements inside it — so nothing was taken away, it was made implicit. The semicolon is not optional: C# ends every statement with one rather than using the end of the line.No Module, no Sub Main
Where a Visual Basic program needs three levels of nesting before the first real line, a C# file can start with the work itself.
Option Strict On
Imports System
Module Program
Sub Main()
Dim total As Integer = 0
For counter As Integer = 1 To 5
total += counter
Next
Console.WriteLine($"Total: {total}")
End Sub
End Moduleint total = 0;
for (int counter = 1; counter <= 5; counter++)
{
total += counter;
}
Console.WriteLine($"Total: {total}");Statements written straight into a file are called top-level statements, and only one file in a project may have them. Everything you would have put in
Module Program goes here directly, and any classes or records you need are declared after the statements, at the bottom of the file. This is why every C# example on this page starts with executable code and defines its types last — the compiler requires that order.String interpolation
One of the few places where you can copy a line across unchanged: interpolated strings are spelled identically in both languages.
Option Strict On
Imports System
Module InterpolationDemo
Sub Main()
Dim name As String = "Ada"
Dim score As Integer = 42
Dim ratio As Double = 0.8756
Console.WriteLine($"Hello, {name}! Score: {score}")
Console.WriteLine($"Padded: {score:D5}, rounded: {ratio:F2}")
End Sub
End Modulestring name = "Ada";
int score = 42;
double ratio = 0.8756;
Console.WriteLine($"Hello, {name}! Score: {score}");
Console.WriteLine($"Padded: {score:D5}, rounded: {ratio:F2}");The
$"..." form and its format specifiers (D5, F2, N0, P1) came from the same compiler team and work the same way. If you are arriving from VB6 or VBA rather than VB.NET, this replaces the Format$() and &-chaining habit entirely, in both languages.Writing without a newline
The same two output methods, and the first sighting of a difference that will follow you everywhere: C# keywords are lowercase.
Option Strict On
Imports System
Module OutputDemo
Sub Main()
Console.Write("Loading")
For dot As Integer = 1 To 3
Console.Write(".")
Next
Console.WriteLine(" done")
Console.WriteLine(True)
Console.WriteLine(3.5)
End Sub
End ModuleConsole.Write("Loading");
for (int dot = 1; dot <= 3; dot++)
{
Console.Write(".");
}
Console.WriteLine(" done");
Console.WriteLine(true);
Console.WriteLine(3.5);Visual Basic capitalizes
True and False; C# writes true and false, and will not accept the capitalized forms. That is not a style preference — C# is case-sensitive, so True is simply an unknown identifier. The same applies to Nothing, which becomes null.Syntax Fundamentals
Blocks: braces instead of End
Every
End Something you have ever typed collapses into a single closing brace.Option Strict On
Imports System
Module BlockDemo
Sub Main()
Dim temperature As Integer = 30
If temperature > 25 Then
Console.WriteLine("Warm")
ElseIf temperature > 10 Then
Console.WriteLine("Mild")
Else
Console.WriteLine("Cold")
End If
End Sub
End Moduleint temperature = 30;
if (temperature > 25)
{
Console.WriteLine("Warm");
}
else if (temperature > 10)
{
Console.WriteLine("Mild");
}
else
{
Console.WriteLine("Cold");
}The condition needs parentheses in C# and no
Then; ElseIf becomes two words, else if. A block of exactly one statement may drop its braces entirely (if (x) DoThing();), which has no Visual Basic parallel except the single-line If ... Then ... form. Most C# codebases keep the braces anyway.Comments and documentation
Three comment forms in Visual Basic, three in C# — but they do not line up one to one.
Option Strict On
Imports System
Module CommentDemo
''' <summary>Doubles a number.</summary>
Function Double_(value As Integer) As Integer
' A line comment starts with an apostrophe
REM REM also works, and nobody uses it
Return value * 2
End Function
Sub Main()
Console.WriteLine(Double_(21))
End Sub
End ModuleConsole.WriteLine(Double_(21));
/// <summary>Doubles a number.</summary>
int Double_(int value)
{
// A line comment starts with two slashes
/* and this form spans
as many lines as you like */
return value * 2;
}C# gains a genuine block comment,
/* ... */, which Visual Basic has never had; that is why long commented-out passages in Visual Basic are a column of apostrophes. Documentation comments survive the trip: ''' becomes ///, and the XML tags inside (<summary>, <param>, <returns>) are identical, because both compilers emit the same documentation file.Case sensitivity
The single largest mental adjustment on this page, and the one most likely to bite silently.
Option Strict On
Imports System
Module CaseDemo
Sub Main()
Dim customerName As String = "Grace"
' Every one of these refers to the SAME variable
Console.WriteLine(customerName)
Console.WriteLine(CustomerName)
Console.WriteLine(CUSTOMERNAME)
End Sub
End Modulestring customerName = "Grace";
string CustomerName = "Hopper";
// Two different variables, and the compiler sees no problem
Console.WriteLine(customerName);
Console.WriteLine(CustomerName);Visual Basic identifiers are case-insensitive — the editor even rewrites your casing to match the declaration — so
customerName and CustomerName are one name. In C# they are two names, and nothing warns you. The convention that saves you is the one the .NET libraries already follow: camelCase for locals and parameters, PascalCase for types, methods, properties and constants. Follow it and the two never collide.Long lines and statement ends
The trailing underscore disappears, and so does the need for it.
Option Strict On
Imports System
Module ContinuationDemo
Sub Main()
Dim total As Integer = 1 + 2 + 3 +
4 + 5 + 6
Dim label As String = String.Format( _
"Total is {0}", total)
Console.WriteLine(label)
End Sub
End Moduleint total = 1 + 2 + 3 +
4 + 5 + 6;
string label = string.Format(
"Total is {0}", total);
Console.WriteLine(label);C# ends a statement at the semicolon, not at the end of the line, so a statement may be broken across as many lines as you like with no continuation character at all. Visual Basic has drifted the same way — since VB 2010 most line breaks are inferred and the
_ is only needed in the remaining awkward spots, as after an opening parenthesis in the example above. In C# there is no awkward spot.Variables & Types
Declaring a variable
The declaration turns inside out: the type moves from the end of the line to the front, and
Dim vanishes.Option Strict On
Imports System
Module DeclarationDemo
Sub Main()
Dim count As Integer = 10
Dim price As Decimal = 19.99D
Dim label As String = "widget"
Dim ready As Boolean = True
Console.WriteLine($"{count} x {label} at {price} (ready: {ready})")
End Sub
End Moduleint count = 10;
decimal price = 19.99m;
string label = "widget";
bool ready = true;
Console.WriteLine($"{count} x {label} at {price} (ready: {ready})");Read
Dim count As Integer right to left and you have C#'s int count. The literal suffixes change case: D for decimal becomes m (for "money", since d was taken by double), F becomes f, L stays L. There is no Option Explicit switch to forget — C# has never allowed an undeclared variable.Type inference: Dim without As
If you already write
Dim x = ... with Option Infer On, you have been writing C#'s var all along.Option Strict On
Option Infer On
Imports System
Imports System.Collections.Generic
Module InferenceDemo
Sub Main()
Dim count = 10
Dim names = New List(Of String) From {"Ada", "Grace"}
Console.WriteLine(count.GetType().Name)
Console.WriteLine(String.Join(", ", names))
End Sub
End Modulevar count = 10;
var names = new List<string> { "Ada", "Grace" };
Console.WriteLine(count.GetType().Name);
Console.WriteLine(string.Join(", ", names));Both are compile-time inference, not dynamic typing:
count is an int forever and assigning a string to it is a compile error. The difference is that C# has no Option Infer to turn off, and no Option Strict either — its inference is always on and its strictness is never negotiable. Note also that the generic type argument moves from (Of String) to <string>.What the built-in types are called
A translation table you will need for about a week, after which it is automatic.
Option Strict On
Imports System
Module TypeNameDemo
Sub Main()
Dim small As Short = 1S
Dim whole As Integer = 2
Dim big As Long = 3L
Dim rough As Single = 4.5F
Dim precise As Double = 6.7
Dim exact As Decimal = 8.9D
Dim letter As Char = "A"c
Dim raw As Byte = 255
Console.WriteLine($"{small} {whole} {big} {rough} {precise} {exact} {letter} {raw}")
End Sub
End Moduleshort small = 1;
int whole = 2;
long big = 3L;
float rough = 4.5f;
double precise = 6.7;
decimal exact = 8.9m;
char letter = 'A';
byte raw = 255;
Console.WriteLine($"{small} {whole} {big} {rough} {precise} {exact} {letter} {raw}");Integer→int, Long→long, Short→short, Single→float, Double→double, Boolean→bool, String→string, Object→object. Both sets are aliases for the same .NET types (System.Int32 and the rest), so nothing changes at runtime. The one that catches people is Char: Visual Basic writes "A"c with a suffix, C# uses single quotes, and single quotes in C# are never a string.Constants and read-only values
Both languages separate "baked into the compiled code" from "assigned once at startup", and both spell the distinction almost the same way.
Option Strict On
Imports System
Module ConstantDemo
Const MaximumRetries As Integer = 3
Private ReadOnly StartedAt As DateTime = New DateTime(2026, 1, 1)
Sub Main()
Console.WriteLine(MaximumRetries)
Console.WriteLine(StartedAt.Year)
End Sub
End ModuleConsole.WriteLine(Settings.MaximumRetries);
Console.WriteLine(Settings.StartedAt.Year);
static class Settings
{
public const int MaximumRetries = 3;
public static readonly DateTime StartedAt = new DateTime(2026, 1, 1);
}Const stays const and must be a compile-time literal in both. ReadOnly becomes readonly, and is what you need for anything computed — a DateTime, a List, anything built with New. Note that a C# const is implicitly static, so it lives on the type rather than the instance, exactly as a Const in a Module does.Nullable value types
The
? suffix means the same thing in both languages; what changes is Nothing.Option Strict On
Imports System
Module NullableDemo
Sub Main()
Dim maybeCount As Integer? = Nothing
If maybeCount.HasValue Then
Console.WriteLine($"Got {maybeCount.Value}")
Else
Console.WriteLine("No value")
End If
Dim safe As Integer = maybeCount.GetValueOrDefault(-1)
Console.WriteLine(safe)
End Sub
End Moduleint? maybeCount = null;
if (maybeCount.HasValue)
{
Console.WriteLine($"Got {maybeCount.Value}");
}
else
{
Console.WriteLine("No value");
}
int safe = maybeCount ?? -1;
Console.WriteLine(safe);Nothing becomes null, and the two are not quite the same idea. Assigning Nothing to an Integer in Visual Basic quietly stores zero — Nothing means "the default value for this type". C# refuses: int x = null; is a compile error, and only int? can hold null. The ?? operator, read "or else use", is C#'s equivalent of the two-argument If(value, fallback).Converting between types
The whole family of
C-prefixed conversion functions has no C# counterpart — here is what replaces each kind.Option Strict On
Imports System
Module ConversionDemo
Sub Main()
Dim text As String = "123"
Dim parsed As Integer = CInt(text)
Dim widened As Double = CDbl(parsed)
Dim boxed As Object = parsed
Dim unboxed As Integer = DirectCast(boxed, Integer)
Dim notANumber As String = "oops"
Dim result As Integer
If Integer.TryParse(notANumber, result) Then
Console.WriteLine(result)
Else
Console.WriteLine("Could not parse")
End If
Console.WriteLine($"{parsed} {widened} {unboxed}")
End Sub
End Modulestring text = "123";
int parsed = int.Parse(text);
double widened = parsed;
object boxed = parsed;
int unboxed = (int)boxed;
string notANumber = "oops";
if (int.TryParse(notANumber, out int result))
{
Console.WriteLine(result);
}
else
{
Console.WriteLine("Could not parse");
}
Console.WriteLine($"{parsed} {widened} {unboxed}");CInt, CDbl, CStr and friends split three ways in C#. Parsing text uses int.Parse / int.TryParse; a widening conversion such as int to double needs nothing at all; a narrowing or unboxing conversion uses a cast in parentheses, (int)boxed, which is DirectCast. TryCast — a cast that yields Nothing instead of throwing — is C#'s as operator, and works only on reference and nullable types.Operators
= assigns, == compares
Visual Basic decides from context whether
= assigns or compares. C# makes you choose.Option Strict On
Imports System
Module EqualityDemo
Sub Main()
Dim left As Integer = 5
Dim right As Integer = 5
' One symbol, two jobs, told apart by position
If left = right Then
Console.WriteLine("equal")
End If
If left <> 6 Then
Console.WriteLine("not six")
End If
End Sub
End Moduleint left = 5;
int right = 5;
// Two symbols, one job each
if (left == right)
{
Console.WriteLine("equal");
}
if (left != 6)
{
Console.WriteLine("not six");
}Comparison is
== and inequality is != rather than <>. The classic C trap — writing if (x = 5) and assigning by accident — cannot happen here, because a C# if requires a bool and an assignment of an int yields an int, so the compiler rejects it. The same rule means if (count) is an error too: there is no truthiness in C#, and no 0 = False convention.AndAlso and OrElse
Both languages have a short-circuiting pair and a non-short-circuiting pair; only the spelling changes.
Option Strict On
Imports System
Module LogicalDemo
Function IsExpensive(value As Integer) As Boolean
Console.WriteLine(" (checked)")
Return value > 100
End Function
Sub Main()
Dim value As Integer = 5
' AndAlso stops early — IsExpensive is never called
If value > 10 AndAlso IsExpensive(value) Then
Console.WriteLine("both")
End If
' And evaluates BOTH sides, always
If value > 10 And IsExpensive(value) Then
Console.WriteLine("both")
End If
Console.WriteLine(Not (value = 5))
End Sub
End Moduleint value = 5;
// && stops early — IsExpensive is never called
if (value > 10 && IsExpensive(value))
{
Console.WriteLine("both");
}
// & evaluates BOTH sides, always
if (value > 10 & IsExpensive(value))
{
Console.WriteLine("both");
}
Console.WriteLine(!(value == 5));
bool IsExpensive(int candidate)
{
Console.WriteLine(" (checked)");
return candidate > 100;
}AndAlso→&&, OrElse→||, And→&, Or→|, Xor→^, Not→!. The mapping is exact, including the trap: And and & both evaluate the right-hand side even when the left has already settled the answer, which is why the second If prints "(checked)". C# convention is to reach for && and || by default and treat & / | as bitwise operators, which is what they mostly are.Integer division
Visual Basic has two division operators. C# has one, and which kind of division you get depends on the operands.
Option Strict On
Imports System
Module DivisionDemo
Sub Main()
Dim quotient As Integer = 17 \ 5
Dim exact As Double = 17 / 5
Dim remainder As Integer = 17 Mod 5
Console.WriteLine($"{quotient} {exact} {remainder}")
End Sub
End Moduleint quotient = 17 / 5;
double exact = 17 / 5.0;
int remainder = 17 % 5;
Console.WriteLine($"{quotient} {exact} {remainder}");This is the single most dangerous line-for-line translation on the page. Visual Basic's
/ always produces a floating-point result and \ is the integer one. C# has no \: 17 / 5 between two ints truncates to 3, and you get the fractional answer only by making an operand floating-point — 17 / 5.0, or (double)a / b. A Visual Basic / copied straight across silently starts truncating. Mod becomes %.Raising to a power
One of the few operators Visual Basic has and C# does not.
Option Strict On
Imports System
Module PowerDemo
Sub Main()
Dim squared As Double = 7 ^ 2
Dim root As Double = 81 ^ 0.5
Console.WriteLine($"{squared} {root}")
End Sub
End Moduledouble squared = Math.Pow(7, 2);
double root = Math.Pow(81, 0.5);
Console.WriteLine($"{squared} {root}");C# has no exponent operator;
Math.Pow is the answer, and it takes and returns double just as ^ does. Do not reach for ^ out of habit — it compiles, but it is the bitwise exclusive-or operator, so 7 ^ 2 quietly evaluates to 5 rather than 49. That is a wrong answer with no error message, which makes it worth memorizing now.Joining strings
The
& you have used for concatenation since VB6 is not a C# operator at all.Option Strict On
Imports System
Module ConcatenationDemo
Sub Main()
Dim first As String = "Grace"
Dim last As String = "Hopper"
Dim joined As String = first & " " & last
Dim withNumber As String = "Answer: " & 42
Console.WriteLine(joined)
Console.WriteLine(withNumber)
End Sub
End Modulestring first = "Grace";
string last = "Hopper";
string joined = first + " " + last;
string withNumber = "Answer: " + 42;
Console.WriteLine(joined);
Console.WriteLine(withNumber);C# concatenates with
+. Visual Basic has both, and the reason it prefers & is that + is ambiguous there — with Option Strict Off, "1" + 2 could add rather than join. C#'s + has no such ambiguity: if either operand is a string, the other is converted with ToString() and the result is a string. & in C# is bitwise-and, so using it by mistake will not compile against strings.If() as an expression
Visual Basic overloads one
If() function for two different jobs; C# gives each its own operator.Option Strict On
Imports System
Module ConditionalDemo
Sub Main()
Dim score As Integer = 72
Dim grade As String = If(score >= 60, "pass", "fail")
Dim supplied As String = Nothing
Dim label As String = If(supplied, "(unnamed)")
Console.WriteLine($"{grade} / {label}")
End Sub
End Moduleint score = 72;
string grade = score >= 60 ? "pass" : "fail";
string? supplied = null;
string label = supplied ?? "(unnamed)";
Console.WriteLine($"{grade} / {label}");Three-argument
If(condition, whenTrue, whenFalse) becomes the conditional operator condition ? whenTrue : whenFalse — read the ? as "then" and the : as "otherwise". Two-argument If(value, fallback) becomes ??. Both C# forms short-circuit exactly as the If() function does, which is what made IIf() — the VB6 version that evaluated everything — worth abandoning.Strings
Building a string in a loop
The same .NET class, reached the same way — the only translation needed is the syntax around it.
Option Strict On
Imports System
Imports System.Text
Module BuilderDemo
Sub Main()
Dim builder As New StringBuilder()
For number As Integer = 1 To 5
builder.Append(number)
If number < 5 Then builder.Append(", ")
Next
Console.WriteLine(builder.ToString())
End Sub
End Modulevar builder = new StringBuilder();
for (int number = 1; number <= 5; number++)
{
builder.Append(number);
if (number < 5) builder.Append(", ");
}
Console.WriteLine(builder.ToString());Dim builder As New StringBuilder() and var builder = new StringBuilder() allocate the identical object. Note that C# has no As New shorthand: new is always an expression on the right of the =. The reason to use StringBuilder is the same in both languages — strings are immutable, so text = text & more inside a loop allocates a fresh string every pass.Strings that contain quotes and backslashes
This is where a copied Visual Basic string literal is most likely to change meaning, because C# reads the backslash.
Option Strict On
Imports System
Module QuotingDemo
Sub Main()
' A doubled quote escapes a quote
Dim quoted As String = "She said ""hello""."
' Backslashes are ordinary characters
Dim path As String = "C:\reports\2026\summary.txt"
' A multi-line string needs explicit newlines
Dim block As String = "line one" & Environment.NewLine & "line two"
Console.WriteLine(quoted)
Console.WriteLine(path)
Console.WriteLine(block)
End Sub
End Module// A backslash escapes, so a quote is \" and a backslash is \\
string quoted = "She said \"hello\".";
// @ turns escaping off — a verbatim string
string path = @"C:\reports\2026\summary.txt";
// Three or more quotes open a raw string, newlines and all
string block = """
line one
line two
""";
Console.WriteLine(quoted);
Console.WriteLine(path);
Console.WriteLine(block);In Visual Basic a string literal has exactly one escape, the doubled quote, and a backslash is just a backslash — which is why Windows paths are so comfortable there. C# treats
\ as an escape character, so "C:\reports" either means something else or fails to compile. Two forms rescue you: @"..." switches escaping off, and the raw literal """...""" switches it off and spans lines, stripping the common indentation of the closing delimiter. Both accept a $ prefix for interpolation.Common string operations
Every method here is the same .NET method — the interesting part is the last line of each column.
Option Strict On
Imports System
Module StringMethodDemo
Sub Main()
Dim text As String = " Visual Basic "
Console.WriteLine($"[{text.Trim()}]")
Console.WriteLine(text.Trim().ToUpper())
Console.WriteLine(text.Contains("Basic"))
Console.WriteLine(text.Trim().Replace(" ", "-"))
Console.WriteLine(text.Trim().Substring(0, 6))
Console.WriteLine(Mid(text.Trim(), 1, 6))
End Sub
End Modulestring text = " Visual Basic ";
Console.WriteLine($"[{text.Trim()}]");
Console.WriteLine(text.Trim().ToUpper());
Console.WriteLine(text.Contains("Basic"));
Console.WriteLine(text.Trim().Replace(" ", "-"));
Console.WriteLine(text.Trim().Substring(0, 6));
Console.WriteLine(text.Trim()[..6]);Because both languages call into
System.String, Trim, ToUpper, Contains, Replace and Substring need no translation at all. What has no C# counterpart is the second family Visual Basic keeps for compatibility — Mid, Left, Right, Len, InStr, UCase. Those come from Microsoft.VisualBasic and are 1-based, which is exactly why they are worth dropping: Mid(text, 1, 6) and text.Substring(0, 6) return the same thing from different starting points. C# adds a range operator, [..6], meaning "from the start up to index 6".Comparing strings
These two columns agree today — and would disagree the moment someone adds one line to the Visual Basic file.
Option Strict On
Imports System
Module ComparisonDemo
Sub Main()
Dim left As String = "Report"
Dim right As String = "report"
Console.WriteLine(left = right)
Console.WriteLine(String.Equals(left, right, StringComparison.OrdinalIgnoreCase))
Console.WriteLine(String.Compare(left, right, StringComparison.Ordinal))
End Sub
End Modulestring left = "Report";
string right = "report";
Console.WriteLine(left == right);
Console.WriteLine(string.Equals(left, right, StringComparison.OrdinalIgnoreCase));
Console.WriteLine(string.Compare(left, right, StringComparison.Ordinal));By default both compare character by character, so
"Report" and "report" are different. The difference is that Visual Basic has a file-level switch, Option Compare Text, which silently makes every = on strings in that file case-insensitive. C# has nothing of the kind and never will: == on strings is always an ordinal value comparison. If you rely on Option Compare Text anywhere, that behavior has to be written out explicitly with StringComparison.OrdinalIgnoreCase when you port the code.Collections
Arrays
Two changes at once here: the brackets, and the number inside them.
Option Strict On
Imports System
Module ArrayDemo
Sub Main()
Dim names() As String = {"Ada", "Grace", "Alan"}
' The number in Dim is the UPPER BOUND, not the length
Dim scores(4) As Integer
scores(0) = 10
scores(4) = 50
Console.WriteLine(names.Length)
Console.WriteLine(scores.Length)
Console.WriteLine(names(1))
End Sub
End Modulestring[] names = { "Ada", "Grace", "Alan" };
// The number in new[] is the LENGTH
int[] scores = new int[5];
scores[0] = 10;
scores[4] = 50;
Console.WriteLine(names.Length);
Console.WriteLine(scores.Length);
Console.WriteLine(names[1]);Indexing moves from parentheses to square brackets, which also removes a genuine ambiguity — in Visual Basic
names(1) could be an array element or a method call, and only the compiler knows which. The number is the real trap: Dim scores(4) declares five slots because it names the highest index, while new int[5] names the count. Both are zero-based and both have .Length, so Dim x(n) becomes new[n + 1]. VB6 programmers should also note that Option Base 1 does not exist in .NET at all.Lists
The generic type argument changes shape; everything else is the same class with the same methods.
Option Strict On
Imports System
Imports System.Collections.Generic
Module ListDemo
Sub Main()
Dim fruits As New List(Of String) From {"apple", "banana"}
fruits.Add("cherry")
fruits.Insert(0, "apricot")
fruits.Remove("banana")
For Each fruit As String In fruits
Console.WriteLine(fruit)
Next
Console.WriteLine(fruits.Count)
End Sub
End Modulevar fruits = new List<string> { "apple", "banana" };
fruits.Add("cherry");
fruits.Insert(0, "apricot");
fruits.Remove("banana");
foreach (string fruit in fruits)
{
Console.WriteLine(fruit);
}
Console.WriteLine(fruits.Count);List(Of String) becomes List<string> — angle brackets instead of (Of ...), and this holds for every generic type: Dictionary(Of K, V) → Dictionary<K, V>, IEnumerable(Of T) → IEnumerable<T>. The From { ... } initializer loses the From. If you are used to VB6's Collection object, note it is 1-based and untyped; List<T> is neither.Dictionaries
The same
Dictionary, with C# offering an extra initializer shape and a tidier TryGetValue.Option Strict On
Imports System
Imports System.Collections.Generic
Module DictionaryDemo
Sub Main()
Dim ages As New Dictionary(Of String, Integer) From {
{"Ada", 36},
{"Grace", 45}
}
ages("Alan") = 41
For Each entry As KeyValuePair(Of String, Integer) In ages
Console.WriteLine($"{entry.Key} is {entry.Value}")
Next
Dim found As Integer
If ages.TryGetValue("Ada", found) Then
Console.WriteLine($"Ada: {found}")
End If
End Sub
End Modulevar ages = new Dictionary<string, int>
{
["Ada"] = 36,
["Grace"] = 45,
};
ages["Alan"] = 41;
foreach (var entry in ages)
{
Console.WriteLine($"{entry.Key} is {entry.Value}");
}
if (ages.TryGetValue("Ada", out int found))
{
Console.WriteLine($"Ada: {found}");
}Visual Basic's nested-brace initializer
{{key, value}, ...} has a C# equivalent, but the indexer form ["Ada"] = 36 shown here reads better and is what most C# code uses. The real convenience is out int found: C# lets you declare the output variable inside the call, so the two-line dance of declaring it first disappears. Note var entry where Visual Basic spelled out KeyValuePair(Of String, Integer) — the type is inferred, not dynamic.Grids and jagged arrays
Both languages distinguish a rectangular grid from an array of arrays — and both make the declaration syntax harder than it needs to be.
Option Strict On
Imports System
Module GridDemo
Sub Main()
Dim grid(,) As Integer = {{1, 2, 3}, {4, 5, 6}}
Dim jagged()() As Integer = New Integer(1)() {}
jagged(0) = New Integer() {1, 2}
jagged(1) = New Integer() {3, 4, 5}
Console.WriteLine(grid(1, 2))
Console.WriteLine(jagged(1).Length)
End Sub
End Moduleint[,] grid = { { 1, 2, 3 }, { 4, 5, 6 } };
int[][] jagged = new int[2][];
jagged[0] = new int[] { 1, 2 };
jagged[1] = new int[] { 3, 4, 5 };
Console.WriteLine(grid[1, 2]);
Console.WriteLine(jagged[1].Length);A rectangular array is
Dim grid(,) As Integer in one language and int[,] grid in the other; both index with a single pair of brackets holding two numbers. A jagged array — rows of independent length — is ()() and [][], indexed one bracket at a time. The C# form reads left to right (int[][] is "array of int arrays"), whereas the Visual Basic form puts the element type after the parentheses, which is why it looks backwards at first.Control Flow
Select Case becomes switch
The shape survives, but C# makes you say where each branch ends.
Option Strict On
Imports System
Module SelectDemo
Sub Main()
Dim code As Integer = 3
Select Case code
Case 1
Console.WriteLine("one")
Case 2, 3
Console.WriteLine("two or three")
Case 4 To 6
Console.WriteLine("four to six")
Case Is > 100
Console.WriteLine("large")
Case Else
Console.WriteLine("something else")
End Select
End Sub
End Moduleint code = 3;
switch (code)
{
case 1:
Console.WriteLine("one");
break;
case 2:
case 3:
Console.WriteLine("two or three");
break;
case >= 4 and <= 6:
Console.WriteLine("four to six");
break;
case > 100:
Console.WriteLine("large");
break;
default:
Console.WriteLine("something else");
break;
}Every C#
case body must end with break (or return, or goto case) — C# forbids silently falling through to the next branch, so the compiler rejects a missing break rather than doing what Visual Basic already does automatically. Sharing one body between two values is done by stacking bare labels, which is the one legal kind of fall-through. Case Is > 100 becomes a relational pattern case > 100, and Case 4 To 6 becomes case >= 4 and <= 6.A switch that produces a value
C# has a second form of switch that is an expression, so it can sit on the right of an assignment.
Option Strict On
Imports System
Module SwitchValueDemo
Function Describe(code As Integer) As String
Select Case code
Case 1 : Return "one"
Case 2, 3 : Return "a couple"
Case Else : Return "many"
End Select
End Function
Sub Main()
Console.WriteLine(Describe(1))
Console.WriteLine(Describe(3))
Console.WriteLine(Describe(9))
End Sub
End ModuleConsole.WriteLine(Describe(1));
Console.WriteLine(Describe(3));
Console.WriteLine(Describe(9));
string Describe(int code) => code switch
{
1 => "one",
2 or 3 => "a couple",
_ => "many",
};The switch expression puts the value first and the keyword second (
code switch), replaces case/:/break with => and a comma, and uses _ for the default arm. Because it is an expression it always produces a value, and the compiler warns when the arms do not cover every possibility. Visual Basic has no equivalent — a Select Case is always a statement, which is why the anchor column needs a whole Function to do the same job.Conditions must be Boolean
If you keep
Option Strict On, this rule already applies to you — and C# applies it whether you like it or not.Option Strict On
Imports System
Module ConditionDemo
Sub Main()
Dim count As Integer = 0
Dim name As String = Nothing
' Option Strict On already forbids If(count),
' so both languages want the comparison spelled out
If count = 0 Then Console.WriteLine("empty")
If name Is Nothing Then Console.WriteLine("no name")
If String.IsNullOrEmpty(name) Then Console.WriteLine("blank")
End Sub
End Moduleint count = 0;
string? name = null;
if (count == 0) Console.WriteLine("empty");
if (name is null) Console.WriteLine("no name");
if (string.IsNullOrEmpty(name)) Console.WriteLine("blank");A C# condition must be a
bool. There is no implicit "zero is false, non-zero is true" and no implicit "empty string is false"; if (count) does not compile. The habit worth carrying over is spelling out what you actually mean — count == 0, name is null, string.IsNullOrEmpty(name). Note is null rather than == null: both work, but is cannot be redefined by an overloaded operator, so it is the safer of the two.Loops
For ... Next
The most-changed construct on the page: a declarative range becomes three explicit clauses.
Option Strict On
Imports System
Module ForDemo
Sub Main()
For index As Integer = 1 To 5
Console.Write(index & " ")
Next
Console.WriteLine()
For countdown As Integer = 10 To 0 Step -2
Console.Write(countdown & " ")
Next
Console.WriteLine()
End Sub
End Modulefor (int index = 1; index <= 5; index++)
{
Console.Write(index + " ");
}
Console.WriteLine();
for (int countdown = 10; countdown >= 0; countdown -= 2)
{
Console.Write(countdown + " ");
}
Console.WriteLine();C#'s
for is "initializer; condition; step", separated by semicolons — you state the ending condition rather than an ending value, which is why the inclusive To 5 becomes <= 5. Getting < and <= the wrong way round is the classic off-by-one here, and it is worth pausing on every time. Step -2 becomes the step clause countdown -= 2, and the loop variable is scoped to the loop exactly as index As Integer is.For Each
The straightforward half is one word; the second half shows the C# idiom for "I also want the position".
Option Strict On
Imports System
Imports System.Collections.Generic
Module ForEachDemo
Sub Main()
Dim words As New List(Of String) From {"alpha", "beta", "gamma"}
For Each word As String In words
Console.WriteLine(word.ToUpper())
Next
For index As Integer = 0 To words.Count - 1
Console.WriteLine($"{index}: {words(index)}")
Next
End Sub
End Modulevar words = new List<string> { "alpha", "beta", "gamma" };
foreach (string word in words)
{
Console.WriteLine(word.ToUpper());
}
foreach ((string word, int index) in words.Select((value, position) => (value, position)))
{
Console.WriteLine($"{index}: {word}");
}For Each x As T In items ... Next becomes foreach (T x in items) { ... } — one word, no Next. Neither language has a built-in index in its for-each, but C# can pair value and position with Select((value, position) => (value, position)) and unpack both in the loop header. The plain counted loop shown in the anchor column is still perfectly good C# too; this is an option, not a replacement.While and Do loops
Visual Basic offers four spellings of the bottom-tested loop; C# offers one, and it only tests for truth.
Option Strict On
Imports System
Module WhileDemo
Sub Main()
Dim remaining As Integer = 3
While remaining > 0
Console.WriteLine($"remaining {remaining}")
remaining -= 1
End While
Dim attempt As Integer = 0
Do
attempt += 1
Console.WriteLine($"attempt {attempt}")
Loop Until attempt >= 2
End Sub
End Moduleint remaining = 3;
while (remaining > 0)
{
Console.WriteLine($"remaining {remaining}");
remaining -= 1;
}
int attempt = 0;
do
{
attempt += 1;
Console.WriteLine($"attempt {attempt}");
}
while (attempt < 2);While ... End While becomes while (...) { }. The Do ... Loop family collapses: Do While c, Do Until c, Loop While c and Loop Until c all become either while or do ... while, and since C# has no Until, the condition has to be inverted — Loop Until attempt >= 2 becomes while (attempt < 2). Note the semicolon after the closing while of a do loop; it is the one place a brace is followed by one.Leaving a loop early
Two keywords that lose the word naming which construct they are leaving.
Option Strict On
Imports System
Module ExitDemo
Sub Main()
For number As Integer = 1 To 10
If number Mod 2 = 0 Then Continue For
If number > 7 Then Exit For
Console.WriteLine(number)
Next
End Sub
End Modulefor (int number = 1; number <= 10; number++)
{
if (number % 2 == 0) continue;
if (number > 7) break;
Console.WriteLine(number);
}Exit For, Exit While and Exit Do all become plain break; Continue For and friends become plain continue. Both always act on the innermost enclosing loop, which is a real loss of clarity in nested loops — Visual Basic at least named the construct. C# has goto with a label for the rare case where you need to leave two levels at once, though extracting the loop into a method and using return is the usual answer. Exit Sub and Exit Function both become return.Methods
Sub and Function both become methods
The
Sub/Function distinction disappears — a method that returns nothing simply returns void.Option Strict On
Imports System
Module MethodDemo
Sub Announce(message As String)
Console.WriteLine($"** {message} **")
End Sub
Function Add(left As Integer, right As Integer) As Integer
Return left + right
End Function
Sub Main()
Announce("starting")
Console.WriteLine(Add(2, 3))
End Sub
End ModuleAnnounce("starting");
Console.WriteLine(Add(2, 3));
void Announce(string message)
{
Console.WriteLine($"** {message} **");
}
int Add(int left, int right)
{
return left + right;
}The return type moves to the front, replacing both keywords:
Sub becomes void and Function ... As Integer becomes int. Return becomes return and is required — C# has nothing like assigning to the function's own name, the VB6 style that Function Add ... Add = 5 allowed. Calling is simpler too: parentheses are always required and always allowed, so the old rule about omitting them when calling a Sub is gone.ByRef, ref and out
C# splits
ByRef into two keywords, and makes the caller say which one it agreed to.Option Strict On
Imports System
Module ReferenceDemo
Sub Double_(ByRef value As Integer)
value *= 2
End Sub
Function TryHalve(input As Integer, ByRef result As Integer) As Boolean
If input Mod 2 <> 0 Then Return False
result = input \ 2
Return True
End Function
Sub Main()
Dim number As Integer = 21
Double_(number)
Console.WriteLine(number)
Dim half As Integer
If TryHalve(10, half) Then Console.WriteLine(half)
End Sub
End Moduleint number = 21;
Double_(ref number);
Console.WriteLine(number);
if (TryHalve(10, out int half)) Console.WriteLine(half);
void Double_(ref int value)
{
value *= 2;
}
bool TryHalve(int input, out int result)
{
result = 0;
if (input % 2 != 0) return false;
result = input / 2;
return true;
}ByRef becomes ref when the value goes in and comes back, and out when it is purely an output — and out obliges the method to assign it on every path, which is why TryHalve sets result = 0 first. The bigger change is at the call site: C# requires ref or out on the argument too, so you can never be surprised by a method that reassigns your variable. ByVal has no keyword because it is the only default.Optional and named arguments
Both features exist in both languages; only two characters change.
Option Strict On
Imports System
Module ArgumentDemo
Function Greet(name As String,
Optional greeting As String = "Hello",
Optional punctuation As String = "!") As String
Return $"{greeting}, {name}{punctuation}"
End Function
Sub Main()
Console.WriteLine(Greet("Ada"))
Console.WriteLine(Greet("Grace", "Welcome"))
Console.WriteLine(Greet("Alan", punctuation:="."))
End Sub
End ModuleConsole.WriteLine(Greet("Ada"));
Console.WriteLine(Greet("Grace", "Welcome"));
Console.WriteLine(Greet("Alan", punctuation: "."));
string Greet(string name,
string greeting = "Hello",
string punctuation = "!")
=> $"{greeting}, {name}{punctuation}";The
Optional keyword disappears — a default value is enough to make a parameter optional, and optional parameters must still come last. A named argument uses name: with a space rather than name:=. Note the method body here: => introduces an expression-bodied member, a one-line method with no braces and no return, which reads much like Visual Basic's single-line Function shorthand.A variable number of arguments
A direct rename, with the same rules on both sides.
Option Strict On
Imports System
Module ParamArrayDemo
Function SumAll(ParamArray values() As Integer) As Integer
Dim total As Integer = 0
For Each value As Integer In values
total += value
Next
Return total
End Function
Sub Main()
Console.WriteLine(SumAll(1, 2, 3))
Console.WriteLine(SumAll())
Console.WriteLine(SumAll(New Integer() {4, 5}))
End Sub
End ModuleConsole.WriteLine(SumAll(1, 2, 3));
Console.WriteLine(SumAll());
Console.WriteLine(SumAll(new int[] { 4, 5 }));
int SumAll(params int[] values)
{
int total = 0;
foreach (int value in values)
{
total += value;
}
return total;
}ParamArray values() As Integer becomes params int[] values. In both languages it must be the last parameter, there can be only one, and the caller may pass either loose arguments or a ready-made array. This is what Console.WriteLine's many overloads and String.Format have always used underneath.Classes & Objects
A class and its constructor
The constructor loses its special name and takes the name of the class instead.
Option Strict On
Imports System
Public Class Person
Private ReadOnly _name As String
Private ReadOnly _age As Integer
Public Sub New(name As String, age As Integer)
_name = name
_age = age
End Sub
Public Function Describe() As String
Return $"{_name}, age {_age}"
End Function
End Class
Module ClassDemo
Sub Main()
Dim person As New Person("Ada", 36)
Console.WriteLine(person.Describe())
End Sub
End Modulevar person = new Person("Ada", 36);
Console.WriteLine(person.Describe());
class Person
{
private readonly string _name;
private readonly int _age;
public Person(string name, int age)
{
_name = name;
_age = age;
}
public string Describe() => $"{_name}, age {_age}";
}Public Sub New(...) becomes a method with no return type whose name is exactly the class name. Me becomes this where you need it. Notice the C# class sits below the executable statements: in a file using top-level statements, type declarations must come after them. Access modifiers are the same words in lower case, and a C# class member with no modifier at all is private, whereas an unmarked Visual Basic member is Public — so leaving the keyword off changes the meaning.Properties
Properties are a Visual Basic invention that C# adopted, so the concept transfers whole — only the punctuation moves.
Option Strict On
Imports System
Public Class Temperature
' Auto-implemented property
Public Property Label As String = "reading"
Private _celsius As Double
Public Property Celsius As Double
Get
Return _celsius
End Get
Set(value As Double)
_celsius = Math.Max(value, -273.15)
End Set
End Property
Public ReadOnly Property Fahrenheit As Double
Get
Return _celsius * 9.0 / 5.0 + 32
End Get
End Property
End Class
Module PropertyDemo
Sub Main()
Dim reading As New Temperature With {.Celsius = 100.0}
Console.WriteLine($"{reading.Label}: {reading.Fahrenheit}")
reading.Celsius = -500
Console.WriteLine(reading.Celsius)
End Sub
End Modulevar reading = new Temperature { Celsius = 100.0 };
Console.WriteLine($"{reading.Label}: {reading.Fahrenheit}");
reading.Celsius = -500;
Console.WriteLine(reading.Celsius);
class Temperature
{
// Auto-implemented property
public string Label { get; set; } = "reading";
private double _celsius;
public double Celsius
{
get => _celsius;
set => _celsius = Math.Max(value, -273.15);
}
public double Fahrenheit => _celsius * 9.0 / 5.0 + 32;
}Public Property Label As String becomes public string Label { get; set; }, and the full Get/Set blocks become get and set accessors, which can be one-liners with =>. The implicit value parameter exists in both. A ReadOnly Property with only a Get collapses all the way to a single expression-bodied member, as Fahrenheit shows. The object initializer With {.Celsius = 100.0} becomes { Celsius = 100.0 } — no With, no leading dots.Shared becomes static
One keyword swap — and the disappearance of the construct you have been using instead.
Option Strict On
Imports System
Public Class Counter
Private Shared _total As Integer = 0
Public Shared Sub Increment()
_total += 1
End Sub
Public Shared ReadOnly Property Total As Integer
Get
Return _total
End Get
End Property
End Class
Module SharedDemo
Sub Main()
Counter.Increment()
Counter.Increment()
Console.WriteLine(Counter.Total)
End Sub
End ModuleCounter.Increment();
Counter.Increment();
Console.WriteLine(Counter.Total);
class Counter
{
private static int _total = 0;
public static void Increment() => _total += 1;
public static int Total => _total;
}Shared becomes static, with the same meaning: one copy on the type rather than one per instance. What has no direct counterpart is the Module itself, which is really a class where everything is Shared and whose members can be called without naming it. The C# equivalent is a static class, but its members always need the type name in front — Counter.Total, never a bare Total. A using static Counter; directive restores the bare form when you want it.Inheritance and overriding
Four Visual Basic keywords are replaced here, and one of them is a colon.
Option Strict On
Imports System
Public Class Animal
Protected ReadOnly Name As String
Public Sub New(name As String)
Me.Name = name
End Sub
Public Overridable Function Speak() As String
Return $"{Name} makes a sound"
End Function
End Class
Public Class Dog
Inherits Animal
Public Sub New(name As String)
MyBase.New(name)
End Sub
Public Overrides Function Speak() As String
Return $"{Name} barks"
End Function
End Class
Module InheritanceDemo
Sub Main()
Dim pets As Animal() = {New Animal("Thing"), New Dog("Rex")}
For Each pet As Animal In pets
Console.WriteLine(pet.Speak())
Next
End Sub
End ModuleAnimal[] pets = { new Animal("Thing"), new Dog("Rex") };
foreach (Animal pet in pets)
{
Console.WriteLine(pet.Speak());
}
class Animal
{
protected readonly string Name;
public Animal(string name) => Name = name;
public virtual string Speak() => $"{Name} makes a sound";
}
class Dog : Animal
{
public Dog(string name) : base(name) { }
public override string Speak() => $"{Name} barks";
}Inherits Animal becomes : Animal on the class header — the same colon C# uses for interfaces, so a class list reads "base class first, then interfaces". Overridable becomes virtual, Overrides becomes override, and MyBase becomes base. Calling the base constructor moves from a statement inside the body (MyBase.New(name)) to a clause on the header (: base(name)). Both languages agree on the important part: a method is not overridable unless it says so.MustInherit and NotInheritable
Two of the longest keywords in Visual Basic become two of the shortest in C#.
Option Strict On
Imports System
Public MustInherit Class Shape
Public MustOverride Function Area() As Double
Public Function Report() As String
Return $"{Me.GetType().Name}: {Area():F2}"
End Function
End Class
Public NotInheritable Class Circle
Inherits Shape
Private ReadOnly _radius As Double
Public Sub New(radius As Double)
_radius = radius
End Sub
Public Overrides Function Area() As Double
Return Math.PI * _radius * _radius
End Function
End Class
Module AbstractDemo
Sub Main()
Dim shape As Shape = New Circle(2.0)
Console.WriteLine(shape.Report())
End Sub
End ModuleShape shape = new Circle(2.0);
Console.WriteLine(shape.Report());
abstract class Shape
{
public abstract double Area();
public string Report() => $"{GetType().Name}: {Area():F2}";
}
sealed class Circle : Shape
{
private readonly double _radius;
public Circle(double radius) => _radius = radius;
public override double Area() => Math.PI * _radius * _radius;
}MustInherit becomes abstract on the class, MustOverride becomes abstract on the member, and NotInheritable becomes sealed. NotOverridable — sealing a single override so no further subclass can change it — is also sealed, written as sealed override. C# reuses one word where Visual Basic used four, which is terser but means you read the position to know which meaning applies.Interfaces
This is one place where Visual Basic is genuinely more flexible than C#, and the flexibility does not survive the trip.
Option Strict On
Imports System
Public Interface IGreeter
Function Greet(name As String) As String
End Interface
Public Class Formal
Implements IGreeter
' Each member names the interface member it satisfies
Public Function SayHello(name As String) As String Implements IGreeter.Greet
Return $"Good day, {name}."
End Function
End Class
Module InterfaceDemo
Sub Main()
Dim greeter As IGreeter = New Formal()
Console.WriteLine(greeter.Greet("Ada"))
End Sub
End ModuleIGreeter greeter = new Formal();
Console.WriteLine(greeter.Greet("Ada"));
interface IGreeter
{
string Greet(string name);
}
class Formal : IGreeter
{
// The name must match the interface member
public string Greet(string name) => $"Good day, {name}.";
}Visual Basic attaches
Implements IGreeter.Greet to each member individually, so the method may be called anything you like — SayHello here satisfies Greet. C# matches by signature: a public method with the right name and parameters implements the interface member automatically, and there is no clause to say otherwise. When two interfaces demand the same signature and you need different bodies, C# offers explicit implementation — string IGreeter.Greet(string name) — which is the closest equivalent, and such a member is callable only through the interface.Structures, Records & Tuples
Structure becomes struct
Both languages have a value type that copies on assignment; the keyword shortens and gains a useful modifier.
Option Strict On
Imports System
Public Structure Point
Public ReadOnly X As Integer
Public ReadOnly Y As Integer
Public Sub New(x As Integer, y As Integer)
Me.X = x
Me.Y = y
End Sub
Public Overrides Function ToString() As String
Return $"({X}, {Y})"
End Function
End Structure
Module StructureDemo
Sub Main()
Dim origin As New Point(0, 0)
Dim copy As Point = origin
Console.WriteLine($"{origin} {copy}")
Console.WriteLine(origin.Equals(copy))
End Sub
End Modulevar origin = new Point(0, 0);
Point copy = origin;
Console.WriteLine($"{origin} {copy}");
Console.WriteLine(origin.Equals(copy));
readonly struct Point
{
public readonly int X;
public readonly int Y;
public Point(int x, int y)
{
X = x;
Y = y;
}
public override string ToString() => $"({X}, {Y})";
}Structure ... End Structure becomes struct { }, with the same semantics — assignment copies the whole value rather than sharing a reference, and equality compares fields. C# adds readonly struct, which promises the compiler that no member mutates the value and lets it skip defensive copies. Overrides becomes override here as with any inherited method, and Me.X becomes plain X where there is no name collision.Records — a class in one line
The whole anchor column is what a C#
record writes for you from a single line.Option Strict On
Imports System
Public Class Money
Public ReadOnly Property Amount As Decimal
Public ReadOnly Property Currency As String
Public Sub New(amount As Decimal, currency As String)
Me.Amount = amount
Me.Currency = currency
End Sub
Public Overrides Function Equals(other As Object) As Boolean
Dim candidate = TryCast(other, Money)
Return candidate IsNot Nothing AndAlso
candidate.Amount = Amount AndAlso
candidate.Currency = Currency
End Function
Public Overrides Function GetHashCode() As Integer
Return HashCode.Combine(Amount, Currency)
End Function
Public Overrides Function ToString() As String
Return $"Money {{ Amount = {Amount}, Currency = {Currency} }}"
End Function
End Class
Module RecordDemo
Sub Main()
Dim price As New Money(9.99D, "USD")
Dim same As New Money(9.99D, "USD")
Console.WriteLine(price)
Console.WriteLine(price.Equals(same))
End Sub
End Modulevar price = new Money(9.99m, "USD");
var same = new Money(9.99m, "USD");
Console.WriteLine(price);
Console.WriteLine(price == same);
var cheaper = price with { Amount = 4.99m };
Console.WriteLine(cheaper);
record Money(decimal Amount, string Currency);A
record declares the properties, the constructor, value-based Equals, GetHashCode, ==, and a readable ToString in one line. It also gets a with expression, which copies the value and changes named parts — an idea Visual Basic has no equivalent for, and unrelated to the With block. Visual Basic cannot declare a record; when you need this shape there, you write it out, which is what the anchor column shows. This is one of the clearest reasons a Visual Basic codebase gains from moving.Tuples and returning two things
Named tuples work the same way in both languages — what C# adds is a way to unpack one in a single line.
Option Strict On
Imports System
Module TupleDemo
Function MinimumAndMaximum(values() As Integer) As (Smallest As Integer, Largest As Integer)
Dim smallest As Integer = values(0)
Dim largest As Integer = values(0)
For Each value As Integer In values
If value < smallest Then smallest = value
If value > largest Then largest = value
Next
Return (smallest, largest)
End Function
Sub Main()
Dim result = MinimumAndMaximum(New Integer() {4, 9, 1, 7})
Console.WriteLine($"{result.Smallest}..{result.Largest}")
Dim low As Integer, high As Integer
Dim pair = MinimumAndMaximum(New Integer() {3, 8})
low = pair.Smallest
high = pair.Largest
Console.WriteLine($"{low}..{high}")
End Sub
End Modulevar result = MinimumAndMaximum(new[] { 4, 9, 1, 7 });
Console.WriteLine($"{result.Smallest}..{result.Largest}");
var (low, high) = MinimumAndMaximum(new[] { 3, 8 });
Console.WriteLine($"{low}..{high}");
(int Smallest, int Largest) MinimumAndMaximum(int[] values)
{
int smallest = values[0];
int largest = values[0];
foreach (int value in values)
{
if (value < smallest) smallest = value;
if (value > largest) largest = value;
}
return (smallest, largest);
}The tuple type
(Smallest As Integer, Largest As Integer) becomes (int Smallest, int Largest), and both let you name the parts so the caller does not have to remember an order. Deconstruction is C#-only: var (low, high) = ... pulls the parts into separate variables in one statement, which is what makes returning a tuple pleasant rather than merely possible. Visual Basic has to name the tuple and read its fields, as the anchor column does.Pattern Matching
Testing a type and using it
The two-step "test the type, then convert it" collapses into one step that also declares the variable.
Option Strict On
Imports System
Module TypePatternDemo
Sub Describe(value As Object)
If TypeOf value Is Integer Then
Dim number = CInt(value)
Console.WriteLine($"integer {number * 2}")
ElseIf TypeOf value Is String Then
Dim text = CStr(value)
Console.WriteLine($"string of {text.Length}")
Else
Console.WriteLine("something else")
End If
End Sub
Sub Main()
Describe(21)
Describe("hello")
Describe(3.5)
End Sub
End ModuleDescribe(21);
Describe("hello");
Describe(3.5);
void Describe(object value)
{
if (value is int number)
{
Console.WriteLine($"integer {number * 2}");
}
else if (value is string text)
{
Console.WriteLine($"string of {text.Length}");
}
else
{
Console.WriteLine("something else");
}
}TypeOf value Is Integer becomes value is int, and adding a name — value is int number — both tests and converts in one go, with number in scope for the rest of the branch. That name is why the cast on the next line vanishes. Note that C# is is a type test, whereas Visual Basic's bare Is compares references; the gotchas section returns to this, because it is the one keyword whose meaning genuinely changes.Matching shapes, not just values
The nested ladder in the anchor column is what a C# switch expression flattens into a table.
Option Strict On
Imports System
Module ShapePatternDemo
Function Classify(value As Object) As String
If TypeOf value Is Integer Then
Dim number = CInt(value)
If number < 0 Then Return "negative"
If number = 0 Then Return "zero"
Return "positive"
End If
If TypeOf value Is String Then
Dim text = CStr(value)
If text.Length = 0 Then Return "empty text"
Return "text"
End If
If value Is Nothing Then Return "nothing"
Return "unknown"
End Function
Sub Main()
Console.WriteLine(Classify(-4))
Console.WriteLine(Classify(0))
Console.WriteLine(Classify(""))
Console.WriteLine(Classify(Nothing))
End Sub
End ModuleConsole.WriteLine(Classify(-4));
Console.WriteLine(Classify(0));
Console.WriteLine(Classify(""));
Console.WriteLine(Classify(null));
string Classify(object? value) => value switch
{
int and < 0 => "negative",
0 => "zero",
int => "positive",
string { Length: 0 } => "empty text",
string => "text",
null => "nothing",
_ => "unknown",
};Patterns compose:
int and < 0 combines a type pattern with a relational one, and string { Length: 0 } is a property pattern that matches a string whose Length is zero. Arms are tried top to bottom, so the narrow cases come first. Visual Basic has no pattern matching at all — its Select Case compares values only — so any code shaped like this is a candidate for real simplification when it moves.Matching on an object's parts
A decision table written as conditions, and the same table written as patterns.
Option Strict On
Imports System
Public Class Order
Public Property Total As Decimal
Public Property Country As String = ""
End Class
Module PropertyPatternDemo
Function Shipping(order As Order) As Decimal
If order.Country = "US" AndAlso order.Total > 100D Then Return 0D
If order.Country = "US" Then Return 5D
If order.Total > 100D Then Return 15D
Return 25D
End Function
Sub Main()
Console.WriteLine(Shipping(New Order With {.Total = 150D, .Country = "US"}))
Console.WriteLine(Shipping(New Order With {.Total = 50D, .Country = "US"}))
Console.WriteLine(Shipping(New Order With {.Total = 150D, .Country = "FR"}))
Console.WriteLine(Shipping(New Order With {.Total = 50D, .Country = "FR"}))
End Sub
End ModuleConsole.WriteLine(Shipping(new Order { Total = 150m, Country = "US" }));
Console.WriteLine(Shipping(new Order { Total = 50m, Country = "US" }));
Console.WriteLine(Shipping(new Order { Total = 150m, Country = "FR" }));
Console.WriteLine(Shipping(new Order { Total = 50m, Country = "FR" }));
decimal Shipping(Order order) => order switch
{
{ Country: "US", Total: > 100 } => 0m,
{ Country: "US" } => 5m,
{ Total: > 100 } => 15m,
_ => 25m,
};
class Order
{
public decimal Total { get; set; }
public string Country { get; set; } = "";
}A property pattern names members inside braces —
{ Country: "US", Total: > 100 } — and each part may itself be any pattern, including another property pattern for a nested object. Compared with the chain of If statements it replaces, the win is that the conditions line up in a column, so a missing combination is visible rather than buried. The compiler also warns if an arm is unreachable because an earlier one already covers it.Error Handling
Try, Catch, Finally
Structured error handling is the part of Visual Basic that needs the least translation.
Option Strict On
Imports System
Module TryDemo
Sub Main()
Try
Dim numbers() As Integer = {1, 2, 3}
Console.WriteLine(numbers(10))
Catch error_ As IndexOutOfRangeException
Console.WriteLine($"Out of range: {error_.Message}")
Catch error_ As Exception
Console.WriteLine($"Something else: {error_.Message}")
Finally
Console.WriteLine("always runs")
End Try
End Sub
End Moduletry
{
int[] numbers = { 1, 2, 3 };
Console.WriteLine(numbers[10]);
}
catch (IndexOutOfRangeException error)
{
Console.WriteLine($"Out of range: {error.Message}");
}
catch (Exception error)
{
Console.WriteLine($"Something else: {error.Message}");
}
finally
{
Console.WriteLine("always runs");
}Catch name As TypeName becomes catch (TypeName name) — the same reordering as a variable declaration. Order still matters: the most specific exception type first, or the broad Exception catch swallows everything below it. Finally runs on every path in both. What does not come across is On Error Resume Next and On Error GoTo: C# has no unstructured error handling at all, and VB6 or VBA code built on it has to be rewritten as try blocks rather than translated.Catching only some of them
Another feature Visual Basic had first and C# borrowed — the keyword is even the same.
Option Strict On
Imports System
Module FilterDemo
Sub Attempt(code As Integer)
Try
Throw New InvalidOperationException($"failure {code}")
Catch error_ As InvalidOperationException When code = 1
Console.WriteLine("handled the code-1 case")
Catch error_ As InvalidOperationException
Console.WriteLine($"passed through: {error_.Message}")
End Try
End Sub
Sub Main()
Attempt(1)
Attempt(2)
End Sub
End ModuleAttempt(1);
Attempt(2);
void Attempt(int code)
{
try
{
throw new InvalidOperationException($"failure {code}");
}
catch (InvalidOperationException) when (code == 1)
{
Console.WriteLine("handled the code-1 case");
}
catch (InvalidOperationException error)
{
Console.WriteLine($"passed through: {error.Message}");
}
}When becomes when, and the condition gains parentheses. The point of a filter rather than an if inside the catch is that a filter runs before the stack unwinds: if it returns false the exception continues to propagate with its original stack intact, which is a real advantage when debugging. Note that C# lets you omit the variable name entirely when you do not need it, as the first catch does.Throwing your own exception
Defining an exception type is the inheritance rules of the previous section applied to one specific base class.
Option Strict On
Imports System
Public Class InsufficientFundsException
Inherits Exception
Public ReadOnly Property Shortfall As Decimal
Public Sub New(shortfall As Decimal)
MyBase.New($"Short by {shortfall:C}")
Me.Shortfall = shortfall
End Sub
End Class
Module ThrowDemo
Sub Withdraw(balance As Decimal, amount As Decimal)
If amount > balance Then
Throw New InsufficientFundsException(amount - balance)
End If
Console.WriteLine($"Withdrew {amount:C}")
End Sub
Sub Main()
Try
Withdraw(50D, 75D)
Catch error_ As InsufficientFundsException
Console.WriteLine($"{error_.Message} (short {error_.Shortfall})")
End Try
End Sub
End Moduletry
{
Withdraw(50m, 75m);
}
catch (InsufficientFundsException error)
{
Console.WriteLine($"{error.Message} (short {error.Shortfall})");
}
void Withdraw(decimal balance, decimal amount)
{
if (amount > balance)
{
throw new InsufficientFundsException(amount - balance);
}
Console.WriteLine($"Withdrew {amount:C}");
}
class InsufficientFundsException : Exception
{
public decimal Shortfall { get; }
public InsufficientFundsException(decimal shortfall)
: base($"Short by {shortfall:C}")
=> Shortfall = shortfall;
}Throw New becomes throw new, and the custom type inherits from Exception with : Exception. Public ReadOnly Property Shortfall with no setter becomes public decimal Shortfall { get; } — a get-only auto-property, settable only from the constructor. Both languages let a bare Throw / throw inside a catch rethrow the current exception without resetting its stack trace, which is what you want when you are logging and passing it on.Releasing a resource
The same construct, plus a C# form that removes a level of indentation.
Option Strict On
Imports System
Imports System.IO
Module UsingDemo
Sub Main()
Dim tempFile As String = Path.Combine(Path.GetTempPath(), "visualbasic-csharp-using.txt")
Using writer As New StreamWriter(tempFile)
writer.WriteLine("first line")
writer.WriteLine("second line")
End Using
Using reader As New StreamReader(tempFile)
Console.WriteLine(reader.ReadToEnd().Trim())
End Using
File.Delete(tempFile)
End Sub
End Modulestring path = Path.Combine(Path.GetTempPath(), "visualbasic-csharp-using-target.txt");
using (var writer = new StreamWriter(path))
{
writer.WriteLine("first line");
writer.WriteLine("second line");
}
using var reader = new StreamReader(path);
Console.WriteLine(reader.ReadToEnd().Trim());
reader.Dispose();
File.Delete(path);Using ... End Using becomes using (...) { }, and both guarantee Dispose is called even when an exception escapes. C# adds the using declaration — using var reader = ... with no block at all — which disposes at the end of the enclosing scope instead, and is the form most new C# code uses when a method holds one resource for its whole body. The explicit Dispose() here is only so the file can be deleted on the next line.Lambdas & LINQ
Lambda expressions
One arrow replaces both
Function(...) and Sub(...), and the closing keyword goes away.Option Strict On
Imports System
Imports System.Collections.Generic
Module LambdaDemo
Sub Main()
Dim double_ As Func(Of Integer, Integer) = Function(value) value * 2
Dim shout As Action(Of String) =
Sub(message)
Console.WriteLine(message.ToUpper())
End Sub
Console.WriteLine(double_(21))
shout("done")
End Sub
End ModuleFunc<int, int> double_ = value => value * 2;
Action<string> shout = message =>
{
Console.WriteLine(message.ToUpper());
};
Console.WriteLine(double_(21));
shout("done");Function(value) value * 2 becomes value => value * 2; a multi-statement lambda uses braces instead of Sub ... End Sub. C# does not distinguish the two forms — whether a lambda returns a value is decided by its body and the delegate type it is assigned to. Func(Of Integer, Integer) becomes Func<int, int> and Action(Of String) becomes Action<string>; in both languages the last type argument of Func is the return type, and Action returns nothing.LINQ with methods
Identical methods on identical types — the only differences are the lambda arrow and where the dot goes.
Option Strict On
Imports System
Imports System.Linq
Module LinqMethodDemo
Sub Main()
Dim numbers() As Integer = {5, 3, 9, 1, 7, 2}
Dim result = numbers.
Where(Function(number) number > 2).
OrderBy(Function(number) number).
Select(Function(number) number * 10).
ToList()
Console.WriteLine(String.Join(", ", result))
Console.WriteLine(numbers.Sum())
Console.WriteLine(numbers.Average())
End Sub
End Moduleint[] numbers = { 5, 3, 9, 1, 7, 2 };
var result = numbers
.Where(number => number > 2)
.OrderBy(number => number)
.Select(number => number * 10)
.ToList();
Console.WriteLine(string.Join(", ", result));
Console.WriteLine(numbers.Sum());
Console.WriteLine(numbers.Average());Once the lambdas are converted, LINQ method chains translate mechanically, because they are the same extension methods on
IEnumerable<T>. One small formatting note: Visual Basic puts the dot at the end of the line to continue the chain, C# puts it at the start of the next line. Both are conventions, but every C# codebase you meet will use the leading dot.LINQ query syntax
The one place where a Visual Basic programmer is likely to find C# slightly poorer.
Option Strict On
Imports System
Imports System.Linq
Module LinqQueryDemo
Sub Main()
Dim words() As String = {"delta", "alpha", "charlie", "bravo"}
Dim query = From word In words
Where word.Length > 5
Order By word
Select word.ToUpper()
For Each item As String In query
Console.WriteLine(item)
Next
End Sub
End Modulestring[] words = { "delta", "alpha", "charlie", "bravo" };
var query = from word in words
where word.Length > 5
orderby word
select word.ToUpper();
foreach (string item in query)
{
Console.WriteLine(item);
}The keywords lower-case and
Order By becomes one word, orderby. C#'s query syntax is the smaller of the two: Visual Basic also has Aggregate, Distinct, Skip, Take and Group By ... Into as query keywords, where C# has only from, where, select, orderby, join, let, group ... by and into. Anything else is written as a method call on the end of the query, which is why most C# code uses method syntax throughout rather than mixing the two.Grouping and aggregating
Grouping is where LINQ earns its keep, and it needs no translation beyond the lambda arrow.
Option Strict On
Imports System
Imports System.Linq
Module GroupingDemo
Sub Main()
Dim words() As String = {"apple", "avocado", "banana", "blueberry", "cherry"}
Dim groups = words.
GroupBy(Function(word) word(0)).
OrderBy(Function(group) group.Key)
For Each group In groups
Console.WriteLine($"{group.Key}: {group.Count()} ({String.Join(", ", group)})")
Next
End Sub
End Modulestring[] words = { "apple", "avocado", "banana", "blueberry", "cherry" };
var groups = words
.GroupBy(word => word[0])
.OrderBy(group => group.Key);
foreach (var group in groups)
{
Console.WriteLine($"{group.Key}: {group.Count()} ({string.Join(", ", group)})");
}GroupBy returns a sequence of groups, each of which has a Key and is itself a sequence you can iterate or aggregate. The only Visual Basic-specific detail that changes is indexing a string: word(0) becomes word[0], because square brackets index and parentheses call. Every aggregate you know — Count, Sum, Average, Min, Max, Aggregate — is the same method in both.Modern C#
Async and await
Another feature both languages shipped together — and one where top-level statements save real ceremony.
Option Strict On
Imports System
Imports System.Threading.Tasks
Module AsyncDemo
Async Function FetchAsync(label As String) As Task(Of String)
Await Task.Delay(10)
Return $"result for {label}"
End Function
Async Function RunAsync() As Task
Dim first As String = Await FetchAsync("one")
Dim second As String = Await FetchAsync("two")
Console.WriteLine(first)
Console.WriteLine(second)
End Function
Sub Main()
RunAsync().GetAwaiter().GetResult()
End Sub
End Modulestring first = await FetchAsync("one");
string second = await FetchAsync("two");
Console.WriteLine(first);
Console.WriteLine(second);
async Task<string> FetchAsync(string label)
{
await Task.Delay(10);
return $"result for {label}";
}Async Function ... As Task(Of String) becomes async Task<string>, and Await becomes await. An Async Sub becomes async void and is just as bad an idea in both — its exceptions cannot be caught by the caller. The genuine convenience is that C# allows await directly in top-level statements: the compiler generates an async Main for you, so the GetAwaiter().GetResult() bridge in the anchor column is not needed.Iterators: Yield
The
Iterator keyword disappears, because in C# the yield is what makes the method an iterator.Option Strict On
Imports System
Imports System.Collections.Generic
Module IteratorDemo
Iterator Function Fibonacci(count As Integer) As IEnumerable(Of Integer)
Dim previous As Integer = 0
Dim current As Integer = 1
For step_ As Integer = 1 To count
Yield previous
Dim next_ As Integer = previous + current
previous = current
current = next_
Next
End Function
Sub Main()
For Each number As Integer In Fibonacci(8)
Console.Write(number & " ")
Next
Console.WriteLine()
End Sub
End Moduleforeach (int number in Fibonacci(8))
{
Console.Write(number + " ");
}
Console.WriteLine();
IEnumerable<int> Fibonacci(int count)
{
int previous = 0;
int current = 1;
for (int step = 1; step <= count; step++)
{
yield return previous;
(previous, current) = (current, previous + current);
}
}Iterator Function ... As IEnumerable(Of Integer) becomes plain IEnumerable<int> and Yield x becomes yield return x. Both are lazy: nothing runs until the caller asks for the first item, and execution suspends at each yield. Exit Function in an iterator becomes yield break. The C# column also slips in tuple assignment — (previous, current) = (current, previous + current) — which swaps two variables without a temporary, something Visual Basic has no syntax for.Extension methods
The same feature, marked in a completely different way — an attribute in one language, a parameter modifier in the other.
Option Strict On
Imports System
Imports System.Runtime.CompilerServices
Module StringExtensions
<Extension()>
Public Function Shout(text As String) As String
Return text.ToUpper() & "!"
End Function
End Module
Module ExtensionDemo
Sub Main()
Console.WriteLine("hello".Shout())
End Sub
End ModuleConsole.WriteLine("hello".Shout());
static class StringExtensions
{
public static string Shout(this string text) => text.ToUpper() + "!";
}Visual Basic marks an extension method with the
<Extension()> attribute and needs Imports System.Runtime.CompilerServices for it. C# marks it by putting this on the first parameter, inside a static class, with no attribute and no import. Both compile to the same thing — an ordinary static method the compiler lets you call as though it were an instance method. Attributes in general translate by swapping the brackets: <Obsolete("...")> becomes [Obsolete("...")].Imports becomes using
One keyword change, plus a wrinkle Visual Basic programmers rarely think about because their project file hides it.
Option Strict On
Imports System
Imports System.Collections.Generic
Imports Shortcut = System.Text.StringBuilder
Namespace Reporting
Public Class Report
Public Shared Function Title() As String
Return "Quarterly"
End Function
End Class
End Namespace
Module NamespaceDemo
Sub Main()
Console.WriteLine(Reporting.Report.Title())
Dim builder As New Shortcut()
builder.Append("built")
Console.WriteLine(builder.ToString())
End Sub
End Moduleusing Shortcut = System.Text.StringBuilder;
Console.WriteLine(Reporting.Report.Title());
var builder = new Shortcut();
builder.Append("built");
Console.WriteLine(builder.ToString());
namespace Reporting
{
class Report
{
public static string Title() => "Quarterly";
}
}Imports becomes using, and an aliased import keeps the same Name = FullType shape. The wrinkle: a Visual Basic project has project-level imports set in the project properties, so System is often already available without a line in the file. C# has the same idea under the name implicit usings, on by default in new projects, which is why the examples on this page never write using System;. Namespaces also gain a file-scoped form — namespace Reporting; on one line, with no braces, indenting nothing.⚠ Gotchas for Visual Basic Programmers
⚠ Two names that used to be one
Worth stating twice because it is the difference most likely to produce a bug that compiles.
Option Strict On
Imports System
Module CaseGotcha
Sub Main()
Dim total As Integer = 10
' The editor rewrites this to match the declaration
Total = Total + 5
Console.WriteLine(total)
End Sub
End Moduleint total = 10;
// int Total; would be a SECOND variable, silently
total = total + 5;
Console.WriteLine(total);In Visual Basic you may type
Total where you declared total and the editor quietly corrects you. In C# nothing corrects you — either the name is undefined and you get an error, or, much worse, a name with that exact casing exists elsewhere and your code compiles while referring to the wrong thing. The habit that removes the risk is the .NET naming convention: locals and parameters camelCase, everything public PascalCase. Follow it and two names never differ by case alone.⚠ Is does not mean Is
The same three letters in both languages, asking two completely different questions.
Option Strict On
Imports System
Module IsGotcha
Sub Main()
Dim left As Object = New Object()
Dim right As Object = left
Dim other As Object = New Object()
' Is compares REFERENCES
Console.WriteLine(left Is right)
Console.WriteLine(left Is other)
Console.WriteLine(left IsNot other)
' TypeOf ... Is asks about the type
Console.WriteLine(TypeOf left Is Object)
End Sub
End Moduleobject left = new object();
object right = left;
object other = new object();
// ReferenceEquals compares references
Console.WriteLine(ReferenceEquals(left, right));
Console.WriteLine(ReferenceEquals(left, other));
Console.WriteLine(!ReferenceEquals(left, other));
// is asks about the TYPE
Console.WriteLine(left is object);Visual Basic's
Is compares references and TypeOf x Is T asks about the type. C# reverses it: is is the type test, and reference comparison is ReferenceEquals(a, b) or (object)a == (object)b. So left Is right translated literally to left is right does not mean the same thing — and against a type name it will happily compile. IsNot has no C# keyword at all; negate with !. The one place is keeps the Visual Basic meaning is x is null, which is a null check in both.⚠ Dim x(5) is not new int[5]
Six slots in one column, five in the other — from the same number.
Option Strict On
Imports System
Module BoundsGotcha
Sub Main()
Dim slots(5) As Integer
Console.WriteLine(slots.Length)
Console.WriteLine(UBound(slots))
slots(5) = 99
Console.WriteLine(slots(5))
End Sub
End Moduleint[] slots = new int[5];
Console.WriteLine(slots.Length);
Console.WriteLine(slots.Length - 1);
slots[4] = 99;
Console.WriteLine(slots[4]);Dim slots(5) declares indices 0 through 5, which is six elements, because the number is the upper bound. new int[5] declares five, because the number is the count. Translating one to the other means adding or subtracting one, every time. UBound(array) has no C# equivalent and becomes array.Length - 1; LBound is always zero and simply disappears. Anyone bringing habits from VB6 should also note that Option Base 1 and ReDim Preserve have no counterparts here at all — resizing means Array.Resize or, far better, a List<T>.⚠ There is no With block
A construct you have used for twenty years, with no replacement — only workarounds, one of which is genuinely better.
Option Strict On
Imports System
Imports System.Text
Module WithGotcha
Sub Main()
Dim builder As New StringBuilder()
With builder
.Append("first")
.Append(" / ")
.Append("second")
End With
Console.WriteLine(builder.ToString())
End Sub
End Modulevar builder = new StringBuilder();
builder.Append("first")
.Append(" / ")
.Append("second");
Console.WriteLine(builder.ToString());C# has no
With block. Three things replace it depending on why you were using it. For setting several properties on a new object, use an object initializer: new Order { Total = 5m, Country = "US" }. For calling several methods in a row, use method chaining where the type supports it, as StringBuilder does above. Otherwise, assign the object to a short local and repeat the name — which is what the compiler was doing for you anyway. Note that C#'s with keyword is unrelated: it copies a record, as the records section showed.⚠ The My namespace does not exist
Both columns do the same work with the same .NET calls — the point is what is missing from the other side.
Option Strict On
Imports System
Imports System.IO
Module MyGotcha
Sub Main()
' My.Computer.FileSystem.ReadAllText(path) and friends
' are a Visual Basic convenience layer over these APIs
Dim tempFile As String = Path.Combine(Path.GetTempPath(), "visualbasic-csharp-my.txt")
File.WriteAllText(tempFile, "written")
Console.WriteLine(File.ReadAllText(tempFile))
Console.WriteLine(Environment.MachineName.Length > 0)
File.Delete(tempFile)
End Sub
End Modulestring path = Path.Combine(Path.GetTempPath(), "visualbasic-csharp-my-target.txt");
File.WriteAllText(path, "written");
Console.WriteLine(File.ReadAllText(path));
Console.WriteLine(Environment.MachineName.Length > 0);
File.Delete(path);The
My namespace — My.Computer, My.Application, My.Settings, My.Resources, My.Forms — is generated by the Visual Basic compiler and has no C# counterpart. Everything it offers is a shortcut to something in the base class library, so porting means finding the real API underneath: My.Computer.FileSystem is System.IO.File and Directory, My.Computer.Name is Environment.MachineName, My.Settings is the generated Settings class, My.Application.Info is Assembly.GetEntryAssembly(). The same is true of the Microsoft.VisualBasic functions — MsgBox, InputBox, IsNumeric, Val, Now.