PONY λ M2 Modula-2

C#.CodeCompared.To/Ruby

An interactive executable cheatsheet comparing C# and Ruby

C# 13 Ruby 4.0
Hello World & Output
Hello, World
Console.WriteLine("Hello, World!");
puts "Hello, World!"
Ruby uses puts to print a value followed by a newline — equivalent to Console.WriteLine. Parentheses on method calls are optional in Ruby; the convention is to omit them for zero- or one-argument calls at the statement level.
String interpolation
var name = "Rubyist"; var score = 42; Console.WriteLine($"Hello, {name}! Score: {score}");
name = "Rubyist" score = 42 puts "Hello, #{name}! Score: #{score}"
Ruby uses #{ } inside double-quoted strings for interpolation. Any Ruby expression is valid inside the braces. Single-quoted strings are literal — no interpolation, no escape sequences (except \' and \\).
Output variants: <code>puts</code>, <code>print</code>, <code>p</code>
Console.WriteLine("hello"); // with newline Console.Write("hello"); // no newline Console.WriteLine("hello"); // full inspect via string formatting
puts "hello" # with newline print "hello " # same; explicit newline p "hello" # inspect: prints "hello" (with quotes) — great for debugging
Ruby has three common output methods. puts adds a newline and calls .to_s on the argument. print writes without a newline. p calls .inspect and prints the developer representation — the equivalent of Console.WriteLine(value.ToString("R")) for debugging.
Variables & Types
Dynamic typing — no type annotations
var greeting = "hello"; // inferred as string var count = 42; // inferred as int var active = true; // inferred as bool Console.WriteLine($"{greeting} {count} {active}");
greeting = "hello" count = 42 active = true puts "#{greeting} #{count} #{active}"
Ruby is dynamically typed — variables are assigned without any type annotation and can hold any object. There is no var, int, or string keyword. Type checks happen at runtime, not compile time. A variable's type is determined by the object currently assigned to it.
Constants
const double GRAVITY = 9.8; const int MAX_PLAYERS = 8; Console.WriteLine($"Gravity: {GRAVITY}, Max: {MAX_PLAYERS}");
GRAVITY = 9.8 MAX_PLAYERS = 8 puts "Gravity: #{GRAVITY}, Max: #{MAX_PLAYERS}"
Ruby constants begin with an uppercase letter — the convention is ALL_CAPS for true constants, CamelCase for class and module names. Ruby only warns when a constant is reassigned; it does not enforce immutability. For frozen values use a frozen object: MAX_SPEED = 100.freeze.
<code>nil</code> — Ruby's null
string? name = null; string display = name ?? "anonymous"; Console.WriteLine(display); Console.WriteLine(name?.Length);
name = nil display = name || "anonymous" puts display puts name&.length
nil is Ruby's equivalent of null — a first-class object of class NilClass. The || operator replaces C#'s ?? (nil and false are the only falsy values in Ruby). The safe navigation operator &. replaces C#'s null-conditional ?..
Multiple assignment (destructuring)
(int first, int second, int third) = (10, 20, 30); Console.WriteLine($"{first} {second} {third}");
first, second, third = 10, 20, 30 puts "#{first} #{second} #{third}"
Ruby supports multiple assignment natively — values on the right are assigned positionally to variables on the left. Splatting captures excess values: head, *rest = [1, 2, 3, 4] sets head to 1 and rest to [2, 3, 4]. Swap two variables without a temp: a, b = b, a.
Type checking at runtime
object value = "hello"; Console.WriteLine(value is string); Console.WriteLine(value.GetType().Name); if (value is string text) Console.WriteLine(text.ToUpper());
value = "hello" puts value.is_a?(String) puts value.class puts value.upcase if value.is_a?(String)
Ruby uses .is_a?(ClassName) to check type membership (returns true if the object is an instance of that class or any subclass). .class returns the object's actual class. .respond_to?(:method_name) is the idiomatic Ruby alternative — duck-typing favors capability over identity.
Strings
Common string methods
string message = "Hello, World!"; Console.WriteLine(message.ToUpper()); Console.WriteLine(message.Contains("World")); Console.WriteLine(message.Replace("World", "Ruby")); Console.WriteLine(message.Length);
message = "Hello, World!" puts message.upcase puts message.include?("World") puts message.gsub("World", "Ruby") puts message.length
Ruby string methods use snake_case. Convention: methods that return a boolean end with ? (include?, empty?, start_with?). Methods that mutate in place end with ! (upcase!, gsub!) — without !, a new string is returned. In Ruby 4.0, string literals are frozen by default.
Split and join
string csv = "alpha,beta,gamma"; string[] parts = csv.Split(','); Console.WriteLine(string.Join(" | ", parts));
csv = "alpha,beta,gamma" parts = csv.split(",") puts parts.join(" | ")
.split(separator) returns an Array. .join(separator) is called on the array, not a class method — the opposite of C#'s string.Join(sep, collection). Calling split with no argument splits on any whitespace and removes leading/trailing whitespace.
Multiline strings (heredoc)
string text = """ Line one Line two Line three """; Console.Write(text);
text = <<~HEREDOC Line one Line two Line three HEREDOC print text
Ruby's <<~HEREDOC strips leading whitespace to the level of the least-indented line, equivalent to C# 11's raw string literals. The delimiter (any word) must appear alone on the closing line. Heredocs support interpolation by default; use single-quoted delimiters (<<~'HEREDOC') to disable it.
String formatting
double value = 3.14159; Console.WriteLine(value.ToString("F2")); Console.WriteLine($"Pi is approximately {value:F2}");
value = 3.14159 puts "%.2f" % value puts format("Pi is approximately %.2f", value)
Ruby uses C-style sprintf formatting via the % operator on strings or the global format/sprintf methods. The % operator is concise for single values; format supports multiple arguments. There is no built-in format-specifier in interpolation — use "#{"%.2f" % value}" inside a string.
Trim and strip
string input = " hello world "; Console.WriteLine(input.Trim()); Console.WriteLine(input.TrimStart()); Console.WriteLine(input.TrimEnd());
input = " hello world " puts input.strip puts input.lstrip puts input.rstrip
Ruby's .strip is equivalent to C#'s .Trim(); .lstrip and .rstrip are TrimStart and TrimEnd. Since Ruby 4.0, string literals are frozen — calling .strip! on a literal will raise a FrozenError; use .strip which returns a new unfrozen string.
Regular expressions
string text = "The price is $42.50"; var match = System.Text.RegularExpressions.Regex.Match(text, @"$(d+.d+)"); if (match.Success) Console.WriteLine(match.Groups[1].Value);
text = "The price is $42.50" match = text.match(/$(d+.d+)/) puts match[1] if match
Regex literals are a first-class syntax in Ruby: /pattern/ is a Regexp object. The =~ operator matches and sets $~, $1, $2, etc. as special globals. .match() returns a MatchData object or nil — cleaner than the global variables for one-off matches.
Numbers & Math
Integer methods
for (int i = 1; i <= 5; i++) Console.WriteLine(i); Console.WriteLine(2 % 2 == 0 ? "even" : "odd");
5.times { |i| puts i + 1 } puts 2.even? ? "even" : "odd"
In Ruby, integers are full objects with methods. .times, .upto, .downto, and .step are iterators that yield each value to a block. .even?, .odd?, and .zero? return booleans. .to_f, .to_s, and .to_r convert to other types.
Arithmetic operators
Console.WriteLine(10 + 3); Console.WriteLine(10 - 3); Console.WriteLine(10 * 3); Console.WriteLine(10 / 3); // integer division: 3 Console.WriteLine(10 % 3); // remainder: 1 Console.WriteLine(Math.Pow(2, 10)); // 1024
puts 10 + 3 puts 10 - 3 puts 10 * 3 puts 10 / 3 # integer division: 3 puts 10 % 3 # remainder: 1 puts 2 ** 10 # exponentiation: 1024
Ruby has a built-in ** exponentiation operator — no Math.Pow call needed. Integer division rounds toward negative infinity (not toward zero as in C#): -7 / 2 is -4 in Ruby but -3 in C#. For float division from integers, convert first: 10.0 / 3 or 10.to_f / 3.
Math methods
Console.WriteLine(Math.Abs(-7)); Console.WriteLine(Math.Sqrt(16.0)); Console.WriteLine(Math.Min(3, 8));
puts(-7.abs) puts Math.sqrt(16.0) puts [3, 8].min
Ruby's Math module contains transcendental functions (Math.sqrt, Math.sin, Math.log). Simple numeric methods live directly on numbers: .abs, .ceil, .floor, .round. .min and .max are array methods — call them on a collection rather than as a static class method.
Numeric type conversion
double ratio = 3.9; int truncated = (int)ratio; // explicit cast: 3 Console.WriteLine(truncated);
ratio = 3.9 truncated = ratio.to_i # truncates toward zero: 3 puts truncated
Ruby converts between numeric types with .to_i, .to_f, .to_r (Rational), and .to_c (Complex). There are no implicit conversions — 1 + 1.5 works because Ruby promotes the integer to a float, but "42".to_i is required to parse a string; there is no automatic coercion.
Arrays
Array creation and access
var numbers = new List<int> { 10, 20, 30, 40 }; Console.WriteLine(numbers[0]); // first Console.WriteLine(numbers[^1]); // last (index from end) Console.WriteLine(numbers.Count);
numbers = [10, 20, 30, 40] puts numbers[0] # first puts numbers[-1] # last — negative indices from the end puts numbers.length
Ruby arrays use [] literal syntax and negative indices to access from the end: array[-1] is the last element. .first and .last also work. Unlike C#'s List<T>, Ruby arrays are untyped — they can hold objects of any class simultaneously.
Adding and removing elements
var items = new List<string> { "alpha", "beta" }; items.Add("gamma"); // append items.Insert(0, "prefix"); // prepend items.RemoveAt(0); // remove first Console.WriteLine(items.Count);
items = ["alpha", "beta"] items.push("gamma") # append (also: items << "gamma") items.unshift("prefix") # prepend items.shift # remove and return first puts items.length
Ruby's shovel operator << is the idiomatic way to append: array << value. push/pop operate at the tail; shift/unshift operate at the head. .delete(value) removes by value; .delete_at(index) removes by index — both return the removed element.
Filter and map (LINQ vs Enumerable)
var numbers = new List<int> { 1, 2, 3, 4, 5, 6 }; var evens = numbers.Where(n => n % 2 == 0).ToList(); var doubled = numbers.Select(n => n * 2).ToList(); Console.WriteLine(string.Join(", ", evens));
numbers = [1, 2, 3, 4, 5, 6] evens = numbers.select { |n| n % 2 == 0 } doubled = numbers.map { |n| n * 2 } puts evens.join(", ")
Ruby's Enumerable module is the equivalent of LINQ. The main methods: .map (transforms, like Select), .select (filters, like Where), .reject (inverse filter, no direct LINQ equivalent), .reduce/.inject (fold, like Aggregate), .find (first match, like FirstOrDefault).
Reduce / fold
var numbers = new List<int> { 1, 2, 3, 4, 5 }; int total = numbers.Aggregate(0, (acc, n) => acc + n); Console.WriteLine(total);
numbers = [1, 2, 3, 4, 5] total = numbers.reduce(0) { |accumulator, n| accumulator + n } puts total
Ruby's .reduce (aliased as .inject) combines all elements into one value. If the block is a single operator, pass it as a symbol: numbers.reduce(:+) sums all elements without a block. This idiom — .sum, .reduce(:*), .reduce(:+) — is common for arithmetic aggregation.
Array utilities: flatten, compact, uniq, sort
var nested = new List<object> { 1, null, new List<object>{2, 3}, null, 2 }; var flat = nested.OfType<int>().ToList(); // approx var unique = flat.Distinct().ToList(); var sorted = flat.OrderBy(n => n).ToList(); Console.WriteLine(string.Join(", ", sorted));
nested = [1, nil, [2, 3], nil, 2] flat = nested.flatten.compact # flatten nested arrays; remove nils unique = flat.uniq sorted = flat.sort puts sorted.join(", ")
.flatten recursively unwraps nested arrays; .flatten(1) goes one level deep. .compact removes all nil values. .uniq removes duplicates (like Distinct()). .sort uses <=> (spaceship operator) for comparison; .sort_by { |item| item.some_attribute } sorts by a computed key.
Hashes
Hash creation and access
var scores = new Dictionary<string, int> { { "Alice", 95 }, { "Bob", 87 }, }; Console.WriteLine(scores["Alice"]);
scores = { alice: 95, bob: 87 } # symbol keys (idiomatic) puts scores[:alice]
Ruby hashes use { key: value } literal syntax with symbol keys (the most common convention) or { "key" => value } with string keys. Access by key returns nil for missing keys (unlike C# which throws). Use .fetch(:key) to raise KeyError on missing keys — matching C#'s behavior.
Access with default values
var config = new Dictionary<string, string> { { "theme", "dark" } }; Console.WriteLine(config.GetValueOrDefault("theme", "light")); Console.WriteLine(config.GetValueOrDefault("volume", "50"));
config = { theme: "dark" } puts config.fetch(:theme, "light") puts config.fetch(:volume, "50")
.fetch(key, default) returns the value if present, or the default if absent — equivalent to GetValueOrDefault. A hash with a built-in default can be created with Hash.new(0) (missing keys return 0). This is useful for counting: counts = Hash.new(0); counts[:word] += 1.
Iterating entries
var population = new Dictionary<string, int> { { "Paris", 2_161_000 }, { "Tokyo", 13_960_000 } }; foreach (var (city, count) in population) Console.WriteLine($"{city}: {count}");
population = { paris: 2_161_000, tokyo: 13_960_000 } population.each do |city, count| puts "#{city}: #{count}" end
Ruby hashes are ordered by insertion (since Ruby 1.9) — iteration always visits entries in the order they were added, unlike C#'s Dictionary which does not guarantee order. .each yields a [key, value] pair; the block parameters are automatically destructured.
Transform: select, map, merge
var prices = new Dictionary<string, double> { { "apple", 0.99 }, { "banana", 0.49 }, { "cherry", 2.49 } }; var cheap = prices.Where(kv => kv.Value < 1.0) .ToDictionary(kv => kv.Key, kv => kv.Value); Console.WriteLine(cheap.Count);
prices = { apple: 0.99, banana: 0.49, cherry: 2.49 } cheap = prices.select { |_product, price| price < 1.0 } puts cheap.length
Ruby's Enumerable methods work on hashes too. .select returns a filtered hash; .map returns an array of the block's return values (use .transform_values { |v| ... } or .transform_keys { |k| ... } to get a new hash). .merge(other_hash) combines two hashes, with the block resolving conflicts.
Counting occurrences (Hash.new default)
var words = new[] { "apple", "banana", "apple", "cherry", "apple" }; var counts = new Dictionary<string, int>(); foreach (var word in words) counts[word] = counts.GetValueOrDefault(word) + 1; Console.WriteLine(counts["apple"]);
words = ["apple", "banana", "apple", "cherry", "apple"] counts = Hash.new(0) words.each { |word| counts[word] += 1 } puts counts[:apple]
Hash.new(0) creates a hash where any missing key returns 0 by default, making increment-or-initialize a single line. Ruby also provides .tally on Enumerable for exactly this pattern: words.tally returns { "apple" => 3, "banana" => 1, "cherry" => 1 }.
Control Flow
if / elsif / else
int score = 75; if (score >= 90) Console.WriteLine("A"); else if (score >= 70) Console.WriteLine("B"); else Console.WriteLine("C");
score = 75 if score >= 90 puts "A" elsif score >= 70 puts "B" else puts "C" end
Ruby uses elsif (not else if or elif). Blocks are closed with end — no braces. The condition needs no parentheses. Ruby's if is an expression that returns a value, so grade = if score >= 90 then "A" else "B" end is valid.
<code>unless</code> — inverse conditional
bool logged_in = false; if (!logged_in) Console.WriteLine("Please log in");
logged_in = false unless logged_in puts "Please log in" end
unless condition is equivalent to if !condition — it runs the body when the condition is falsy. It is idiomatic when a positive condition reads awkwardly with a leading negation. Avoid unless...else; the else branch makes the intent confusing. There is no C# equivalent.
<code>case / when</code> (switch equivalent)
int day = 3; string name = day switch { 1 => "Monday", 2 => "Tuesday", 3 => "Wednesday", _ => "Other", }; Console.WriteLine(name);
day = 3 name = case day when 1 then "Monday" when 2 then "Tuesday" when 3 then "Wednesday" else "Other" end puts name
Ruby's case/when uses the === operator for comparison, which enables powerful matching: when String checks type, when 1..10 checks range membership, when /pattern/ matches a regex. There is no fall-through — each arm is independent.
Inline / postfix conditionals
int count = 5; if (count > 0) Console.WriteLine("positive"); // inline Console.WriteLine(count > 0 ? "pos" : "neg"); // ternary
count = 5 puts "positive" if count > 0 # postfix if puts count > 0 ? "pos" : "neg" # ternary (works but postfix is preferred)
Postfix if and unless place the condition after the statement: expression if condition. This is idiomatic Ruby — it reads like plain English ("do this if that") and is preferred over a ternary for simple guards. The ternary operator exists but is less common for conditionals with no return value.
Type-based case matching
object value = 42; string description = value switch { int n => $"integer: {n}", string s => $"string: {s}", _ => "unknown", }; Console.WriteLine(description);
value = 42 description = case value when Integer then "integer: #{value}" when String then "string: #{value}" else "unknown" end puts description
Because case/when uses ===, writing when Integer calls Integer === value, which is equivalent to value.is_a?(Integer). This makes type-dispatching concise without needing pattern matching syntax. Classes, ranges, regexes, and lambdas all implement === for this purpose.
Loops & Iterators
<code>times</code>, <code>upto</code>, <code>downto</code>
for (int i = 0; i < 5; i++) Console.Write(i + " "); Console.WriteLine();
5.times { |i| print "#{i} " } puts
Ruby replaces C-style for loops with integer methods: 5.times yields 0–4; 1.upto(5) yields 1–5; 5.downto(1) yields 5–1; 1.step(10, 2) yields 1, 3, 5, 7, 9. The block receives the current value. This is the idiomatic Ruby alternative to the three-clause C# for.
Iterating over a collection
var fruits = new List<string> { "apple", "banana", "cherry" }; foreach (var fruit in fruits) Console.WriteLine(fruit);
fruits = ["apple", "banana", "cherry"] fruits.each do |fruit| puts fruit end
.each is the primary Ruby iteration method — it calls the block once per element. The do...end block syntax is equivalent to { ... } braces; convention uses do...end for multi-line blocks and braces for single-line. There is no C-style for loop; Ruby's for...in exists but is rarely used.
<code>while</code> and <code>until</code>
int counter = 0; while (counter < 5) { Console.WriteLine(counter); counter++; }
counter = 0 while counter < 5 puts counter counter += 1 end
Ruby has while (loop while true) and until (loop until true, the inverse). Both exist as postfix forms too: counter += 1 while counter < 10. There is no do...while; use loop { ... break if condition } for a guaranteed-first-iteration loop.
Iteration with index
var colors = new List<string> { "red", "green", "blue" }; foreach (var (color, index) in colors.Select((c, i) => (c, i))) Console.WriteLine($"{index}: {color}");
colors = ["red", "green", "blue"] colors.each_with_index do |color, index| puts "#{index}: #{color}" end
.each_with_index yields the element and its zero-based index simultaneously — equivalent to LINQ's .Select((item, index) => ...). .each_with_object(accumulator) yields the element and a mutable accumulator, avoiding a separate variable for building a result during iteration.
Range iteration
foreach (int n in Enumerable.Range(1, 5)) Console.WriteLine(n);
(1..5).each { |n| puts n }
Ruby's range literal 1..5 is an inclusive range; 1...5 is exclusive of the end (yields 1, 2, 3, 4). Ranges respond to all Enumerable methods: .map, .select, .to_a, etc. ('a'..'z').to_a creates an array of letters.
Blocks & Lambdas
Blocks — Ruby's fundamental callable unit
var numbers = new List<int> { 1, 2, 3, 4, 5 }; numbers.ForEach(n => Console.WriteLine(n * 2));
numbers = [1, 2, 3, 4, 5] numbers.each { |n| puts n * 2 }
A block is an anonymous chunk of code passed to a method — like a lambda in C# but without the Func<T> type requirement. Any method can accept a block implicitly. Single-line blocks use braces; multi-line use do...end. The |n| pipe-delimited identifiers are the block parameters.
Lambdas and Procs
Func<int, int> double_it = x => x * 2; Console.WriteLine(double_it(5));
double_it = ->(x) { x * 2 } # lambda (arrow syntax) puts double_it.call(5) puts double_it.(5) # alternative call syntax
Ruby has two callable objects: lambda (strict argument checking, return exits only the lambda) and Proc.new (loose argument checking, return exits the enclosing method). The ->(args) { body } syntax is the modern lambda literal. Call with .call(args), .(args), or [args].
Symbol-to-proc shorthand
var words = new List<string> { "hello", "world", "ruby" }; var upper = words.Select(w => w.ToUpper()).ToList(); Console.WriteLine(string.Join(", ", upper));
words = ["hello", "world", "ruby"] upper = words.map(&:upcase) puts upper.join(", ")
The &:method_name shorthand converts a symbol to a block that calls that method on each element — equivalent to { |item| item.method_name }. It works wherever a block is expected. Common examples: .map(&:to_s), .select(&:even?), .reject(&:nil?). There is no C# equivalent.
Defining methods that accept blocks (<code>yield</code>)
static void Repeat(int count, Action action) { for (int i = 0; i < count; i++) action(); } Repeat(3, () => Console.WriteLine("hello"));
def repeat(count) count.times { yield } end repeat(3) { puts "hello" }
yield calls the block passed to the current method — no need to name or type it. block_given? returns true if a block was supplied, enabling optional-block APIs. To store a block for later use, capture it with an explicit parameter: def repeat(count, &callback) — then callback is a Proc.
Method references (<code>method(:name)</code>)
static int Double(int n) => n * 2; Func<int, int> reference = Double; Console.WriteLine(new[] { 1, 2, 3 }.Select(reference).Sum());
def double_it(n) = n * 2 # endless method syntax (Ruby 3+) puts [1, 2, 3].map(&method(:double_it)).sum
method(:name) returns a Method object — a first-class callable wrapping an existing method. Prepending & converts it to a block for use with iterators. This is Ruby's equivalent of a method group in C# — passing a method's name without calling it.
Closures — capturing the environment
int base_value = 10; Func<int, int> add_base = x => x + base_value; Console.WriteLine(add_base(5)); // 15
base_value = 10 add_base = ->(x) { x + base_value } puts add_base.call(5)
Ruby blocks, procs, and lambdas are closures — they capture variables from their surrounding scope by reference. Mutating a captured variable inside a block also mutates it outside. Unlike Rust, there is no explicit "move" or borrow distinction — Ruby's garbage collector tracks references, so captured objects simply stay alive.
Classes & Objects
Class definition with initialize
var person = new Person("Alice", 30); Console.WriteLine(person.Describe()); class Person { public string Name { get; } public int Age { get; } public Person(string name, int age) { Name = name; Age = age; } public string Describe() => $"{Name} is {Age}"; }
class Person def initialize(name, age) @name = name @age = age end def describe "#{@name} is #{@age}" end end person = Person.new("Alice", 30) puts person.describe
Ruby uses initialize as the constructor (called by ClassName.new). Instance variables begin with @; class variables with @@; class-level constants begin with a capital letter. There is no this keyword — use self when needed (though it's rarely required inside instance methods).
Accessors: <code>attr_reader</code>, <code>attr_writer</code>, <code>attr_accessor</code>
var person = new Person("Alice", 30); person.Name = "Bob"; Console.WriteLine(person.Name); class Person { public string Name { get; set; } // read-write property public int Age { get; } // read-only property public Person(string name, int age) { Name = name; Age = age; } }
class Person attr_reader :age # read-only (getter only) attr_accessor :name # read-write (getter + setter) def initialize(name, age) @name = name @age = age end end person = Person.new("Alice", 30) person.name = "Bob" puts person.name
attr_reader :field generates a getter method; attr_writer generates a setter; attr_accessor generates both. These are class-level macros, not language keywords. They replace the boilerplate of writing def name; @name; end and def name=(value); @name = value; end by hand.
Inheritance
var dog = new Dog("Rex"); dog.Speak(); class Animal { protected string Name; public Animal(string name) { Name = name; } public virtual void Speak() => Console.WriteLine($"{Name} makes a sound"); } class Dog : Animal { public Dog(string name) : base(name) {} public override void Speak() => Console.WriteLine($"{Name} says: Woof!"); }
class Animal def initialize(name) @name = name end def speak puts "#{@name} makes a sound" end end class Dog < Animal def speak puts "#{@name} says: Woof!" end end dog = Dog.new("Rex") dog.speak
Ruby uses < for single inheritance. All methods are virtual by default — no override keyword needed. Call the parent method with super (passes the same arguments) or super(args). Ruby has no protected/private access per subclass — visibility is runtime-enforced, not compile-time.
Class methods (<code>self.</code>)
int count = Counter.Count; Console.WriteLine(count); class Counter { private static int count = 0; public static int Count => count; public Counter() { count++; } } var counter1 = new Counter(); var counter2 = new Counter(); Console.WriteLine(Counter.Count);
class Counter @@count = 0 def initialize @@count += 1 end def self.count @@count end end Counter.new Counter.new puts Counter.count
Class methods are defined with def self.method_name — they are called on the class itself, not on an instance. Class variables (@@name) are shared across the class and all subclasses. Ruby's equivalent of C# static members. self.name inside a class definition body refers to the class, not an instance.
Open classes — extending existing types
// C# extension methods (read-only — cannot add instance variables or virtual overrides) static class IntExtensions { public static bool IsEven(this int n) => n % 2 == 0; } Console.WriteLine(42.IsEven());
class Integer def factorial return 1 if self <= 1 self * (self - 1).factorial end end puts 5.factorial # 120
Ruby classes are "open" — they can be reopened and extended at any time, even built-in classes like Integer, String, and Array. This is more powerful than C# extension methods (which are syntactic sugar, not true method additions) but carries the risk of monkey-patching conflicts in large codebases.
Modules & Mixins
Module as namespace
namespace Geometry { class Circle { public double Radius; public Circle(double radius) { Radius = radius; } public double Area() => Math.PI * Radius * Radius; } } // top-level statements must precede namespace declarations in a C# program file var circle = new Geometry.Circle(5.0); Console.WriteLine(circle.Area().ToString("F2"));
module Geometry class Circle def initialize(radius) @radius = radius end def area Math::PI * @radius ** 2 end end end circle = Geometry::Circle.new(5.0) puts circle.area.round(2)
Ruby modules serve dual duty: as namespaces (grouping related classes) and as mixins (adding methods to classes). Constants and classes inside a module are accessed with ::. Math::PI accesses the PI constant from the standard library's Math module.
Mixins — <code>include</code> a module
// C# 8 default interface methods — must call through the interface type IGreeter obj = new FriendlyClass(); Console.WriteLine(obj.Greet("Alice")); interface IGreeter { string Greet(string name) => $"Hello, {name}!"; } class FriendlyClass : IGreeter {}
module Greetable def greet(name) "Hello, #{name}!" end end class FriendlyClass include Greetable end obj = FriendlyClass.new puts obj.greet("Alice")
include ModuleName adds the module's methods as instance methods of the class — this is Ruby's primary mechanism for code reuse without inheritance. It is more powerful than C# default interface methods because the mixin can carry state (via @instance_variables) and the class can include multiple modules.
The <code>Comparable</code> module
record Temperature(double Celsius) : IComparable<Temperature> { public int CompareTo(Temperature? other) => Celsius.CompareTo(other?.Celsius); } var temperatures = new[] { new Temperature(22), new Temperature(18), new Temperature(30) }; Array.Sort(temperatures); Console.WriteLine(temperatures[0].Celsius);
class Temperature include Comparable attr_reader :celsius def initialize(celsius) @celsius = celsius end def <=>(other) # implement one method; get < > <= >= == between? for free @celsius <=> other.celsius end end temps = [Temperature.new(22), Temperature.new(18), Temperature.new(30)] puts temps.min.celsius
Including Comparable and implementing <=> (the spaceship operator) gives the class all six comparison operators and the .between? method for free — equivalent to implementing IComparable<T> in C#. The <=> convention (return -1, 0, or 1) is used throughout Ruby's standard library.
The <code>Enumerable</code> module
class NumberRange : IEnumerable<int> { public int From, To; public IEnumerator<int> GetEnumerator() { for (int i = From; i <= To; i++) yield return i; } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); } var range = new NumberRange { From = 1, To = 5 }; Console.WriteLine(range.Sum());
class NumberRange include Enumerable def initialize(from, to) @from = from @to = to end def each # implement one method; get map, select, sort, min, max, etc. for free @from.upto(@to) { |n| yield n } end end range = NumberRange.new(1, 5) puts range.sum
Including Enumerable and implementing a single each method gives the class all 60+ Enumerable methods for free: .map, .select, .sort, .min, .max, .count, .flat_map, .group_by, etc. Equivalent to implementing IEnumerable<T> in C#, but vastly more powerful.
Symbols
Symbols — immutable, interned identifiers
// C# has no direct equivalent; the closest is string interning or enums. string status1 = string.Intern("active"); string status2 = string.Intern("active"); Console.WriteLine(object.ReferenceEquals(status1, status2)); // true
status1 = :active status2 = :active puts status1.equal?(status2) # true — same object in memory puts status1 == status2 # true — same value puts :active.object_id == :active.object_id # always true
Ruby symbols (:name) are immutable, interned identifiers — every use of :active is the exact same object in memory (verified by .object_id). This makes them ideal as hash keys (fast comparison, no accidental mutation) and as method name references (method(:name), respond_to?(:name)).
Symbols as hash keys
// C# conventionally uses string keys in Dictionary<string, T> var config = new Dictionary<string, string> { { "host", "localhost" }, { "port", "3000" }, }; Console.WriteLine(config["host"]);
config = { host: "localhost", port: "3000" } puts config[:host]
Symbol hash keys ({ host: ... }) are the dominant Ruby convention. The key: value syntax is shorthand for { :key => value }. Symbols are faster as hash keys than strings because identity comparison (==) is a single pointer comparison, not a character-by-character string comparison.
Symbol-to-proc and method dispatch
var words = new[] { "hello", "world", "ruby" }; Console.WriteLine(string.Join(", ", words.Select(w => w.ToUpper())));
words = ["hello", "world", "ruby"] puts words.map(&:upcase).join(", ")
&:upcase creates a block equivalent to { |word| word.upcase } — the & calls .to_proc on the symbol, which returns a proc that sends that method name to its argument. This pattern works with any method that takes zero arguments: &:to_s, &:strip, &:freeze, etc.
Error Handling
<code>begin / rescue / ensure</code>
try { int result = int.Parse("not a number"); Console.WriteLine(result); } catch (FormatException exception) { Console.WriteLine($"Error: {exception.Message}"); } finally { Console.WriteLine("always runs"); }
begin result = Integer("not a number") puts result rescue ArgumentError => error puts "Error: #{error.message}" ensure puts "always runs" end
Ruby's begin/rescue/ensure/end maps directly to C#'s try/catch/finally. rescue ExceptionClass => variable names the exception. ensure always runs, like finally. Inside a method body, begin/end is optional — you can write rescue directly in the method.
Raising exceptions
static void Validate(int value) { if (value < 0) throw new ArgumentException($"Value must be non-negative, got {value}"); } try { Validate(-1); } catch (ArgumentException exception) { Console.WriteLine(exception.Message); }
def validate(value) raise ArgumentError, "Value must be non-negative, got #{value}" if value < 0 end begin validate(-1) rescue ArgumentError => error puts error.message end
Ruby uses raise (or the alias fail) instead of throw. The two-argument form raise ExceptionClass, "message" is the most common. raise with no arguments re-raises the current exception — useful inside a rescue block to handle-and-rethrow.
Custom exception classes
try { throw new ValidationError("email", "Invalid email format"); } catch (ValidationError exception) { Console.WriteLine($"{exception.Field}: {exception.Message}"); } class ValidationError : Exception { public string Field; public ValidationError(string field, string message) : base(message) { Field = field; } }
class ValidationError < StandardError attr_reader :field def initialize(field, message) super(message) @field = field end end begin raise ValidationError.new("email", "Invalid email format") rescue ValidationError => error puts "#{error.field}: #{error.message}" end
Custom exceptions inherit from StandardError (the base for most application errors). Inheriting from RuntimeError is also common. All Ruby exception classes have .message and .backtrace methods from the base Exception class. StandardError is what bare rescue (without a class name) catches.
Multiple rescue clauses
try { string? input = null; int value = int.Parse(input!.Trim()); Console.WriteLine(value); } catch (NullReferenceException) { Console.WriteLine("Input was null"); } catch (FormatException exception) { Console.WriteLine($"Parse failed: {exception.Message}"); }
begin input = nil value = Integer(input.strip) puts value rescue NoMethodError puts "Input was nil" rescue ArgumentError => error puts "Parse failed: #{error.message}" end
Multiple rescue clauses are checked in order — only the first matching one runs. To catch several exception types with one handler, list them: rescue TypeError, ArgumentError => error. A bare rescue with no class catches StandardError and its subclasses — the same set that most application code raises.