Skip to content

Ruby Spickzettel

Dynamische, elegante Sprache, optimiert für Entwicklerglück.

01

Grundlagen

Variablen & Typen

Ruby ist dynamisch typisiert – Variablen brauchen keine Typdeklarationen. Verwenden Sie .class, um den Typ zu inspizieren, und is_a? für Typ-Prüfungen. Alles ist ein Objekt, einschließlich Zahlen und Booleans.

ruby
name = "Alice"    # string
age = 30           # integer
pi = 3.14          # float
is_dev = true      # boolean
nums = [1, 2, 3]   # array
puts name.class    # String
puts age.is_a?(Integer)  # true

Symbole

Symbole (:name) sind unveränderliche, interne Strings – nur eine Kopie existiert im Speicher. Verwenden Sie sie für Hash-Schlüssel, Methodennamen und Identifikatoren, wo Identität wichtiger ist als Inhalt. Speichereffizienter als Strings für wiederholte Verwendung.

ruby
status = :active
puts status.class        # Symbol
puts status.to_s         # "active"
# Symbols are immutable, reusable strings
hash = { name: "Alice", status: :active }
puts hash[:status]       # active

Nil & Truthiness

In Ruby sind nur nil und false falsy – alles andere (einschließlich 0, '' und []) ist truthy. Dies unterscheidet sich von vielen Sprachen, wo 0 falsy ist. Verwenden Sie || für Standardwerte und nil?, um spezifisch auf nil zu prüfen.

ruby
x = nil
puts x.nil?        # true
puts x || "default"  # default
# Only nil and false are falsy; 0 and "" are truthy!
puts 0 ? "truthy" : "falsy"  # truthy
puts "" ? "truthy" : "falsy" # truthy
puts nil ? "truthy" : "falsy" # falsy

Typumwandlung

to_i/to_s/to_f sind nachsichtige Konvertierungen (geben 0 bei Fehlschlag). Integer()/Float() sind streng (werfen ArgumentError). Verwenden Sie strenge Konvertierung, wenn Sie Validierung brauchen, nachsichtige, wenn Sie anmutige Degradation wollen.

ruby
puts "42".to_i      # 42
puts 42.to_s        # "42"
puts "3.14".to_f    # 3.14
puts 3.14.to_i      # 3
puts "abc".to_i     # 0 (no error)
puts Integer("42")  # 42 (raises if invalid)

String-Interpolation

Doppelte Anführungszeichen unterstützen #{expr}-Interpolation – jeder Ruby-Ausdruck innen. Einfache Anführungszeichen sind wörtlich (keine Interpolation oder Escapes außer \\ und \'). Verwenden Sie doppelte Anführungszeichen, wenn Sie Interpolation oder Escape-Sequenzen brauchen.

ruby
name = "Alice"
age = 30
puts "Name: #{name}, Age: #{age}"
puts "5 + 3 = #{5 + 3}"
puts "Upper: #{name.upcase}"
# Single quotes don't interpolate
puts 'No #{name} here'  # No #{name} here
02

Strings

Häufige String-Methoden

Ruby-Strings haben reiche Methoden. Die meisten geben neue Strings zurück (Strings sind in Ruby veränderlich). Verwenden Sie !-Varianten (upcase!, gsub!) für In-Place-Modifikation – diese modifizieren den Empfänger und geben nil zurück, wenn keine Änderung.

ruby
s = "Hello, World"
puts s.length         # 12
puts s.upcase         # HELLO, WORLD
puts s.downcase       # hello, world
puts s.reverse        # dlroW ,olleH
puts s.split(", ")    # ["Hello", "World"]
puts s.gsub("o", "0") # Hell0, W0rld

String-Mutation (Bang-Methoden)

Methoden, die auf ! enden, modifizieren das Objekt In-Place und sind oft effizienter. Sie können nil zurückgeben, wenn keine Änderung vorgenommen wurde. Verwenden Sie, wenn Sie Kopien vermeiden wollen, aber beachten Sie Nebeneffekte.

ruby
s = "hello"
s.upcase!    # s is now "HELLO"
s.gsub!(/L/, "1")  # s is now "HE11O"
puts s       # HE11O
# Bang methods modify in-place; use with caution
arr = [3, 1, 2]
arr.sort!    # arr is now [1, 2, 3]

Heredoc & Mehrzeilig

Heredocs (<<TEXT ... TEXT) erstellen mehrzeilige Strings. <<~ (Squiggly-Heredoc) entfernt führende Whitespace für sauberen Code. Nützlich für SQL-Queries, HTML-Templates oder lange Nachrichten.

ruby
text = <<~HEREDOC
  Hello,
  World!
  Indentation is stripped.
HEREDOC
puts text
# <<~ strips leading whitespace (Ruby 2.3+)
# <<HEREDOC preserves indentation

Formatierung (sprintf)

Verwenden Sie %-Operator oder format/sprintf für C-Style-String-Formatierung. %-10s linksbündigt in 10 Zeichen, %08.2f zero-paddet auf 8 Zeichen mit 2 Dezimalen. Nützlich für tabellarische Ausgabe und Fixbreiten-Formatierung.

ruby
puts sprintf("%s is %d", "Alice", 30)
puts "%-10s|%5d" % ["Name", 42]
puts format("%.2f", 3.14159)  # 3.14
puts "%08.2f" % 3.14          # 00003.14
# %s string, %d integer, %f float

String-Verkettung

+ erstellt einen neuen String, << hängt In-Place an (effizienter zum Erstellen von Strings). * wiederholt einen String. join kombiniert Array-Elemente mit einem Trennzeichen. Bevorzugen Sie << oder join gegenüber wiederholtem + für Leistung.

ruby
s1 = "Hello" + ", " + "World"
s2 = ["a", "b", "c"].join("-")  # a-b-c
s3 = "Hello"
s3 << " " << "World"  # in-place append
s4 = "x" * 3          # xxx (repetition)
puts s1, s2, s3, s4
03

Datenstrukturen

Arrays

Arrays sind geordnet, null-indiziert und können gemischte Typen halten. << und push fügen am Ende hinzu. Verwenden Sie include? für Mitgliedschaft, sum für Summen. Arrays sind veränderlich – verwenden Sie freeze, um sie unveränderlich zu machen.

ruby
nums = [1, 2, 3, 4, 5]
nums.push(6)          # [1,2,3,4,5,6]
nums << 7             # same as push
nums[0] = 0
puts nums.first       # 0
puts nums.length      # 7
puts nums.sum         # 28
puts nums.include?(3) # true

Hashes

Hashes sind Schlüssel-Wert-Wörterbücher. Symbol-Schlüssel (name:) sind idiomatisch und effizient. Verwenden Sie key? für Existenz, fetch für sicheren Zugriff (wirft bei fehlend), transform_values für Bulk-Updates.

ruby
user = { name: "Alice", age: 30 }
user[:email] = "[email protected]"
puts user[:name]          # Alice
puts user.key?(:name)     # true
user.each { |k, v| puts "#{k}: #{v}" }
puts user.values          # ["Alice", 30, "[email protected]"]
puts user.transform_values(&:to_s)

Ranges

'..' inkludiert das Ende, '...' schließt es aus. Ranges sind lazy und speichereffizient für große Sequenzen. Nützlich für Iteration, Slicing und Generieren von Sequenzen. Kann mit jedem Comparable-Typ verwendet werden.

ruby
(1..5).each { |n| puts n }      # 1 2 3 4 5
puts (1...5).to_a                # [1, 2, 3, 4]
puts (1..10).select(&:even?)     # [2, 4, 6, 8, 10]
puts ('a'..'e').include?('c')    # true
puts (1..5).map { |n| n ** 2 }   # [1, 4, 9, 16, 25]

Enumerable-Methoden

Enumerable ist Rubys mächtigster Mixin – map, select, reject, reduce, find, group_by, sort_by usw. Verwenden Sie &:method als Shorthand für { |x| x.method }. Diese ermöglichen ausdrucksstarke Daten-Transformations-Pipelines.

ruby
nums = [1, 2, 3, 4, 5]
puts nums.map { |n| n * 2 }.inspect    # [2,4,6,8,10]
puts nums.select(&:even?).inspect      # [2, 4]
puts nums.reject(&:odd?).inspect       # [2, 4]
puts nums.reduce(0) { |sum, n| sum + n } # 15
puts nums.find { |n| n > 3 }           # 4
puts nums.group_by(&:even?).inspect    # {false=>[1,3,5], true=>[2,4]}

Sets

Set (aus 'set'-Bibliothek) speichert eindeutige Elemente mit O(1)-Lookup. Verwenden Sie für Deduplizierung und Mengenoperationen (Union, Intersection, Difference). Konvertieren Sie mit to_a, wenn Sie ein Array brauchen. Erfordert 'set' require.

ruby
require 'set'
a = Set.new([1, 2, 3])
b = Set.new([3, 4, 5])
puts a.union(b).to_a.inspect        # [1,2,3,4,5]
puts a.intersection(b).to_a.inspect # [3]
puts a.subtract([1]).to_a.inspect   # [2, 3]
puts a.subset?(Set.new([1,2,3,4]))  # true
a.add(6)
04

Kontrollfluss

If / Elsif / Unless

if/elsif/else ist Standard-Verzweigung. unless ist das Gegenteil von if (führt aus, wenn Bedingung false ist). Modifier-Form (statement if condition) ist idiomatisch für einzeilige Guards – verbessert Lesbarkeit für einfache Fälle.

ruby
score = 85
if score >= 90
  puts "A"
elsif score >= 80
  puts "B"
else
  puts "C"
end
# Modifier form
puts "Pass" if score >= 60
puts "Fail" unless score >= 60

Case (When)

case/when verwendet === für Matching und ermöglicht Ranges, Klassen und Regex. Mehrere Werte durch Kommas getrennt. then erlaubt einzeilige Bodys. Ohne Ziel agiert case als sauberere if/elsif-Kette.

ruby
grade = "B"
case grade
when "A" then puts "Excellent"
when "B", "C" then puts "Good"
when "D".."F" then puts "Poor"
else puts "Unknown"
end
# Case without value = multi-condition if
case
when score > 90 then puts "Top"
when score > 60 then puts "Pass"
end

While / Until / Loop

while läuft, während true, until läuft, bis true (while false). loop ist unendlich – verwenden Sie break zum Verlassen. Bevorzugen Sie Iteratoren (each, map) gegenüber while für Collections – sie sind idiomatischer und weniger fehleranfällig.

ruby
count = 0
while count < 3
  puts count
  count += 1
end
# until = opposite of while
n = 3
until n == 0
  puts n
  n -= 1
end
# Infinite loop with break
loop do
  puts "forever"
  break if rand > 0.8
end

Iteratoren (Each/Times/Upto)

Rubys Iteratoren sind idiomatischer als for-Schleifen. times für Zählen, upto/downto für Ranges, each_with_index für Index+Wert. step kontrolliert Inkrement. Diese sind das Rückgrat der Ruby-Iteration.

ruby
3.times { |i| puts i }          # 0 1 2
1.upto(3) { |n| puts n }        # 1 2 3
3.downto(1) { |n| puts n }      # 3 2 1
[1,2,3].each_with_index do |n, i|
  puts "#{i}: #{n}"
end
(1..3).step(1) { |n| puts n }   # 1 2 3

Break / Next / Redo

next springt zur nächsten Iteration (wie continue), break verlässt die Schleife früh. break kann einen Wert aus einem Block zurückgeben. redo startet die aktuelle Iteration neu, ohne die Bedingung zu prüfen – selten verwendet.

ruby
[1, 2, 3, 4, 5].each do |n|
  next if n.even?    # skip even
  break if n > 4     # stop at 5
  puts n             # prints 1, 3
end
# break value returns from the block
result = [1,2,3].each { |n| break n * 10 if n == 2 }
puts result  # 20
05

Methoden & Blöcke

Methodendefinition

Methoden verwenden def/end. Keyword-Argumente (key:) verbessern Lesbarkeit für viele Parameter. *args sammelt extra positionale Argumente in ein Array, **kwargs sammelt Keyword-Argumente in einen Hash. Standardwerte verwenden =.

ruby
def greet(name, greeting: "Hello")
  "#{greeting}, #{name}!"
end
puts greet("Alice")               # Hello, Alice!
puts greet("Bob", greeting: "Hi") # Hi, Bob!
def add(*nums)  # splat (variadic)
  nums.sum
end
puts add(1, 2, 3, 4)  # 10

Blöcke & Yield

Blöcke sind anonyme Code-Chunks, die an Methoden weitergegeben werden. yield ruft den Block auf. Blöcke sind überall in Ruby (each, map usw.). Verwenden Sie yield, um Ihre Methoden flexibel zu machen – Aufrufer stellen das Verhalten bereit.

ruby
def repeat(n)
  n.times { yield }
end
repeat(3) { puts "hi" }  # prints hi 3 times
def with_result
  yield(5)
end
puts with_result { |x| x * 2 }  # 10

Procs & Lambdas

Procs und Lambdas sind wiederverwendbare Blöcke, die in Variablen gespeichert sind. Hauptunterschied: Lambdas prüfen Argumentanzahl (werfen bei Nichtübereinstimmung) und return nur aus sich selbst; Procs sind nachsichtig und return aus der umschließenden Methode. Bevorzugen Sie lambda für strenges Verhalten.

ruby
square = proc { |x| x * x }
puts square.call(5)   # 25
puts square.(5)       # 25 (shortcut)
double = lambda { |x| x * 2 }
puts double.call(5)   # 10
# Lambda checks arity; Proc doesn't
# Lambda returns from itself; Proc returns from enclosing method

Method-Objekt (&)

&:method konvertiert einen Methodennamen zu einem Proc. Es ist der idiomatische Shorthand für einfache Ein-Methoden-Blöcke: map(&:to_i) statt map { |s| s.to_i }. Sauberer und lesbarer für einfache Transformationen.

ruby
nums = ["1", "2", "3"]
ints = nums.map(&:to_i)  # [1, 2, 3]
puts ints.inspect
# &:to_i is shorthand for { |s| s.to_i }
names = ["alice", "bob"]
up = names.map(&:upcase)  # ["ALICE", "BOB"]
puts up.inspect

Rückgabewerte

Ruby-Methoden geben implizit den zuletzt ausgewerteten Ausdruck zurück – kein explizites return nötig. Verwenden Sie return für frühes Verlassen. Mehrere Werte werden als Array zurückgegeben und können destrukturiert werden. Dies macht Code prägnant.

ruby
def status(ok)
  return "error" unless ok
  "ok"  # implicit return of last expression
end
puts status(true)   # ok
puts status(false)  # error
# Multiple assignment
def coords
  return 1, 2  # returns [1, 2]
end
x, y = coords
06

Klassen & OOP

Klasse & Instanzvariablen

@var = Instanzvariable (pro Objekt), @@var = Klassenvariable (geteilt). attr_accessor generiert Getter+Setter, attr_reader nur Getter, attr_writer nur Setter. self.method definiert Klassenmethoden.

ruby
class Person
  attr_accessor :name, :age
  attr_reader :id
  @@count = 0  # class variable
  def initialize(name, age)
    @name = name  # instance variable
    @age = age
    @@count += 1
  end
  def self.count; @@count; end
end
p = Person.new("Alice", 30)
puts p.name, Person.count

Vererbung & Super

< bezeichnet Vererbung (nur einfache Vererbung). super ruft die Parent-Version der aktuellen Methode auf. Verwenden Sie super (mit Klammern, um Argumente weiterzugeben, ohne, um dieselben Argumente weiterzugeben), um Parent-Verhalten zu erweitern. Ruby verwendet einfache Vererbung + Mixins.

ruby
class Animal
  def initialize(name); @name = name; end
  def speak; "..."; end
end
class Dog < Animal
  def speak; "#{@name}: Woof!"; end
end
class Puppy < Dog
  def speak; "#{super} (small)"; end
end
puts Puppy.new("Rex").speak  # Rex: Woof! (small)

Module (Mixins)

Module gruppieren wiederverwendbare Methoden. include fügt Instanzmethoden hinzu, extend fügt Klassenmethoden hinzu. Dies ist Rubys Lösung für mehrfache Vererbung. Enumerable ist ein berühmter Mixin – inkludieren Sie es und definieren Sie each, um map, select usw. zu erhalten.

ruby
module Walkable
  def walk; "#{@name} is walking"; end
end
module Swimmable
  def swim; "#{@name} is swimming"; end
end
class Duck
  include Walkable    # instance methods
  extend Swimmable    # class methods
  def initialize(n); @name = n; end
end
puts Duck.new("Donald").walk  # Donald is walking

Zugriffskontrolle

public (Standard), private (nur ohne expliziten Empfänger aufrufbar), protected (innerhalb der Klassenhierarchie aufrufbar). Verwenden Sie private für interne Helfer, protected für Methoden, die zwischen Instanzen derselben Klasse geteilt werden.

ruby
class BankAccount
  def initialize(bal); @balance = bal; end
  def deposit(amt); @balance += amt; end
  def balance; @balance; end
  private
  def audit; "auditing..."; end
  protected
  def compare(other); @balance > other.balance; end
end
a = BankAccount.new(100)
puts a.balance  # 100
# a.audit  # Error: private method

Self & Klassenmethoden

self bezieht sich auf das aktuelle Objekt. Innerhalb des Klassen-Bodys ist self die Klasse – def self.method definiert Klassenmethoden. Klassenmethoden werden auf der Klasse aufgerufen (Counter.total), Instanzmethoden auf Instanzen. alias_method erstellt einen Methoden-Alias.

ruby
class Counter
  @@total = 0
  def initialize; @@total += 1; end
  def self.total; @@total; end
  def self.reset!; @@total = 0; end
  def instance_method; "I'm an instance"; end
  alias_method :count, :instance_method
end
Counter.new; Counter.new
puts Counter.total  # 2
07

Fehlerbehandlung

Begin / Rescue / Ensure

begin/rescue ist Rubys try/catch. Retten Sie spezifische Exception-Klassen für zielgerichtete Behandlung. => e erfasst das Exception-Objekt. ensure läuft immer – verwenden Sie für Cleanup (Dateien schließen, Locks freigeben).

ruby
begin
  result = 10 / 0
rescue ZeroDivisionError => e
  puts "Caught: #{e.message}"
rescue => e
  puts "Other error: #{e.class}"
ensure
  puts "Always runs (cleanup)"
end
# ensure runs regardless of success/failure

Raise & benutzerdefinierte Exceptions

raise wirft eine Exception (raise ohne Argumente re-raiset). Benutzerdefinierte Exceptions erben von StandardError (oder einer spezifischeren Klasse). Benennen Sie sie mit Error-Suffix. Retten Sie nach Klasse, um spezifische Fehlermodi zu behandeln.

ruby
class InvalidAgeError < StandardError; end
def set_age(age)
  raise InvalidAgeError, "Age cannot be negative" if age < 0
  raise ArgumentError, "Must be Integer" unless age.is_a?(Integer)
  @age = age
end
begin
  set_age(-5)
rescue InvalidAgeError => e
  puts "Custom: #{e.message}"
end

Retry

retry startet den begin-Block von vorne. Verwenden Sie für vorübergehende Fehler (Netzwerk, Rate-Limits) mit einem Zähler, um Endlosschleifen zu vermeiden. Ohne Limit kann retry Ihr Programm hängen lassen – immer sichern.

ruby
attempts = 0
begin
  attempts += 1
  fetch_data  # might fail
rescue NetworkError
  retry if attempts < 3
  puts "Failed after 3 attempts"
end
# retry re-runs the begin block from the top

Rescue-Modifier

rescue als Modifier ist eine prägnante Möglichkeit, einen Fallback-Wert zu liefern. Es fängt StandardError und gibt die rechte Seite zurück. Verwenden Sie für einfache Fälle – vermeiden Sie für komplexe Logik, da es Fehler verbirgt. Toll für Parsing oder optionale Operationen.

ruby
result = risky_operation rescue "default"
puts result
# Same as:
# result = begin; risky_operation; rescue; "default"; end
# Catches StandardError only
json = JSON.parse(str) rescue nil
puts "Invalid JSON" if json.nil?

Throw / Catch (Kontrollfluss)

throw/catch ist KEINE Exception-Behandlung – es ist ein Kontrollfluss-Mechanismus für frühes Verlassen aus tiefer Verschachtelung (im Gegensatz zu anderen Sprachen). throw :symbol springt zum passenden catch. Verwenden Sie für das Brechen verschachtelter Schleifen; verwenden Sie begin/rescue für tatsächliche Fehler.

ruby
catch(:done) do
  [1, 2, 3, 4, 5].each do |n|
    throw :done, n if n > 3
    puts n
  end
end
# Prints 1, 2, 3 and returns 4
# Not for errors—use for early exit from nested loops
08

Datei-I/O

Dateien lesen & schreiben

File.write/File.read sind einfache Einmal-Methoden. File.open mit einem Block schließt die Datei automatisch. File.foreach liest zeilenweise, ohne die ganze Datei zu laden – speichereffizient für große Dateien. chomp entfernt abschließenden Zeilenumbruch.

ruby
# Write
File.write("test.txt", "Hello, File!")
# Read
content = File.read("test.txt")
puts content  # Hello, File!
# Append
File.open("log.txt", "a") { |f| f.puts "new line" }
# Read line by line
File.foreach("test.txt") { |line| puts line.chomp }

Datei-Block (Auto-Close)

Verwenden Sie immer File.open mit einem Block – es garantiert, dass die Datei geschlossen wird, selbst wenn ein Fehler auftritt. Die Block-Form ist der idiomatische, sichere Weg, Dateien zu behandeln. Ohne Block müssen Sie manuell close aufrufen.

ruby
File.open("data.txt", "w") do |f|
  f.puts "Line 1"
  f.puts "Line 2"
  f.write("No newline")
end  # file auto-closed here
File.open("data.txt", "r") do |f|
  f.each_line.with_index { |line, i| puts "#{i}: #{line}" }
end

Datei-Existenz & Info

Die File-Klasse bietet Dateisystem-Queries. file?/directory? unterscheiden Typen. mtime/ctime/atime geben Zeitstempel. rename/delete modifizieren das Dateisystem. Prüfen Sie immer Existenz vor Operationen, um Fehler zu vermeiden.

ruby
puts File.exist?("test.txt")  # true
puts File.file?("test.txt")   # true (regular file)
puts File.directory?(".")     # true
puts File.size("test.txt")    # bytes
puts File.mtime("test.txt")   # modification time
File.rename("old.txt", "new.txt")
File.delete("new.txt") if File.exist?("new.txt")

Verzeichnis-Operationen

Dir verwaltet Verzeichnisse. mkdir erstellt, chdir wechselt (Block-Form stellt nachher wieder her). glob matcht Datei-Patterns – * matcht beliebig, ** matcht rekursiv. Nützlich für Datei-Discovery und Batch-Verarbeitung.

ruby
Dir.mkdir("test_dir") unless Dir.exist?("test_dir")
Dir.chdir("test_dir") do
  File.write("a.txt", "a")
  puts Dir.pwd  # current path
end
Dir.glob("*.txt") { |f| puts f }  # list .txt files
Dir.glob("**/*.rb") { |f| puts f } # recursive

CSV & JSON

CSV und JSON sind in der Standardbibliothek. CSV.foreach streamt Zeilen (speichereffizient). JSON.parse gibt Hashes/Arrays mit String-Schlüsseln zurück. to_json serialisiert jedes Objekt. Diese sind essenziell für Datenaustausch.

ruby
require 'csv'
require 'json'
CSV.write("data.csv", [["a", 1], ["b", 2]])
CSV.foreach("data.csv") { |row| puts row.inspect }
data = { name: "Alice", age: 30 }
File.write("data.json", data.to_json)
parsed = JSON.parse(File.read("data.json"))
puts parsed["name"]  # Alice
09

Datum/Zeit & Regex

Time & Date

Time repräsentiert einen Moment (mit Zeitzone). Date repräsentiert ein Kalenderdatum (ohne Zeit). Arithmetik auf Time verwendet Sekunden. strftime formatiert mit %Y (Jahr), %m (Monat), %d (Tag), %H:%M (Zeit). Require 'time' für ISO8601-Parsing.

ruby
require 'time'
now = Time.now
puts now                    # 2024-01-15 14:30:00 +0800
puts now.strftime("%Y-%m-%d %H:%M")  # 2024-01-15 14:30
tomorrow = now + 86400      # +1 day (seconds)
puts tomorrow.strftime("%A") # weekday name
date = Date.today
puts date.next_day          # tomorrow

Date-Parsing & Arithmetik

Date-Arithmetik funktioniert natürlich – Addition von Integers addiert Tage. next_month/prev_month behandeln Monatsgrenzen. Date - Date gibt einen Rational (Tage). upto/downto iterieren über Datums-Bereiche. Verwenden Sie Date für Kalenderlogik, Time für Zeitstempel.

ruby
require 'date'
d = Date.parse("2024-01-15")
puts d.year   # 2024
puts d + 7    # 2024-01-22 (+7 days)
puts d.next_month  # 2024-02-15
diff = (Date.today - d).to_i
puts "#{diff} days since"
puts Date.today.upto(Date.today + 6).map(&:wday)

Regex-Matching

=~ gibt die Match-Position oder nil zurück. $1, $2 halten erfasste Gruppen nach einem Match. .match gibt ein MatchData-Objekt für mehr Details. Verwenden Sie =~ für einfache Prüfungen, .match für das Extrahieren von Captures. Regex-Literale verwenden /pattern/.

ruby
s = "Phone: 123-4567"
if s =~ /(\d+)-(\d+)/
  puts "Area: #{$1}, Number: #{$2}"
end
m = /\w+@(\w+)/.match("[email protected]")
puts m[1]  # example
puts "abc123" =~ /\d/ ? "has digit" : "no digit"

Regex-Substitution

gsub ersetzt alle Matches (sub ersetzt das erste). Geben Sie einen Block für dynamische Ersetzung. scan extrahiert alle Matches in ein Array. Backreferences (\1, \2) in Ersetzungs-Strings referenzieren erfasste Gruppen. Mächtig für Text-Verarbeitung.

ruby
s = "Hello, World"
puts s.gsub(/o/, "0")       # Hell0, W0rld
puts s.gsub(/\w+/) { |w| w.capitalize }
puts s.scan(/\w+/).inspect  # ["Hello", "World"]
puts "2024-01-15".gsub(/(\d+)-(\d+)-(\d+)/, '\3/\2/\1')
# => 15/01/2024

Regex-Optionen

Regex-Optionen: i (case-insensitive), m (multiline – Punkt matcht Zeilenumbruch), x (extended – erlaubt Whitespace/Kommentare im Pattern). %r{} ist ein alternatives Trennzeichen, nützlich wenn das Pattern Slashes enthält (wie URLs).

ruby
puts /hello/i =~ "HELLO"  # 0 (case-insensitive)
puts /line/m =~ "a\nb"    # 0 (multiline: . matches \n)
puts /x.y/x =~ "x y"       # 0 (extended: ignore whitespace)
# Common patterns
email = /[\w.]+@[\w]+\.[a-z]+/
url = %r{https?://[\w./]+}
puts email.match("[email protected]") ? "valid" : "invalid"
10

Metaprogrammierung & Nebenläufigkeit

Dynamische Methodendefinition

define_method erstellt Methoden zur Laufzeit. Verwenden Sie, um mehrere ähnliche Methoden zu generieren oder DSLs zu bauen. Dies ist Metaprogrammierung – Code, der Code schreibt. Mächtig, aber verwenden Sie umsichtig; es kann Code schwerer verständlich machen.

ruby
class Dynamic
  [:foo, :bar, :baz].each do |name|
    define_method(name) { puts "Called #{name}" }
  end
end
d = Dynamic.new
d.foo  # Called foo
d.bar  # Called bar
# Useful for generating similar methods

method_missing

method_missing fängt Aufrufe an undefinierte Methoden ab. Verwenden Sie, um flexible APIs oder Proxies zu bauen. Überschreiben Sie immer auch respond_to_missing?, damit Reflection funktioniert. Verwenden Sie sparsam – es kann Bugs verbergen und statische Analyse verwirren.

ruby
class Proxy
  def method_missing(name, *args)
    puts "Called: #{name} with #{args.inspect}"
  end
  def respond_to_missing?(name, include_private = false)
    true
  end
end
p = Proxy.new
p.anything(1, 2)  # Called: anything with [1, 2]

Threads

Threads führen Code nebenläufig aus. join wartet auf Vervollständigung. MRI (Standard-Ruby) hat einen GIL, daher laufen CPU-gebundene Threads nicht wirklich parallel – I/O-gebundene Threads schon. Für echten Parallelismus verwenden Sie Ractor (Ruby 3.0+) oder mehrere Prozesse.

ruby
threads = [1, 2, 3].map do |n|
  Thread.new { puts "Thread #{n}: #{n * n}" }
end
threads.each(&:join)  # wait for all
# MRI has GIL—threads don't run truly parallel for CPU work
# Use for I/O concurrency (network, file)

Fibers (kooperativ)

Fibers sind leichte, kooperative Nebenläufigkeit – sie pausieren und setzen manuell via yield/resume fort. Im Gegensatz zu Threads laufen sie nicht parallel. Verwenden Sie für Generatoren, verzögerte Auswertung oder pausierbare Berechnungen. Geringerer Overhead als Threads.

ruby
fiber = Fiber.new do
  Fiber.yield 1
  Fiber.yield 2
  3
end
puts fiber.resume  # 1
puts fiber.resume  # 2
puts fiber.resume  # 3
# Fibers yield control cooperatively, not preemptively

Send & Eval

send ruft jede Methode nach Namen auf (einschließlich private). public_send respektiert Sichtbarkeit. eval führt einen String als Ruby-Code aus – extrem mächtig, aber gefährlich mit nicht vertrauenswürdiger Eingabe (Code-Injection). Verwenden Sie send für dynamisches Dispatch, vermeiden Sie eval in Produktion.

ruby
class Obj
  def secret; "hidden"; end
end
o = Obj.new
puts o.send(:secret)  # hidden (calls private too)
puts o.public_send(:secret)  # hidden (respects visibility)
x = 5
result = eval("x * 2")
puts result  # 10
# eval runs a string as code—avoid with untrusted input!
11

Blöcke, Procs & Lambdas

Block-Grundlagen

Blöcke sind Rubys häufigste Closure – anonymer Code, der an Methoden via { } oder do...end weitergegeben wird. yield ruft den Block auf. |n| deklariert Block-Parameter. block_given? prüft, ob ein Block weitergegeben wurde. Blöcke sind keine Objekte (können nicht in Variablen gespeichert werden) – verwenden Sie Proc/Lambda dafür. Jede Methode kann einen impliziten Block akzeptieren, was DSLs natürlich macht (Rails verwendet dies intensiv).

ruby
# Block: anonymous chunk of code passed to a method
[1, 2, 3].each { |n| puts n }          # do-end for multi-line
[1, 2, 3].each do |n|
  puts n * 2
end

# yield: call the block from a method
def greet
  puts "before"
  yield  # invokes the block
  puts "after"
end
greet { puts "in block" }
# before / in block / after

# yield with arguments
def compute
  result = yield(10, 20)
  puts "got: #{result}"
end
compute { |a, b| a + b }  # got: 30

# block_given? checks if a block was passed
def maybe_yield
  return "no block" unless block_given?
  yield
end

Procs vs. Lambdas

Procs und Lambdas sind beide callable-Objekte (Blöcke, die zu Objekten gemacht wurden). Procs sind nachsichtig: extra Argumente werden nil, fehlende Argumente sind nil, und 'return' verlässt die umschließende Methode. Lambdas sind streng: sie prüfen Argumentanzahl und 'return' verlässt nur das Lambda. Verwenden Sie Lambdas, wenn Sie methodenähnliches Verhalten wollen; Procs, wenn Sie blockähnliches Verhalten wollen. -> (Stabby-Lambda) ist die moderne prägnante Syntax.

ruby
# Proc: a block stored as an object
p = Proc.new { |x| puts x * 2 }
p.call(5)      # 10
p.(5)          # 10 (shorthand)
p[5]           # 10 (another shorthand)

# Lambda: stricter Proc
l = lambda { |x| puts x * 2 }
l = ->(x) { puts x * 2 }  # stabby lambda syntax
l.call(5)

# Key differences:
# 1. Argument checking
p = Proc.new { |a, b| puts a }
p.call(1)        # 1 (b is nil, no error)
l = ->(a, b) { puts a }
l.call(1)        # ArgumentError (wrong number of args)

# 2. return behavior
def proc_test
  p = Proc.new { return 1 }
  p.call
  return 2  # never reached—Proc return exits method
end
proc_test  # 1

def lambda_test
  l = lambda { return 1 }
  l.call  # returns from lambda, not method
  return 2
end
lambda_test  # 2

Method-Objekte

method(:name) ruft eine Methode als Method-Objekt ab (callable wie ein Proc). &:symbol konvertiert ein Symbol zu einem Proc, das diese Methode sendet – extrem häufige Idiom: array.map(&:to_s). Das &-Präfix konvertiert einen Proc zu einem Block (oder umgekehrt). Dies ermöglicht das Weitergeben von Methoden als Argumente elegant. Method-Objekte behalten ihren Empfänger, also sind sie an das Objekt gebunden.

ruby
class Calculator
  def add(a, b) a + b end
end

calc = Calculator.new
# method(:name) gets a Method object
m = calc.method(:add)
m.call(2, 3)  # 5
m.(2, 3)      # 5
m[2, 3]       # 5

# Convert method to Proc
p = m.to_proc
p.call(4, 5)  # 9

# &:method shorthand (common in map, each)
["a", "b", "c"].map(&:upcase)  # ["A", "B", "C"]
# Equivalent to: .map { |s| s.upcase }

# Symbol#to_proc
:upcase.to_proc.call("hello")  # "HELLOW"

# Useful for passing methods as blocks
[1, 2, 3].each(&method(:puts))  # prints 1, 2, 3

Closures und Binding

Closures (Blöcke, Procs, Lambdas) erfassen Variablen per Referenz – sie sehen Updates zu erfassten Variablen. Dies ermöglicht zustandsbehaftete Closures (Zähler, Akkumulatoren). Proc#binding gibt Zugriff auf die Closure-Umgebung (für fortgeschrittene Metaprogrammierung). Die Kernel#binding-Methode erfasst den aktuellen Ausführungskontext für eval. Closures sind der Grund, warum Ruby-Blöcke so mächtig für Callbacks und Iteratoren sind.

ruby
# Closures capture surrounding variables
counter = 0
increment = lambda { counter += 1 }
increment.call  # 1
increment.call  # 2
puts counter    # 2 (closure modified it)

# Multiple closures share the same variables
x = 10
add = lambda { |n| x += n }
get = lambda { x }
add.call(5)
get.call  # 15

# Proc#binding: access the closure's environment
def make_counter
  count = 0
  lambda { count += 1 }
end
c = make_counter
c.call  # 1
# c.binding.eval("count")  # 1 (peek at captured var)

# Binding for eval in a specific context
b = binding
x = 42
eval("x", b)  # 42

Benutzerdefinierte Iteratoren mit Blöcken

Das Inkludieren von Enumerable und Definieren von #each gibt Ihnen map, select, reduce und 50+ Methoden kostenlos – dies ist Rubys Iterator-Protokoll. each_with_object ist sauberer als inject zum Erstellen von Akkumulatoren. tap fügt Nebeneffekte in Methoden-Ketten ein (toll für Debugging). Benutzerdefinierte Methoden mit yield lassen Sie Ihre eigenen DSLs erstellen (with_timing, with_database usw.). Blöcke machen Rubys Iterations- und Callback-Pattern elegant.

ruby
# Define your own iterator using yield
class LinkedList
  include Enumerable  # gets map, select, etc. for free

  def each
    node = @head
    while node
      yield node.value
      node = node.next
    end
  end
end

# each_with_object (memo pattern)
result = [1, 2, 3].each_with_object({}) do |n, hash|
  hash[n] = n * n
end
# {1=>1, 2=>4, 3=>9}

# tap (for debugging chains)
[1, 2, 3].map { |n| n * 2 }
         .tap { |arr| puts "after map: #{arr}" }
         .select { |n| n > 2 }

# Custom method with block
def with_timing
  start = Time.now
  yield
  puts "took #{Time.now - start}s"
end
with_timing { sleep(1) }
12

Enumerable & Iteratoren

Kern-Enumerable-Methoden

Enumerable ist Rubys mächtigster Mixin – inkludieren Sie es und definieren Sie #each, um 50+ Methoden zu erhalten. map transformiert, select/reject filtern, reduce/inject aggregieren, find gibt ersten Match zurück, group_by/partition clustern. Symbol-Shortcuts (reduce(:+)) sind idiomatisch. Diese Methoden funktionieren auf Arrays, Hashes, Ranges, Files – allem mit #each. Enumerable zu meistern ist Schlüssel zu idiomatischem Ruby.

ruby
nums = [1, 2, 3, 4, 5, 6]

# map/collect: transform
nums.map { |n| n * 2 }        # [2, 4, 6, 8, 10, 12]

# select/filter: keep matching
nums.select { |n| n.even? }   # [2, 4, 6]
nums.reject { |n| n.even? }   # [1, 3, 5]

# reduce/inject: accumulate
nums.reduce(0) { |sum, n| sum + n }  # 21
nums.reduce(:+)                        # 21 (symbol shorthand)
nums.reduce(:*)                        # 720

# find/detect: first match
nums.find { |n| n > 3 }       # 4

# group_by: cluster
nums.group_by { |n| n.even? } # {false=>[1,3,5], true=>[2,4,6]}

# partition: split into two
nums.partition { |n| n.even? } # [[2,4,6], [1,3,5]]

# chunk: consecutive grouping
[1,1,2,2,3].chunk { |n| n }.to_a

Verzögerte Auswertung

lazy konvertiert ein Enumerable zu einem verzögerten – Werte werden nur bei Bedarf berechnet. Dies ermöglicht unendliche Sequenzen und vermeidet das Berechnen der gesamten Collection für Ketten, die nur wenige Ergebnisse brauchen. Ohne lazy erstellen map/select intermediäre Arrays. Mit lazy zieht die Pipeline Werte einzeln. Verwenden Sie lazy für große/unendliche Datensätze oder teure Transformationen. Der Trade-off: lazy hat Per-Element-Overhead, also ist es langsamer für kleine Collections.

ruby
# Lazy: evaluate on demand (infinite sequences possible)
require 'prime'

# Without lazy: infinite loop!
# primes = (1..Float::INFINITY).select(&:prime?).first(5)

# With lazy: works!
primes = (1..Float::INFINITY).lazy.select(&:prime?).first(5)
# [2, 3, 5, 7, 11]

# Chain without intermediate arrays
(1..1_000_000).lazy
  .map { |n| n * 2 }
  .select { |n| n > 1_000_000 }
  .first(10)  # only computes what's needed

# Build a lazy enumerator
enum = Enumerator::Lazy.new(1..Float::INFINITY) do |yielder, n|
  yielder << n if n.prime?
end
enum.first(5)  # [2, 3, 5, 7, 11]

Enumerator und externe Iteration

Enumerator wickelt eine Iteration als Objekt – Sie können .next manuell aufrufen (externe Iteration) statt einen Block zu verwenden (interne Iteration). Dies ermöglicht Pausieren/Fortsetzen der Iteration, Peeking und das Erstellen unendlicher Sequenzen. Enumerator.new mit einem Block lässt Sie benutzerdefinierte Iteratoren (Generatoren) bauen. Die meisten Enumerable-Methoden geben Enumerators zurück, wenn ohne Block aufgerufen: [1,2,3].map gibt einen Enumerator zurück, den Sie verketten können.

ruby
# Enumerator: an iterator object you can control
enum = [10, 20, 30].each
enum.next  # 10
enum.next  # 20
enum.next  # 30
enum.next  # StopIteration

# External iteration with loop (handles StopIteration)
enum = [1, 2, 3].each
loop do
  puts enum.next
end

# Create custom Enumerator
fib = Enumerator.new do |yielder|
  a, b = 0, 1
  loop do
    yielder << a
    a, b = b, a + b
  end
end
fib.first(10)  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

# Convert to array
fib.take(10).to_a

Sortieren und Vergleichen

sort nimmt einen Block, der zwei Elemente vergleicht (gibt -1, 0 oder 1 zurück). sort_by ist effizienter – es berechnet den Sortierschlüssel einmal pro Element (Schwartzian-Transform) statt bei jedem Vergleich. Verwenden Sie sort_by für komplexe Schlüssel. Der <=> (Spaceship)-Operator gibt -1/0/1 und ist die Basis von Rubys Sortierung. min/max/min_by/max_by finden Extreme. Inkludieren Sie Comparable und definieren Sie <=> für natürliche Ordnung Ihrer Objekte.

ruby
arr = [3, 1, 4, 1, 5, 9, 2, 6]

# sort: returns new array
arr.sort                    # [1, 1, 2, 3, 4, 5, 6, 9]
arr.sort { |a, b| b <=> a } # descending (9, 6, 5, ...)

# sort_by: more efficient (Schwartzian transform)
arr.sort_by { |n| -n }      # descending
words = ["banana", "apple", "cherry"]
words.sort_by { |w| w.length }  # ["apple", "banana", "cherry"]

# min/max
arr.min        # 1
arr.max        # 9
arr.minmax     # [1, 9]
arr.min_by { |n| n.abs }  # 1

# min/max with block
people.max_by { |p| p[:age] }

# <=> (spaceship): -1, 0, 1
1 <=> 2   # -1
2 <=> 2   # 0
3 <=> 2   # 1

Hash-Iteration und Transformation

Hashes sind auch Enumerable. each/each_pair iterieren Schlüssel-Wert-Paare. transform_keys/transform_values (Ruby 2.5+) erstellen neue Hashes mit modifizierten Schlüsseln/Werten. select/reject filtern in einen neuen Hash. merge kombiniert Hashes (Block löst Konflikte). group_by baut einen Hash aus einem Array. Hash-Iteration ist die Grundlage der Datenverarbeitung in Ruby – meistern Sie diese für saubere, ausdrucksstarke Datenmanipulation.

ruby
hash = { a: 1, b: 2, c: 3 }

# Iterate
hash.each { |k, v| puts "#{k}=#{v}" }
hash.each_key { |k| puts k }
hash.each_value { |v| puts v }
hash.each_pair { |k, v| puts "#{k}: #{v}" }

# Transform keys/values
hash.transform_keys { |k| k.to_s }  # {"a"=>1, "b"=>2, "c"=>3}
hash.transform_values { |v| v * 10 } # {a:10, b:20, c:30}

# Filter
hash.select { |k, v| v > 1 }  # {b:2, c:3}
hash.reject { |k, v| v > 1 }  # {a:1}

# Merge
{ a: 1 }.merge({ b: 2 })  # {a:1, b:2}
{ a: 1 }.merge({ a: 2 }) { |k, old, new| old + new }  # {a:3}

# Invert (values become keys)
{ a: 1, b: 2 }.invert  # {1=>:a, 2=>:b}

# Group arrays into hash
%w[apple apricot banana].group_by { |w| w[0] }
# {"a"=>["apple", "apricot"], "b"=>["banana"]}
13

Gems & Bundler

Gem-Grundlagen

Gems sind Ruby-Pakete (Bibliotheken). gem install verwaltet System-Gems. Für Projekte verwenden Sie Bundler mit einem Gemfile, um Versionen zu pinnen und Abhängigkeiten zu verwalten. Version-Spezifizierer: '~> 1.4' (pessimistisch, erlaubt Patches), '>= 5.0' (optimistisch). Groups (:development, :test, :production) lassen Sie nur benötigte Gems pro Umgebung laden. require: false bedeutet, Bundler wird es nicht auto-requiren (Sie requiren manuell bei Bedarf).

ruby
# Install a gem
$ gem install rails
$ gem install rails -v 7.0.0
$ gem install rails --pre  # pre-release

# List installed gems
$ gem list
$ gem list rails

# Uninstall
$ gem uninstall rails

# Gemfile: project dependencies
# Gemfile
source 'https://rubygems.org'
gem 'rails', '7.0.0'
gem 'pg', '~> 1.4'  # pessimistic: >= 1.4, < 2.0
gem 'puma', '>= 5.0'  # optimistic: >= 5.0
gem 'rspec', group: :test  # group-specific
gem 'pry', require: false  # don't auto-require

# Groups
group :development, :test do
  gem 'rspec-rails'
  gem 'factory_bot_rails'
end

group :production do
  gem 'newrelic_rpm'
end

Bundler-Befehle

Bundler stellt sicher, dass Ihr Projekt die exakten Gem-Versionen verwendet. bundle install liest Gemfile und schreibt Gemfile.lock (exakte Versionen für Reproduzierbarkeit – committen Sie dies!). bundle exec führt Befehle mit den korrekten Gem-Versionen aus (vermeidet Konflikte). bundle update ändert Versionen (vorsichtig – kann Dinge kaputt machen). Verwenden Sie immer bundle exec für rake/rspec/rails, um sicherzustellen, dass die richtigen Gems laden. Gemfile.lock macht Deployments reproduzierbar.

ruby
# Install all gems from Gemfile
$ bundle install
$ bundle install --without production  # skip group

# Update gems
$ bundle update              # update all
$ bundle update rails        # update specific gem
$ bundle outdated            # show outdated gems

# Run commands in bundle context
$ bundle exec rails server
$ bundle exec rspec
$ bundle exec rake db:migrate

# Check for dependency issues
$ bundle check
$ bundle doctor

# Lock file: Gemfile.lock
# Records exact versions installed (for reproducibility)
# Commit Gemfile.lock to version control!

# Add/remove gems
$ bundle add rspec
$ bundle remove rspec

# Clean old gems
$ bundle clean

Gemspec (Gem erstellen)

Die .gemspec-Datei definiert die Metadaten und Abhängigkeiten eines Gems. spec.files listet inkludierte Dateien; require_paths teilt Ruby mit, wo sie zu finden sind. add_dependency für Laufzeit-Abhängigkeiten, add_development_dependency für Test/Build-Abhängigkeiten. required_ruby_version erzwingt Ruby-Version. gem build erstellt das .gem-Paket; gem install installiert es lokal. Veröffentlichen Sie auf rubygems.org mit gem push. Struktur: lib/ für Code, spec/ für Tests.

ruby
# my_gem.gemspec
Gem::Specification.new do |spec|
  spec.name = "my_gem"
  spec.version = "0.1.0"
  spec.summary = "A useful Ruby gem"
  spec.description = "Longer description..."
  spec.authors = ["Alice"]
  spec.email = ["[email protected]"]
  spec.homepage = "https://github.com/alice/my_gem"
  spec.license = "MIT"

  spec.files = Dir["lib/**/*.rb"] + ["README.md"]
  spec.require_paths = ["lib"]

  spec.required_ruby_version = ">= 3.0"

  spec.add_dependency "httparty", "~> 0.21"
  spec.add_development_dependency "rspec", "~> 3.12"
end

# Directory structure:
# my_gem/
#   lib/my_gem.rb        (main file)
#   lib/my_gem/version.rb
#   my_gem.gemspec
#   Gemfile
#   spec/                (tests)

# Build and install
$ gem build my_gem.gemspec  # creates my_gem-0.1.0.gem
$ gem install ./my_gem-0.1.0.gem

Rake-Tasks

Rake ist Rubys make – ein Task-Runner. Definieren Sie Tasks mit task :name do ... end. Namespaces gruppieren verwandte Tasks. File-Tasks haben Abhängigkeiten (Rebuild, wenn Quelle sich ändert). sh führt Shell-Befehle aus. Rake wird für Tests, Builds, Deployments und Datenbank-Tasks verwendet (Rails verwendet es intensiv). Führen Sie mit rake task_name aus. Der Default-Task läuft, wenn Sie nur rake eingeben. Geben Sie Argumente mit rake greet[Alice].

ruby
# Rakefile
require 'rspec/core/rake_task'

RSpec::Core::RakeTask.new(:spec)

task default: :spec

# Custom task
task :greet, [:name] do |t, args|
  puts "Hello, #{args.name}!"
end
# $ rake greet[Alice]

# Namespace
namespace :db do
  task :migrate do
    puts "migrating..."
  end
  task :seed do
    puts "seeding..."
  end
end
# $ rake db:migrate

# File tasks (build dependencies)
file 'output.txt' => 'input.txt' do |t|
  sh "cp #{t.source} #{t.name}"
end

# Multi-line task
task :deploy do
  sh 'git push heroku main'
  sh 'heroku run rails db:migrate'
end

Rbenv und RVM (Ruby-Versionen)

rbenv und RVM verwalten mehrere Ruby-Versionen auf einer Maschine. rbenv ist leichtgewichtig (Shims); RVM ist schwerer (überschreibt Shell-Befehle). .ruby-version-Datei (committet) stellt sicher, dass alle dieselbe Ruby-Version verwenden. Gemsets (RVM) oder bundle config path isolieren Projekt-Gems. Für Produktion verwenden Sie bundle config set path, um Gems lokal zum Projekt zu installieren und System-Gem-Verschmutzung zu vermeiden. Pinnen Sie immer die Ruby-Version in .ruby-version.

ruby
# rbenv: lightweight Ruby version manager
$ rbenv install 3.2.0        # install a version
$ rbenv global 3.2.0         # set global version
$ rbenv local 3.1.0          # set per-project (.ruby-version)
$ rbenv versions             # list installed

# .ruby-version file (committed to project)
# 3.2.0

# RVM: alternative version manager
$ rvm install 3.2.0
$ rvm use 3.2.0
$ rvm gemset create myapp    # isolated gem sets
$ rvm use 3.2.0@myapp

# Bundler config
$ bundle config set path 'vendor/bundle'  # install locally
$ bundle config set without 'development test'  # for production

# Check versions
$ ruby -v
$ gem -v
$ bundle -v
14

Rails-Grundlagen

MVC-Struktur

Rails ist ein MVC-Framework: Models (ActiveRecord) behandeln Daten, Controllers (ActionController) behandeln HTTP-Requests, Views (ActionView) rendern Antworten. resources generiert 7 RESTful-Routen automatisch. Routen mappen URLs zu Controller-Aktionen. Konvention über Konfiguration: nennen Sie Ihr Modell User, Controller UsersController, und Rails verdrahtet alles. Diese Struktur ist das Rückgrat jeder Rails-App.

ruby
# Rails follows Model-View-Controller
# app/
#   models/    (ActiveRecord: data + business logic)
#   controllers/ (ActionController: handle requests)
#   views/     (ActionView: render responses)
#   helpers/   (view helpers)
# config/routes.rb (URL routing)

# Route → Controller → Model → View
# GET /users → UsersController#index → User.all → index.html.erb

# config/routes.rb
Rails.application.routes.draw do
  resources :users  # generates 7 RESTful routes
  # GET    /users          → index
  # GET    /users/new      → new
  # POST   /users          → create
  # GET    /users/:id      → show
  # GET    /users/:id/edit → edit
  # PATCH  /users/:id      → update
  # DELETE /users/:id      → destroy

  root 'pages#home'  # root path
  get 'about', to: 'pages#about'  # custom route
end

ActiveRecord-Modelle

ActiveRecord ist Rails' ORM – Modelle mappen auf Datenbanktabellen. validates erzwingt Datenintegrität. Assoziationen (has_many, belongs_to, has_one) definieren Beziehungen. Callbacks (before_save, after_create) haken in den Lebenszyklus ein. Scopes sind wiederverwendbare Query-Fragmente. ActiveRecord verwendet Konvention: User-Modell → users-Tabelle, created_at/updated_at-Spalten. Es ist das Herz von Rails – meistern Sie es für effektive Rails-Entwicklung.

ruby
# app/models/user.rb
class User < ApplicationRecord
  # Validations
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, uniqueness: true,
            format: { with: URI::MailTo::EMAIL_REGEXP }

  # Associations
  has_many :posts, dependent: :destroy
  has_one :profile
  belongs_to :company
  has_and_belongs_to_many :tags

  # Callbacks
  before_save :normalize_email
  after_create :send_welcome_email

  # Scopes (reusable queries)
  scope :active, -> { where(active: true) }
  scope :recent, -> { order(created_at: :desc).limit(10) }

  private
  def normalize_email
    self.email = email.downcase
  end
end

# Usage
User.create(name: "Alice", email: "[email protected]")
User.active.recent
User.find(1)
User.where("age > ?", 18)

Controller und Strong Params

Controller behandeln HTTP-Requests und koordinieren Modelle/Views. before_action führt Filter aus (Authentifizierung, Ressourcen laden). Strong Parameters (permit) verhindern Mass-Assignment-Schwachstellen – nur Whitelist-Felder können gesetzt werden. redirect_to sendet den Benutzer woanders; render zeigt eine View. Die @Instanz-Variablen sind in Views verfügbar. RESTful-Aktionen (index, show, new, create, edit, update, destroy) sind konventionell.

ruby
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :set_user, only: %i[show edit update destroy]

  def index
    @users = User.all
  end

  def show; end  # @user set by before_action

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to @user, notice: 'User created'
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def set_user
    @user = User.find(params[:id])
  end

  # Strong parameters: whitelist allowed fields
  def user_params
    params.require(:user).permit(:name, :email, :age)
  end
end

Views und Helfer

ERB (Embedded Ruby) ist Rails' Standard-Templating: <%= %> gibt aus, <% %> führt aus. link_to/button_to generieren HTML-Links/Formulare. form_with baut Formulare, die an Modelle gebunden sind. Partials (_form.html.erb) sind wiederverwendbare View-Fragmente, die mit render gerendert werden. Path-Helfer (new_user_path, user_path(user)) generieren URLs aus Routen. Helfer halten Views sauber. Für komplexe Logik verwenden Sie View-Helfer (app/helpers/) oder Dekoratoren.

ruby
<!-- app/views/users/index.html.erb -->
<h1>Users</h1>
<%= link_to "New User", new_user_path %>
<ul>
  <% @users.each do |user| %>
    <li>
      <%= link_to user.name, user_path(user) %>
      (<%= user.email %>)
      <%= link_to "Edit", edit_user_path(user) %>
      <%= button_to "Delete", user, method: :delete %>
    </li>
  <% end %>
</ul>

<!-- Partials: reusable view fragments -->
<!-- app/views/users/_form.html.erb -->
<%= form_with model: user do |form| %>
  <% if user.errors.any? %>
    <div class="errors">
      <% user.errors.full_messages.each do |msg| %>
        <p><%= msg %></p>
      <% end %>
    </div>
  <% end %>
  <%= form.text_field :name %>
  <%= form.email_field :email %>
  <%= form.submit %>
<% end %>

<!-- Render partial -->
<%= render "form", user: @user %>

Migrationen und Datenbank

Migrationen entwickeln das Datenbankschema über die Zeit, versioniert. create_table definiert Tabellen; add_column/remove_column modifizieren sie. t.references erstellt Foreign Keys. t.timestamps fügt created_at/updated_at hinzu. db:migrate wendet ausstehende Migrationen an; db:rollback macht die letzte rückgängig. Die schema.rb-Datei ist die autoritative Quelle des aktuellen Schemas (auto-generiert). Bearbeiten Sie schema.rb nie direkt – verwenden Sie Migrationen. Dies macht Datenbankänderungen über Umgebungen hinweg reproduzierbar.

ruby
# Generate a migration
$ rails generate migration CreateUsers name:string email:string

# db/migrate/20240101_create_users.rb
class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :name, null: false
      t.string :email, null: false, index: { unique: true }
      t.integer :age, default: 0
      t.text :bio
      t.references :company, foreign_key: true

      t.timestamps  # created_at, updated_at
    end
  end
end

# Run migrations
$ rails db:migrate
$ rails db:rollback      # undo last migration
$ rails db:seed          # load seed data
$ rails db:reset         # drop + create + migrate + seed

# Schema file (don't edit manually)
# db/schema.rb reflects current database structure

# Add a column later
$ rails generate migration AddAgeToUsers age:integer
# creates: add_column :users, :age, :integer
15

RSpec-Testing

Basis-Syntax (describe, it, expect)

RSpec ist Rubys dominierendes Test-Framework. describe gruppiert verwandte Tests; it definiert einen einzelnen Test. expect(...).to / not_to machen Assertionen. let definiert lazy memoisierte Variablen (einmal berechnet, wenn zuerst zugegriffen). context ist ein Alias für describe, verwendet für Zweige (when...). Die shoulda-matchers-Gem bietet Einzeiler-Syntax für häufige Rails-Validierungen/Assoziationen. Tests gehen in spec/ und spiegeln app/-Struktur wider.

ruby
# spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  # Setup with let (lazy, memoized)
  let(:user) { User.new(name: "Alice", email: "[email protected]") }

  describe '#name' do
    it 'returns the name' do
      expect(user.name).to eq("Alice")
    end

    # Multiple expectations with context
    context 'when name is blank' do
      let(:user) { User.new(name: "") }
      it 'is invalid' do
        expect(user).not_to be_valid
      end
    end
  end

  # One-liner syntax
  it { should validate_presence_of(:email) }
  it { should have_many(:posts) }
end

Mocks und Stubs

Stubs (allow) ersetzen Methoden-Rückgabewerte; Mocks (expect) verifizieren, dass eine Methode aufgerufen wurde. Doubles sind Fake-Objekte für Testen (schneller als echte Objekte). Verwenden Sie Stubs, um den zu testenden Code von externen Abhängigkeiten zu isolieren (APIs, Datenbanken). Verwenden Sie Mocks, um Interaktionen zu verifizieren. Over-Mocking macht Tests brüchig – bevorzugen Sie echte Objekte, wenn schnell genug. FactoryBot erstellt Testdaten; verwenden Sie create (speichert in DB) oder build (nur In-Memory).

ruby
RSpec.describe PaymentService do
  let(:user) { create(:user) }

  it 'charges the card' do
    # Stub: replace a method's return value
    allow(user).to receive(:premium?).and_return(true)

    # Mock: expect a method to be called
    expect(Stripe::Charge).to receive(:create).with(
      amount: 1000, currency: 'usd'
    ).and_return(double(id: 'ch_123'))

    service = PaymentService.new(user)
    result = service.charge(10)
    expect(result).to eq('ch_123')
  end

  it 'handles failure' do
    # Stub to raise an error
    allow(Stripe::Charge).to receive(:create)
      .and_raise(Stripe::StripeError.new("card declined"))

    service = PaymentService.new(user)
    expect { service.charge(10) }.to raise_error(PaymentError)
  end
end

# Double: test stand-in for an object
fake_card = double('Card', last4: '4242', brand: 'Visa')

FactoryBot und Fixtures

FactoryBot erstellt Test-Objekte mit sinnvollen Defaults. Traits erstellen Variationen (:admin, :inactive). Sequences generieren eindeutige Werte (E-Mails). create persistiert in die Datenbank; build nicht. create_list erstellt mehrere. Überschreiben Sie jedes Attribut durch Weitergeben. Factories sind flexibler als Fixtures (YAML), aber langsamer (DB-Schreibvorgänge). Verwenden Sie build_stubbed für schnelle Tests, die die DB nicht treffen. Halten Sie Factories einfach – komplexe Factories deuten auf komplexe Modelle hin.

ruby
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { "Alice" }
    email { "[email protected]" }
    age { 30 }
    active { true }

    # Traits: variations
    trait :admin do
      role { "admin" }
    end

    trait :inactive do
      active { false }
    end

    # Associations
    company { association(:company) }

    # Sequences for unique values
    sequence(:email) { |n| "user#{n}@example.com" }
  end
end

# Usage in specs
let(:user) { create(:user) }           # saved to DB
let(:admin) { create(:user, :admin) }  # with trait
let(:user) { build(:user) }            # not saved
let(:users) { create_list(:user, 5) }  # 5 users

# Override attributes
let(:alice) { create(:user, name: "Alice") }

Before/After-Hooks

before-Hooks führen Setup-Code aus: before(:each) (häufigste) vor jedem Test, before(:all) einmal pro Gruppe. after-Hooks räumen auf. DatabaseCleaner verwaltet Test-Datenbank-Zustand (Transaktion für Geschwindigkeit, Trunkierung für Gründlichkeit). Bevorzugen Sie let gegenüber before(:each) – let ist lazy (berechnet nur bei Verwendung) und memoisiert, während before läuft, auch wenn der Test es nicht braucht. Verwenden Sie before für Nebeneffekte, die passieren müssen (Logging, Time-Freezing).

ruby
RSpec.describe User do
  before(:all) do
    # Runs once before all tests in this group
    @shared_data = load_expensive_data
  end

  before(:each) do  # or just 'before'
    # Runs before each test
    @user = User.create(name: "test")
  end

  after(:each) do
    # Runs after each test (cleanup)
    User.delete_all
  end

  after(:all) do
    # Runs once after all tests
    @shared_data = nil
  end

  # Database cleaner for transactional tests
  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:truncation)
  end
end

# Use before for setup, after for cleanup
# Prefer let over before(:each) for lazy evaluation

Integration- und System-Tests

Request-Specs testen den gesamten Stack (Routing → Controller → Modell → View) via HTTP. System-Specs (Capybara) steuern einen echten Browser – Formulare ausfüllen, Klicken, Seiteninhalt prüfen. Unit-Tests (Modell-Specs) sind schnell und isoliert; Integration/System-Tests sind langsamer, finden aber Verdrahtungs-Bugs. Verwenden Sie die Test-Pyramide: viele schnelle Unit-Tests, weniger Integration-Tests, minimale System-Tests. have_http_status prüft Response-Codes; visit/fill_in/click_button steuern den Browser.

ruby
# spec/requests/users_spec.rb (integration)
require 'rails_helper'

RSpec.describe 'Users API', type: :request do
  describe 'GET /users' do
    before { create_list(:user, 3) }

    it 'returns all users' do
      get '/users'
      expect(response).to have_http_status(200)
      expect(JSON.parse(response.body).size).to eq(3)
    end
  end

  describe 'POST /users' do
    it 'creates a user' do
      post '/users', params: { user: { name: 'Bob' } }
      expect(response).to have_http_status(:created)
    end
  end
end

# spec/system/login_spec.rb (browser tests)
require 'rails_helper'

RSpec.describe 'Login', type: :system do
  it 'logs in a user' do
    user = create(:user, password: 'secret')
    visit login_path
    fill_in 'Email', with: user.email
    fill_in 'Password', with: 'secret'
    click_button 'Log in'
    expect(page).to have_content('Welcome')
  end
end
16

Datei- & Verzeichnis-Operationen

Dateien lesen

File.read lädt die gesamte Datei in den Speicher – in Ordnung für kleine Dateien. File.foreach liest zeilenweise – essenziell für große Dateien (sprengt nicht den Speicher). File.open mit einem Block schließt die Datei automatisch (RAII). readlines gibt ein Array von Zeilen zurück (mit Zeilenumbrüchen – verwenden Sie chomp). binread für Binärdateien. Prüfen Sie immer File.exist? vor dem Lesen, wenn die Datei nicht existieren könnte, oder retten Sie Errno::ENOENT.

ruby
# Read entire file
content = File.read("data.txt")

# Read line by line (memory efficient)
File.foreach("large.txt") do |line|
  puts line.chomp  # chomp removes trailing newline
end

# Read all lines into array
lines = File.readlines("data.txt")

# With a block (auto-closes)
File.open("data.txt") do |f|
  f.each_line { |line| puts line }
end

# Read with options
File.read("data.txt", encoding: "utf-8")
File.binread("image.png")  # binary mode

# Check existence
File.exist?("data.txt")
File.file?("data.txt")   # is it a regular file?
File.directory?("path")  # is it a directory?

Dateien schreiben

File.write ist der einfachste Weg, eine Datei zu schreiben (überschreibt standardmäßig). Verwenden Sie mode: 'a' zum Anhängen. File.open mit einem Block stellt sicher, dass die Datei geschlossen wird, selbst bei einer Exception. puts fügt einen Zeilenumbruch hinzu; write nicht. << ist ein Alias für write (häufig in Ruby). Für Logs öffnen Sie einmal und schreiben mehrmals (gepuffert für Leistung). Schließen Sie immer Dateien oder verwenden Sie die Block-Form, um Ressourcenlecks zu vermeiden.

ruby
# Write (overwrites)
File.write("output.txt", "Hello, World!")

# Append
File.write("output.txt", "More text\n", mode: "a")

# With a block (buffered, auto-closes)
File.open("log.txt", "w") do |f|
  f.puts "Line 1"
  f.puts "Line 2"
  f.write("No newline")
  f << "appended"  # << is an alias for write
end

# Binary write
File.binwrite("data.bin", binary_data)

# Modes: r (read), w (write/truncate), a (append),
#        r+ (read/write), w+ (read/write/truncate),
#        b (binary, Windows)

# Flush buffer
f = File.open("log.txt", "w")
f.write("data")
f.flush  # write without closing
f.close

Verzeichnis-Operationen

Dir.glob mit Patterns findet Dateien (** für rekursiv). FileUtils bietet robuste Datei-Operationen: mkdir_p erstellt verschachtelte Verzeichnisse, cp_r kopiert rekursiv, rm_rf entfernt kraftvoll (vorsichtig!). Dir.chdir wechselt das Arbeitsverzeichnis (verwenden Sie die Block-Form, um temporär zu wechseln). Dir.entries inkludiert . und ..; glob nicht. Bevorzugen Sie FileUtils gegenüber manuellen File-Operationen für plattformübergreifende Sicherheit.

ruby
require 'fileutils'

# List directory contents
Dir.entries(".")  # [".", "..", "file.txt", ...]
Dir.glob("*.rb")  # ["script.rb", "test.rb"]
Dir.glob("**/*.rb")  # recursive
Dir.glob("src/**/*.{rb,erb}")  # multiple extensions

# Create directories
Dir.mkdir("new_dir")
Dir.mkdir("nested/path") rescue Errno::ENOENT  # fails if parent missing
FileUtils.mkdir_p("nested/path/deep")  # creates all parents

# Remove
Dir.rmdir("empty_dir")  # only works if empty
FileUtils.rm_rf("dir")  # recursive force (dangerous!)

# Copy/Move
FileUtils.cp("a.txt", "b.txt")
FileUtils.mv("old.txt", "new.txt")
FileUtils.cp_r("src_dir", "dest_dir")  # recursive

# Change directory
Dir.chdir("/tmp") { puts Dir.pwd }  # temporarily
Dir.chdir("/tmp")  # permanently for process

# Check if directory
File.directory?("path")
File.exist?("path")

Pathname und Tempfile

Pathname ist ein objektorientierter Wrapper für Dateipfade – sauberer als String-Manipulation. Es bietet dirname, basename, extname, join und Datei-Prüfungen als Methoden. Tempfile erstellt temporäre Dateien, die automatisch gelöscht werden (verwenden Sie die Block-Form). Dir.mktmpdir erstellt temporäre Verzeichnisse. Verwenden Sie diese für saubere, sichere Pfad-Handhabung und temporäre Dateiverwaltung. Pathname komponiert Pfade sicher über Plattformen hinweg (behandelt / vs. \).

ruby
require 'pathname'
require 'tempfile'

# Pathname: object-oriented path manipulation
path = Pathname.new("/home/user/docs/file.txt")
path.dirname   # #<Pathname:/home/user/docs>
path.basename  # #<Pathname:file.txt>
path.extname   # ".txt"
path.parent    # #<Pathname:/home/user/docs>
path.join("sub", "file.rb")  # /home/user/docs/sub/file.rb

path.exist?
path.directory?
path.file?
path.readable?

# Tempfile: auto-deleted file
Tempfile.create("prefix") do |f|
  f.write("temporary data")
  f.rewind
  puts f.read
end  # file deleted after block

# Tempfile without block (must close/unlink manually)
tf = Tempfile.new("prefix")
tf.write("data")
tf.close
tf.unlink  # delete

# Dir.mktmpdir for temp directories
Dir.mktmpdir do |dir|
  # work in dir
end  # dir deleted after

CSV und JSON

CSV und JSON sind in Rubys Standardbibliothek eingebaut. CSV.foreach liest zeilenweise (speichereffizient); CSV.read lädt alles. headers: true behandelt die erste Zeile als Spaltennamen. JSON.parse konvertiert JSON zu Ruby-Hashes/Arrays; to_json serialisiert Ruby-Objekte. symbolize_names gibt Symbol-Schlüssel (sauberer). Für YAML verwenden Sie require 'yaml' und YAML.load_file. Diese sind essenziell für Datenaustausch in Ruby-Skripten und Web-Apps.

ruby
require 'csv'
require 'json'

# CSV reading
CSV.foreach("data.csv", headers: true) do |row|
  puts row['name']  # access by header
  puts row[0]       # access by index
end

# CSV reading all at once
rows = CSV.read("data.csv", headers: true)
rows.first['name']  # first row's name

# CSV writing
CSV.open("output.csv", "w") do |csv|
  csv << ["name", "age"]  # header
  csv << ["Alice", 30]
  csv << ["Bob", 25]
end

# JSON
data = { name: "Alice", age: 30 }
json = data.to_json  # '{"name":"Alice","age":30}'
parsed = JSON.parse(json)  # {"name"=>"Alice", "age"=>30}

# JSON with symbols
JSON.parse(json, symbolize_names: true)  # {name: "Alice", age: 30}

# Read/write JSON files
File.write("data.json", data.to_json)
loaded = JSON.parse(File.read("data.json"))
17

Encoding & String-Internas

String-Encodings

Ruby-Strings tragen ihr Encoding (normalerweise UTF-8). force_encoding reinterpretiert Bytes als ein anderes Encoding (keine Konvertierung – verwenden Sie, wenn Sie wissen, dass die Bytes bereits in diesem Encoding sind). encode konvertiert tatsächlich zwischen Encodings. valid_encoding? prüft, ob die Bytes gültig für das Encoding des Strings sind. Encoding-Probleme verursachen die gefürchtete Encoding::CompatibilityError. Wissen Sie immer das Encoding Ihrer Daten; standardmäßig UTF-8.

ruby
# Ruby strings have an encoding
"hello".encoding  # #<Encoding:UTF-8>
"café".encoding   # #<Encoding:UTF-8>

# Default external encoding (for file I/O)
Encoding.default_external  # #<Encoding:UTF-8>

# Force an encoding (reinterprets bytes, doesn't convert)
bytes = "café".bytes  # [99, 97, 102, 195, 169]
latin1 = bytes.pack("C*").force_encoding("ISO-8859-1")
# "caf" + é (as single byte)

# Convert encoding (transcodes)
utf8 = "café"
latin1 = utf8.encode("ISO-8859-1")  # converts
back = latin1.encode("UTF-8")        # converts back

# Check if valid
"abc".valid_encoding?  # true
"\xff".valid_encoding?  # false (invalid byte)

# Common encodings: UTF-8, ASCII, ISO-8859-1, Windows-1252
# Always use UTF-8 unless you have a specific reason

Encoding-Konvertierung und I/O

Datei-I/O verwendet Encoding.default_external für Lesen. Spezifizieren Sie Encoding pro Datei mit der encoding:-Option. Die 'source:target'-Syntax (ISO-8859-1:UTF-8) liest im Quell-Encoding und konvertiert zum Ziel. Das Setzen von default_internal lässt Ruby alle gelesenen Strings automatisch in dieses Encoding konvertieren. Für Web-Apps sollte alles UTF-8 sein. Bei der Verarbeitung von Legacy-Daten spezifizieren Sie Encodings explizit, um Korruption zu vermeiden.

ruby
# Read a file with specific encoding
content = File.read("data.txt", encoding: "ISO-8859-1")
# content.encoding is ISO-8859-1

# Read and convert to UTF-8
content = File.read("data.txt", encoding: "ISO-8859-1:UTF-8")
# The "source:target" syntax converts while reading

# Write with specific encoding
File.write("output.txt", "café", encoding: "UTF-8")

# Open with encoding
File.open("data.txt", "r:ISO-8859-1") do |f|
  f.read  # ISO-8859-1 encoded string
end

# Convert while opening
File.open("data.txt", "r:ISO-8859-1:UTF-8") do |f|
  f.read  # UTF-8 encoded string
end

# Set default encodings
Encoding.default_external = Encoding::UTF_8
Encoding.default_internal = Encoding::UTF_8

String-Methoden Deep Dive

Ruby-Strings haben reiche Methoden. Inspektion (length, include?, start_with?) prüft Eigenschaften. Transformationsmethoden geben neue Strings zurück (Strings sind in Ruby veränderlich, aber diese mutieren nicht). Substring-Zugriff verwendet [start, length] oder Ranges. sub ersetzt ersten Match; gsub ersetzt alle (unterstützt Regex und Blöcke). split/join konvertieren zwischen Strings und Arrays. Hinweis: Ruby 3.0+ frozen_string_literal-Pragma macht Strings unveränderlich für Leistung – verwenden Sie << oder + zum Erstellen.

ruby
s = "Hello, World"

# Inspection
s.length    # 12
s.empty?    # false
s.include?("World")  # true
s.start_with?("Hello")  # true
s.end_with?("World")  # true

# Transformation (return new string)
s.upcase     # "HELLO, WORLD"
s.downcase   # "hello, world"
s.capitalize # "Hello, world"
s.swapcase   # "hELLO, wORLD"
s.reverse    # "dlroW ,olleH"
s.strip      # remove leading/trailing whitespace
s.chomp(",") # remove trailing substring
s.chop       # remove last char

# Substrings
s[0, 5]      # "Hello" (start, length)
s[7..11]     # "World" (range)
s[-5..]      # "World" (negative index)

# Replace
s.sub("World", "Ruby")    # first match
s.gsub("o", "0")          # all matches
s.gsub(/\w+/) { |w| w.upcase }  # with block

# Split/Join
"a,b,c".split(",")  # ["a", "b", "c"]
["a", "b"].join("-")  # "a-b"

Frozen Strings und Performance

frozen_string_literal: true (Magic-Comment am Datei-Anfang) macht alle String-Literale unveränderlich – dies ist eine Performance-Optimierung (frozen Strings können Speicher teilen) und verhindert versehentliche Mutations-Bugs. Zum Erstellen von Strings verwenden Sie << (In-Place-Anhängen, O(n)) nicht += (erstellt jedes Mal einen neuen String, O(n²)). join ist am saubersten für Arrays. StringIO agiert wie eine Datei, schreibt aber in einen String – nützlich zum Erstellen komplexer Ausgaben. Ruby 3.x ermutigt standardmäßig zu frozen Strings.

ruby
# frozen_string_literal: true  (at top of file)
# Makes all string literals in the file frozen (immutable)

# Mutable vs frozen
s = "hello"
s << " world"  # "hello world" (mutates)

f = "hello".freeze
# f << " world"  # Error: can't modify frozen String

# Why freeze? Performance: frozen strings share memory
# "abc".freeze is the same object everywhere
"a".freeze.equal?("a".freeze)  # true (same object)

# Building strings efficiently
# BAD: creates many intermediate strings
result = ""
items.each { |i| result += i.to_s }  # O(n²)

# GOOD: use << or join
result = ""
items.each { |i| result << i.to_s }  # O(n)
result = items.map(&:to_s).join  # cleanest

# StringIO for buffered building
require 'stringio'
io = StringIO.new
io << "line 1\n"
io << "line 2\n"
result = io.string

Symbole vs. Strings

Symbole (:name) sind unveränderliche, Singleton-Identifikatoren – nur ein :foo existiert jemals im Speicher. Strings sind veränderliche Textdaten mit mehreren Instanzen. Verwenden Sie Symbole für Hash-Schlüssel (schnellere Gleichheitsprüfungen), Methodennamen und Enum-ähnliche Werte. Verwenden Sie Strings für tatsächlichen Text. Symbole sind etwas schneller für Hash-Schlüssel und Vergleich. In modernem Ruby (2.2+) können Symbole garbage-collected werden, also ist die alte Sorge um 'Symbol-Memory-Leaks' vorbei. Rails verwendet Symbole extensiv für Schlüssel und Statuswerte.

ruby
# Symbol: immutable, reusable identifier
:hello
:world
status = :active

# String: mutable, can have many instances
"hello"
"world"
status = "active"

# Key difference: identity
"hello".equal?("hello")  # false (different objects)
:hello.equal?(:hello)    # true (same object, singleton)

# Memory: symbols are singletons
1000.times { :foo }  # one :foo object
1000.times { "foo" }  # 1000 "foo" objects (unless frozen)

# Use symbols for:
# - Hash keys (faster comparison)
# - Method names (send(:method_name))
# - Enum-like values (:active, :pending, :closed)
# - Identifiers (not text data)

# Use strings for:
# - Text data (names, content)
# - Things that change

# Conversion
:hello.to_s  # "hello"
"hello".to_sym  # :hello
"hello world".to_sym  # :"hello world" (valid but ugly)
18

Module & Mixins

Modul-Grundlagen

Module dienen zwei Zwecken: Namespacing (Gruppieren verwandten Codes, Verhindern von Namenskonflikten) und Mixins (Teilen von Verhalten ohne Vererbung). Modul-Methoden (def self.method) werden auf dem Modul aufgerufen. Instanzmethoden (def method) sind zum Mixen in Klassen. Module können nicht instanziiert werden. Verwenden Sie Module, um Klassen zu namespacen (MyApp::User) und um Konstanten und Utility-Funktionen zu organisieren. Dies ist Rubys Alternative zur mehrfachen Vererbung.

ruby
# Module: a namespace + collection of methods
module MathUtils
  PI = 3.14159

  def self.circle_area(radius)  # module method
    PI * radius ** 2
  end

  def square(x)  # instance method (for mixins)
    x * x
  end
end

# Access constants and module methods
MathUtils::PI  # 3.14159
MathUtils.circle_area(5)  # 78.54

# Namespacing classes
module MyApp
  class User
    # MyApp::User
  end
end

# Prevent instantiation (modules can't be instantiated)
# MathUtils.new  # NoMethodError

Include vs. Extend vs. Prepend

Drei Wege, Module einzumischen: include (fügt Instanzmethoden hinzu, geht unter die Klasse im Lookup), extend (fügt Klassenmethoden hinzu), prepend (fügt Instanzmethoden hinzu, geht über die Klasse – kann wrappen/überschreiben). prepend ist mächtig für Before/After-Hooks (rufen Sie super auf, um das Original aufzurufen). Method-Lookup: prepend → Klasse → include → Superklasse. Verwenden Sie include für normale Mixins, prepend, wenn Sie vorhandene Methoden wrappen müssen, extend für Klassen-Ebene-Funktionalität.

ruby
module Greetable
  def greet
    "Hello from #{self.class}"
  end
end

class User
  include Greetable  # adds as instance methods
end
User.new.greet  # "Hello from User"

class Service
  extend Greetable  # adds as class methods
end
Service.greet  # "Hello from Service"

# Method lookup order:
# prepend → class → include → super
module Logging
  def save
    puts "before save"
    super  # calls the original save
    puts "after save"
  end
end

class Record
  prepend Logging  # Logging#save runs first
  def save; puts "saving"; end
end
Record.new.save
# before save / saving / after save

Enumerable Mixin

Das Inkludieren von Enumerable und Definieren von #each gibt Ihnen map, select, reduce, sort, min, max und 40+ weitere Methoden – dies ist Rubys Iterator-Protokoll. Das Inkludieren von Comparable und Definieren von <=> gibt Ihnen <, >, ==, between?, clamp und Sort-Support. Diese Mixins sind der Grund, warum Ruby-Collections so mächtig sind. Jede Klasse, die eine Collection repräsentiert oder eine natürliche Ordnung hat, sollte diese inkludieren. Es ist Komposition über Vererbung.

ruby
# Include Enumerable + define #each = 50+ methods free
class Playlist
  include Enumerable

  def initialize(songs)
    @songs = songs
  end

  def each
    @songs.each { |song| yield song }
  end
end

playlist = Playlist.new(["Song A", "Song B", "Song C"])
playlist.map { |s| s.upcase }   # ["SONG A", "SONG B", "SONG C"]
playlist.select { |s| s.include?("A") }  # ["Song A"]
playlist.reduce(:+)            # "Song ASong BSong C"
playlist.sort                  # ["Song A", "Song B", "Song C"]
playlist.first(2)              # ["Song A", "Song B"]
playlist.include?("Song B")    # true
playlist.count                 # 3

# Comparable: define <=> for natural ordering
class Temperature
  include Comparable
  attr_reader :celsius

  def initialize(c) @celsius = c end
  def <=>(other) celsius <=> other.celsius end
end

t1 = Temperature.new(20)
t2 = Temperature.new(30)
t1 < t2   # true (uses <=>)
t1 == t2  # false

Singleton-Methoden und Klassenmethoden

Singleton-Methoden gehören zu einem spezifischen Objekt. Klassenmethoden sind nur Singleton-Methoden auf dem Klassen-Objekt. 'class << self' öffnet die Singleton-Klasse (Eigenclass), um mehrere Klassenmethoden sauber zu definieren. Das Singleton-Pattern verwendet eine Klassenvariable, um eine Instanz zu halten. Singleton-Klassen zu verstehen, ist Schlüssel zu Rubys Objektmodell – jedes Objekt hat eine Singleton-Klasse, die seine einzigartigen Methoden hält. Dies ermöglicht Per-Objekt-Anpassung und Metaprogrammierung.

ruby
# Singleton method: defined on one object only
str = "hello"
def str.shout
  upcase + "!"
end
str.shout  # "HELLO!"
# "world".shout  # NoMethodError (only str has it)

# Class methods are singleton methods on the class
class Calculator
  def self.add(a, b)  # singleton method on Calculator
    a + b
  end
end
Calculator.add(1, 2)  # 3

# Singleton class (eigenclass): where singleton methods live
class Calculator
  class << self  # opens the singleton class
    def multiply(a, b) a * b end
    def divide(a, b) a / b end
  end
end
Calculator.multiply(2, 3)  # 6

# Singleton pattern
class Logger
  @instance = Logger.new
  class << self
    attr_reader :instance
  end
end
Logger.instance.equal?(Logger.instance)  # true

Refinements (Scoped Monkey Patching)

Refinements (Ruby 2.1+) erlauben scoped Monkey-Patching – fügen Sie Methoden zu vorhandenen Klassen hinzu, aber nur wo Sie die Refinement explizit 'using' verwenden. Dies ist sicherer als globales Monkey-Patching (das anderen Code brechen kann). Refinements werden pro Scope aktiviert (Datei, Klasse, Methode). Sie sind nützlich, um Convenience-Methoden hinzuzufügen, ohne den globalen Namespace zu verschmutzen. Seltener als sie sein sollten aufgrund von Performance und einigen Scoping-Quirks, aber sie sind der 'richtige' Weg, Core-Klassen zu erweitern.

ruby
# Refinement: limited monkey patching
module StringRefinements
  refine String do
    def shout
      upcase + "!!!"
    end

    def palindrome?
      downcase == downcase.reverse
    end
  end
end

# Without using: shout is undefined
# "hello".shout  # NoMethodError

# With using: refinement is active in this scope
using StringRefinements
"hello".shout        # "HELLO!!!"
"racecar".palindrome?  # true

# Scoped to file or module
class App
  using StringRefinements
  def greet
    "hi".shout  # works here
  end
end
# "hi".shout  # NoMethodError (outside App)

# Safer than global monkey patching
# Refinements only affect code that opts in
19

Blöcke Procs Lambdas Deep

Block-Grundlagen

Blöcke sind anonyme Closures, die an Methoden via { } oder do...end weitergegeben werden. yield ruft den Block aus der Methode heraus auf. Blöcke können Parameter via |var| nehmen. Eine Methode kann mit block_given? prüfen, ob ein Block weitergegeben wurde. Blöcke sind die Grundlage von Rubys Iterator-Pattern und DSLs. Sie erfassen Variablen aus dem umschließenden Scope (Closures).

ruby
# Block: anonymous chunk of code passed to a method
[1, 2, 3].each { |n| puts n }      # Single-line
[1, 2, 3].each do |n|              # Multi-line
  puts n
end

# yield invokes the block
def greet
  print "Hello, "
  yield          # Calls the block
  puts "!"
end

greet { print "World" }   # Hello, World!

# yield with arguments
def calculate(a, b)
  yield(a + b)
  yield(a * b)
end

calculate(3, 4) { |result| puts "Result: #{result}" }

Procs & Lambdas

Procs und Lambdas sind Objekte, die Blöcke wrappen und das Speichern in Variablen und Weitergeben ermöglichen. Procs haben nachsichtige Argument-Prüfung (extra Argumente ignoriert) und return aus der umschließenden Methode. Lambdas haben strenge Argument-Prüfung und return nur aus sich selbst. Verwenden Sie Procs für Flexibilität (wie Methoden, die Blöcke akzeptieren), Lambdas für anonyme Funktionen mit vorhersagbarem Verhalten. Die ->() {}-Syntax (Stabby-Lambda) ist prägnant für Einzeiler.

ruby
# Proc: object wrapping a block
my_proc = Proc.new { |x| puts x * 2 }
my_proc.call(5)         # 10
my_proc.(5)             # 10 (shorthand)
my_proc[5]              # 10 (another shorthand)

# Lambda: stricter Proc
my_lambda = lambda { |x| puts x * 2 }
my_lambda = ->(x) { puts x * 2 }   # Stabby lambda

# Difference 1: argument checking
my_proc.call(1, 2, 3)    # OK (ignores extra args)
my_lambda.call(1, 2, 3)  # ArgumentError (wrong number of args)

# Difference 2: return behavior
def proc_test
  p = Proc.new { return 1 }
  p.call
  return 2  # Never reached
end

def lambda_test
  l = lambda { return 1 }
  l.call    # Returns 1 to the lambda, not the method
  return 2  # Reached
end

Method-Objekte

Die method()-Methode gibt ein Method-Objekt zurück, das eine vorhandene Methode wrapt. Der &-Operator konvertiert ein Method- oder Proc zu einem Block (und umgekehrt). Symbol#to_proc konvertiert :upcase zu { |x| x.upcase } und ermöglicht die prägnante &:symbol-Syntax. Dies ist idiomatisches Ruby für kurze Block-Operationen. Method-Objekte sind an ihren Empfänger gebunden, also merken sie sich self beim Weitergeben.

ruby
# Convert method to Proc with method()
class Calculator
  def add(a, b) a + b end
  def multiply(a, b) a * b end
end

calc = Calculator.new
add_proc = calc.method(:add)
puts add_proc.call(2, 3)   # 5

# Pass method as block with & operator
def apply_op(a, b, op)
  op.call(a, b)
end

puts apply_op(2, 3, calc.method(:add))       # 5
puts apply_op(2, 3, calc.method(:multiply))  # 6

# Symbol#to_proc
words = ["hello", "world"]
upcased = words.map(&:upcase)   # ["HELLO", "WORLD"]
# Equivalent to: words.map { |w| w.upcase }

# Common with &:map(&:to_i), select(&:positive?), sort_by(&:length)

Closures & Variablen

Closures erfassen Variablen per Referenz, nicht per Wert. Mehrere Closures können Zustand teilen (das Zähler-Beispiel). Block-lokale Variablen (nach ; in Parametern deklariert) shadowen äußere Variablen, ohne sie zu modifizieren. Dies ermöglicht funktionale Patterns wie Akkumulatoren, Generatoren und Memoization. Vorsicht: Closures, die Referenzen halten, können Memory-Lecks verursachen, wenn nicht freigegeben. Verwenden Sie dieses Pattern für private State-Kapselung.

ruby
# Blocks/Procs/Lambdas capture variables (closures)
counter = 0
increment = lambda { counter += 1 }

increment.call
increment.call
puts counter   # 2

# Multiple closures sharing state
def make_counters
  count = 0
  [
    lambda { count += 1 },
    lambda { count },
    lambda { count = 0 }
  ]
end

inc, get, reset = make_counters
inc.call; inc.call
puts get.call   # 2
reset.call
puts get.call   # 0

# Block-local variables (shadow outer)
x = 10
[1, 2, 3].each do |y; x|   # x is block-local
  x = y * 2
end
puts x   # 10 (unchanged)

Benutzerdefinierte Iteratoren

Das Inkludieren von Enumerable und Implementieren von each gibt Ihrer Klasse alle Iterator-Methoden (map, select, reduce, sort usw.) kostenlos. Geben Sie den Block mit &block weiter und rufen Sie ihn auf, oder verwenden Sie yield. Der &-Operator konvertiert einen Block zu einem Proc und zurück. Dies ist der idiomatische Weg, benutzerdefinierte Collections iterierbar zu machen. Implementieren Sie each für Vorwärtsiteration; fügen Sie reverse_each für bidirektional hinzu.

ruby
class Tree
  include Enumerable

  def initialize(value, children = [])
    @value = value
    @children = children
  end

  def each(&block)
    block.call(@value)
    @children.each { |child| child.each(&block) }
  end
end

tree = Tree.new(1, [
  Tree.new(2, [Tree.new(4), Tree.new(5)]),
  Tree.new(3)
])

tree.each { |v| puts v }            # 1 2 4 5 3
puts tree.map { |v| v * 2 }.inspect # [2, 4, 8, 10, 6]
puts tree.select(&:even?).inspect   # [2, 4]
puts tree.reduce(:+)                # 15

# Including Enumerable gives you map, select, reduce, etc.
20

Metaprogrammierung

Dynamische Methoden

method_missing fängt Aufrufe an undefinierte Methoden ab und ermöglicht dynamisches Dispatch. Überschreiben Sie immer respond_to_missing?, um zu matchen. define_method erstellt Methoden zur Laufzeit, nützlich zum Generieren ähnlicher Methoden (wie ActiveRecords find_by_*). instance_variable_get/set greift auf Instanzvariablen nach Namen zu. Verwenden Sie Metaprogrammierung sparsam – sie macht Code schwerer zu verstehen und zu debuggen. Bevorzugen Sie explizite Definitionen, wenn möglich.

ruby
class Person
  attr_accessor :name, :age

  def initialize(name, age)
    @name = name
    @age = age
  end

  # Method missing: catch undefined method calls
  def method_missing(name, *args, &block)
    if name.to_s =~ /^(.*)_with_prefix$/
      attr_name = $1
      "PREFIX_#{send(attr_name)}"
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    name.to_s =~ /^(.*)_with_prefix$/ || super
  end
end

p = Person.new("Alice", 30)
puts p.name_with_prefix   # PREFIX_Alice

# Define methods dynamically
class Person
  [:home, :work, :mobile].each do |type|
    define_method("#{type}_phone") do
      instance_variable_get("@#{type}_phone")
    end

    define_method("#{type}_phone=") do |value|
      instance_variable_set("@#{type}_phone", value)
    end
  end
end

Offene Klassen & Monkey Patching

Ruby-Klassen sind offen: Sie können Methoden zu jeder Klasse hinzufügen, einschließlich Built-ins wie String. Dies ist mächtig, aber gefährlich (Monkey-Patching kann anderen Code brechen). Refinements (Ruby 2.1+) bieten scoped Monkey-Patches: Sie gelten nur innerhalb von Dateien/Klassen, die das Modul verwenden. Bevorzugen Sie Refinements gegenüber globalen Monkey-Patches für sicherere Metaprogrammierung. Dokumentieren Sie Patches klar und vermeiden Sie Änderungen am Core-Verhalten.

ruby
# Reopen existing classes (monkey patching)
class String
  def shout
    upcase + "!"
  end

  def word_count
    split.size
  end
end

puts "hello world".shout       # HELLO WORLD!
puts "one two three".word_count # 3

# Refinements: scoped monkey patches
module ShoutRefinement
  refine String do
    def shout
      upcase + "!"
    end
  end
end

class MyClass
  using ShoutRefinement

  def greet(name)
    "hello #{name}".shout   # Works here
  end
end

# "test".shout  # NoMethodError (refinement not active)

Hooks & Callbacks

Ruby bietet Lifecycle-Hooks: inherited (Subklasse erstellt), included (Modul inkludiert), prepended (Modul prependet), method_added (Methode definiert), method_removed, method_undefined. Diese ermöglichen Frameworks, automatisch auf Klassenänderungen zu reagieren. ActiveRecord verwendet diese, um Attribute zu verfolgen, Rails verwendet sie für Routing. Überschreiben Sie Hooks als Klassenmethoden (self.inherited) für Klassen-Ebene-Events, Instanzmethoden für Methoden-Events.

ruby
class Observable
  # Hook: called when class is subclassed
  def self.inherited(subclass)
    puts "#{subclass} inherits from #{self}"
  end

  # Hook: called when a module is included
  def self.included(base)
    puts "#{self} included in #{base}"
  end

  # Hook: called when a method is added
  def method_added(name)
    puts "Added method: #{name}"
  end

  # Hook: called when method is undefined
  def method_removed(name)
    puts "Removed method: #{name}"
  end
end

class Child < Observable
  def my_method; end
end
# Output:
# Child inherits from Observable
# Added method: my_method

# Other hooks: method_undefined, extended, prepended

eval & Binding

eval führt einen String als Ruby-Code aus, gefährlich bei nicht vertrauenswürdiger Eingabe (Code-Injection). binding erfasst den aktuellen Ausführungskontext (Variablen, self) für späteres eval. class_eval führt Code in einem Klassen-Kontext aus (definiert Methoden). instance_eval ändert self zum Empfänger. Verwenden Sie diese für DSLs und Code-Generierung, aber vermeiden Sie eval bei Benutzereingabe. Bevorzugen Sie Blöcke und define_method für dynamischen Code. Sanitisieren Sie immer Eingabe, wenn eval unvermeidbar ist.

ruby
# eval: execute a string as Ruby code
result = eval("1 + 2 * 3")
puts result   # 7

# eval with binding (context)
def eval_in_context(code)
  x = 10
  binding   # Returns a Binding object capturing local variables
end

b = eval_in_context("binding")
puts eval("x * 2", b)   # 20

# class_eval: execute code in class context
String.class_eval do
  def palindrome?
    self == reverse
  end
end

puts "racecar".palindrome?   # true

# instance_eval: execute in instance context
"hello".instance_eval do
  puts length   # 5 (self is the string)
end

# NEVER eval user input (security risk)

Reflection & Introspektion

Ruby bietet reiche Reflection: methods listet alle Methoden, instance_variables listet Instanzvariablen, ancestors zeigt die Vererbungskette. parameters offenbart Parameternamen und -typen einer Methode. respond_to? prüft, ob ein Objekt auf eine Methode reagiert. Verwenden Sie Reflection für Debugging, Serialisierung (Attribute inspizieren) und das Bauen generischer Tools (wie ORMs). Vermeiden Sie übermäßige Reflection – sie umgeht Typsicherheit und macht Code schwerer zu folgen.

ruby
class User
  attr_accessor :name, :email

  def initialize(name, email)
    @name = name
    @email = email
  end

  def save; end
  def valid?; true; end
end

u = User.new("Alice", "[email protected]")

# Inspect methods
puts u.methods - Object.methods    # [:name, :email, :save, :valid?, ...]
puts u.public_methods(false)       # Only this class's methods
puts User.instance_methods(false)  # [:save, :valid?, ...]

# Inspect variables
puts u.instance_variables.inspect   # [:@name, :@email]
puts u.instance_variable_get(:@name) # Alice

# Inspect class
puts User.ancestors.inspect
puts User.instance_method(:save).parameters

# Check if responds to method
puts u.respond_to?(:save)   # true
puts u.is_a?(User)          # true
21

Testing (RSpec/Minitest)

RSpec-Grundlagen

RSpec ist ein BDD-Style-Test-Framework. describe gruppiert verwandte Tests, context gruppiert nach Bedingung, it definiert einen einzelnen Test. let erstellt lazy memoisierte Variablen. expect(...).to matcher ist die Assertions-Syntax. Häufige Matcher: eq, be_valid, include, raise_error. subject + is_expected reduziert Boilerplate. Führen Sie mit rspec (alle) oder rspec path/to/spec (spezifisch) aus. Verwenden Sie Factories (FactoryBot) statt Fixtures für Testdaten.

ruby
# spec/spec_helper.rb
require 'rspec'
require_relative '../lib/my_app'

# spec/models/user_spec.rb
require 'spec_helper'

RSpec.describe User do
  # Setup with let (lazy, memoized)
  let(:user) { User.new(name: 'Alice', email: '[email protected]') }

  describe '#name' do
    it 'returns the name' do
      expect(user.name).to eq('Alice')
    end
  end

  describe '#valid?' do
    context 'with valid attributes' do
      it 'is valid' do
        expect(user).to be_valid
      end
    end

    context 'without name' do
      let(:user) { User.new(email: '[email protected]') }
      it 'is invalid' do
        expect(user).not_to be_valid
        expect(user.errors[:name]).to include("can't be blank")
      end
    end
  end

  # Multiple examples
  describe '#age' do
    subject { user.age }
    it { is_expected.to be >= 0 }
  end
end

# Run: rspec spec/models/user_spec.rb
# Run all: rspec

RSpec Mocks & Stubs

Stubs (allow) ersetzen Methoden-Rückgabewerte; Mocks (expect) verifizieren, dass die Methode aufgerufen wurde. double erstellt einen Test-Double (Fake-Objekt). and_return setzt Rückgabewerte, and_raise simuliert Fehler, with setzt erwartete Argumente. Verwenden Sie Mocks sparsam – Over-Mocking macht Tests brüchig. Testen Sie Verhalten, nicht Implementierung. Bevorzugen Sie echte Objekte, wenn machbar; verwenden Sie Mocks für externe Services (APIs, E-Mail, Payment-Gateways).

ruby
RSpec.describe PaymentProcessor do
  let(:gateway) { double('PaymentGateway') }
  let(:processor) { PaymentProcessor.new(gateway) }

  describe '#charge' do
    it 'charges the gateway' do
      # Stub: replace method return value
      allow(gateway).to receive(:charge).and_return(success: true)

      result = processor.charge(100)
      expect(result[:success]).to be true
    end

    it 'raises on gateway error' do
      allow(gateway).to receive(:charge)
        .and_raise(GatewayError, 'Network failure')

      expect { processor.charge(100) }
        .to raise_error(GatewayError, /Network/)
    end

    it 'verifies the gateway was called' do
      # Mock: expect method to be called
      expect(gateway).to receive(:charge).with(100)
      processor.charge(100)
    end

    it 'receives multiple calls' do
      allow(gateway).to receive(:charge).and_return(true, false, true)
      expect(gateway.charge(1)).to be true
      expect(gateway.charge(2)).to be false
    end
  end
end

Minitest

Minitest ist Rubys Standard-Test-Bibliothek (mit Ruby geliefert). Sie unterstützt sowohl Unit-Style (assert/refute) als auch Spec-Style (must_equal)-Syntax. setup läuft vor jedem Test. Assertionen: assert_equal, assert_nil, assert_raises, assert_includes. Minitest ist schneller als RSpec und hat weniger Abhängigkeiten. Verwenden Sie Minitest::Mock für Mocking oder Mocha-Gem für mehr Features. Rails verwendet standardmäßig Minitest (ActiveSupport::TestCase).

ruby
# test/test_helper.rb
require 'minitest/autorun'
require_relative '../lib/my_app'

# test/models/user_test.rb
require 'test_helper'

class UserTest < Minitest::Test
  def setup
    @user = User.new(name: 'Alice', email: '[email protected]')
  end

  def test_name
    assert_equal 'Alice', @user.name
  end

  def test_valid_with_attributes
    assert @user.valid?
  end

  def test_invalid_without_name
    @user.name = nil
    refute @user.valid?
    assert_includes @user.errors[:name], "can't be blank"
  end

  def test_raises_on_error
    assert_raises(ArgumentError) { User.new! }
  end
end

# Spec-style Minitest
class UserSpec < Minitest::Spec
  let(:user) { User.new(name: 'Alice') }

  it 'has a name' do
    _(user.name).must_equal 'Alice'
  end
end

# Run: rake test
# Run specific: ruby -Itest test/models/user_test.rb

Testdaten & Factories

FactoryBot generiert Test-Objekte mit Standardwerten, Traits für Variationen und Assoziationen. create persistiert in DB; build nicht. Traits komponieren (factory :admin_with_posts, traits: [:admin, :with_posts]). Factories sind flexibler als Fixtures, aber langsamer. Fixtures (YAML) sind schneller, aber weniger flexibel. Wählen Sie basierend auf Bedarf: Fixtures für einfache Daten, Factories für komplexe Beziehungen. Vermeiden Sie Factories mit zu vielen Traits – halten Sie sie fokussiert.

ruby
# FactoryBot (gem 'factory_bot')
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    name { 'Alice' }
    email { '[email protected]' }
    age { 30 }

    trait :admin do
      role { 'admin' }
    end

    trait :with_posts do
      after(:create) do |user|
        create_list(:post, 3, user: user)
      end
    end

    factory :admin_user, traits: [:admin]
    factory :user_with_posts, traits: [:with_posts]
  end
end

# Usage in specs
let(:user) { create(:user) }            # Saved to DB
let(:admin) { create(:admin_user) }
let(:user_with_posts) { create(:user_with_posts) }
let(:unsaved_user) { build(:user) }     # Not saved

# Fixtures (alternative)
# test/fixtures/users.yml
# alice:
#   name: Alice
#   email: [email protected]

# In tests: users(:alice)

Integration- & System-Tests

Request-Specs testen den gesamten Stack (Routing, Controller, Modelle) via HTTP. System-Specs (Capybara) steuern einen echten Browser und testen JavaScript-Interaktionen. Verwenden Sie Request-Specs für API-Endpunkte, System-Specs für User-Flows. have_http_status prüft Response-Codes. visit/fill_in/click_button simulieren Benutzeraktionen. System-Tests sind langsamer, finden aber Integrations-Bugs. Führen Sie System-Tests mit js: true für JavaScript-getriebene Seiten aus.

ruby
# spec/requests/users_spec.rb (integration)
require 'spec_helper'

RSpec.describe 'Users API', type: :request do
  describe 'GET /users' do
    before { create_list(:user, 3) }

    it 'returns all users' do
      get '/users', as: :json
      expect(response).to have_http_status(200)
      expect(JSON.parse(response.body).size).to eq(3)
    end
  end

  describe 'POST /users' do
    it 'creates a user' do
      post '/users', params: { user: { name: 'Bob', email: '[email protected]' } }
      expect(response).to have_http_status(201)
      expect(User.last.name).to eq('Bob')
    end
  end
end

# System tests (browser automation)
# spec/system/login_spec.rb
require 'spec_helper'

RSpec.describe 'Login', type: :system do
  it 'logs in a user' do
    user = create(:user, password: 'password123')
    visit login_path
    fill_in 'Email', with: user.email
    fill_in 'Password', with: 'password123'
    click_button 'Log In'
    expect(page).to have_content('Welcome')
  end
end
22

Threads & Fibers

Thread-Grundlagen

Threads in Ruby (MRI) sind Green-Threads, die von der VM geschedult werden – sie laufen aufgrund des GIL (Global Interpreter Lock) nicht wirklich parallel. Allerdings sind Threads nützlich für I/O-Nebenläufigkeit (Netzwerk, Datei-Operationen). join wartet auf die Vervollständigung eines Threads. value ruft den Rückgabewert ab. Für CPU-gebundenen Parallelismus verwenden Sie mehrere Prozesse (fork, Sidekiq) oder JRuby (kein GIL). Joinen Sie immer Threads, um zu vermeiden, dass sie gekillt werden, wenn der Main-Thread beendet wird.

ruby
# Create and run threads
threads = (1..3).map do |i|
  Thread.new(i) do |n|
    puts "Thread #{n} started"
    sleep(1)
    puts "Thread #{n} finished"
  end
end

# Wait for all threads
threads.each(&:join)
puts "All done"

# Thread with return value
thread = Thread.new { 1 + 2 }
result = thread.value   # 3 (waits for completion)

# Thread status
thread = Thread.new { sleep(1) }
puts thread.status   # 'run', 'sleep', false (finished), nil (error)
thread.join

# Current thread
Thread.current
Thread.list   # All threads
Thread.main   # Main thread

Thread-Synchronisation

Mutex.synchronize stellt sicher, dass nur ein Thread einen Block gleichzeitig ausführt und verhindert Race-Conditions. Queue ist eine Thread-sichere FIFO – Produzenten pushen, Konsumenten popen (blockiert, wenn leer). ConditionVariable koordiniert Threads: wait gibt den Mutex frei und schläft, signal/broadcast weckt wartende Threads. Verwenden Sie immer Synchronisation für geteilten veränderlichen Zustand. Deadlocks entstehen, wenn Threads aufeinander warten – akquirieren Sie Locks in konsistenter Reihenfolge.

ruby
# Mutex: mutual exclusion
counter = 0
mutex = Mutex.new

threads = 10.times.map do
  Thread.new do
    1000.times do
      mutex.synchronize { counter += 1 }
    end
  end
end
threads.each(&:join)
puts counter   # 10000 (without mutex, would be less)

# Queue: thread-safe FIFO
require 'thread'
queue = Queue.new

producer = Thread.new do
  5.times { |i| queue << "item #{i}" }
  queue << :done
end

consumer = Thread.new do
  loop do
    item = queue.pop
    break if item == :done
    puts "Processed: #{item}"
  end
end

[producer, consumer].each(&:join)

# ConditionVariable: signal between threads
mutex = Mutex.new
cv = ConditionVariable.new

Fibers

Fibers sind kooperative leichte Threads: Sie yielden Kontrolle manuell statt präemptiert zu werden. resume startet/setzt eine Fiber fort; Fiber.yield pausiert sie und gibt einen Wert zurück. Fibers sind nützlich für Generatoren, verzögerte Auswertung und Parsing-Zustandsautomaten. Im Gegensatz zu Threads läuft nur eine Fiber gleichzeitig, also ist keine Synchronisation nötig. Fibers sind billiger als Threads, können aber nicht mehrere Cores nutzen. Verwenden Sie sie für I/O-Multiplexing (EventMachine, Async).

ruby
# Fiber: cooperative concurrency (manual scheduling)
fiber = Fiber.new do
  puts "Fiber started"
  Fiber.yield "first yield"
  puts "Fiber resumed"
  Fiber.yield "second yield"
  puts "Fiber ending"
  "fiber done"
end

puts fiber.resume   # "Fiber started", returns "first yield"
puts fiber.resume   # "Fiber resumed", returns "second yield"
puts fiber.resume   # "Fiber ending", returns "fiber done"

# Generator pattern with Fiber
def fibonacci
  Fiber.new do
    a, b = 0, 1
    loop do
      Fiber.yield a
      a, b = b, a + b
    end
  end
end

fib = fibonacci
10.times { print fib.resume, " " }
# 0 1 1 2 3 5 8 13 21 34

Async & Nebenläufiges Ruby

Die async-Gem bietet modernes Async-I/O unter Verwendung von Fibers und ermöglicht hochnebenläufigen Netzwerk-Code. concurrent-ruby bietet Thread-sichere Abstraktionen: Future (Async-Ergebnis), Promise (verkettbar), Thread-Pools und atomare Variablen. Verwenden Sie async für I/O-gebundene Arbeit (HTTP, Datenbanken), Thread-Pools für CPU-gebundene Arbeit. Fahren Sie Pools immer herunter, um Hänger zu vermeiden. Diese Gems umgehen MRIs GIL für praktische Nebenläufigkeit.

ruby
# Async gem for modern async I/O
# gem install async
require 'async'
require 'async/http/internet'

Async do
  internet = Async::HTTP::Internet.new

  # Run requests concurrently
  tasks = 3.times.map do |i|
    Async do
      response = internet.get("https://httpbin.org/delay/#{i}")
      puts "Request #{i}: #{response.status}"
    end
  end

  tasks.each(&:wait)
end

# Concurrent Ruby (gem 'concurrent-ruby')
require 'concurrent'

# Future: async computation
future = Concurrent::Future.execute { sleep(1); 42 }
puts future.value   # 42 (blocks until ready)

# Promise chain
Concurrent::Promise.execute { 1 }
  .then { |v| v + 1 }
  .then { |v| v * 2 }
  .then { |v| puts v }   # 4

# Thread pool
pool = Concurrent::FixedThreadPool.new(4)
10.times do |i|
  pool.post { puts "Task #{i} on #{Thread.current.object_id}" }
end
pool.shutdown

Ractors (Ruby 3.0+)

Ractors (Ruby 3.0+) bieten echten Parallelismus, indem sie den GIL vermeiden. Jeder Ractor hat seinen eigenen Heap, also keine Data Races. Kommunikation erfolgt via Messages (send/receive, take). Gesendete Objekte werden kopiert (oder mit Ractor.move verschoben), um Isolation zu wahren. Frozen Objects können geteilt werden. Ractors sind die Zukunft von parallellem Ruby, haben aber Einschränkungen: Die meisten Gems sind noch nicht Ractor-sicher. Verwenden Sie sie für CPU-gebundene parallele Berechnung, wo Isolation akzeptabel ist.

ruby
# Ractor: true parallelism (no GIL)
# Ruby 3.0+ feature for thread-safe parallel execution

ractor = Ractor.new do
  value = receive    # Receive from main
  value * 2
end

ractor.send(21)
puts ractor.take     # 42

# Parallel computation
ractors = (1..4).map do |i|
  Ractor.new(i) do |n|
    sleep(1)
    n * n
  end
end

results = Ractor.select(*ractors)
# Or: ractors.map(&:take)  # [1, 4, 9, 16]

# Ractor restrictions:
# - Cannot share mutable objects
# - Communication only via messages
# - Copy or move semantics for sending

# Shared frozen objects are OK
SHARED = Ractor.new { [1, 2, 3].freeze }
# Multiple ractors can read SHARED safely
23

Encoding & IO

String-Encoding

Jeder Ruby-String hat ein Encoding (Standard UTF-8). bytesize ist die Byte-Anzahl; length ist die Zeichen-Anzahl (unterschiedlich für Multibyte-Encodings). force_encoding ändert das Encoding-Tag ohne Bytes zu konvertieren (verwenden Sie, wenn Sie das tatsächliche Encoding kennen). encode konvertiert zwischen Encodings. Prüfen Sie immer valid_encoding? vor der Verarbeitung externer Daten. Setzen Sie das Quell-Datei-Encoding mit dem Magic-Comment # encoding: utf-8 (obwohl UTF-8 in Ruby 2.0+ Standard ist).

ruby
# Ruby strings have encodings
s = "hello"
puts s.encoding              # <Encoding:UTF-8>
puts s.bytesize              # 5
puts s.length               # 5

# Multibyte characters
japanese = "こんにちは"
puts japanese.encoding       # UTF-8
puts japanese.bytesize       # 15 (3 bytes per char)
puts japanese.length         # 5 (5 characters)

# Force encoding (interpret bytes differently)
bytes = "caf\u00e9".force_encoding('ASCII-8BIT')
puts bytes.encoding          # ASCII-8BIT

# Encode (convert to different encoding)
utf8 = "café"
latin1 = utf8.encode('ISO-8859-1')
puts latin1.encoding         # ISO-8859-1

# Invalid bytes
bad = "\xFF\xFE".force_encoding('UTF-8')
puts bad.valid_encoding?     # false
fixed = bad.encode('UTF-8', invalid: :replace, replace: '?')

Datei-IO

File.read lädt die gesamte Datei in den Speicher; File.foreach liest zeilenweise (speichereffizient für große Dateien). File.write überschreibt; mode 'a' hängt an; 'r+' liest und schreibt. Binär-Modus ('rb', 'wb') verhindert Encoding-Konvertierung. Verwenden Sie immer die Block-Form (File.open), um sicherzustellen, dass die Datei geschlossen wird. FileUtils bietet höhere Operationen (cp, mv, mkdir_p). Prüfen Sie Existenz mit File.exist? vor dem Lesen.

ruby
# Read entire file
content = File.read('data.txt')

# Read line by line (memory efficient)
File.foreach('large.log') do |line|
  puts line.chomp
end

# Write to file
File.write('output.txt', 'Hello, World!')

# Append
File.open('log.txt', 'a') do |f|
  f.puts "New log entry"
end

# Read/Write modes
File.open('file.txt', 'r+') do |f|
  content = f.read
  f.rewind
  f.write("Updated: " + content)
end

# Binary mode
File.open('image.png', 'rb') do |f|
  bytes = f.read
  puts bytes.bytesize
end

# File operations
File.exist?('file.txt')
File.size('file.txt')
File.mtime('file.txt')
File.delete('file.txt')
File.rename('old.txt', 'new.txt')
FileUtils.cp('a.txt', 'b.txt')   # require 'fileutils'

StringIO & Tempfile

StringIO wickelt einen String in eine IO-ähnliche Schnittstelle, nützlich zum Testen von Datei-Operationen ohne Disk-Berührung. Tempfile erstellt temporäre Dateien, die automatisch gelöscht werden, wenn der Block endet (Tempfile.create) oder wenn unlinked (Tempfile.new). Verwenden Sie Tempfile für große Daten, die nicht in den Speicher passen, oder zum Weitergeben an externe Programme via Pfad. Stellen Sie immer Cleanup mit ensure-Blöcken sicher. StringIO ist toll für Unit-Tests von IO-Code.

ruby
require 'stringio'
require 'tempfile'

# StringIO: in-memory file-like object
io = StringIO.new
io.puts "Hello"
io.puts "World"
io.rewind
puts io.read   # "Hello\nWorld\n"

# Use like a file
io = StringIO.new("line1\nline2\nline3")
io.each_line { |line| puts line.chomp }

# Tempfile: auto-deleted temporary file
Tempfile.create('myapp') do |f|
  f.write('temporary data')
  f.rewind
  puts f.read
end   # File automatically deleted

# Tempfile with explicit cleanup
temp = Tempfile.new('myapp')
begin
  temp.write('data')
  temp.rewind
  # Use temp.path to pass to external programs
ensure
  temp.close
  temp.unlink
end

Netzwerk-IO

TCPSocket/TCPServer bieten Low-Level-TCP-Zugriff. Net::HTTP ist der Standard-HTTP-Client (eingebaut). Für komplexe HTTP-Bedürfnisse (Sessions, Cookies, Retries) verwenden Sie die httparty- oder faraday-Gems. Setzen Sie immer use_ssl = true für HTTPS. Für hochperformantes HTTP ziehen Sie async-http oder typhoeus in Betracht. URI parst URLs sicher. Behandeln Sie Net::ReadTimeout und Errno::ECONNREFUSED für robusten Netzwerk-Code.

ruby
require 'socket'
require 'net/http'
require 'uri'

# TCP Client
TCPSocket.open('example.com', 80) do |socket|
  socket.write("GET / HTTP/1.0\r\n\r\n")
  puts socket.read
end

# TCP Server
server = TCPServer.new(2000)
loop do
  client = server.accept
  client.puts "Hello from server"
  client.close
end

# HTTP Client
uri = URI('https://api.example.com/data')
response = Net::HTTP.get_response(uri)
puts response.code          # 200
puts response.body

# HTTP POST
response = Net::HTTP.post_form(uri, key: 'value')

# HTTP with custom headers
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Get.new(uri)
request['Authorization'] = 'Bearer token'
response = http.request(request)

STDIO & Pipelines

$stdout, $stderr, $stdin sind globale IO-Objekte (verwenden Sie Konstanten STDOUT/STDERR/STDIN für die Originalen). Redirecten Sie durch Neuzuweisen der Globals. StringIO erfasst Output für Testen. Open3.popen3 gibt volle Kontrolle über stdin/stdout/stderr eines Subprozesses. Open3.pipeline verkettet Befehle wie eine Shell-Pipe. Capture3 gibt stdout, stderr und status zurück. Schließen Sie immer stdin, um EOF an den Subprozess zu signalisieren. Verwenden Sie wait.value, um den Exit-Status zu erhalten.

ruby
# Standard IO
$stdout.puts "To stdout"
$stderr.puts "To stderr"
input = $stdin.gets   # Read from stdin

# Redirect IO
$stdout = File.open('log.txt', 'w')
puts "This goes to file"
$stdout = $stdout   # Restore (or use STDOUT constant)

# Capture output
captured = StringIO.new
original = $stdout
$stdout = captured
puts "Captured"
$stdout = original
puts captured.string   # "Captured\n"

# Open3: run commands with full IO control
require 'open3'

Open3.popen3('grep hello') do |stdin, stdout, stderr, wait|
  stdin.puts "hello world"
  stdin.puts "goodbye"
  stdin.close
  puts stdout.read      # "hello world"
  puts wait.value.exitstatus
end

# Pipeline
output = Open3.pipeline('ls', 'grep .rb', 'wc -l')
24

Pattern Matching (3.0+)

Basis-Pattern-Matching

Pattern Matching (in-Schlüsselwort) destrukturiert Arrays und Hashes und bindet Variablen. => bindet gematchte Werte an Variablen. *rest erfasst verbleibende Array-Elemente. Hash-Matching ist partiell: extra Schlüssel werden ignoriert. | matcht mehrere Patterns. case/in ist die primäre Form; die Einzeilen-Form ist expression in pattern. Pattern Matching ist besonders mächtig zum Parsen strukturierter Daten (JSON, ASTs) und zum Ersetzen komplexer if-else-Ketten.

ruby
# in: pattern matching (Ruby 2.7+, stable in 3.0)
case [1, 2, 3]
in [1, *rest]
  puts "Starts with 1, rest: #{rest}"
end

case {name: 'Alice', age: 30}
in {name: String => name, age: Integer => age}
  puts "#{name} is #{age}"
end

# Multiple patterns with |
case status
in :success | :ok
  puts "Good"
else
  puts "Other"
end

# Array patterns
case [1, 2, 3, 4]
in [_, _, *rest]
  puts "Rest: #{rest}"   # [3, 4]
end

# Hash patterns (partial match by default)
case {a: 1, b: 2, c: 3}
in {a: Integer}
  puts "Has a"   # Matches (ignores b, c)
end

Variablen-Binding & Guards

=> bindet gematchte Werte an Variablen. Der Pin-Operator (^var) matcht gegen den aktuellen Wert der Variablen (nicht bindend). Guards (if/unless) fügen Bedingungen hinzu. Array-Patterns unterstützen * für Splats an jeder Position. Find-Pattern [*, target, *] sucht nach einem Element innerhalb eines Arrays. Pattern Matching ist deklarativ und prägnant für komplexe Datenextraktion. In Patterns gebundene Variablen sind nach der case-Anweisung verfügbar.

ruby
user = {name: 'Alice', age: 30, role: :admin}

case user
in {name: String => name, age: Integer => age, role: :admin} if age > 18
  puts "#{name} is an adult admin"
end

# Pin operator (^): match against variable value
expected = 'Alice'
case user
in {name: ^expected}
  puts "Matched expected name"
end

# Array destructuring with binding
case [1, 2, 3]
in [first, *middle, last]
  puts "First: #{first}, Middle: #{middle}, Last: #{last}"
end

# Find pattern (search within array)
case [1, 2, 3, 4, 5]
in [*, 3 => three, *]
  puts "Found 3: #{three}"
end

Klassen- & Typ-Patterns

Klassen-Patterns matchen nach Typ. Benutzerdefinierte Klassen unterstützen Destrukturierung durch Implementieren von deconstruct (Array-Pattern) und deconstruct_keys (Hash-Pattern). Das Pattern Class[args] verwendet deconstruct; Class(key:) verwendet deconstruct_keys. Viele Built-in-Klassen (Time, Date, MatchData) unterstützen Destrukturierung. Dies ermöglicht ausdrucksstarkes Matching auf Domänenobjekten. Implementieren Sie diese Methoden, um Ihre Klassen pattern-matchbar zu machen.

ruby
# Match by class
case value
in Integer
  puts "Integer"
in String
  puts "String"
in Array
  puts "Array"
end

# Class with destructuring
class Point
  attr_reader :x, :y
  def initialize(x, y) @x, @y = x, y end
  def deconstruct = [@x, @y]              # Array pattern
  def deconstruct_keys(keys) = {x: @x, y: @y}  # Hash pattern
end

p = Point.new(1, 2)
case p
in Point[x, y]
  puts "Point at (#{x}, #{y})"
end

case p
in Point(x:, y:)
  puts "X: #{x}, Y: #{y}"
end

# Built-in classes support destructuring
case Time.now
in Time(hour: h) if h < 12
  puts "Morning"
end

Einzeilen-Pattern-Matching

Die Einzeilen-Form (expr in pattern) gibt true/false statt NoMatchingPatternError zu werfen. Sie bindet Variablen bei erfolgreichem Match. Nützlich für Guard-Klauseln und bedingte Extraktion. Die Rightward-Form (pattern => var) bindet den gesamten Match. Einzeilen-Matching ist prägnant für einfache Fälle; verwenden Sie case/in für komplexe Mehr-Zweig-Logik. In fehlgeschlagenen Matches gebundene Variablen bleiben nil.

ruby
# One-line form (Ruby 3.0+)
{status: :ok, data: 42} in {status: :ok, data: Integer => data}
puts data   # 42

# Returns true/false (does not raise)
[1, 2, 3] in [Integer, Integer, Integer]   # true
[1, 2, 3] in [String, *, *]                # false

# Useful for conditionals
if user in {role: :admin}
  puts "Admin access"
end

# Guard clauses
def process(data)
  return unless data in {type: String, value: Integer}
  # ...
end

# Extracting from JSON
json = JSON.parse('{"user":{"name":"Alice","age":30}}')
json in {"user" => {"name" => String => name, "age" => Integer => age}}
puts "#{name}, #{age}"   # Alice, 30

Praktische Anwendungsfälle

Pattern Matching glänzt bei: Parsen strukturierter Daten (JSON, XML), Zustandsautomaten, AST-Traversierung und Dispatching nach Datenform. Es ersetzt ausführliche if-else-Ketten durch deklarative Patterns. Die else-Klausel behandelt unerwartete Formate. Kombinieren Sie mit Guards für komplexe Bedingungen. Pattern Matching macht Code für komplexe Datenverarbeitung lesbarer und wartbarer. Es ist eines von Ruby 3s mächtigsten Features für sauberen, ausdrucksstarken Code.

ruby
# 1. Parse JSON API responses
def handle_response(response)
  case JSON.parse(response)
  in {"status" => "success", "data" => Array => items}
    items.each { |item| process(item) }
  in {"status" => "error", "message" => String => msg}
    raise "API error: #{msg}"
  in {"status" => "error", "code" => Integer => code} if code >= 500
    retry_request
  else
    raise "Unknown response format"
  end
end

# 2. State machines
case state
in :idle, event: :start
  transition_to :running
in :running, event: :pause
  transition_to :paused
in :paused, event: :resume
  transition_to :running
in :running | :paused, event: :stop
  transition_to :idle
end

# 3. AST traversal
def evaluate(node)
  case node
  in {type: 'number', value: n}
    n
  in {type: 'add', left:, right:}
    evaluate(left) + evaluate(right)
  in {type: 'mul', left:, right:}
    evaluate(left) * evaluate(right)
  end
end
25

Metaprogrammierung

send & define_method

define_method erstellt Methoden dynamisch. send ruft Methoden nach Namen auf (sogar private). public_send respektiert Sichtbarkeit. Nützlich für DSLs und Reduzierung von Boilerplate. Seien Sie vorsichtig mit Benutzereingaben, um Sicherheitsprobleme zu vermeiden. Metaprogrammierung ist mächtig, kann aber Code schwerer verständlich machen.

ruby
class Foo
  define_method(:greet) do |name|
    "Hello, #{name}"
  end
end
Foo.new.greet("Alice")  # "Hello, Alice"
# Dynamic method call
Foo.new.send(:greet, "Bob")  # "Hello, Bob"
Foo.new.public_send(:greet, "Bob")  # Respects visibility

method_missing

method_missing fängt Aufrufe an undefinierte Methoden ab. Nützlich für dynamisches Dispatch und DSLs. Überschreiben Sie immer respond_to_missing?, um zu matchen. Kann langsam sein und Bugs verbergen. Bevorzugen Sie define_method, wenn die Menge der Methoden bekannt ist. ActiveRecord verwendet es für Attribut-Accessoren.

ruby
class DynamicHash
  def method_missing(name, *args)
    if name.to_s.end_with?('=')
      self[name.to_s.chomp('=')] = args.first
    else
      self[name.to_s]
    end
  end
  def respond_to_missing?(name, include_private = false)
    true
  end
end

eval

eval führt einen String als Ruby-Code aus. Extrem mächtig, aber gefährlich. Wenden Sie niemals eval auf nicht vertrauenswürdige Eingabe an (Code-Injection). Verwenden Sie binding.eval für einen spezifischen Kontext. Für sichere Auswertung verwenden Sie einen Parser wie Ripper oder eine Sandbox. Die meisten Anwendungsfälle haben sicherere Alternativen.

ruby
# Execute a string as Ruby code
result = eval("1 + 2 * 3")  # 7
# Dynamic method definition
eval("def dynamic_method; 42; end")
dynamic_method  # 42
# WARNING: never eval untrusted input!

Offene Klassen

Ruby-Klassen sind offen: Sie können Methoden zu jeder Klasse hinzufügen, einschließlich Built-ins. Dies wird Monkey-Patching genannt. Mächtig für schnelle Fixes, kann aber Konflikte und Verwirrung verursachen. Verwenden Sie Refinements für scoped Modifikationen. Dokumentieren Sie Änderungen klar. Bevorzugen Sie Komposition über Monkey-Patching.

ruby
class String
  def shout
    upcase + "!"
  end
end
"hello".shout  # "HELLO!"
# Monkey patching: modifying existing classes
# Use sparingly, can cause conflicts

Klassen-Makros

Klassen-Makros sind Klassenmethoden, die andere Methoden definieren. has_many, belongs_to, attr_accessor sind Beispiele. Sie verwenden define_method intern. So erstellt Rails dynamische Methoden. DSLs wie RSpec und Sinatra verwenden dieses Pattern. Macht Code deklarativ und lesbar.

ruby
class ActiveRecord::Base
  def self.has_many(name)
    define_method(name) { [] }
  end
end
class Post < ActiveRecord::Base
  has_many :comments
end
Post.new.comments  # []

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.