Output & Running
Hello, World
The smallest complete program in each language — the Ruby column is the whole file.
Option Strict On
Imports System
Module HelloWorld
Sub Main()
Console.WriteLine("Hello, World!")
End Sub
End Moduleputs "Hello, World!"No module, no entry point, no imports, no type declarations, and no parentheses. A Ruby file is a program and its statements run top to bottom.
puts is the counterpart of Console.WriteLine — the name is short for "put string", and like almost everything in Ruby it is a method call whose parentheses you may leave off.String interpolation
Interpolation exists, spelled with a hash and braces, and any expression may go inside.
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 Modulename = "Ada"
score = 42
ratio = 0.8756
puts "Hello, #{name}! Score: #{score}"
puts format("Padded: %05d, rounded: %.2f", score, ratio)
puts "Doubled: #{score * 2}"$"...{name}..." becomes "...#{name}...", with no prefix on the string — every double-quoted Ruby string interpolates, and single-quoted ones do not. Format specifiers do not carry over: {score:D5} becomes format("%05d", score), using the same printf vocabulary C uses. Anything inside #{} is evaluated and has to_s called on it, so #{score * 2} works.Printing something that is not a string
Three printing methods, and the difference between them is worth learning on the first day.
Option Strict On
Imports System
Imports System.Collections.Generic
Module OutputDemo
Sub Main()
Dim numbers As New List(Of Integer) From {1, 2, 3}
Console.WriteLine(String.Join(", ", numbers))
Console.Write("no newline")
Console.WriteLine(" — then newline")
End Sub
End Modulenumbers = [1, 2, 3]
puts numbers.join(", ")
p numbers
print "no newline"
puts " — then newline"
puts numbers.inspectputs adds a newline and calls to_s — given an array it prints one element per line, which surprises people. print is Console.Write: no newline. p prints inspect instead, which shows the structure — [1, 2, 3] with its brackets and quotes intact — and returns its argument, making it the debugging tool you reach for. Every object has both to_s (for people) and inspect (for programmers), a distinction .NET collapses into ToString.Syntax Fundamentals
Blocks still end with end
Of every target on this anchor, this is the one whose block structure will look most familiar.
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 Moduletemperature = 30
if temperature > 25
puts "Warm"
elsif temperature > 10
puts "Mild"
else
puts "Cold"
endRuby closes blocks with
end — not End If, End Sub or End Module, just end, one word for every construct. There are no braces and no Then, the condition needs no parentheses, and ElseIf becomes elsif (one e, no e in the middle). Indentation is two spaces by convention and carries no meaning, unlike Python. If the End keyword is the thing you would miss, you do not have to.Comments
The comment character changes, and the documentation convention is a plain comment rather than a special one.
Option Strict On
Imports System
Module CommentDemo
''' <summary>Doubles a number.</summary>
Function Twice(value As Integer) As Integer
' A comment starts with an apostrophe
Return value * 2
End Function
Sub Main()
Console.WriteLine(Twice(21))
End Sub
End Module# Doubles a number.
def twice(value)
# A comment starts with a hash
value * 2
end
=begin
This form spans lines, and is used
almost nowhere in practice.
=end
puts twice(21)' becomes #. There is a block form, =begin/=end, but it must start at column zero and is so rarely used that many Ruby programmers have never written one. Documentation is written as ordinary # comments above the method: the RDoc and YARD tools read them, so there is no XML and no <summary> tag. A magic comment on the very first line — # frozen_string_literal: true — is a directive rather than a comment, and the strings section explains it.Case is significant — and it decides what a name is
Ruby is case-sensitive, and it goes further: the first letter of a name changes what kind of thing it is.
Option Strict On
Imports System
Module NamingDemo
Const MaximumRetries As Integer = 3
Sub Main()
Dim customerName As String = "Grace"
' One variable, whatever case you type
Console.WriteLine(CustomerName)
Console.WriteLine(MaximumRetries)
End Sub
End Modulecustomer_name = "Grace"
MAXIMUM_RETRIES = 3
puts customer_name
puts MAXIMUM_RETRIES
MAXIMUM_RETRIES = 5 # warning: already initialized constant
puts MAXIMUM_RETRIESIdentifiers are case-sensitive, so
customerName and CustomerName differ. More than that, a name beginning with a capital letter is a constant — that is how Ruby knows MAXIMUM_RETRIES and every class name are constants, without a Const keyword. Reassigning one is allowed but warns, which is Ruby's style throughout: it tells you and trusts you. Conventions are snake_case for variables and methods, CamelCase for classes, SCREAMING_SNAKE_CASE for constants, @name for instance fields.Conditions written after the statement
A guard clause can be written the way you would say it — the action first, the condition after.
Option Strict On
Imports System
Module ModifierDemo
Sub Main()
Dim count As Integer = 0
Dim ready As Boolean = True
If ready Then Console.WriteLine("go")
If Not ready Then Console.WriteLine("wait")
While count < 3
count += 1
End While
Console.WriteLine(count)
End Sub
End Modulecount = 0
ready = true
puts "go" if ready
puts "wait" unless ready
count += 1 while count < 3
puts countAny statement may carry a trailing
if, unless, while or until. unless is If Not with the negation built into the keyword, and it reads far better than if !ready. These statement modifiers are idiomatic for short guard clauses — return if list.empty? — and considered poor style when the statement grows long enough that the reader meets the condition too late.Variables & Types
There is no Dim
The whole left half of a Visual Basic declaration disappears, and with it the guarantee about what the name will hold.
Option Strict On
Imports System
Module DeclarationDemo
Sub Main()
Dim count As Integer = 10
Dim label As String = "widget"
Dim ready As Boolean = True
count = 20
Console.WriteLine($"{count} {label} {ready}")
End Sub
End Modulecount = 10
label = "widget"
ready = true
count = 20
count = "now a string"
puts "#{count} #{label} #{ready}"Assignment creates the variable; there is no
Dim, no As, and no Option Explicit to forget. The type belongs to the value, not the name, so a variable can hold an integer and then a string. Note that true and false are lower case, and that a name you only read without assigning is a method call as far as Ruby is concerned — which is why a typo produces NameError: undefined local variable or method rather than nil.Everything is an object
There are no primitives and no value types — the integer
42 is an object with methods, and so is nil.Option Strict On
Imports System
Module ObjectDemo
Sub Main()
Dim number As Integer = 42
' Integer is a value type; boxing makes it an Object
Console.WriteLine(number.GetType().Name)
Console.WriteLine(number.ToString().Length)
Console.WriteLine(Math.Abs(-5))
End Sub
End Modulenumber = 42
puts number.class
puts 42.to_s.length
puts(-5.abs)
puts 3.times.to_a.inspect
puts nil.class
puts 42.class.ancestors.first(3).inspectIn .NET an
Integer is a value type that must be boxed to be treated as an Object. In Ruby there is no such split: 42 is an instance of Integer, nil is the single instance of NilClass, and both answer .class. That is why -5.abs and 3.times read the way they do — the operation belongs to the number. It also means Math.Abs(x)-style static helpers are rare; the method is on the object instead.Nothing becomes nil
One object means "nothing here", and unlike
Nothing it never quietly turns into a zero.Option Strict On
Imports System
Module NilDemo
Sub Main()
Dim missingText As String = Nothing
Dim missingNumber As Integer = Nothing
Console.WriteLine(missingText Is Nothing)
Console.WriteLine(missingNumber)
End Sub
End Modulemissing_text = nil
missing_number = nil
puts missing_text.nil?
puts missing_number.inspect
puts missing_text.to_s.empty?
puts (missing_number || 0)Nothing becomes nil, and the test is value.nil? — a method, because nil is an object. The mismatch worth noting is on the Visual Basic side: Dim n As Integer = Nothing stores zero, since Nothing means "the default value for this type". Ruby has no such notion. nil answers a surprising number of methods usefully (to_s is "", to_a is []), and || supplies a fallback the way the two-argument If() does.Numbers
The integer types collapse into one with no ceiling, and there is a decimal type — it just is not built in.
Option Strict On
Imports System
Module NumberDemo
Sub Main()
Dim whole As Integer = 2147483647
Dim bigger As Long = 9223372036854775807
Dim precise As Double = 0.1 + 0.2
Dim exact As Decimal = 0.1D + 0.2D
Console.WriteLine(whole)
Console.WriteLine(bigger)
Console.WriteLine(precise)
Console.WriteLine(exact)
End Sub
End Modulerequire "bigdecimal"
require "bigdecimal/util"
whole = 2147483647
huge = 2**200
precise = 0.1 + 0.2
exact = "0.1".to_d + "0.2".to_d
puts whole
puts huge
puts precise
puts exact.to_s("F")
puts Rational(1, 3) + Rational(1, 6)Short, Integer and Long all become Integer, which grows to whatever size is needed — 2**200 is exact, and there is no overflow. Double becomes Float and misbehaves identically, which is why 0.1 + 0.2 is untidy in both columns. Decimal becomes BigDecimal from the standard library, constructed from a string so the precision is not already lost. Ruby also has exact Rational numbers, which .NET has no counterpart for at all.Converting between types
Two families of conversion, and the difference between them is exactly the difference between
CInt and TryParse.Option Strict On
Imports System
Module ConversionDemo
Sub Main()
Dim text As String = "123"
Dim parsed As Integer = CInt(text)
Dim asText As String = CStr(parsed * 2)
Dim value As Integer
If Integer.TryParse("12x", value) Then
Console.WriteLine(value)
Else
Console.WriteLine("not a number")
End If
Console.WriteLine($"{parsed} {asText}")
End Sub
End Moduletext = "123"
parsed = Integer(text)
as_text = (parsed * 2).to_s
value = Integer("12x", exception: false)
if value
puts value
else
puts "not a number"
end
puts "12x".to_i
puts "#{parsed} #{as_text}"The
to_ methods — to_i, to_f, to_s, to_a — never fail: "12x".to_i is 12 and "oops".to_i is 0, which is convenient and dangerous in equal measure. The capitalized functions — Integer(), Float() — are strict and raise on anything malformed, which is CInt under Option Strict On. Integer(text, exception: false) returns nil instead of raising, and is the closest thing to TryParse.Operators
Comparing values
Comparison gains a character, inequality changes shape, and
Is becomes a method.Option Strict On
Imports System
Module EqualityDemo
Sub Main()
Dim left As String = "abc"
Dim right As String = "ab" & "c"
Console.WriteLine(left = right)
Console.WriteLine(left <> "other")
Console.WriteLine(left Is right)
End Sub
End Moduleleft = "abc"
right = "ab" + "c"
puts left == right
puts left != "other"
puts left.equal?(right)
puts 1 == 1.0
puts 1.eql?(1.0)= for comparison becomes ==, and <> becomes !=. Visual Basic's Is — reference identity — becomes equal?, a method rather than an operator. Ruby has a third form, eql?, which compares value and type: 1 == 1.0 is true, 1.eql?(1.0) is false, and it is eql? that hashes use for keys. There is no loose-equality trap as in JavaScript — == never converts between unrelated types.Logical operators
Both symbolic and word forms exist — and mixing them up has bitten a lot of people, because they differ in precedence rather than meaning.
Option Strict On
Imports System
Module LogicalDemo
Sub Main()
Dim age As Integer = 30
Dim member As Boolean = True
If age > 18 AndAlso member Then Console.WriteLine("eligible")
If age < 18 OrElse member Then Console.WriteLine("either")
If Not member Then Console.WriteLine("not a member")
End Sub
End Moduleage = 30
member = true
puts "eligible" if age > 18 && member
puts "either" if age < 18 || member
puts "not a member" if !member
# && and || return an OPERAND, not a boolean
puts (nil || "fallback")
puts ("value" && "second")AndAlso becomes &&, OrElse becomes ||, Not becomes !. Ruby also has and, or and not, which do the same thing at much lower precedence — low enough that x = a or b assigns a and then evaluates b, which is almost never what was meant. Use the symbols for conditions and reserve the words for control flow (do_thing or raise "failed"). As in JavaScript, || returns its first truthy operand rather than a boolean, which is what makes it a fallback operator.Arithmetic and integer division
This is the one line most likely to be wrong after a mechanical translation.
Option Strict On
Imports System
Module ArithmeticDemo
Sub Main()
Dim quotient As Integer = 17 \ 5
Dim exact As Double = 17 / 5
Dim remainder As Integer = 17 Mod 5
Dim squared As Double = 7 ^ 2
Dim negative As Integer = -17 \ 5
Console.WriteLine($"{quotient} {exact} {remainder} {squared} {negative}")
End Sub
End Modulequotient = 17 / 5
exact = 17 / 5.0
remainder = 17 % 5
squared = 7**2
negative = -17 / 5
puts [quotient, exact, remainder, squared, negative].join(" ")
puts 17.fdiv(5)
puts 17.divmod(5).inspectRuby has no
\ operator, and / between two integers truncates — so 17 / 5 is 3, not 3.4. A Visual Basic / copied straight across silently starts doing integer division, exactly the trap C# sets. Make an operand a float (17 / 5.0) or call fdiv. Mod becomes %, ^ becomes ** (^ is bitwise exclusive-or). Ruby floors rather than truncating, so -17 / 5 is -4 where Visual Basic gives -3.If() becomes || and &.
Both halves of
If() have counterparts, and a third operator arrives for reaching into something that might be nil.Option Strict On
Imports System
Module ConditionalDemo
Sub Main()
Dim supplied As String = Nothing
Dim label As String = If(supplied, "(unnamed)")
Dim score As Integer = 72
Dim grade As String = If(score >= 60, "pass", "fail")
Dim length As Integer = If(supplied Is Nothing, 0, supplied.Length)
Console.WriteLine($"{label} / {grade} / {length}")
End Sub
End Modulesupplied = nil
label = supplied || "(unnamed)"
score = 72
grade = score >= 60 ? "pass" : "fail"
length = supplied&.length || 0
puts "#{label} / #{grade} / #{length}"Two-argument
If(value, fallback) becomes || — note this falls back on false as well as nil, since those are the only falsy values. Three-argument If(condition, a, b) becomes condition ? a : b. The new one is safe navigation, &.: if the receiver is nil the call yields nil instead of raising, which collapses a whole nested null check into one expression.Comparison and sorting
The comparison method becomes an operator, and sorting by a computed key becomes one word.
Option Strict On
Imports System
Imports System.Collections.Generic
Module CompareDemo
Sub Main()
Dim words As New List(Of String) From {"pear", "fig", "apple"}
words.Sort(Function(left, right) left.Length.CompareTo(right.Length))
Console.WriteLine(String.Join(", ", words))
Console.WriteLine("a".CompareTo("b"))
End Sub
End Modulewords = ["pear", "fig", "apple"]
puts words.sort_by(&:length).join(", ")
puts words.sort { |left, right| left.length <=> right.length }.join(", ")
puts ("a" <=> "b")
puts words.max_by(&:length)CompareTo becomes <=>, the "spaceship" operator, returning -1, 0 or 1. Defining it on your own class and including the Comparable module gives you <, >, between? and sorting for free. sort_by is what you almost always want over sort: it takes the key rather than a comparison, so OrderBy(Function(x) x.Length) becomes sort_by(&:length). That &:length is a symbol turned into a block, covered in the blocks section.Strings & Symbols
Common string operations
The same operations under
snake_case names, plus a convention worth noticing in the method names themselves.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().StartsWith("Visual"))
Console.WriteLine(text.Trim().Length)
End Sub
End Moduletext = " Visual Basic "
puts "[#{text.strip}]"
puts text.strip.upcase
puts text.include?("Basic")
puts text.strip.tr(" ", "-")
puts text.strip.start_with?("Visual")
puts text.strip.length
puts text.strip.center(20, ".")Trim→strip, ToUpper→upcase, Contains→include?, StartsWith→start_with?, Replace→sub/gsub/tr. The question mark is part of the name: a method ending in ? returns a boolean, by convention throughout Ruby. A method ending in ! is the dangerous or mutating variant — strip returns a new string, strip! changes it in place and returns nil if nothing changed. Neither punctuation mark has any meaning to the parser; they are conventions the whole ecosystem keeps.Substrings
One pair of brackets does
Substring, Left, Right, indexing and a regular-expression match.Option Strict On
Imports System
Module SubstringDemo
Sub Main()
Dim text As String = "Visual Basic"
Console.WriteLine(text.Substring(0, 6))
Console.WriteLine(text.Substring(7))
Console.WriteLine(text.Substring(text.Length - 5))
Console.WriteLine(text(0))
Console.WriteLine(text.IndexOf("Basic"))
End Sub
End Moduletext = "Visual Basic"
puts text[0, 6]
puts text[7..]
puts text[-5..]
puts text[0]
puts text.index("Basic")
puts text[/B\w+/]text[start, length] is Substring exactly. text[range] takes a range, and negative positions count from the end, so text[-5..] is Right(text, 5). text[0] gives a one-character string — Ruby has no character type. Passing a regular expression returns the match, which is a genuinely different capability: text[/B\w+/] pulls out the first word starting with B. InStr becomes index, and it is zero-based rather than one-based.Strings, mutability and freezing
Ruby strings are mutable — unlike .NET's — and that is changing, which is why the buffer below has a
+ in front of it.Option Strict On
Imports System
Imports System.Text
Module MutabilityDemo
Sub Main()
Dim text As String = "abc"
Dim changed As String = text.Replace("a", "z")
Console.WriteLine($"{text} {changed}")
Dim builder As New StringBuilder()
For number As Integer = 1 To 5
builder.Append(number)
Next
Console.WriteLine(builder.ToString())
End Sub
End Moduletext = "abc"
changed = text.sub("a", "z")
puts "#{text} #{changed}"
buffer = +""
(1..5).each { |number| buffer << number.to_s }
puts buffer
frozen = "cannot change".freeze
puts frozen.frozen?
puts (frozen + "!").frozen?A .NET
String is immutable, so StringBuilder exists. A Ruby String is mutable, so << appends in place and no builder is needed. But Ruby is moving toward frozen literals: today mutating one works and warns, and the magic comment # frozen_string_literal: true on line one makes it a FrozenError now. The forward-compatible way to get a mutable string is the unary plus, +"", or String.new — both used above so this example neither warns today nor breaks when the default flips. .freeze makes any object immutable, not just strings.Symbols — a name that is not a string
A Ruby idea with no .NET counterpart, and you will meet it in the first hour.
Option Strict On
Imports System
Imports System.Collections.Generic
Module SymbolDemo
Sub Main()
' The nearest Visual Basic equivalent is an Enum
Dim settings As New Dictionary(Of String, Integer) From {
{"width", 100},
{"height", 50}
}
Console.WriteLine(settings("width"))
Console.WriteLine("width".Equals("width"))
End Sub
End Modulesettings = { width: 100, height: 50 }
puts settings[:width]
puts :width.class
puts :width.equal?(:width)
puts "width".equal?("width")
puts :width.to_s + " / " + "height".to_sym.inspectA symbol is written
:name and is an immutable, interned name — the same symbol written twice is the same object, which two identical strings are not. That makes symbols the natural choice for hash keys, method names and any fixed label, and { width: 100 } is shorthand for { :width => 100 }. Think of them as the role a Visual Basic Enum member plays, without needing to declare the enum first. Convert with to_s and to_sym.Multi-line and quoted strings
Single quotes mean something different from double quotes here, and that difference solves the Windows-path problem.
Option Strict On
Imports System
Module QuotingDemo
Sub Main()
Dim quoted As String = "She said ""hello""."
Dim path As String = "C:\reports\summary.txt"
Dim block As String = "line one" & Environment.NewLine & "line two"
Console.WriteLine(quoted)
Console.WriteLine(path)
Console.WriteLine(block)
End Sub
End Modulequoted = 'She said "hello".'
path = 'C:\reports\summary.txt'
block = <<~TEXT
line one
line two
TEXT
puts quoted
puts path
puts blockA double-quoted string interpolates and honors escapes; a single-quoted string does neither, so
'C:\reports' needs no doubling — the closest thing to @"..." in C#. A heredoc, <<~TEXT ... TEXT, runs until its terminator; the squiggle strips the common leading indentation, so the text can be indented with the code around it. Heredocs interpolate by default, and <<~'TEXT' turns that off.Arrays, Hashes & Ranges
Arrays replace both arrays and List(Of T)
One growable type does the work of arrays,
List(Of T) and the VB6 Collection.Option Strict On
Imports System
Imports System.Collections.Generic
Module ArrayDemo
Sub Main()
Dim fruits As New List(Of String) From {"apple", "banana"}
fruits.Add("cherry")
fruits.Insert(0, "apricot")
fruits.Remove("banana")
Console.WriteLine(fruits.Count)
Console.WriteLine(fruits(0))
Console.WriteLine(String.Join(", ", fruits))
End Sub
End Modulefruits = ["apple", "banana"]
fruits << "cherry"
fruits.unshift("apricot")
fruits.delete("banana")
puts fruits.length
puts fruits.first
puts fruits.last
puts fruits.join(", ")
puts fruits[-1]Add becomes << (or push), Insert(0, x) becomes unshift, Count becomes length or size. Indexing uses square brackets, and negative indices count from the end — fruits[-1] is the last item, so UBound has no counterpart and needs none. There is also first and last, which say what they mean. An array holds anything, mixed, with no element type to declare.Hashes
The dictionary, its literal, and a constructor trick that removes a whole class of loop.
Option Strict On
Imports System
Imports System.Collections.Generic
Module HashDemo
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
Console.WriteLine(ages.ContainsKey("Ada"))
Dim found As Integer
ages.TryGetValue("Nobody", found)
Console.WriteLine(found)
End Sub
End Moduleages = { "Ada" => 36, "Grace" => 45 }
ages["Alan"] = 41
ages.each do |name, age|
puts "#{name} is #{age}"
end
puts ages.key?("Ada")
puts ages.fetch("Nobody", 0)
puts ages.fetch("Ada")
counts = Hash.new(0)
counts["x"] += 1
puts counts["x"]Dictionary(Of K, V) becomes a Hash, written { key => value } — or { key: value } when the keys are symbols. Iterating yields the key and value as two block parameters, so there is no KeyValuePair to unpack. ContainsKey becomes key?, and TryGetValue becomes fetch(key, default) — fetch with no default raises rather than returning nil, which is what you want when a missing key is a bug. Hash.new(0) gives every unseen key a default of zero, which turns a tally loop into one line.Ranges
A range is a first-class object, not a loop header — so it can be tested, iterated, sliced with and matched against.
Option Strict On
Imports System
Imports System.Linq
Module RangeDemo
Sub Main()
Dim value As Integer = 15
If value >= 10 AndAlso value <= 20 Then
Console.WriteLine("in range")
End If
Console.WriteLine(String.Join(", ", Enumerable.Range(1, 5)))
Console.WriteLine(String.Join(", ", Enumerable.Range(1, 5).Where(Function(n) n Mod 2 = 1)))
End Sub
End Modulevalue = 15
puts "in range" if (10..20).cover?(value)
puts (1..5).to_a.join(", ")
puts (1...5).to_a.join(", ")
puts (1..5).select(&:odd?).join(", ")
puts ("a".."e").to_a.join(", ")
case value
when 1..9 then puts "small"
when 10..99 then puts "medium"
end1..5 includes 5; 1...5 (three dots) stops at 4. A range is an object with methods, so it answers cover? for a containment test, to_a for a list, and every Enumerable method directly. It works on anything comparable, including strings. And because case uses ===, a range can be a when clause — which is exactly Visual Basic's Case 4 To 6, reached from a completely different direction.Multiple assignment
Returning two things needs no tuple type — an array on the left of an
= unpacks itself.Option Strict On
Imports System
Module MultipleDemo
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}")
End Sub
End Moduledef minimum_and_maximum(values)
[values.min, values.max]
end
low, high = minimum_and_maximum([4, 9, 1, 7])
puts "#{low}..#{high}"
first, *rest = [1, 2, 3, 4]
puts "#{first} #{rest.inspect}"
left, right = 1, 2
left, right = right, left
puts "#{left} #{right}"A method returns its last expression, so
[values.min, values.max] is the return. On the left, low, high = ... takes the array apart, and *rest collects whatever remains. The same mechanism swaps two variables in one line with no temporary. Ruby has no named tuple as Visual Basic does; when the parts need names, the answer is a Struct, a Data object or a hash.Sets
The same data structure, with the set algebra written as operators.
Option Strict On
Imports System
Imports System.Collections.Generic
Module SetDemo
Sub Main()
Dim seen As New HashSet(Of String) From {"cat", "dog", "cat"}
seen.Add("bird")
Console.WriteLine(seen.Count)
Console.WriteLine(seen.Contains("dog"))
Dim other As New HashSet(Of String) From {"dog", "fish"}
Dim shared_ As New HashSet(Of String)(seen)
shared_.IntersectWith(other)
Console.WriteLine(String.Join(", ", shared_))
End Sub
End Modulerequire "set"
seen = Set["cat", "dog", "cat"]
seen << "bird"
puts seen.size
puts seen.include?("dog")
other = Set["dog", "fish"]
puts (seen & other).to_a.join(", ")
puts (seen | other).to_a.sort.join(", ")
puts (seen - other).to_a.sort.join(", ")HashSet(Of T) becomes Set, which needs require "set". Duplicates collapse on construction in both. Where .NET spells the operations as mutating methods, Ruby offers &, | and - as non-mutating operators returning a new set — the same reading as the mathematics. Contains becomes include?, matching arrays and strings, which is the sort of consistency Ruby is built on.Control Flow
Only nil and false are falsy
Ruby has truthiness, and its falsy list is the shortest of any language on this anchor — which makes it the least surprising.
Option Strict On
Imports System
Imports System.Collections.Generic
Module TruthDemo
Sub Main()
Dim items As New List(Of String)
Dim text As String = ""
Dim count As Integer = 0
If items.Count = 0 Then Console.WriteLine("no items")
If String.IsNullOrEmpty(text) Then Console.WriteLine("no text")
If count = 0 Then Console.WriteLine("zero")
End Sub
End Moduleitems = []
text = ""
count = 0
puts "no items" if items.empty?
puts "no text" if text.empty?
puts "zero" if count.zero?
puts "an empty array is TRUTHY" if items
puts "zero is TRUTHY" if count
puts "an empty string is TRUTHY" if textOnly
nil and false are falsy. Zero is truthy, an empty string is truthy, an empty array is truthy. That is different from Python (where all three are falsy) and from JavaScript (where zero and empty string are falsy but an empty array is not), and it is the easiest of the three to remember. The practical effect is that if value means exactly "is there a value", so emptiness is asked for explicitly: empty?, zero?, any?.Select Case becomes case ... when
The closest counterpart to
Select Case on this whole anchor — and it is an expression, so it produces a value.Option Strict On
Imports System
Module SelectDemo
Function Describe(code As Integer) As String
Select Case code
Case 1
Return "one"
Case 2, 3
Return "two or three"
Case 4 To 6
Return "four to six"
Case Is > 100
Return "large"
Case Else
Return "something else"
End Select
End Function
Sub Main()
Console.WriteLine(Describe(1))
Console.WriteLine(Describe(3))
Console.WriteLine(Describe(5))
Console.WriteLine(Describe(200))
End Sub
End Moduledef describe(code)
case code
when 1 then "one"
when 2, 3 then "two or three"
when 4..6 then "four to six"
when ->(value) { value > 100 } then "large"
else "something else"
end
end
puts describe(1)
puts describe(3)
puts describe(5)
puts describe(200)Select Case→case, Case→when, Case Else→else, End Select→end. Case 2, 3 and Case 4 To 6 translate almost letter for letter. No branch falls through, as in Visual Basic. Two things are better: the whole case is an expression whose value is the chosen branch, so the returns disappear; and each when is tested with ===, so a class, a regular expression, a range or a lambda can be a branch — which is what replaces Case Is > 100.if returns a value
The statement/expression divide mostly does not exist here, and it changes how code is shaped.
Option Strict On
Imports System
Module ExpressionDemo
Sub Main()
Dim score As Integer = 72
Dim grade As String
If score >= 90 Then
grade = "A"
ElseIf score >= 70 Then
grade = "B"
Else
grade = "F"
End If
Console.WriteLine(grade)
End Sub
End Modulescore = 72
grade = if score >= 90
"A"
elsif score >= 70
"B"
else
"F"
end
puts grade
puts (score > 50 ? "pass" : "fail")An
if evaluates to whatever its chosen branch evaluated to, so it can sit on the right of an assignment — no repeated grade =, no variable declared empty and filled in later. The same is true of case, begin/rescue, and a method body, which returns its last expression. Visual Basic has only the three-argument If() function for this, limited to one expression per branch; Ruby has no such limit.Pattern matching on shape
A second
case form, using in rather than when, that matches the shape of a value and pulls pieces out of it.Option Strict On
Imports System
Imports System.Collections.Generic
Module PatternDemo
Sub Describe(payload As Dictionary(Of String, Object))
If payload.ContainsKey("name") AndAlso payload.ContainsKey("age") Then
Console.WriteLine($"{payload("name")} is {payload("age")}")
ElseIf payload.ContainsKey("name") Then
Console.WriteLine($"{payload("name")}, age unknown")
Else
Console.WriteLine("unrecognized")
End If
End Sub
Sub Main()
Describe(New Dictionary(Of String, Object) From {{"name", "Ada"}, {"age", 36}})
Describe(New Dictionary(Of String, Object) From {{"name", "Grace"}})
Describe(New Dictionary(Of String, Object)())
End Sub
End Moduledef describe(payload)
case payload
in { name: String => name, age: Integer => age }
puts "#{name} is #{age}"
in { name: String => name }
puts "#{name}, age unknown"
else
puts "unrecognized"
end
end
describe({ name: "Ada", age: 36 })
describe({ name: "Grace" })
describe({})Written with
in, a case becomes pattern matching: { name: String => name } matches a hash with a name key whose value is a String, and binds it to a local called name. Arrays, objects, ranges and guards all work the same way. This is what replaces the chain of ContainsKey tests in the anchor column, and Visual Basic has nothing like it — the nearest equivalent anywhere in .NET is C#'s property patterns.Blocks & Enumerable
For Each becomes each
Iteration is not a language construct here — it is a method you pass a block of code to, and that is the central idea of the language.
Option Strict On
Imports System
Imports System.Collections.Generic
Module EachDemo
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
For number As Integer = 1 To 3
Console.WriteLine(number)
Next
End Sub
End Modulewords = ["alpha", "beta", "gamma"]
words.each do |word|
puts word.upcase
end
words.each_with_index do |word, index|
puts "#{index}: #{word}"
end
3.times { |number| puts number + 1 }
(1..3).each { |number| puts number }For Each ... Next becomes each with a block: the code between do and end (or between braces, for one-liners), with its parameters between vertical bars. The collection controls the loop and hands each item to your block. That is why each_with_index, 3.times and (1..3).each all read the same way — they are ordinary methods, not syntax, and you can write your own. Braces bind more tightly than do/end; convention is braces for one line, do/end for more.Enumerable replaces LINQ
Every LINQ operator has a counterpart, mostly under a shorter name.
Option Strict On
Imports System
Imports System.Linq
Module LinqDemo
Sub Main()
Dim numbers() As Integer = {5, 3, 9, 1, 7, 2}
Dim result = numbers.
Where(Function(number) number > 2).
Select(Function(number) number * 10).
ToList()
Console.WriteLine(String.Join(", ", result))
Console.WriteLine(numbers.Sum())
Console.WriteLine(numbers.Any(Function(number) number > 8))
Console.WriteLine(numbers.FirstOrDefault(Function(number) number > 6))
Console.WriteLine(numbers.OrderBy(Function(number) number).First())
End Sub
End Modulenumbers = [5, 3, 9, 1, 7, 2]
result = numbers.select { |number| number > 2 }
.map { |number| number * 10 }
puts result.join(", ")
puts numbers.sum
puts numbers.any? { |number| number > 8 }
puts numbers.find { |number| number > 6 }
puts numbers.min
puts numbers.each_slice(2).to_a.inspect
puts numbers.group_by(&:even?).inspectWhere→select, Select→map, Any→any?, All→all?, FirstOrDefault→find, OrderBy→sort_by, Aggregate→reduce, GroupBy→group_by, Sum/Min/Max/Count keep their names in lower case. Note the false friend: Ruby's select is LINQ's Where, not its Select. All of it comes from one module, Enumerable, which any class gets by defining each and including it — so your own types become fully queryable for the price of one method.Passing a method name as a block
A shorthand you will see on nearly every line of real Ruby, and it is worth knowing what it actually is.
Option Strict On
Imports System
Imports System.Linq
Module ShorthandDemo
Sub Main()
Dim words() As String = {"pear", "fig", "apple"}
Console.WriteLine(String.Join(", ", words.Select(Function(word) word.ToUpper())))
Console.WriteLine(String.Join(", ", words.OrderBy(Function(word) word.Length)))
End Sub
End Modulewords = ["pear", "fig", "apple"]
puts words.map { |word| word.upcase }.join(", ")
puts words.map(&:upcase).join(", ")
puts words.sort_by(&:length).join(", ")
puts words.map(&:length).sumWhen a block does nothing but call one method on its argument,
{ |word| word.upcase } can be written &:upcase. The & converts its operand into a block, and a symbol knows how to become one — so &:upcase means "call upcase on each item". Visual Basic's AddressOf is the nearest thing in spirit, but it names a method in scope rather than one to be called on the argument. The same & in a parameter list captures a block as an object, which the next row uses.Writing a method that takes a block
Any method can take a block without declaring a parameter for it, and
yield is how it runs it.Option Strict On
Imports System
Imports System.Diagnostics
Module CallbackDemo
Function Timed(Of T)(label As String, work As Func(Of T)) As T
Console.WriteLine($"start {label}")
Dim result As T = work()
Console.WriteLine($"end {label}")
Return result
End Function
Sub Main()
Dim total As Integer = Timed("sum", Function() 1 + 2 + 3)
Console.WriteLine(total)
End Sub
End Moduledef timed(label)
puts "start #{label}"
result = yield
puts "end #{label}"
result
end
total = timed("sum") { 1 + 2 + 3 }
puts total
def maybe_twice
return enum_for(:maybe_twice) unless block_given?
yield 1
yield 2
end
maybe_twice { |value| puts value }There is no
Func(Of T) parameter and no delegate type. Every method may be passed a block, yield calls it, and block_given? asks whether one arrived. This is the mechanism behind each, map, File.open and essentially every Ruby API that wraps something — the method controls setup and cleanup while your code supplies the middle. That is also what makes Using unnecessary, as the errors section shows. To keep the block as an object instead, name it with &work in the parameter list and call it with work.call.Blocks as objects: procs and lambdas
When a block needs to be stored rather than passed immediately, it becomes an object — and there is no delegate type to declare.
Option Strict On
Imports System
Imports System.Collections.Generic
Module DelegateDemo
Sub Main()
Dim twice As Func(Of Integer, Integer) = Function(value) value * 2
Dim shout As Action(Of String) = Sub(message) Console.WriteLine(message.ToUpper())
Dim operations As New Dictionary(Of String, Func(Of Integer, Integer)) From {
{"twice", twice}
}
Console.WriteLine(twice(21))
shout("done")
Console.WriteLine(operations("twice")(5))
End Sub
End Moduletwice = ->(value) { value * 2 }
shout = ->(message) { puts message.upcase }
operations = { "twice" => twice }
puts twice.call(21)
puts twice.(21)
puts twice[21]
shout.call("done")
puts operations["twice"].call(5)->(value) { ... } creates a lambda, Ruby's equivalent of a Func or Action — one object type covers both, since a lambda always returns its last expression. Call it with call, or the shorthands .() and []. There is a looser cousin, Proc.new / proc, which does not check its argument count and whose return returns from the enclosing method; lambdas behave the way a .NET delegate does, so prefer them.Methods
Sub and Function both become def
One keyword covers both, the return type is gone, and so is the
Return.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 Moduledef announce(message)
puts "** #{message} **"
end
def add(left, right)
left + right
end
def twice(value) = value * 2
announce "starting"
puts add(2, 3)
puts twice(21)Sub and Function both become def. A method returns its last expression, so Return is only written for an early exit — anyone who remembers VB6's Function Add ... Add = 5 will find this familiar in spirit. Parentheses are optional at the call site, which is why announce "starting" and puts add(2, 3) both read as they do. The one-line form def twice(value) = value * 2 is Ruby 3.0 and later, and is the direct counterpart of Function Twice(value) As Integer written on one line.Optional and named arguments
Named arguments exist, and Ruby lets a method insist on them.
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 Moduledef greet(name, greeting: "Hello", punctuation: "!")
"#{greeting}, #{name}#{punctuation}"
end
puts greet("Ada")
puts greet("Grace", greeting: "Welcome")
puts greet("Alan", punctuation: ".")
def sum_all(*values, **options)
total = values.sum
options[:double] ? total * 2 : total
end
puts sum_all(1, 2, 3)
puts sum_all(1, 2, 3, double: true)A parameter written
greeting: "Hello" is a keyword argument: it must be passed by name, in any order, which removes the "what is the third argument again" problem entirely. Writing greeting: with no default makes it required. name:= becomes name:. ParamArray becomes *values, and **options collects any extra keyword arguments into a hash — the half Visual Basic has no counterpart for.There is no ByRef
Every argument is passed the same way, and what happens next depends on the object rather than the call.
Option Strict On
Imports System
Imports System.Collections.Generic
Module ByRefDemo
Sub Twice(ByRef value As Integer)
value *= 2
End Sub
Sub AddItem(items As List(Of Integer))
items.Add(99)
End Sub
Sub Main()
Dim number As Integer = 21
Twice(number)
Console.WriteLine(number)
Dim numbers As New List(Of Integer) From {1}
AddItem(numbers)
Console.WriteLine(numbers.Count)
End Sub
End Moduledef twice(value)
value *= 2 # rebinds the local name only
value
end
def add_item(items)
items << 99 # mutates the caller's array
end
number = 21
twice(number)
puts number
number = twice(number)
puts number
numbers = [1]
add_item(numbers)
puts numbers.lengthThere is no
ByRef. Assigning to a parameter rebinds the local name and the caller sees nothing, which is why twice has to return its result. Calling a mutating method on a passed object changes the object the caller holds. The rule is "rebinding versus mutating", not "value versus reference" — and Ruby gives you a way to opt out: freeze the object and the mutation raises instead.There is no overloading
Two methods with the same name and different signatures cannot coexist — the second simply replaces the first.
Option Strict On
Imports System
Module OverloadDemo
Function Area(side As Double) As Double
Return side * side
End Function
Function Area(width As Double, height As Double) As Double
Return width * height
End Function
Sub Main()
Console.WriteLine(Area(3))
Console.WriteLine(Area(3, 4))
End Sub
End Moduledef area(width, height = nil)
height.nil? ? width * width : width * height
end
puts area(3)
puts area(3, 4)
def describe(value)
case value
when Integer then "integer #{value}"
when String then "text #{value}"
else "something else"
end
end
puts describe(4)
puts describe("four")Ruby dispatches on the method name alone, so there is no overload resolution and no way to have
Area(side) and Area(width, height) at once — defining the second silently discards the first. The two replacements are default arguments (one method that notices what it was given) and a case on the argument's class. Constructors have the same limitation: one initialize per class, so multiple Sub New overloads become class-level factory methods such as Point.from_polar(...).Classes, Modules & Mixins
A class and its constructor
The constructor gets a fixed name, and instance fields are marked by a sigil rather than declared.
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 Moduleclass Person
def initialize(name, age)
@name = name
@age = age
end
def describe
"#{@name}, age #{@age}"
end
end
person = Person.new("Ada", 36)
puts person.describePublic Sub New becomes initialize, and New Person(...) becomes Person.new(...) — new is an ordinary class method, not a keyword. A name beginning with @ is an instance variable: it needs no declaration, springs into existence on first assignment, and is always private. Me becomes self, and is rarely written because a bare method call already goes to self. Everything in a class body executes when the class is defined, which is what makes the next row possible.Properties
There is no property construct at all — a property is a pair of ordinary methods, and one line writes them for you.
Option Strict On
Imports System
Public Class Temperature
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()
reading.Celsius = 100.0
Console.WriteLine($"{reading.Label}: {reading.Fahrenheit}")
reading.Celsius = -500
Console.WriteLine(reading.Celsius)
End Sub
End Moduleclass Temperature
attr_accessor :label
attr_reader :celsius
def initialize
@label = "reading"
@celsius = 0.0
end
def celsius=(value)
@celsius = [value, -273.15].max
end
def fahrenheit
@celsius * 9.0 / 5.0 + 32
end
end
reading = Temperature.new
reading.celsius = 100.0
puts "#{reading.label}: #{reading.fahrenheit}"
reading.celsius = -500
puts reading.celsiusPublic Property Label becomes attr_accessor :label, which defines a reader and a writer; attr_reader alone is ReadOnly Property. A setter with logic is just a method whose name ends in = — def celsius=(value) — and Ruby lets you call it as reading.celsius = 100. Fahrenheit with only a getter is simply a method. Because there is no syntactic difference between a field read and a method call, you can start with attr_accessor and replace it with a real method later without any caller changing.Inheritance and overriding
Three of the four keywords in the left column have nothing to translate to, because every method is overridable.
Option Strict On
Imports System
Public MustInherit 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 MyBase.Speak() & " — a bark"
End Function
End Class
Module InheritanceDemo
Sub Main()
Dim pets As Animal() = {New Dog("Rex")}
For Each pet As Animal In pets
Console.WriteLine(pet.Speak())
Next
End Sub
End Moduleclass Animal
def initialize(name)
@name = name
end
def speak
"#{@name} makes a sound"
end
end
class Dog < Animal
def speak
super + " — a bark"
end
end
[Dog.new("Rex")].each { |pet| puts pet.speak }Inherits Animal becomes < Animal on the class header. Overridable and Overrides have no equivalent — redefining a method in a subclass is all it takes, which also means a misspelled name quietly adds a method rather than replacing one. MyBase becomes super, and bare super passes the same arguments through, which is why Dog needs no initialize at all. MustInherit has no keyword: a base class that should not be used directly raises NotImplementedError from the method instead.Interfaces become mixins
The construct that replaces an interface can carry working code, which is what makes it a different tool rather than a renamed one.
Option Strict On
Imports System
Public Interface IGreeter
Function Greet(name As String) As String
End Interface
Public Class Formal
Implements IGreeter
Public Function Greet(name As String) As String Implements IGreeter.Greet
Return $"Good day, {name}."
End Function
Public Function GreetTwice(name As String) As String
Return Greet(name) & " " & Greet(name)
End Function
End Class
Module MixinDemo
Sub Main()
Console.WriteLine(New Formal().GreetTwice("Ada"))
End Sub
End Modulemodule Greeting
# A mixin may carry real behavior, not just a signature
def greet_twice(name)
"#{greet(name)} #{greet(name)}"
end
end
class Formal
include Greeting
def greet(name)
"Good day, #{name}."
end
end
puts Formal.new.greet_twice("Ada")
puts Formal.ancestors.first(3).inspect
puts Formal.new.is_a?(Greeting)An
Interface becomes a module, and Implements becomes include. The difference is that a module may contain real method bodies — so greet_twice is shared code, not a declaration, and any class that supplies greet gets it. This is a mixin, and it is how Comparable and Enumerable give you dozens of methods for defining one. A class may include any number of modules, which is multiple inheritance of behavior without the diamond problem, and is_a? still answers correctly.Value objects without the boilerplate
One line declares the fields, the constructor, value equality and a readable inspection — the same trade C# records offer.
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 ValueDemo
Sub Main()
Dim origin As New Point(1, 2)
Dim same As New Point(1, 2)
Console.WriteLine(origin)
Console.WriteLine(origin.Equals(same))
End Sub
End ModulePoint = Data.define(:x, :y) do
def to_s = "(#{x}, #{y})"
end
origin = Point.new(x: 1, y: 2)
same = Point.new(x: 1, y: 2)
puts origin
puts origin == same
puts origin.inspect
puts origin.with(y: 9).inspectData.define (Ruby 3.2 and later) builds an immutable value object: readers for each field, a keyword constructor, == comparing by value, a useful inspect, and with for a copy with changes. That is what a Visual Basic Structure gives you plus what it does not. Its older sibling Struct.new is mutable and allows positional construction. Note the class is assigned to a constant — Point — which is exactly why class names begin with a capital letter.Requiring Code & Gems
Imports becomes require
Two different jobs that
Imports does at once are split apart here.Option Strict On
Imports System
Imports System.Text.Json
Module RequireDemo
Sub Main()
Dim payload = New With {.name = "Ada", .age = 36}
Dim text As String = JsonSerializer.Serialize(payload)
Console.WriteLine(text)
End Sub
End Modulerequire "json"
require "date"
payload = { name: "Ada", age: 36 }
text = JSON.generate(payload)
puts text
puts JSON.parse(text)["name"]
puts Date.new(2026, 1, 1).yearImports both loads an assembly reference and shortens names. Ruby separates them: require loads a file and runs it, once; shortening a name is include for a module's methods or a plain constant assignment (Serializer = JSON). Names are not scoped by file — once a class is defined it is visible everywhere — so require is about making sure the definition has happened, not about visibility. Nesting is written with ::, as in Net::HTTP.NuGet becomes gems
The packaging story maps across almost one to one, with different names for each piece.
Option Strict On
Imports System
Imports System.Collections.Generic
Module PackageDemo
Sub Main()
' A .csproj/.vbproj lists PackageReference entries;
' NuGet restores them into the build
Dim versions As New Dictionary(Of String, String) From {
{"runtime", ".NET 10"},
{"packages", "NuGet"}
}
For Each entry In versions
Console.WriteLine($"{entry.Key}: {entry.Value}")
Next
End Sub
End Module# A Gemfile lists gems; `bundle install` resolves and locks them:
#
# source "https://rubygems.org"
# gem "rails", "~> 8.0"
# gem "rspec", group: :test
#
# then `bundle exec ruby app.rb` runs with exactly those versions.
versions = { runtime: RUBY_VERSION, packages: "RubyGems" }
versions.each do |key, value|
puts "#{key}: #{value}"
endA gem is a NuGet package; RubyGems is nuget.org; the Gemfile is the
PackageReference list in your project file, and Gemfile.lock is packages.lock.json. bundle install is dotnet restore, and bundle exec runs a command against exactly the locked versions — the step with no .NET counterpart, because .NET binds versions at build time. The version constraint ~> 8.0 means "at least 8.0, less than 9.0".What replaces the base class library
The habit worth forming: before writing a loop over a collection, check whether
Enumerable already named it.Option Strict On
Imports System
Imports System.Collections.Generic
Imports System.Linq
Module LibraryDemo
Sub Main()
Dim words() As String = {"pear", "apple", "pear", "fig"}
Dim counts As New Dictionary(Of String, Integer)
For Each word As String In words
If counts.ContainsKey(word) Then
counts(word) += 1
Else
counts(word) = 1
End If
Next
For Each entry In counts.OrderBy(Function(pair) pair.Key)
Console.WriteLine($"{entry.Key}: {entry.Value}")
Next
End Sub
End Modulewords = ["pear", "apple", "pear", "fig"]
counts = words.tally
counts.sort.each do |word, count|
puts "#{word}: #{count}"
end
puts words.uniq.inspect
puts words.each_with_object(Hash.new(0)) { |word, memo| memo[word] += 1 }.inspectThe whole tally loop is
tally. That is representative — Enumerable alone has tally, partition, chunk_while, each_cons, each_slice, zip, flat_map, sum and minmax, most of which LINQ has no counterpart for. Beyond it the standard library ships json, csv, date, set, net/http, digest and logger. Anything more comes from a gem, and the ecosystem is unusually deep for web work.Error Handling
Try/Catch becomes begin/rescue
The same construct with different keywords — plus a clause .NET does not have, and one important difference in what a bare
rescue catches.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 Modulebegin
Integer("not a number")
rescue ArgumentError => error
puts "Bad number: #{error.message}"
rescue StandardError => error
puts "Something else: #{error.message}"
else
puts "only if nothing was raised"
ensure
puts "always runs"
endTry→begin, Catch x As T→rescue T => x, Finally→ensure, End Try→end. The extra clause is else, which runs only when nothing was raised. The important difference: a bare rescue catches StandardError, not every exception — SignalException, NoMemoryError and SystemExit descend from Exception and are deliberately left alone. Writing rescue Exception to be thorough is a well-known mistake, because it swallows Ctrl-C.Raising your own error
Defining an error is the inheritance you already saw, applied to one particular 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}")
Me.Shortfall = shortfall
End Sub
End Class
Module RaiseDemo
Sub Withdraw(balance As Decimal, amount As Decimal)
If amount > balance Then
Throw New InsufficientFundsException(amount - balance)
End If
End Sub
Sub Main()
Try
Withdraw(50D, 75D)
Catch error_ As InsufficientFundsException
Console.WriteLine($"{error_.Message} (short {error_.Shortfall})")
End Try
End Sub
End Moduleclass InsufficientFundsError < StandardError
attr_reader :shortfall
def initialize(shortfall)
@shortfall = shortfall
super("Short by #{shortfall}")
end
end
def withdraw(balance, amount)
raise InsufficientFundsError, amount - balance if amount > balance
end
begin
withdraw(50, 75)
rescue InsufficientFundsError => error
puts "#{error.message} (short #{error.shortfall})"
endThrow New becomes raise, and Inherits Exception becomes < StandardError — inherit from StandardError, not Exception, so a bare rescue catches it. MyBase.New(message) becomes super(message). Note the statement modifier doing the guard on one line, and that raise Class, argument is shorthand for raise Class.new(argument). Convention ends the class name in Error, not Exception. A bare raise inside a rescue re-raises with the backtrace intact.Using becomes a method that takes a block
There is no
Using keyword, because the block mechanism already covers it — and covers it better.Option Strict On
Imports System
Imports System.IO
Module UsingDemo
Sub Main()
Dim contents As String = ""
Using writer As New StringWriter()
writer.WriteLine("first line")
writer.WriteLine("second line")
contents = writer.ToString()
End Using
Console.WriteLine(contents.Trim())
End Sub
End Modulerequire "stringio"
contents = StringIO.open do |writer|
writer.puts "first line"
writer.puts "second line"
writer.string
end
puts contents.strip
def with_resource(name)
puts "open #{name}"
yield
ensure
puts "close #{name}"
end
puts with_resource("thing") { "did the work" }Ruby has no
Using and no IDisposable. Instead, the method that opens the resource takes a block, does its own ensure, and hands you the open thing — File.open, StringIO.open, Net::HTTP.start all work this way. The caller cannot forget the cleanup, because the caller never owns it, which is a stronger guarantee than Using gives. Writing your own takes four lines, as with_resource shows; note that a method body can carry ensure directly with no begin.Metaprogramming
Classes stay open
Where .NET lets you add the appearance of a method to a type you do not own, Ruby lets you actually add one.
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 OpenDemo
Sub Main()
Console.WriteLine("hello".Shout())
End Sub
End Moduleclass String
def shout
upcase + "!"
end
end
puts "hello".shout
module Politeness
refine String do
def please = "#{self}, please"
end
end
using Politeness
puts "pass the salt".pleaseA class body can be reopened at any time and gains whatever you put in it — so
String really does get a shout method, on every string in the program. That is far more powerful than an extension method, and far more dangerous: two libraries defining the same method on String silently overwrite each other, which is why this is called monkey patching and treated with caution. Refinements are the disciplined version: refine plus using scopes the change to one file. Prefer them, and prefer your own class over either.Writing methods at runtime
A class body is ordinary code that runs when the class is defined — so a loop inside it can write methods.
Option Strict On
Imports System
Public Class Settings
Public Property Width As Integer
Public Property Height As Integer
Public Property Depth As Integer
End Class
Module DefineDemo
Sub Main()
Dim settings As New Settings With {.Width = 10, .Height = 20, .Depth = 30}
Console.WriteLine($"{settings.Width} {settings.Height} {settings.Depth}")
End Sub
End Moduleclass Settings
[:width, :height, :depth].each do |name|
define_method(name) { @values[name] }
define_method("#{name}=") { |value| @values[name] = value }
end
def initialize
@values = {}
end
end
settings = Settings.new
settings.width = 10
settings.height = 20
settings.depth = 30
puts "#{settings.width} #{settings.height} #{settings.depth}"
puts settings.respond_to?(:depth)The
each loop above runs at class-definition time and define_method creates a real method for each name. This is how attr_accessor itself is implemented, and how Rails generates a method per database column without anyone writing them. .NET can do this only by emitting IL or generating source at build time; here it is three lines of ordinary Ruby. respond_to? asks whether an object has a method, which is the duck-typed replacement for an interface check.Answering a method that does not exist
A call to a method that was never defined is not automatically an error — the object gets a chance to answer it.
Option Strict On
Imports System
Imports System.Collections.Generic
Public Class Record
Private ReadOnly _values As New Dictionary(Of String, Object)
Default Public Property Item(key As String) As Object
Get
Return If(_values.ContainsKey(key), _values(key), Nothing)
End Get
Set(value As Object)
_values(key) = value
End Set
End Property
End Class
Module MissingDemo
Sub Main()
Dim record As New Record()
record("title") = "Report"
Console.WriteLine(record("title"))
Console.WriteLine(record("missing") Is Nothing)
End Sub
End Moduleclass Record
def initialize = @values = {}
def method_missing(name, *args)
key = name.to_s
if key.end_with?("=")
@values[key.chomp("=")] = args.first
else
@values.fetch(key, nil)
end
end
def respond_to_missing?(_name, _private = false) = true
end
record = Record.new
record.title = "Report"
puts record.title
puts record.missing.nil?When no method matches, Ruby calls
method_missing with the name and arguments, and whatever that returns is the result. So record.title works without a title method existing. The nearest .NET equivalents are DynamicObject and Default Property, both far more constrained. Always define respond_to_missing? alongside it, or respond_to? will lie about the object. This is powerful and easy to overuse: it makes typos into silent successes, which is exactly the trade the anchor column's explicit indexer avoids.⚠ Gotchas for Visual Basic Programmers
⚠ / truncates between integers
The single most likely line to be silently wrong after translating Visual Basic code by hand.
Option Strict On
Imports System
Module DivisionGotcha
Sub Main()
Dim average As Double = (3 + 4) / 2
Console.WriteLine(average)
End Sub
End Moduleaverage = (3 + 4) / 2
puts average
correct = (3 + 4) / 2.0
puts correct
puts (3 + 4).fdiv(2)
puts [3, 4].sum.fdiv(2)Visual Basic reserves
\ for integer division and makes / always floating-point. Ruby has no \, and / between two Integers truncates — so an average calculation that was correct becomes wrong with no error, no warning and a plausible-looking result. Make one operand a Float (2.0) or call fdiv. Worth grepping every / for when porting.⚠ Zero and "" are truthy
Ruby's falsy list is short, and short in a direction that catches people arriving from Python or JavaScript.
Option Strict On
Imports System
Module TruthyGotcha
Sub Main()
Dim count As Integer = 0
Dim text As String = ""
If count = 0 Then Console.WriteLine("no items")
If text.Length = 0 Then Console.WriteLine("no text")
End Sub
End Modulecount = 0
text = ""
puts "count is truthy" if count
puts "text is truthy" if text
puts "no items" if count.zero?
puts "no text" if text.empty?
value = false
puts "false and nil are the only falsy values" unless valueOnly
nil and false are falsy. 0 is truthy, "" is truthy, [] and {} are truthy. If you have written any Python this is backwards from what you expect, and if you have written JavaScript it is half backwards. In practice it is the least surprising of the three rules once learned, because if value then means precisely "is there a value here" — but a ported If count = 0 written as if !count is simply wrong, and prints nothing rather than raising.⚠ Assignment shares the object
The aliasing rule is the same as .NET's — but there is no
As clause to remind you which kind of thing you are holding.Option Strict On
Imports System
Imports System.Collections.Generic
Module SharingGotcha
Sub Main()
Dim original As New List(Of Integer) From {1, 2, 3}
Dim alias_ As List(Of Integer) = original
Dim copy As New List(Of Integer)(original)
alias_.Add(4)
Console.WriteLine(original.Count)
Console.WriteLine(copy.Count)
End Sub
End Moduleoriginal = [1, 2, 3]
alias_list = original
copy = original.dup
alias_list << 4
puts original.length
puts copy.length
row = ["-"]
grid = Array.new(3, row)
grid[0] << "x"
puts grid.inspect # every row changed
better = Array.new(3) { ["-"] }
better[0] << "x"
puts better.inspectalias_list = original gives the same array a second name; dup makes a shallow copy, matching New List(Of Integer)(original). The version worth memorizing is the second half: Array.new(3, row) stores the same object three times, so mutating one row mutates all three. Passing a block instead — Array.new(3) { ["-"] } — runs it once per element and gives three separate arrays. The same trap applies to Hash.new([]).⚠ A typo waits until that line runs
The most consequential difference on the page, and the one no amount of care in the code itself can fix.
Option Strict On
Imports System
Module LateErrorGotcha
Sub Main()
Dim total As Integer = 10
If total > 100 Then
' A misspelling here fails to COMPILE,
' so the program never ships
Console.WriteLine(total)
End If
Console.WriteLine("finished")
End Sub
End Moduletotal = 10
if total > 100
puts totl # misspelled — but this line never runs
end
puts "finished"Ruby checks syntax before running and nothing else. A misspelled name, a call with the wrong arity, a method that does not exist — all surface when the line executes, and a branch that never runs is never checked. There is no
Option Strict and no Option Explicit. What replaces them is discipline the language does not enforce: a test suite that exercises every branch (RSpec or Minitest), a linter (RuboCop), and optionally type signatures in RBS checked by Steep or Sorbet. Set those up first, not after the first production surprise.⚠ Redefining a method replaces it silently
There is no overloading, and the second definition of a name quietly wins.
Option Strict On
Imports System
Module RedefineGotcha
Function Area(side As Double) As Double
Return side * side
End Function
' A second definition with the SAME signature
' would not compile — this one differs, so it overloads
Function Area(width As Double, height As Double) As Double
Return width * height
End Function
Sub Main()
Console.WriteLine(Area(3))
Console.WriteLine(Area(3, 4))
End Sub
End Moduledef area(side)
side * side
end
def area(width, height)
width * height
end
puts area(3, 4)
begin
puts area(3)
rescue ArgumentError => error
puts "the one-argument version is gone: #{error.message}"
endRuby dispatches on the method name alone, so the second
area replaces the first with no error and no warning — and the only sign is an ArgumentError at the call site later. The same applies across files, which is what makes monkey patching risky. The replacements are default arguments, keyword arguments, or a case on the argument's class. When you genuinely need two constructors, write class-level factory methods instead of two initialize definitions.⚠ No My namespace, and no Windows
Everything the
My namespace offered has an equivalent — the larger change is what kind of program you end up writing.Option Strict On
Imports System
Module PlatformGotcha
Sub Main()
' My.Computer, My.Application, MsgBox, InputBox and the
' WinForms designer are Visual Basic conveniences
Console.WriteLine(Environment.MachineName.Length > 0)
Console.WriteLine(IsNumeric("42"))
Console.WriteLine(Now.Year > 2000)
End Sub
End Modulerequire "socket"
require "date"
puts Socket.gethostname.length > 0
puts !Integer("42", exception: false).nil?
puts Date.today.year > 2000
puts RUBY_PLATFORM.length > 0My.Computer.Name is Socket.gethostname, My.Computer.FileSystem is File, Dir and FileUtils, IsNumeric is Integer(x, exception: false), Now is Time.now or Date.today. MsgBox and InputBox have no counterpart: Ruby has essentially no desktop-GUI story, so a WinForms application does not port — it becomes a command-line tool or, far more commonly, a web application in Rails or Sinatra. That is the honest shape of this move, and it is a bigger decision than any syntax on this page.