Bases
Variables et types
Ruby est typé dynamiquement—les variables n'ont pas besoin de déclarations de type. Utilisez .class pour inspecter le type et is_a? pour les vérifications de type. Tout est un objet, y compris les nombres et les booléens.
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) # trueSymboles
Les symboles (:name) sont des chaînes immuables et internées—une seule copie existe en mémoire. Utilisez-les pour les clés de hash, les noms de méthodes et les identifiants où l'identité compte plus que le contenu. Plus économes en mémoire que les chaînes pour un usage répété.
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 et vérité
Dans Ruby, seuls nil et false sont falsy—tout le reste (y compris 0, '' et []) est truthy. Cela diffère de nombreux langages où 0 est falsy. Utilisez || pour les valeurs par défaut et nil? pour vérifier spécifiquement nil.
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" # falsyConversion de type
to_i/to_s/to_f sont des conversions tolérantes (retournent 0 en cas d'échec). Integer()/Float() sont strictes (lèvent une ArgumentError). Utilisez la conversion stricte lorsque vous avez besoin de validation, tolérante lorsque vous voulez une dégradation gracieuse.
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)Interpolation de chaînes
Les chaînes à guillemets doubles prennent en charge l'interpolation #{expr}—n'importe quelle expression Ruby à l'intérieur. Les chaînes à guillemets simples sont littérales (pas d'interpolation ni d'échappements sauf \\ et \'). Utilisez les guillemets doubles lorsque vous avez besoin d'interpolation ou de séquences d'échappement.
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} hereChaînes de caractères
Méthodes courantes de chaînes
Les chaînes Ruby ont des méthodes riches. La plupart retournent de nouvelles chaînes (les chaînes sont mutables dans Ruby). Utilisez les variantes ! (upcase!, gsub!) pour la modification sur place—elles modifient le récepteur et retournent nil si aucun changement.
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, W0rldMutation de chaînes (méthodes bang)
Les méthodes se terminant par ! modifient l'objet sur place et sont souvent plus efficaces. Elles peuvent retourner nil si aucun changement n'a été fait. Utilisez-les lorsque vous voulez éviter de créer des copies, mais soyez conscient des effets de bord.
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 et multiligne
Les heredocs (<<TEXT ... TEXT) créent des chaînes multilignes. <<~ (squiggly heredoc) supprime les espaces de début communs pour un code propre. Utile pour les requêtes SQL, les templates HTML ou les longs messages.
text = <<~HEREDOC
Hello,
World!
Indentation is stripped.
HEREDOC
puts text
# <<~ strips leading whitespace (Ruby 2.3+)
# <<HEREDOC preserves indentationFormatage (sprintf)
Utilisez l'opérateur % ou format/sprintf pour le formatage de chaînes style C. %-10s aligne à gauche sur 10 caractères, %08.2f remplit de zéros sur 8 caractères avec 2 décimales. Utile pour la sortie tabulaire et le formatage à largeur fixe.
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 floatConcaténation de chaînes
+ crée une nouvelle chaîne, << ajoute sur place (plus efficace pour construire des chaînes). * répète une chaîne. join combine les éléments d'un tableau avec un séparateur. Préférez << ou join à + répété pour la performance.
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, s4Structures de données
Tableaux
Les tableaux sont ordonnés, indexés à partir de zéro et peuvent contenir des types mixtes. << et push ajoutent à la fin. Utilisez include? pour l'appartenance, sum pour les totaux. Les tableaux sont mutables—utilisez freeze pour les rendre immuables.
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
Les hashes sont des dictionnaires clé-valeur. Les clés symboles (name:) sont idiomatiques et efficaces. Utilisez key? pour l'existence, fetch pour un accès sûr (lève une exception si manquant), transform_values pour les mises à jour en masse.
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)Intervalles
'..' inclut la fin, '...' l'exclut. Les ranges sont paresseux et économes en mémoire pour les grandes séquences. Utiles pour l'itération, le découpage et la génération de séquences. Peuvent être utilisés avec n'importe quel type Comparable.
(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]Méthodes Enumerable
Enumerable est le mixin le plus puissant de Ruby—map, select, reject, reduce, find, group_by, sort_by, etc. Utilisez &:method comme raccourci pour { |x| x.method }. Ils permettent des pipelines expressifs de transformation de données.
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 (de la bibliothèque 'set') stocke des éléments uniques avec une recherche en O(1). Utilisez-le pour la déduplication et les opérations ensemblistes (union, intersection, différence). Convertissez avec to_a lorsque vous avez besoin d'un tableau. Nécessite require 'set'.
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)Flux de contrôle
If / Elsif / Unless
if/elsif/else est le branchement standard. unless est l'opposé de if (s'exécute lorsque la condition est fausse). La forme modificateur (statement if condition) est idiomatique pour les gardes sur une ligne—améliore la lisibilité pour les cas simples.
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 utilise === pour la correspondance, permettant les ranges, classes et regex. Plusieurs valeurs séparées par des virgules. then permet les corps sur une ligne. Sans cible, case agit comme une chaîne if/elsif plus propre.
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 s'exécute tant que vrai, until s'exécute jusqu'à vrai (tant que faux). loop est infini—utilisez break pour sortir. Préférez les itérateurs (each, map) à while pour les collections—ils sont plus idiomatiques et moins sujets aux erreurs.
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
endItérateurs (Each/Times/Upto)
Les itérateurs de Ruby sont plus idiomatiques que les boucles for. times pour compter, upto/downto pour les ranges, each_with_index pour index+valeur. step contrôle l'incrément. Ce sont l'épine dorsale de l'itération Ruby.
3.times { |i| puts i } # 0 1 2
1.upto(3) { |n| puts n } # 1 2 3
3.downto(1) { |n| puts n } # 3 2 1
[1,2,3].each_with_index do |n, i|
puts "#{i}: #{n}"
end
(1..3).step(1) { |n| puts n } # 1 2 3Break / Next / Redo
next passe à l'itération suivante (comme continue), break sort de la boucle prématurément. break peut retourner une valeur depuis un bloc. redo redémarre l'itération courante sans revérifier la condition—rarement utilisé.
[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 # 20Méthodes et blocs
Définition de méthode
Les méthodes utilisent def/end. Les arguments par mot-clé (key:) améliorent la lisibilité pour de nombreux paramètres. *args collecte les arguments positionnels supplémentaires dans un tableau, **kwargs collecte les arguments par mot-clé dans un hash. Les valeurs par défaut utilisent =.
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) # 10Blocs et yield
Les blocs sont des morceaux de code anonymes passés aux méthodes. yield invoque le bloc. Les blocs sont partout dans Ruby (each, map, etc.). Utilisez yield pour rendre vos méthodes flexibles—les appelants fournissent le comportement.
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 et lambdas
Les Procs et lambdas sont des blocs réutilisables stockés dans des variables. Différence clé : les lambdas vérifient le nombre d'arguments (lèvent une exception en cas d'inadéquation) et ne retournent que d'eux-mêmes ; les procs sont tolérants et retournent depuis la méthode englobante. Préférez lambda pour un comportement strict.
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 methodObjet Method (&)
&:method convertit un nom de méthode en Proc. C'est le raccourci idiomatique pour les blocs simples à une méthode : map(&:to_i) au lieu de map { |s| s.to_i }. Plus propre et plus lisible pour les transformations simples.
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.inspectValeurs de retour
Les méthodes Ruby retournent implicitement la dernière expression évaluée—pas besoin de return explicite. Utilisez return pour les sorties anticipées. Plusieurs valeurs sont retournées sous forme de tableau et peuvent être déstructurées. Cela rend le code concis.
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 = coordsClasses et POO
Variables de classe et d'instance
@var = variable d'instance (par objet), @@var = variable de classe (partagée). attr_accessor génère getter+setter, attr_reader getter uniquement, attr_writer setter uniquement. self.method définit les méthodes de classe.
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.countHéritage et super
< dénote l'héritage (héritage simple uniquement). super appelle la version parent de la méthode courante. Utilisez super (avec parenthèses pour passer des arguments, sans pour passer les mêmes arguments) pour étendre le comportement parent. Ruby utilise l'héritage simple + les 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)Modules (mixins)
Les modules regroupent des méthodes réutilisables. include ajoute des méthodes d'instance, extend ajoute des méthodes de classe. C'est la solution de Ruby à l'héritage multiple. Enumerable est un mixin célèbre—incluez-le et définissez each pour obtenir map, select, etc.
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 walkingContrôle d'accès
public (par défaut), private (appelable uniquement sans récepteur explicite), protected (appelable dans la hiérarchie de classes). Utilisez private pour les helpers internes, protected pour les méthodes partagées entre les instances d'une même classe.
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 et méthodes de classe
self fait référence à l'objet courant. Dans le corps de la classe, self est la classe—def self.method définit les méthodes de classe. Les méthodes de classe sont appelées sur la classe (Counter.total), les méthodes d'instance sur les instances. alias_method crée un alias de méthode.
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 # 2Gestion des erreurs
Begin / Rescue / Ensure
begin/rescue est le try/catch de Ruby. Rescuez des classes d'exception spécifiques pour un traitement ciblé. => e capture l'objet exception. ensure s'exécute toujours—utilisez-le pour le nettoyage (fermer les fichiers, libérer les verrous).
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 et exceptions personnalisées
raise lance une exception (raise sans argument relance). Les exceptions personnalisées héritent de StandardError (ou d'une classe plus spécifique). Nommez-les avec un suffixe Error. Rescuez par classe pour gérer des modes d'échec spécifiques.
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 redémarre le bloc begin depuis le début. Utilisez-le pour les échecs transitoires (réseau, limites de débit) avec un compteur pour éviter les boucles infinies. Sans limite, retry peut bloquer votre programme—gardez-le toujours sous contrôle.
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 topModificateur rescue
rescue comme modificateur est une manière concise de fournir une valeur de repli. Il attrape StandardError et retourne le côté droit. Utilisez-le pour les cas simples—évitez-le pour la logique complexe car il masque les erreurs. Excellent pour l'analyse ou les opérations optionnelles.
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 (flux de contrôle)
throw/catch n'est PAS de la gestion d'exceptions—c'est un mécanisme de flux de contrôle pour une sortie anticipée d'imbrication profonde (contrairement à d'autres langages). throw :symbol saute au catch correspondant. Utilisez-le pour sortir des boucles imbriquées ; utilisez begin/rescue pour les erreurs réelles.
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 loopsEntrées/Sorties fichier
Lire et écrire des fichiers
File.write/File.read sont des méthodes simples à usage unique. File.open avec un bloc ferme automatiquement le fichier. File.foreach lit ligne par ligne sans charger tout le fichier—économe en mémoire pour les gros fichiers. chomp supprime la nouvelle ligne de fin.
# 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 }Bloc de fichier (fermeture automatique)
Utilisez toujours File.open avec un bloc—il garantit que le fichier est fermé même si une erreur se produit. La forme bloc est la manière idiomatique et sûre de gérer les fichiers. Sans bloc, vous devez appeler close manuellement.
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}" }
endExistence et infos de fichier
La classe File fournit des requêtes sur le système de fichiers. file?/directory? distinguent les types. mtime/ctime/atime donnent les horodatages. rename/delete modifient le système de fichiers. Vérifiez toujours l'existence avant les opérations pour éviter les erreurs.
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")Opérations sur les répertoires
Dir gère les répertoires. mkdir crée, chdir change (la forme bloc restaure après). glob correspond aux motifs de fichiers—* correspond à n'importe quoi, ** correspond récursivement. Utile pour la découverte de fichiers et le traitement par lots.
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 et JSON
CSV et JSON sont dans la bibliothèque standard. CSV.foreach diffuse les lignes (économe en mémoire). JSON.parse retourne des hashes/tableaux avec des clés de chaîne. to_json sérialise n'importe quel objet. Ils sont essentiels pour l'échange de données.
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"] # AliceDate/Heure et regex
Time et Date
Time représente un instant (avec fuseau horaire). Date représente une date calendaire (sans heure). L'arithmétique sur Time utilise les secondes. strftime formate avec %Y (année), %m (mois), %d (jour), %H:%M (heure). Require 'time' pour l'analyse ISO8601.
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 # tomorrowAnalyse et arithmétique de dates
L'arithmétique de dates fonctionne naturellement—ajouter des entiers ajoute des jours. next_month/prev_month gèrent les limites de mois. Date - Date retourne un Rational (jours). upto/downto itèrent sur les intervalles de dates. Utilisez Date pour la logique calendaire, Time pour les horodatages.
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)Correspondance regex
=~ retourne la position de correspondance ou nil. $1, $2 contiennent les groupes capturés après une correspondance. .match retourne un objet MatchData pour plus de détails. Utilisez =~ pour les vérifications simples, .match pour extraire les captures. Les littéraux regex utilisent /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"Substitution regex
gsub remplace toutes les correspondances (sub remplace la première). Passez un bloc pour un remplacement dynamique. scan extrait toutes les correspondances dans un tableau. Les backreferences (\1, \2) dans les chaînes de remplacement font référence aux groupes capturés. Puissant pour le traitement de texte.
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/2024Options regex
Options regex : i (insensible à la casse), m (multiligne—le point correspond à la nouvelle ligne), x (étendu—autorise les espaces/commentaires dans le motif). %r{} est un délimiteur alternatif utile lorsque le motif contient des barres obliques (comme les 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"Métaprogrammation et concurrence
Définition dynamique de méthode
define_method crée des méthodes à l'exécution. Utilisez-le pour générer plusieurs méthodes similaires ou construire des DSL. C'est de la métaprogrammation—du code qui écrit du code. Puissant mais à utiliser judicieusement ; cela peut rendre le code plus difficile à comprendre.
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 intercepte les appels aux méthodes non définies. Utilisez-le pour construire des APIs flexibles ou des proxies. Surchargez toujours respond_to_missing? aussi, pour que la réflexion fonctionne. Utilisez-le avec parcimonie—il peut masquer des bugs et confondre l'analyse statique.
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
Les threads exécutent du code en parallèle. join attend la fin. MRI (Ruby standard) a un GIL, donc les threads liés au CPU ne s'exécutent pas en véritable parallèle—les threads liés aux I/O oui. Pour un véritable parallélisme, utilisez Ractor (Ruby 3.0+) ou plusieurs processus.
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 (coopératif)
Les fibers sont une concurrence légère et coopérative—elles se mettent en pause et reprennent manuellement via yield/resume. Contrairement aux threads, elles ne s'exécutent pas en parallèle. Utilisez-les pour les générateurs, l'évaluation paresseuse ou les calculs interrompables. Surcharge moindre que les 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 et eval
send appelle n'importe quelle méthode par nom (y compris privées). public_send respecte la visibilité. eval exécute une chaîne comme du code Ruby—extrêmement puissant mais dangereux avec des entrées non fiables (injection de code). Utilisez send pour le dispatch dynamique, évitez eval en production.
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!Blocs, Procs et lambdas
Bases des blocs
Les blocs sont la closure la plus courante de Ruby—du code anonyme passé aux méthodes via { } ou do...end. yield invoque le bloc. |n| déclare les paramètres de bloc. block_given? vérifie si un bloc a été passé. Les blocs ne sont pas des objets (ne peuvent pas être stockés dans des variables)—utilisez Proc/Lambda pour cela. Chaque méthode peut accepter un bloc implicite, rendant les DSL naturels (Rails utilise cela massivement).
# 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
Les Procs et lambdas sont tous deux des objets appelables (des blocs transformés en objets). Les Procs sont tolérants : les arguments supplémentaires deviennent nil, les arguments manquants sont nil, et 'return' quitte la méthode englobante. Les lambdas sont stricts : ils vérifient le nombre d'arguments et 'return' ne quitte que le lambda. Utilisez les lambdas lorsque vous voulez un comportement de type méthode ; les Procs lorsque vous voulez un comportement de type bloc. -> (stabby lambda) est la syntaxe moderne concise.
# 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 # 2Objets Method
method(:name) récupère une méthode comme un objet Method (appelable comme un Proc). &:symbol convertit un symbole en proc qui envoie cette méthode—idiome extrêmement courant : array.map(&:to_s). Le préfixe & convertit un Proc en bloc (ou vice versa). Cela permet de passer des méthodes comme arguments élégamment. Les objets Method conservent leur récepteur, ils sont donc liés à l'objet.
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 et binding
Les closures (blocs, procs, lambdas) capturent les variables par référence—elles voient les mises à jour des variables capturées. Cela permet des closures avec état (compteurs, accumulateurs). Proc#binding donne accès à l'environnement de la closure (pour la métaprogrammation avancée). La méthode Kernel#binding capture le contexte d'exécution courant pour eval. Les closures sont la raison pour laquelle les blocs Ruby sont si puissants pour les callbacks et les itérateurs.
# 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) # 42Itérateurs personnalisés avec blocs
Inclure Enumerable et définir #each vous donne map, select, reduce et plus de 50 méthodes gratuitement—c'est le protocole d'itération de Ruby. each_with_object est plus propre que inject pour construire des accumulateurs. tap insère des effets de bord dans les chaînes de méthodes (excellent pour le débogage). Les méthodes personnalisées avec yield vous permettent de créer vos propres DSL (with_timing, with_database, etc.). Les blocs rendent élégants les patterns d'itération et de callback de Ruby.
# Define your own iterator using yield
class LinkedList
include Enumerable # gets map, select, etc. for free
def each
node = @head
while node
yield node.value
node = node.next
end
end
end
# each_with_object (memo pattern)
result = [1, 2, 3].each_with_object({}) do |n, hash|
hash[n] = n * n
end
# {1=>1, 2=>4, 3=>9}
# tap (for debugging chains)
[1, 2, 3].map { |n| n * 2 }
.tap { |arr| puts "after map: #{arr}" }
.select { |n| n > 2 }
# Custom method with block
def with_timing
start = Time.now
yield
puts "took #{Time.now - start}s"
end
with_timing { sleep(1) }Enumerable et itérateurs
Méthodes Enumerable principales
Enumerable est le mixin le plus puissant de Ruby—incluez-le et définissez #each pour obtenir plus de 50 méthodes. map transforme, select/reject filtrent, reduce/inject agrège, find retourne la première correspondance, group_by/partition regroupent. Les raccourcis symboles (reduce(:+)) sont idiomatiques. Ces méthodes fonctionnent sur les Arrays, Hashes, Ranges, Files—tout ce qui a #each. Maîtriser Enumerable est la clé du Ruby idiomatique.
nums = [1, 2, 3, 4, 5, 6]
# map/collect: transform
nums.map { |n| n * 2 } # [2, 4, 6, 8, 10, 12]
# select/filter: keep matching
nums.select { |n| n.even? } # [2, 4, 6]
nums.reject { |n| n.even? } # [1, 3, 5]
# reduce/inject: accumulate
nums.reduce(0) { |sum, n| sum + n } # 21
nums.reduce(:+) # 21 (symbol shorthand)
nums.reduce(:*) # 720
# find/detect: first match
nums.find { |n| n > 3 } # 4
# group_by: cluster
nums.group_by { |n| n.even? } # {false=>[1,3,5], true=>[2,4,6]}
# partition: split into two
nums.partition { |n| n.even? } # [[2,4,6], [1,3,5]]
# chunk: consecutive grouping
[1,1,2,2,3].chunk { |n| n }.to_aÉvaluation paresseuse
lazy convertit un enumerable en un enumerable paresseux—les valeurs sont calculées uniquement lorsque nécessaire. Cela permet les séquences infinies et évite de calculer toute la collection pour les chaînes qui n'ont besoin que de quelques résultats. Sans lazy, map/select créent des tableaux intermédiaires. Avec lazy, le pipeline tire les valeurs une à la fois. Utilisez lazy pour les grands/infinis jeux de données ou les transformations coûteuses. Le compromis : lazy a une surcharge par élément, il est donc plus lent pour les petites 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 et itération externe
Enumerator enveloppe une itération comme un objet—vous pouvez appeler .next manuellement (itération externe) au lieu d'utiliser un bloc (itération interne). Cela permet de mettre en pause/reprendre l'itération, de jeter un coup d'œil et de créer des séquences infinies. Enumerator.new avec un bloc vous permet de construire des itérateurs personnalisés (générateurs). La plupart des méthodes Enumerable retournent des Enumerators lorsqu'elles sont appelées sans bloc : [1,2,3].map retourne un Enumerator que vous pouvez chaîner.
# 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_aTri et comparaison
sort prend un bloc comparant deux éléments (retourne -1, 0 ou 1). sort_by est plus efficace—il calcule la clé de tri une fois par élément (transformation de Schwartz) plutôt qu'à chaque comparaison. Utilisez sort_by pour les clés complexes. L'opérateur <=> (spaceship) retourne -1/0/1 et est la base du tri de Ruby. min/max/min_by/max_by trouvent les extrêmes. Incluez Comparable et définissez <=> pour un ordre naturel de vos objets.
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 # 1Itération et transformation de hash
Les hashes sont aussi Enumerable. each/each_pair itèrent les paires clé-valeur. transform_keys/transform_values (Ruby 2.5+) créent de nouveaux hashes avec des clés/valeurs modifiées. select/reject filtrent dans un nouveau hash. merge combine les hashes (un bloc résout les conflits). group_by construit un hash à partir d'un tableau. L'itération de hash est la fondation du traitement de données dans Ruby—maîtrisez-les pour une manipulation de données propre et expressive.
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 et Bundler
Bases des gems
Les gems sont des paquets Ruby (bibliothèques). gem install gère les gems système. Pour les projets, utilisez Bundler avec un Gemfile pour fixer les versions et gérer les dépendances. Spécificateurs de version : '~> 1.4' (pessimiste, autorise les correctifs), '>= 5.0' (optimiste). Les groupes (:development, :test, :production) vous permettent de charger uniquement les gems nécessaires par environnement. require: false signifie que Bundler ne l'auto-requerra pas (vous le requirez manuellement lorsque nécessaire).
# 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'
endCommandes Bundler
Bundler garantit que votre projet utilise les versions exactes de gem spécifiées. bundle install lit le Gemfile et écrit Gemfile.lock (versions exactes pour la reproductibilité—commitez-le !). bundle exec exécute les commandes avec les bonnes versions de gem (évite les conflits). bundle update change les versions (attention—peut casser des choses). Utilisez toujours bundle exec pour rake/rspec/rails pour garantir le chargement des bonnes gems. Gemfile.lock rend les déploiements reproductibles.
# 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 (créer un gem)
Le fichier .gemspec définit les métadonnées et les dépendances d'un gem. spec.files liste les fichiers inclus ; require_paths indique à Ruby où les trouver. add_dependency pour les dépendances d'exécution, add_development_dependency pour les dépendances de test/build. required_ruby_version impose la version de Ruby. gem build crée le paquet .gem ; gem install l'installe localement. Publiez sur rubygems.org avec gem push. Structure : lib/ pour le code, spec/ pour les 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.gemTâches Rake
Rake est le make de Ruby—un exécuteur de tâches. Définissez les tâches avec task :name do ... end. Les namespaces regroupent les tâches associées. Les tâches de fichier ont des dépendances (reconstruire si la source change). sh exécute des commandes shell. Rake est utilisé pour les tests, builds, déploiements et tâches de base de données (Rails l'utilise massivement). Lancez avec rake task_name. La tâche default s'exécute lorsque vous tapez juste rake. Passez des arguments avec 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 et RVM (versions de Ruby)
rbenv et RVM gèrent plusieurs versions de Ruby sur une machine. rbenv est léger (shims) ; RVM est plus lourd (surcharge les commandes shell). Le fichier .ruby-version (committé) garantit que tout le monde utilise la même version de Ruby. Les gemsets (RVM) ou bundle config path isolent les gems du projet. Pour la production, utilisez bundle config set path pour installer les gems localement au projet, évitant la pollution des gems système. Fixez toujours la version de Ruby dans .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 -vBases de Rails
Structure MVC
Rails est un framework MVC : les Models (ActiveRecord) gèrent les données, les Controllers (ActionController) gèrent les requêtes HTTP, les Views (ActionView) affichent les réponses. resources génère 7 routes RESTful automatiquement. Les routes mappent les URLs aux actions du contrôleur. Convention plutôt que configuration : nommez votre modèle User, contrôleur UsersController, et Rails relie tout. Cette structure est l'épine dorsale de chaque application Rails.
# 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
endModèles ActiveRecord
ActiveRecord est l'ORM de Rails—les modèles mappent aux tables de base de données. validates assure l'intégrité des données. Les associations (has_many, belongs_to, has_one) définissent les relations. Les callbacks (before_save, after_create) se connectent au cycle de vie. Les scopes sont des fragments de requête réutilisables. ActiveRecord utilise la convention : modèle User → table users, colonnes created_at/updated_at. C'est le cœur de Rails—maîtrisez-le pour un développement Rails efficace.
# 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)Contrôleurs et strong params
Les contrôleurs gèrent les requêtes HTTP et coordonnent les modèles/vues. before_action exécute des filtres (authentification, chargement de ressources). Les strong parameters (permit) préviennent les vulnérabilités d'affectation en masse—seuls les champs autorisés peuvent être définis. redirect_to envoie l'utilisateur ailleurs ; render affiche une vue. Les variables @instance sont disponibles dans les vues. Les actions RESTful (index, show, new, create, edit, update, destroy) sont conventionnelles.
# 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
endVues et helpers
ERB (Embedded Ruby) est le templating par défaut de Rails : <%= %> sort, <% %> exécute. link_to/button_to génèrent des liens/formulaires HTML. form_with construit des formulaires liés aux modèles. Les partials (_form.html.erb) sont des fragments de vue réutilisables affichés avec render. Les helpers de chemin (new_user_path, user_path(user)) génèrent des URLs à partir des routes. Les helpers gardent les vues propres. Pour la logique complexe, utilisez les helpers de vue (app/helpers/) ou les décorateurs.
<!-- 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 %>Migrations et base de données
Les migrations font évoluer le schéma de base de données au fil du temps, avec contrôle de version. create_table définit les tables ; add_column/remove_column les modifient. t.references crée des clés étrangères. t.timestamps ajoute created_at/updated_at. db:migrate applique les migrations en attente ; db:rollback annule la dernière. Le fichier schema.rb est la source faisant autorité du schéma courant (auto-généré). Ne modifiez jamais schema.rb directement—utilisez les migrations. Cela rend les changements de base de données reproductibles entre les environnements.
# 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, :integerTests RSpec
Syntaxe de base (describe, it, expect)
RSpec est le framework de test dominant de Ruby. describe regroupe les tests associés ; it définit un test unique. expect(...).to / not_to font des assertions. let définit des variables paresseuses mémoïsées (calculées une fois lors du premier accès). context est un alias pour describe, utilisé pour les branches (quand...). La gem shoulda-matchers fournit une syntaxe one-liner pour les validations/associations Rails courantes. Les tests vont dans spec/ reflétant la structure app/.
# 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 et stubs
Les stubs (allow) remplacent les valeurs de retour des méthodes ; les mocks (expect) vérifient qu'une méthode a été appelée. Les doubles sont de faux objets pour les tests (plus rapides que les vrais objets). Utilisez les stubs pour isoler le code testé des dépendances externes (APIs, bases de données). Utilisez les mocks pour vérifier les interactions. Trop de mocks rend les tests fragiles—préférez les vrais objets lorsqu'ils sont assez rapides. FactoryBot crée des données de test ; utilisez create (sauvegarde en DB) ou build (en mémoire uniquement).
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 et fixtures
FactoryBot crée des objets de test avec des valeurs par défaut sensées. Les traits créent des variations (:admin, :inactive). Les séquences génèrent des valeurs uniques (emails). create persiste en base de données ; build non. create_list en crée plusieurs. Surchargez n'importe quel attribut en le passant. Les factories sont plus flexibles que les fixtures (YAML) mais plus lentes (écritures en DB). Utilisez build_stubbed pour des tests rapides qui n'accèdent pas à la DB. Gardez les factories simples—des factories complexes indiquent des modèles complexes.
# 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") }Hooks before/after
Les hooks before exécutent le code de configuration : before(:each) (le plus courant) avant chaque test, before(:all) une fois par groupe. Les hooks after nettoient. DatabaseCleaner gère l'état de la base de données de test (transaction pour la vitesse, truncation pour l'exhaustivité). Préférez let à before(:each)—let est paresseux (calcule uniquement lorsqu'utilisé) et mémoïsé, tandis que before s'exécute même si le test n'en a pas besoin. Utilisez before pour les effets de bord qui doivent se produire (logging, gel du temps).
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 evaluationTests d'intégration et système
Les request specs testent toute la pile (routage → contrôleur → modèle → vue) via HTTP. Les system specs (Capybara) pilotent un vrai navigateur—remplir des formulaires, cliquer, vérifier le contenu de la page. Les tests unitaires (model specs) sont rapides et isolés ; les tests d'intégration/système sont plus lents mais détectent les bugs de câblage. Utilisez la pyramide de tests : beaucoup de tests unitaires rapides, moins de tests d'intégration, des tests système minimaux. have_http_status vérifie les codes de réponse ; visit/fill_in/click_button pilotent le navigateur.
# 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
endOpérations sur fichiers et répertoires
Lecture de fichiers
File.read charge tout le fichier en mémoire—correct pour les petits fichiers. File.foreach lit ligne par ligne—essentiel pour les gros fichiers (ne sature pas la mémoire). File.open avec un bloc ferme automatiquement le fichier (RAII). readlines retourne un tableau de lignes (avec nouvelles lignes—utilisez chomp). binread pour les fichiers binaires. Vérifiez toujours File.exist? avant de lire si le fichier pourrait ne pas exister, ou rescuez 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?Écriture de fichiers
File.write est la manière la plus simple d'écrire un fichier (écrase par défaut). Utilisez mode: 'a' pour ajouter. File.open avec un bloc garantit que le fichier est fermé même si une exception se produit. puts ajoute une nouvelle ligne ; write non. << est un alias pour write (courant dans Ruby). Pour les logs, ouvrez une fois et écrivez plusieurs fois (mis en tampon pour la performance). Fermez toujours les fichiers ou utilisez la forme bloc pour éviter les fuites de ressources.
# 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.closeOpérations sur les répertoires
Dir.glob avec des motifs trouve des fichiers (** pour récursif). FileUtils fournit des opérations robustes sur les fichiers : mkdir_p crée des répertoires imbriqués, cp_r copie récursivement, rm_rf supprime avec force (attention !). Dir.chdir change le répertoire de travail (utilisez la forme bloc pour changer temporairement). Dir.entries inclut . et .. ; glob non. Préférez FileUtils aux opérations File manuelles pour la sécurité multiplateforme.
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 et Tempfile
Pathname est un wrapper orienté objet pour les chemins de fichiers—plus propre que la manipulation de chaînes. Il fournit dirname, basename, extname, join et les vérifications de fichier comme méthodes. Tempfile crée des fichiers temporaires qui sont automatiquement supprimés (utilisez la forme bloc). Dir.mktmpdir crée des répertoires temporaires. Utilisez-les pour une gestion propre et sûre des chemins et des fichiers temporaires. Pathname compose les chemins de manière sûre sur toutes les plateformes (gère / 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 et JSON
CSV et JSON sont intégrés dans la bibliothèque standard de Ruby. CSV.foreach lit ligne par ligne (économe en mémoire) ; CSV.read charge tout. headers: true traite la première ligne comme noms de colonnes. JSON.parse convertit le JSON en hashes/tableaux Ruby ; to_json sérialise les objets Ruby. symbolize_names donne des clés symboles (plus propre). Pour YAML, utilisez require 'yaml' et YAML.load_file. Ils sont essentiels pour l'échange de données dans les scripts Ruby et les applications web.
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"))Encodage et internes des chaînes
Encodages de chaînes
Les chaînes Ruby portent leur encodage (généralement UTF-8). force_encoding réinterprète les octets comme un encodage différent (pas de conversion—utilisez-le lorsque vous savez que les octets sont déjà dans cet encodage). encode convertit réellement entre les encodages. valid_encoding? vérifie si les octets sont valides pour l'encodage de la chaîne. Les problèmes d'encodage causent la redoutée Encoding::CompatibilityError. Connaissez toujours l'encodage de vos données ; par défaut 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 reasonConversion d'encodage et I/O
Les I/O de fichier utilisent Encoding.default_external pour la lecture. Spécifiez l'encodage par fichier avec l'option encoding:. La syntaxe 'source:target' (ISO-8859-1:UTF-8) lit dans l'encodage source et convertit vers la cible. Définir default_internal fait que Ruby convertit automatiquement toutes les chaînes lues vers cet encodage. Pour les applications web, tout devrait être en UTF-8. Lors du traitement de données héritées, spécifiez explicitement les encodages pour éviter la corruption.
# 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_8Méthodes de chaînes en profondeur
Les chaînes Ruby ont des méthodes riches. L'inspection (length, include?, start_with?) vérifie les propriétés. Les méthodes de transformation retournent de nouvelles chaînes (les chaînes sont mutables dans Ruby, mais celles-ci ne mutent pas). L'accès aux sous-chaînes utilise [start, length] ou les ranges. sub remplace la première correspondance ; gsub remplace tout (supporte les regex et les blocs). split/join convertissent entre chaînes et tableaux. Note : le pragma frozen_string_literal de Ruby 3.0+ rend les chaînes immuables pour la performance—utilisez << ou + pour la construction.
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"Chaînes gelées et performance
frozen_string_literal: true (commentaire magique en haut de fichier) rend tous les littéraux de chaîne immuables—c'est une optimisation de performance (les chaînes gelées peuvent partager la mémoire) et prévient les bugs de mutation accidentelle. Pour construire des chaînes, utilisez << (ajout sur place, O(n)) pas += (crée une nouvelle chaîne à chaque fois, O(n²)). join est le plus propre pour les tableaux. StringIO agit comme un fichier mais écrit dans une chaîne—utile pour construire une sortie complexe. Ruby 3.x encourage les chaînes gelées par défaut.
# 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.stringSymboles vs chaînes
Les symboles (:name) sont des identifiants immuables et singleton—un seul :foo existe en mémoire, jamais. Les chaînes sont des données texte mutables avec plusieurs instances. Utilisez les symboles pour les clés de hash (vérifications d'égalité plus rapides), les noms de méthodes et les valeurs de type enum. Utilisez les chaînes pour le texte réel. Les symboles sont légèrement plus rapides pour les clés de hash et la comparaison. Dans Ruby moderne (2.2+), les symboles peuvent être collectés par le garbage collector, donc l'ancienne crainte de 'fuite de mémoire de symboles' a disparu. Rails utilise massivement les symboles pour les clés et les valeurs de statut.
# 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)Modules et mixins
Bases des modules
Les modules servent deux objectifs : le namespacing (regrouper le code associé, prévenir les conflits de noms) et les mixins (partager du comportement sans héritage). Les méthodes de module (def self.method) sont appelées sur le module. Les méthodes d'instance (def method) sont pour être mixées dans les classes. Les modules ne peuvent pas être instanciés. Utilisez les modules pour namespacer les classes (MyApp::User) et pour organiser les constantes et les fonctions utilitaires. C'est l'alternative de Ruby à l'héritage multiple.
# 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
Trois manières de mixer les modules : include (ajoute des méthodes d'instance, va sous la classe dans la recherche), extend (ajoute des méthodes de classe), prepend (ajoute des méthodes d'instance, va au-dessus de la classe—peut envelopper/redéfinir). prepend est puissant pour les hooks avant/après (appelez super pour invoquer l'original). Recherche de méthode : prepend → classe → include → superclass. Utilisez include pour les mixins normaux, prepend lorsque vous devez envelopper des méthodes existantes, extend pour la fonctionnalité au niveau de la classe.
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 saveMixin Enumerable
Inclure Enumerable et définir #each vous donne map, select, reduce, sort, min, max et plus de 40 méthodes—c'est le protocole d'itération de Ruby. Inclure Comparable et définir <=> vous donne <, >, ==, between?, clamp et le support du tri. Ces mixins sont la raison pour laquelle les collections Ruby sont si puissantes. Toute classe qui représente une collection ou a un ordre naturel devrait les inclure. C'est la composition plutôt que l'héritage.
# 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 # falseMéthodes singleton et méthodes de classe
Les méthodes singleton appartiennent à un objet spécifique. Les méthodes de classe sont juste des méthodes singleton sur l'objet classe. 'class << self' ouvre la classe singleton (eigenclass) pour définir proprement plusieurs méthodes de classe. Le pattern singleton utilise une variable de classe pour contenir une instance. Comprendre les classes singleton est clé pour le modèle objet de Ruby—chaque objet a une classe singleton contenant ses méthodes uniques. Cela permet la personnalisation par objet et la métaprogrammation.
# 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 (monkey patching à portée)
Les refinements (Ruby 2.1+) permettent le monkey patching à portée—ajoutez des méthodes aux classes existantes mais uniquement où vous 'using' explicitement le refinement. C'est plus sûr que le monkey patching global (qui peut casser d'autre code). Les refinements sont activés par portée (fichier, classe, méthode). Ils sont utiles pour ajouter des méthodes utilitaires sans polluer l'espace de noms global. Moins courants qu'ils ne devraient l'être en raison de la performance et de quelques bizarreries de portée, mais c'est la 'bonne' manière d'étendre les classes centrales.
# 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 inBlocs, Procs, lambdas en profondeur
Bases des blocs
Les blocs sont des closures anonymes passées aux méthodes en utilisant { } ou do...end. yield invoque le bloc depuis la méthode. Les blocs peuvent prendre des paramètres via |var|. Une méthode peut vérifier si un bloc a été passé avec block_given?. Les blocs sont la fondation du pattern d'itération et des DSL de Ruby. Ils capturent les variables de la portée englobante (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 et lambdas
Les Procs et lambdas sont des objets qui enveloppent des blocs, permettant le stockage dans des variables et le passage. Les Procs ont une vérification d'arguments tolérante (arguments supplémentaires ignorés) et retournent depuis la méthode englobante. Les lambdas ont une vérification stricte des arguments et ne retournent que d'eux-mêmes. Utilisez les Procs pour la flexibilité (comme des méthodes qui acceptent des blocs), les lambdas pour les fonctions anonymes au comportement prévisible. La syntaxe ->() {} (stabby lambda) est concise pour les one-liners.
# 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
endObjets Method
La méthode method() retourne un objet Method enveloppant une méthode existante. L'opérateur & convertit un Method ou Proc en bloc (et vice versa). Symbol#to_proc convertit :upcase en { |x| x.upcase }, permettant la syntaxe concise &:symbol. C'est du Ruby idiomatique pour les opérations courtes sur bloc. Les objets Method sont liés à leur récepteur, ils se souviennent donc de self lorsqu'ils sont passés.
# 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 et variables
Les closures capturent les variables par référence, pas par valeur. Plusieurs closures peuvent partager l'état (l'exemple du compteur). Les variables locales au bloc (déclarées après ; dans les paramètres) masquent les variables externes sans les modifier. Cela permet des patterns fonctionnels comme les accumulateurs, générateurs et mémoïsation. Attention : les closures tenant des références peuvent causer des fuites de mémoire si non libérées. Utilisez ce pattern pour l'encapsulation d'état privé.
# 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)Itérateurs personnalisés
Inclure Enumerable et implémenter each donne à votre classe toutes les méthodes d'itération (map, select, reduce, sort, etc.) gratuitement. Passez le bloc avec &block et appelez-le, ou utilisez yield. L'opérateur & convertit un bloc en Proc et inversement. C'est la manière idiomatique de rendre les collections personnalisées itérables. Implémentez each pour l'itération avant ; ajoutez reverse_each pour bidirectionnel.
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.Métaprogrammation
Méthodes dynamiques
method_missing intercepte les appels aux méthodes non définies, permettant le dispatch dynamique. Surchargez toujours respond_to_missing? pour correspondre. define_method crée des méthodes à l'exécution, utile pour générer des méthodes similaires (comme find_by_* d'ActiveRecord). instance_variable_get/set accèdent aux variables d'instance par nom. Utilisez la métaprogrammation avec parcimonie—elle rend le code plus difficile à comprendre et déboguer. Préférez les définitions explicites lorsque possible.
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
endClasses ouvertes et monkey patching
Les classes Ruby sont ouvertes : vous pouvez ajouter des méthodes à n'importe quelle classe, y compris les built-ins comme String. C'est puissant mais dangereux (le monkey patching peut casser d'autre code). Les refinements (Ruby 2.1+) fournissent des monkey patches à portée : ils s'appliquent uniquement dans les fichiers/classes qui utilisent le module. Préférez les refinements aux monkey patches globaux pour une métaprogrammation plus sûre. Documentez les patches clairement et évitez de changer le comportement central.
# 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 et callbacks
Ruby fournit des hooks de cycle de vie : inherited (sous-classe créée), included (module inclus), prepended (module prepended), method_added (méthode définie), method_removed, method_undefined. Ils permettent aux frameworks de réagir automatiquement aux changements de classe. ActiveRecord les utilise pour suivre les attributs, Rails les utilise pour le routage. Surchargez les hooks comme méthodes de classe (self.inherited) pour les événements de classe, méthodes d'instance pour les événements de méthode.
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 et binding
eval exécute une chaîne comme du code Ruby, dangereux si l'entrée n'est pas fiable (injection de code). binding capture le contexte d'exécution courant (variables, self) pour un eval ultérieur. class_eval exécute du code dans un contexte de classe (définit des méthodes). instance_eval change self vers le récepteur. Utilisez-les pour les DSL et la génération de code, mais évitez eval sur l'entrée utilisateur. Préférez les blocs et define_method pour le code dynamique. Assainissez toujours l'entrée si eval est inévitable.
# 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)Réflexion et introspection
Ruby fournit une réflexion riche : methods liste toutes les méthodes, instance_variables liste les variables d'instance, ancestors montre la chaîne d'héritage. parameters révèle les noms et types des paramètres d'une méthode. respond_to? vérifie si un objet répond à une méthode. Utilisez la réflexion pour le débogage, la sérialisation (inspection des attributs) et la construction d'outils génériques (comme les ORM). Évitez d'abuser de la réflexion—elle contourne la sécurité de type et rend le code plus difficile à suivre.
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) # trueTests (RSpec/Minitest)
Bases de RSpec
RSpec est un framework de test de style BDD. describe regroupe les tests associés, context groupe par condition, it définit un test unique. let crée des variables paresseuses mémoïsées. expect(...).to matcher est la syntaxe d'assertion. Matchers courants : eq, be_valid, include, raise_error. subject + is_expected réduit le boilerplate. Lancez avec rspec (tous) ou rspec path/to/spec (spécifique). Utilisez les factories (FactoryBot) au lieu des fixtures pour les données de test.
# 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: rspecMocks et stubs RSpec
Les stubs (allow) remplacent les valeurs de retour des méthodes ; les mocks (expect) vérifient que la méthode a été appelée. double crée un double de test (faux objet). and_return définit les valeurs de retour, and_raise simule des erreurs, with définit les arguments attendus. Utilisez les mocks avec parcimonie—trop de mocks rend les tests fragiles. Testez le comportement, pas l'implémentation. Préférez les vrais objets lorsque c'est faisable ; utilisez les mocks pour les services externes (APIs, email, passerelles de paiement).
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 est la bibliothèque de test par défaut de Ruby (livrée avec Ruby). Elle supporte les syntaxes Unit-style (assert/refute) et Spec-style (must_equal). setup s'exécute avant chaque test. Assertions : assert_equal, assert_nil, assert_raises, assert_includes. Minitest est plus rapide que RSpec et a moins de dépendances. Utilisez Minitest::Mock pour les mocks, ou la gem Mocha pour plus de fonctionnalités. Rails utilise Minitest par défaut (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.rbDonnées de test et factories
FactoryBot génère des objets de test avec des valeurs par défaut, des traits pour les variations et des associations. create persiste en DB ; build non. Les traits composent (factory :admin_with_posts, traits: [:admin, :with_posts]). Les factories sont plus flexibles que les fixtures mais plus lentes. Les fixtures (YAML) sont plus rapides mais moins flexibles. Choisissez selon les besoins : fixtures pour les données simples, factories pour les relations complexes. Évitez les factories avec trop de traits—gardez-les ciblées.
# 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)Tests d'intégration et système
Les request specs testent toute la pile (routage, contrôleurs, modèles) via HTTP. Les system specs (Capybara) pilotent un vrai navigateur, testant les interactions JavaScript. Utilisez les request specs pour les endpoints d'API, les system specs pour les flux utilisateur. have_http_status vérifie les codes de réponse. visit/fill_in/click_button simulent les actions utilisateur. Les tests système sont plus lents mais détectent les bugs d'intégration. Lancez les tests système avec js: true pour les pages pilotées par JavaScript.
# 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 et fibers
Bases des threads
Les threads dans Ruby (MRI) sont des green threads planifiées par la VM—ils ne s'exécutent pas vraiment en parallèle en raison du GIL (Global Interpreter Lock). Cependant, les threads sont utiles pour la concurrence d'I/O (réseau, opérations sur fichiers). join attend qu'un thread se termine. value récupère la valeur de retour. Pour le parallélisme CPU-bound, utilisez plusieurs processus (fork, Sidekiq) ou JRuby (pas de GIL). Joignez toujours les threads pour éviter qu'ils ne soient tués lorsque le thread principal se termine.
# 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 threadSynchronisation de threads
Mutex.synchronize garantit qu'un seul thread exécute un bloc à la fois, prévenant les race conditions. Queue est un FIFO thread-safe—les producteurs push, les consommateurs pop (bloque si vide). ConditionVariable coordonne les threads : wait relâche le mutex et dort, signal/broadcast réveille les threads en attente. Utilisez toujours la synchronisation pour l'état mutable partagé. Les deadlocks se produisent lorsque les threads s'attendent mutuellement—acquérez les verrous dans un ordre cohérent.
# 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
Les fibers sont des threads légers coopératifs : ils yield le contrôle manuellement au lieu d'être préemptés. resume démarre/reprend une fiber ; Fiber.yield la met en pause et retourne une valeur. Les fibers sont utiles pour les générateurs, l'évaluation paresseuse et l'analyse des machines à états. Contrairement aux threads, une seule fiber s'exécute à la fois, donc pas de synchronisation nécessaire. Les fibers sont moins coûteuses que les threads mais ne peuvent pas utiliser plusieurs cœurs. Utilisez-les pour le multiplexage d'I/O (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 34Ruby async et concurrent
La gem async fournit de l'I/O asynchrone moderne en utilisant les Fibers en dessous, permettant du code réseau à haute concurrence. concurrent-ruby fournit des abstractions thread-safe : Future (résultat async), Promise (chaînable), thread pools et variables atomiques. Utilisez async pour le travail I/O-bound (HTTP, bases de données), les thread pools pour le travail CPU-bound. Arrêtez toujours les pools pour éviter les blocages. Ces gems contournent le GIL de MRI pour une concurrence pratique.
# 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+)
Les Ractors (Ruby 3.0+) fournissent un véritable parallélisme en évitant le GIL. Chaque Ractor a son propre heap, donc pas de data races. La communication se fait via des messages (send/receive, take). Les objets envoyés sont copiés (ou déplacés avec Ractor.move) pour maintenir l'isolation. Les objets gelés peuvent être partagés. Les Ractors sont l'avenir du Ruby parallèle mais ont des restrictions : la plupart des gems ne sont pas encore Ractor-safe. Utilisez-les pour le calcul parallèle CPU-bound où l'isolation est acceptable.
# 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 safelyEncodage et I/O
Encodage de chaînes
Chaque chaîne Ruby a un encodage (UTF-8 par défaut). bytesize est le nombre d'octets ; length est le nombre de caractères (différent pour les encodages multibyte). force_encoding change le tag d'encodage sans convertir les octets (utilisez-le lorsque vous connaissez l'encodage réel). encode convertit entre les encodages. Vérifiez toujours valid_encoding? avant de traiter des données externes. Définissez l'encodage du fichier source avec le commentaire magique # encoding: utf-8 (bien que UTF-8 soit par défaut dans Ruby 2.0+).
# 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: '?')I/O fichier
File.read charge tout le fichier en mémoire ; File.foreach lit ligne par ligne (économe en mémoire pour les gros fichiers). File.write écrase ; mode 'a' ajoute ; 'r+' lit et écrit. Le mode binaire ('rb', 'wb') empêche la conversion d'encodage. Utilisez toujours la forme bloc (File.open) pour garantir la fermeture du fichier. FileUtils fournit des opérations de plus haut niveau (cp, mv, mkdir_p). Vérifiez l'existence avec File.exist? avant de lire.
# 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 et Tempfile
StringIO enveloppe une chaîne dans une interface de type IO, utile pour tester des opérations de fichier sans toucher au disque. Tempfile crée des fichiers temporaires qui sont automatiquement supprimés à la sortie du bloc (Tempfile.create) ou lorsqu'unlinked (Tempfile.new). Utilisez Tempfile pour les grandes données qui ne tiennent pas en mémoire ou pour passer à des programmes externes via le chemin. Garantissez toujours le nettoyage avec des blocs ensure. StringIO est excellent pour les tests unitaires de code I/O.
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
endI/O réseau
TCPSocket/TCPServer fournissent un accès TCP de bas niveau. Net::HTTP est le client HTTP standard (intégré). Pour des besoins HTTP complexes (sessions, cookies, retries), utilisez les gems httparty ou faraday. Définissez toujours use_ssl = true pour HTTPS. Pour du HTTP haute performance, considérez async-http ou typhoeus. URI parse les URLs en toute sécurité. Gérez Net::ReadTimeout et Errno::ECONNREFUSED pour un code réseau robuste.
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 et pipelines
$stdout, $stderr, $stdin sont des objets I/O globaux (utilisez les constantes STDOUT/STDERR/STDIN pour les originaux). Redirigez en réassignant les globaux. StringIO capture la sortie pour les tests. Open3.popen3 donne un contrôle complet sur stdin/stdout/stderr d'un sous-processus. Open3.pipeline chaîne les commandes comme un pipe shell. Capture3 retourne stdout, stderr et le statut. Fermez toujours stdin pour signaler EOF au sous-processus. Utilisez wait.value pour obtenir le statut de sortie.
# 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+)
Pattern matching de base
Le pattern matching (mot-clé in) déstructure les tableaux et hashes, liant les variables. => lie les valeurs correspondantes aux variables. *rest capture les éléments restants du tableau. La correspondance de hash est partielle : les clés supplémentaires sont ignorées. | correspond à plusieurs patterns. case/in est la forme principale ; la forme one-liner est expression in pattern. Le pattern matching est particulièrement puissant pour analyser des données structurées (JSON, AST) et remplacer les chaînes if-else complexes.
# 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)
endLiaison de variables et gardes
=> lie les valeurs correspondantes aux variables. L'opérateur pin (^var) correspond à la valeur courante de la variable (pas de liaison). Les gardes (if/unless) ajoutent des conditions. Les patterns de tableau supportent * pour les splats à n'importe quelle position. Le pattern find [*, target, *] recherche un élément dans un tableau. Le pattern matching est déclaratif et concis pour l'extraction de données complexes. Les variables liées dans les patterns sont disponibles après l'instruction case.
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}"
endPatterns de classe et de type
Les patterns de classe correspondent par type. Les classes personnalisées supportent la déstructuration en implémentant deconstruct (pattern de tableau) et deconstruct_keys (pattern de hash). Le pattern Class[args] utilise deconstruct ; Class(key:) utilise deconstruct_keys. De nombreuses classes built-in (Time, Date, MatchData) supportent la déstructuration. Cela permet une correspondance expressive sur les objets de domaine. Implémentez ces méthodes pour rendre vos classes pattern-matchables.
# 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"
endPattern matching one-liner
La forme one-liner (expr in pattern) retourne true/false au lieu de lever NoMatchingPatternError. Elle lie les variables en cas de correspondance réussie. Utile pour les clauses de garde et l'extraction conditionnelle. La forme rightward (pattern => var) lie toute la correspondance. Le matching one-liner est concis pour les cas simples ; utilisez case/in pour la logique complexe multi-branches. Les variables liées dans les correspondances échouées restent 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, 30Cas d'usage pratiques
Le pattern matching excelle dans : l'analyse de données structurées (JSON, XML), les machines à états, le parcours d'AST et le dispatch selon la forme des données. Il remplace les chaînes if-else verbeuses par des patterns déclaratifs. La clause else gère les formats inattendus. Combinez avec des gardes pour des conditions complexes. Le pattern matching rend le code plus lisible et maintenable pour la gestion de données complexes. C'est l'une des fonctionnalités les plus puissantes de Ruby 3 pour un code propre et expressif.
# 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
endMétaprogrammation
send et define_method
define_method crée des méthodes dynamiquement. send appelle les méthodes par nom (même privées). public_send respecte la visibilité. Utile pour les DSL et réduire le boilerplate. Attention avec l'entrée utilisateur pour éviter les problèmes de sécurité. La métaprogrammation est puissante mais peut rendre le code plus difficile à comprendre.
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 intercepte les appels aux méthodes non définies. Utile pour le dispatch dynamique et les DSL. Surchargez toujours respond_to_missing? pour correspondre. Peut être lent et masquer des bugs. Préférez define_method lorsque l'ensemble des méthodes est connu. ActiveRecord l'utilise pour les accesseurs d'attributs.
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 exécute une chaîne comme du code Ruby. Extrêmement puissant mais dangereux. N'évaluez jamais d'entrée non fiable (injection de code). Utilisez binding.eval pour un contexte spécifique. Pour une évaluation sûre, utilisez un parser comme Ripper ou un sandbox. La plupart des cas d'usage ont des alternatives plus sûres.
# 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!Classes ouvertes
Les classes Ruby sont ouvertes : vous pouvez ajouter des méthodes à n'importe quelle classe, y compris les built-ins. C'est appelé monkey patching. Puissant pour les corrections rapides mais peut causer des conflits et de la confusion. Utilisez les refinements pour les modifications à portée. Documentez clairement les changements. Préférez la composition au monkey patching.
class String
def shout
upcase + "!"
end
end
"hello".shout # "HELLO!"
# Monkey patching: modifying existing classes
# Use sparingly, can cause conflictsMacros de classe
Les macros de classe sont des méthodes de classe qui définissent d'autres méthodes. has_many, belongs_to, attr_accessor sont des exemples. Elles utilisent define_method en interne. C'est ainsi que Rails crée des méthodes dynamiques. Les DSL comme RSpec et Sinatra utilisent ce pattern. Rend le code déclaratif et lisible.
class ActiveRecord::Base
def self.has_many(name)
define_method(name) { [] }
end
end
class Post < ActiveRecord::Base
has_many :comments
end
Post.new.comments # []Snippets Ruby associés
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?