Skip to content

Ruby Aide-mémoire

Langage dynamique et élégant optimisé pour le bonheur du développeur.

01

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.

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

Symboles

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é.

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

Nil 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.

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

Conversion 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.

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

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.

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

Chaî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.

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

Mutation 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.

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

Heredoc 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.

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

Formatage (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.

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

Concaté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.

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

Structures 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.

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

Hashes

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.

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

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.

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

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.

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

Sets

Set (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'.

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

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.

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

Case (When)

case/when 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.

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

While / Until / Loop

while 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.

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

Ité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.

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

Break / Next / Redo

next 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é.

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

Mé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 =.

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

Blocs 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.

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

Procs 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.

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

Objet 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.

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

Valeurs 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.

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

Classes 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.

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

Hé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.

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

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.

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

Contrô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.

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

Self 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.

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

Gestion 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).

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

Raise 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.

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

Retry

retry 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.

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

Modificateur 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.

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

Throw / Catch (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.

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

Entré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.

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

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.

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

Existence 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.

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

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.

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

CSV 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.

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

Date/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.

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

Analyse 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.

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

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/.

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

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.

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

Options 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).

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

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.

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

method_missing

method_missing 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.

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

Threads

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.

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

Fibers (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.

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

Send 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.

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

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).

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

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

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

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

Procs vs lambdas

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.

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

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

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

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

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

Objets 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.

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

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

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

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

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

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

Closures 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.

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

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

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

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

Ité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.

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

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

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

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

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

Enumerable 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.

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

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

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

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

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

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

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

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

É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.

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

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

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

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

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

Enumerator 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.

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

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

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

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

Tri 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.

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

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

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

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

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

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

Ité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.

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

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

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

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

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

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

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

Gems 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).

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

# List installed gems
$ gem list
$ gem list rails

# Uninstall
$ gem uninstall rails

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

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

group :production do
  gem 'newrelic_rpm'
end

Commandes 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.

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

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

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

# Check for dependency issues
$ bundle check
$ bundle doctor

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

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

# Clean old gems
$ bundle clean

Gemspec (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.

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

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

  spec.required_ruby_version = ">= 3.0"

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

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

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

Tâ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].

ruby
# Rakefile
require 'rspec/core/rake_task'

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

task default: :spec

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

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

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

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

Rbenv 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.

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

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

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

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

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

Bases 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.

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

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

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

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

Modè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.

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

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

  # Callbacks
  before_save :normalize_email
  after_create :send_welcome_email

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

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

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

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.

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

  def index
    @users = User.all
  end

  def show; end  # @user set by before_action

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

  private

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

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

Vues 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.

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

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

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

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.

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

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

      t.timestamps  # created_at, updated_at
    end
  end
end

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

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

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

Tests 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/.

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

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

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

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

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

Mocks 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).

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

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

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

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

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

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

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

FactoryBot 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.

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

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

    trait :inactive do
      active { false }
    end

    # Associations
    company { association(:company) }

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

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

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

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).

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

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

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

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

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

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

Tests 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.

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

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

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

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

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

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

Opé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.

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

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

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

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

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

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

É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.

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

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

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

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

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

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

Opé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.

ruby
require 'fileutils'

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

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

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

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

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

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

Pathname 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 \).

ruby
require 'pathname'
require 'tempfile'

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

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

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

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

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

CSV 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.

ruby
require 'csv'
require 'json'

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

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

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

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

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

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

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
# Ruby strings have an encoding
"hello".encoding  # #<Encoding:UTF-8>
"café".encoding   # #<Encoding:UTF-8>

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

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

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

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

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

Conversion 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.

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

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

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

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

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

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

Mé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.

ruby
s = "Hello, World"

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

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

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

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

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

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.

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

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

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

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

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

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

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

Symboles 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.

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

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

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

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

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

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

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

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.

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

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

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

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

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

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

Include vs Extend vs Prepend

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.

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

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

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

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

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

Mixin 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.

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

  def initialize(songs)
    @songs = songs
  end

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

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

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

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

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

Mé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.

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

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

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

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

Refinements (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.

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

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

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

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

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

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

Blocs, 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).

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

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

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

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

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

Procs 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.

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

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

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

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

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

Objets 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.

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

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

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

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

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

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

Closures 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é.

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

increment.call
increment.call
puts counter   # 2

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

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

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

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.

ruby
class Tree
  include Enumerable

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

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

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

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

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

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.

ruby
class Person
  attr_accessor :name, :age

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

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

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

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

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

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

Classes 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.

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

  def word_count
    split.size
  end
end

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

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

class MyClass
  using ShoutRefinement

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

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

Hooks 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.

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

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

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

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

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

# Other hooks: method_undefined, extended, prepended

eval 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.

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

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

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

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

puts "racecar".palindrome?   # true

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

# NEVER eval user input (security risk)

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.

ruby
class User
  attr_accessor :name, :email

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

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

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

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

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

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

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

Tests (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.

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

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

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

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

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

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

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

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

Mocks 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).

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

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

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

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

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

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

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

Minitest

Minitest 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).

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

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

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

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

  def test_valid_with_attributes
    assert @user.valid?
  end

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

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

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

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

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

Donné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.

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

    trait :admin do
      role { 'admin' }
    end

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

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

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

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

# In tests: users(:alice)

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.

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

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

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

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

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

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

Threads 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.

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

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

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

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

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

Synchronisation 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.

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

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

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

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

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

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

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

Fibers

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).

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

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

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

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

Ruby 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.

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

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

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

  tasks.each(&:wait)
end

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

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

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

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

Ractors (Ruby 3.0+)

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.

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

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

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

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

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

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

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

Encodage 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
# 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.

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

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

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

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

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

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

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

StringIO 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.

ruby
require 'stringio'
require 'tempfile'

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

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

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

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

I/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.

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

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

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

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

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

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

STDIO 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.

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

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

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

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

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

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

Pattern matching (3.0+)

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.

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

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

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

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

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

Liaison 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.

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

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

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

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

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

Patterns 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.

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

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

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

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

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

Pattern 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.

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

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

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

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

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

Cas 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.

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

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

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

Mé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.

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

method_missing

method_missing 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.

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

eval

eval 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.

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

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.

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

Macros 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.

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

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.