PONYλM2Modula-2

Visual Basic.CodeCompared.To/Ruby

An interactive executable cheatsheet comparing Visual Basic and Ruby

Visual Basic (.NET 10) Ruby 4.0
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 Module
puts "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 Module
name = "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 Module
numbers = [1, 2, 3] puts numbers.join(", ") p numbers print "no newline" puts " — then newline" puts numbers.inspect
puts 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 Module
temperature = 30 if temperature > 25 puts "Warm" elsif temperature > 10 puts "Mild" else puts "Cold" end
Ruby 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 Module
customer_name = "Grace" MAXIMUM_RETRIES = 3 puts customer_name puts MAXIMUM_RETRIES MAXIMUM_RETRIES = 5 # warning: already initialized constant puts MAXIMUM_RETRIES
Identifiers 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 Module
count = 0 ready = true puts "go" if ready puts "wait" unless ready count += 1 while count < 3 puts count
Any 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 Module
count = 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 Module
number = 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).inspect
In .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 Module
missing_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 Module
require "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 Module
text = "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 Module
left = "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 Module
age = 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 Module
quotient = 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).inspect
Ruby 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 Module
supplied = 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 Module
words = ["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 Module
text = " 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, ".")
Trimstrip, ToUpperupcase, Containsinclude?, StartsWithstart_with?, Replacesub/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 Module
text = "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 Module
text = "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 Module
settings = { 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.inspect
A 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 Module
quoted = 'She said "hello".' path = 'C:\reports\summary.txt' block = <<~TEXT line one line two TEXT puts quoted puts path puts block
A 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 Module
fruits = ["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 Module
ages = { "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 Module
value = 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" end
1..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 Module
def 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 Module
require "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 Module
items = [] 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 text
Only 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 Module
def 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 Casecase, Casewhen, Case Elseelse, End Selectend. 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 Module
score = 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 Module
def 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 Module
words = ["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 Module
numbers = [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?).inspect
Whereselect, Selectmap, Anyany?, Allall?, FirstOrDefaultfind, OrderBysort_by, Aggregatereduce, GroupBygroup_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 Module
words = ["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).sum
When 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 Module
def 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 Module
twice = ->(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 Module
def 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 Module
def 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 Module
def 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.length
There 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 Module
def 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 Module
class Person def initialize(name, age) @name = name @age = age end def describe "#{@name}, age #{@age}" end end person = Person.new("Ada", 36) puts person.describe
Public 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 Module
class 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.celsius
Public 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 Module
class 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 Module
module 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 Module
Point = 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).inspect
Data.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 constantPoint — 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 Module
require "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).year
Imports 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}" end
A 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 Module
words = ["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 }.inspect
The 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 Module
begin 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" end
Trybegin, Catch x As Trescue T => x, Finallyensure, End Tryend. 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 Module
class 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})" end
Throw 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 Module
require "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 Module
class 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".please
A 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 Module
class 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 Module
class 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 Module
average = (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 Module
count = 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 value
Only 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 Module
original = [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.inspect
alias_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 Module
total = 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 Module
def 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}" end
Ruby 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 Module
require "socket" require "date" puts Socket.gethostname.length > 0 puts !Integer("42", exception: false).nil? puts Date.today.year > 2000 puts RUBY_PLATFORM.length > 0
My.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.