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.
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) # trueSymbole
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.
status = :active
puts status.class # Symbol
puts status.to_s # "active"
# Symbols are immutable, reusable strings
hash = { name: "Alice", status: :active }
puts hash[:status] # activeNil & 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.
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" # falsyTypumwandlung
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.
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.
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} hereStrings
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.
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, W0rldString-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.
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.
text = <<~HEREDOC
Hello,
World!
Indentation is stripped.
HEREDOC
puts text
# <<~ strips leading whitespace (Ruby 2.3+)
# <<HEREDOC preserves indentationFormatierung (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.
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 floatString-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.
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, s4Datenstrukturen
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.
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) # trueHashes
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.
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.
(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.
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.
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)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.
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 >= 60Case (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.
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"
endWhile / 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.
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
endIteratoren (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.
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 3Break / 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.
[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 # 20Methoden & 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 =.
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) # 10Blö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.
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 } # 10Procs & 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.
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 methodMethod-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.
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.inspectRü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.
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 = coordsKlassen & 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.
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.countVererbung & 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.
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.
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 walkingZugriffskontrolle
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.
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 methodSelf & 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.
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 # 2Fehlerbehandlung
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).
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/failureRaise & 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.
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}"
endRetry
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.
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 topRescue-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.
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.
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 loopsDatei-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.
# 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.
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}" }
endDatei-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.
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.
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 } # recursiveCSV & 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.
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"] # AliceDatum/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.
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 # tomorrowDate-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.
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/.
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.
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/2024Regex-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).
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"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.
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 methodsmethod_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.
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.
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.
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 preemptivelySend & 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.
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!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).
# 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
endProcs 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.
# 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 # 2Method-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.
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, 3Closures 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.
# 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) # 42Benutzerdefinierte 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.
# 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) }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.
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_aVerzö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.
# 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.
# 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_aSortieren 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.
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 # 1Hash-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.
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"]}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).
# 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'
endBundler-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.
# 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 cleanGemspec (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.
# 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.gemRake-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].
# 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'
endRbenv 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.
# 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 -vRails-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.
# 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
endActiveRecord-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.
# 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.
# 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
endViews 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.
<!-- 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.
# 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, :integerRSpec-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.
# 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) }
endMocks 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).
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.
# 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).
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 evaluationIntegration- 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.
# 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
endDatei- & 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.
# 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.
# 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.closeVerzeichnis-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.
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. \).
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 afterCSV 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.
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"))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 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 reasonEncoding-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.
# 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_8String-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.
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.
# 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.stringSymbole 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.
# 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)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.
# 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 # NoMethodErrorInclude 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.
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 saveEnumerable 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.
# 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 # falseSingleton-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.
# 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) # trueRefinements (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.
# 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 inBlö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).
# 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.
# 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
endMethod-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.
# 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.
# 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.
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.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.
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
endOffene 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.
# 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.
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, prependedeval & 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.
# 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.
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) # trueTesting (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.
# 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: rspecRSpec 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).
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
endMinitest
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).
# 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.rbTestdaten & 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.
# 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.
# 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
endThreads & 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.
# 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 threadThread-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.
# 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.newFibers
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).
# 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 34Async & 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.
# 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.shutdownRactors (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.
# 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 safelyEncoding & 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 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.
# 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.
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
endNetzwerk-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.
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.
# 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')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.
# 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)
endVariablen-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.
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}"
endKlassen- & 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.
# 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"
endEinzeilen-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.
# 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, 30Praktische 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.
# 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
endMetaprogrammierung
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.
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 visibilitymethod_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.
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
endeval
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.
# 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.
class String
def shout
upcase + "!"
end
end
"hello".shout # "HELLO!"
# Monkey patching: modifying existing classes
# Use sparingly, can cause conflictsKlassen-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.
class ActiveRecord::Base
def self.has_many(name)
define_method(name) { [] }
end
end
class Post < ActiveRecord::Base
has_many :comments
end
Post.new.comments # []Verwandte Ruby-Snippets
Copy-paste ready code for common tasks.
Blocks, Procs, Lambdas
Use blocks with yield, Procs, lambdas, and the & operator.
Classes and Modules
Define classes with inheritance, mix in modules, and add class methods.
Iterators
Use each, map, select, reduce, group_by, and lazy enumerators.
Strings
Interpolate, trim, split, replace, and pattern-match strings.
Hashes
Build, default, transform, merge, and group with Hash.
Metaprogramming
Define methods dynamically, intercept with method_missing, and build DSLs.
Error Handling
Raise and rescue typed exceptions with ensure and retry.
File I/O
Read, write, append, traverse directories, and process CSV files.
Was this helpful?