変数・定数・データ型
基礎Ruby の変数は先頭文字でスコープが決まる。ローカル変数は小文字/アンダースコア始まり、インスタンス変数は @、クラス変数は @@、グローバル変数は $ が先頭に付く。
local_var = "ローカル" # ローカル変数
class Counter
@@count = 0 # クラス変数(全インスタンス共有)
def initialize(name)
@name = name # インスタンス変数
@@count += 1
end
def self.count = @@count # クラスメソッド(Ruby 3.0+ の短縮形)
end
$debug = true # グローバル変数(使用は最小限に)
Counter.new("a")
Counter.new("b")
p Counter.count # => 2@@ は継承ツリー全体で共有されるため、サブクラスで意図せず書き換わることがある。インスタンス変数 @ や class << self 内のインスタンス変数で代替するのが一般的。大文字始まりの識別子は定数。再代入すると警告が出るが実行は止まらない。完全に凍結するには freeze を呼ぶ。
MAX_RETRIES = 3
PI = 3.14159
# MAX_RETRIES = 5 # warning: already initialized constant MAX_RETRIES
FROZEN_LIST = [1, 2, 3].freeze
# FROZEN_LIST << 4 # FrozenError: can't modify frozen Arrayfreeze はオブジェクト自体を凍結するが、配列・ハッシュの**要素**は凍結しない(浅い凍結)。完全な不変性が必要なら要素ごとに freeze を呼ぶか deep_freeze 相当のロジックが必要。Ruby の主要な組み込み型。true/false は TrueClass/FalseClass の唯一のインスタンス。nil は NilClass のインスタンスで「値なし」を表す。
42 # Integer(任意精度)
3.14 # Float
"hello" # String
:name # Symbol(不変・内部的にキャッシュ)
true # TrueClass
false # FalseClass
nil # NilClass
[1, 2, 3] # Array
{ a: 1 } # Hashis_a? / kind_of?(同義語)は継承・モジュール込みで判定する。instance_of? は厳密な型一致のみ。class は実際のクラスを返す。
n = 42
p n.class # => Integer
p n.is_a?(Integer) # => true
p n.is_a?(Numeric) # => true ← 親クラスも true
p n.instance_of?(Numeric) # => false ← 厳密一致のみ
p "hi".is_a?(Comparable) # => true ← モジュールも true制御フロー
基礎if は末尾に end が必要。unless は「〜でなければ」を意味する否定形 if。どちらも式であり値を返す。
score = 75
result = if score >= 90
"S"
elsif score >= 70
"A"
else
"B"
end
p result # => "A"
# unless(否定形)
puts "ゲスト" unless score >= 90case/when は === で比較する。Ruby 3.0 以降は case/in でパターンマッチが使える。
# 値による分岐(=== を使用)
val = "hello"
case val
when String then puts "文字列"
when Integer then puts "整数"
else puts "その他"
end
# Ruby 3.0+ パターンマッチ
data = { name: "Alice", age: 30 }
case data
in { name: String => name, age: (18..) }
puts "成人: #{name}"
endwhile は条件が真の間繰り返し、until は偽の間繰り返す。loop は無限ループで break で脱出する。
i = 0
while i < 3
print "#{i} "
i += 1
end
# => 0 1 2
j = 3
until j == 0
print "#{j} "
j -= 1
end
# => 3 2 1
loop do
puts "one shot"
break
end式 if 条件 / 式 unless 条件 の形式で 1 行に書ける。ガード節としてメソッド冒頭の早期 return に多用する。
puts "正" if 1 > 0
puts "偽ではない" unless false
def greet(name)
return "名前が空です" if name.nil? || name.empty?
"Hello, #{name}!"
end
p greet("") # => "名前が空です"
p greet("Ruby") # => "Hello, Ruby!"メソッド
基礎def でメソッドを定義する。Ruby では最後に評価した式の値が自動的に返値になる(暗黙の return)。明示的な return も使える。
def add(a, b)
a + b # 最後の式が戻り値
end
def sign(n)
return "正" if n > 0
return "負" if n < 0
"ゼロ" # ← これが暗黙の return
end
p add(3, 4) # => 7
p sign(-5) # => "負"Ruby はデフォルト引数・キーワード引数(name:)・スプラット演算子(*args/**kwargs)を組み合わせられる。
def greet(name, greeting: "Hello")
"#{greeting}, #{name}!"
end
def sum(*nums)
nums.sum
end
def show(**opts)
opts.each { |k, v| puts "#{k}: #{v}" }
end
p greet("Alice") # => "Hello, Alice!"
p greet("Bob", greeting: "Hi") # => "Hi, Bob!"
p sum(1, 2, 3, 4) # => 10
show(lang: "Ruby", ver: "3.3")private メソッドはクラス外から呼び出せない。protected は同クラスおよびサブクラスのインスタンスから呼べる(比較メソッドなどに使用)。
class BankAccount
def initialize(balance)
@balance = balance
end
def >(other)
balance > other.balance # protected メソッドを呼べる
end
protected
def balance = @balance
private
def secret_pin = "1234"
end
a = BankAccount.new(100)
b = BankAccount.new(200)
p a > b # => false
# a.balance # NoMethodError
# a.secret_pin # NoMethodError! で終わるメソッドは破壊的(レシーバ自体を変更)または例外を発生させる危険な版。? で終わるメソッドは真偽値を返す述語メソッド。
s = "hello"
p s.upcase # => "HELLO"(元は変わらない)
p s # => "hello"
s.upcase! # 破壊的
p s # => "HELLO"
p [1, nil, 2, nil].compact # => [1, 2](コピー)
p "".empty? # => true
p nil.nil? # => true
p 5.between?(1, 10) # => true文字列
基礎"..." はエスケープシーケンスと文字列補間が有効。'...' はほぼそのまま(\\' と \\\\ のみ解釈)。
name = "Ruby"
p "Hello, #{name}!" # => "Hello, Ruby!" ← 補間あり
p 'Hello, #{name}!' # => "Hello, #{name}!" ← 補間なし
p "改行:
次の行"
p '改行:
次の行' #
はそのまま文字列#{...} の中には任意の Ruby 式を書ける。式の評価結果が to_s で文字列化されて埋め込まれる。
x = 7
p "7 の 2 乗は #{x ** 2}" # => "7 の 2 乗は 49"
p "現在時刻: #{Time.now}"
p "配列: #{[1,2,3].join(", ")}"<<~HEREDOC はインデントを自動的に除去する(Ruby 2.3+)。SQL・HTML などの複数行テキストに便利。
sql = <<~SQL
SELECT *
FROM users
WHERE age > #{18}
SQL
puts sqlString クラスには豊富なメソッドが揃っている。gsub は正規表現対応の全置換、% は printf 風フォーマット演算子。
s = " Hello, Ruby! "
p s.strip # => "Hello, Ruby!"
p s.upcase # => " HELLO, RUBY! "
p s.gsub("Ruby", "World") # => " Hello, World! "
p s.split(", ") # => [" Hello", "Ruby! "]
p "%.2f" % 3.14159 # => "3.14"
p "Name: %s, Age: %d" % ["Alice", 30]配列とハッシュ
基礎通常の配列リテラルのほか、%w[] で文字列配列、%i[] でシンボル配列を簡潔に作れる。
nums = [1, 2, 3]
words = %w[apple banana cherry] # => ["apple", "banana", "cherry"]
syms = %i[red green blue] # => [:red, :green, :blue]
p words[0] # => "apple"
p nums[-1] # => 3(後ろから)
p nums[1, 2] # => [2, 3](start, length)
p nums[0..1] # => [1, 2](範囲)map は変換、select は絞り込み、reject は除外、reduce は畳み込み。これらは Enumerable モジュールが提供する。
nums = [1, 2, 3, 4, 5]
p nums.map { |n| n * 2 } # => [2, 4, 6, 8, 10]
p nums.select { |n| n.odd? } # => [1, 3, 5]
p nums.reject { |n| n.odd? } # => [2, 4]
p nums.reduce(:+) # => 15
p [[1, [2]], [3]].flatten # => [1, 2, 3]
p [1, 2].zip([3, 4]) # => [[1, 3], [2, 4]]シンボルキーには key: value の省略記法が使える(Ruby 1.9+)。fetch はキーが存在しない場合に例外またはデフォルト値を返す。
user = { name: "Alice", age: 30, role: :admin }
p user[:name] # => "Alice"
p user.fetch(:age) # => 30
p user.fetch(:missing, 0) # => 0(デフォルト値)
# ネストしたハッシュには dig
config = { db: { host: "localhost", port: 5432 } }
p config.dig(:db, :host) # => "localhost"
p config.dig(:db, :pass) # => nil(KeyErrorではない)merge は 2 つのハッシュを統合(後者が優先)。transform_values / transform_keys は値・キーを一括変換する。
a = { x: 1, y: 2 }
b = { y: 99, z: 3 }
p a.merge(b) # => {x:1, y:99, z:3}
p a.select { |k, v| v > 1 } # => {y:2}
p a.transform_values { |v| v * 10 } # => {x:10, y:20}
p a.transform_keys { |k| k.to_s } # => {"x"=>1, "y"=>2}例外処理
基礎rescue で例外をキャッチ。ensure は例外の有無に関わらず必ず実行(クリーンアップ用)。else は例外が発生しなかった場合に実行。
begin
result = 10 / Integer(gets.chomp)
rescue ZeroDivisionError => e
puts "ゼロ除算: #{e.message}"
rescue ArgumentError => e
puts "不正な入力: #{e.message}"
else
puts "結果: #{result}"
ensure
puts "処理終了(必ず実行)"
endraise で例外を発生させる。rescue ブロック内で retry を呼ぶと begin から再試行できる。無限ループに注意してカウンタで制限する。
attempts = 0
begin
attempts += 1
raise "一時的なエラー" if attempts < 3
puts "成功(#{attempts} 回目)"
rescue RuntimeError => e
puts "リトライ #{attempts}: #{e.message}"
retry if attempts < 3
endStandardError を継承して独自例外を定義する。RuntimeError の継承でも良いが、ライブラリでは StandardError を推奨。
class InsufficientFundsError < StandardError
def initialize(amount, balance)
super("残高不足: 引き出し額 #{amount}、残高 #{balance}")
@amount = amount
@balance = balance
end
attr_reader :amount, :balance
end
begin
raise InsufficientFundsError.new(500, 200)
rescue InsufficientFundsError => e
puts e.message
puts "不足額: #{e.amount - e.balance}"
endメソッド全体を例外処理の対象にする場合は begin/end を省略して def 直後に rescue を書ける。
def parse_int(str)
Integer(str)
rescue ArgumentError
nil
end
p parse_int("42") # => 42
p parse_int("abc") # => nilクラスとモジュール
基礎initialize はコンストラクタ。attr_accessor は getter/setter を自動生成する。読み取り専用は attr_reader、書き込み専用は attr_writer。
class Person
attr_accessor :name, :age
attr_reader :id
@@next_id = 1
def initialize(name, age)
@name = name
@age = age
@id = @@next_id
@@next_id += 1
end
def to_s = "#{@name}(#{@age})"
end
alice = Person.new("Alice", 30)
alice.age = 31
p alice.to_s # => "Alice(31)"
p alice.id # => 1class Sub < Parent で継承。super は親クラスの同名メソッドを呼び出す。引数なしの super は全引数をそのまま転送する。
class Animal
def initialize(name)
@name = name
end
def speak = "#{@name} が鳴く"
end
class Dog < Animal
def speak = "#{super}: ワン!" # super で親を呼ぶ
end
d = Dog.new("ポチ")
p d.speak # => "ポチ が鳴く: ワン!"
p d.is_a?(Animal) # => trueRuby はクラスの多重継承を持たないが、module と include による Mixin で複数の機能を合成できる。モジュールはインスタンス化できない。
module Greetable
def greet = "Hello, I'm #{name}"
end
module Farewell
def bye = "Goodbye from #{name}"
end
class User
include Greetable
include Farewell
attr_reader :name
def initialize(name)
@name = name
end
end
u = User.new("Bob")
p u.greet # => "Hello, I'm Bob"
p u.bye # => "Goodbye from Bob"include がインスタンスメソッドを追加するのに対し、extend はクラス(またはオブジェクト)のシングルトンクラスにメソッドを追加する。
module ClassLogger
def log(msg) = puts("[LOG] #{self}: #{msg}")
end
class Service
extend ClassLogger
end
Service.log("起動") # クラスメソッドとして使える
# 個別オブジェクトへの extend
obj = Object.new
obj.extend(ClassLogger)
obj.log("hello")イテレータとブロック
基礎Ruby では for ループよりメソッドにブロックを渡す書き方が慣用的。数値には times・upto・step などが使える。
[10, 20, 30].each { |n| print "#{n} " } # => 10 20 30
[10, 20, 30].map { |n| n / 10 } # => [1, 2, 3]
[1..5].first(5).select { |n| n.even? } # => [2, 4]
3.times { |i| print "#{i} " } # => 0 1 2
1.upto(5) { |i| print "#{i} " } # => 1 2 3 4 5
(1..10).step(3) { |i| print "#{i} " } # => 1 4 7 10do...end と {...} は同義だが優先順位が異なる。{} は do...end より強く束縛される。複数行なら do...end、1行なら {} が慣習。
# 1 行ブロックは {} が慣習
[1, 2, 3].map { |n| n ** 2 }
# 複数行は do...end が慣習
[1, 2, 3].each do |n|
squared = n ** 2
puts "#{n}^2 = #{squared}"
end
# 優先順位の違い(注意)
p [1,2].map { |n| n * 2 } # p([1,2].map{...}) と解釈
# p [1,2].map do |n| n * 2 end # (p [1,2]).map do...end と解釈されることがあるyield で呼び出し側のブロックに制御を渡す。block_given? でブロックの有無を確認できる。&block 引数でブロックを Proc として受け取れる。
def repeat(n)
n.times { yield }
end
repeat(3) { print "Ruby! " }
# => Ruby! Ruby! Ruby!
def measure(&block)
start = Time.now
block.call
puts "経過: #{Time.now - start}s"
end
measure { sleep(0.01) }Proc.new と lambda/-> は引数チェックと return の挙動が異なる。lambda は引数が合わないと ArgumentError、return はラムダ内のみで完結する。
square = ->(n) { n ** 2 } # lambda(短縮形)
cube = lambda { |n| n ** 3 } # lambda(明示形)
greeting = Proc.new { |n| "Hi #{n}" }
p square.call(4) # => 16
p cube.(3) # => 27 (.() は .call() の糖衣)
p greeting.("Bob") # => "Hi Bob"
p square.lambda? # => true
p greeting.lambda? # => false比較・等値・宇宙船演算子
基礎== は値の等値(オーバーライド可能)。equal? はオブジェクト同一性(object_id の比較)。eql? はハッシュキー比較に使われる(型も含めた厳密な等値)。
a = "hello"
b = "hello"
c = a
p a == b # => true 値が等しい
p a.equal?(b) # => false 別オブジェクト
p a.equal?(c) # => true 同じオブジェクト
p 1 == 1.0 # => true 数値として等しい
p 1.eql?(1.0) # => false 型が違う<=> は -1/0/1 を返す汎用比較演算子。sort はこれを使って並べ替える。sort_by は任意のキーでソートする際に便利。
p 1 <=> 2 # => -1
p 2 <=> 2 # => 0
p 3 <=> 2 # => 1
words = %w[banana apple cherry]
p words.sort # => ["apple", "banana", "cherry"]
p words.sort_by { |w| w.length } # => ["apple", "banana", "cherry"]
people = [{ name: "Bob", age: 30 }, { name: "Alice", age: 25 }]
p people.sort_by { |p| p[:age] }.map { |p| p[:name] }Comparable を include して <=> を実装すれば、<・>・<=・>=・between?・clamp が自動で使えるようになる。
class Temperature
include Comparable
attr_reader :degrees
def initialize(d) = (@degrees = d)
def <=>(other) = degrees <=> other.degrees
end
temps = [30, 20, 25].map { |d| Temperature.new(d) }
sorted = temps.sort.map(&:degrees)
p sorted # => [20, 25, 30]
t = Temperature.new(22)
p t.between?(Temperature.new(20), Temperature.new(25)) # => truecase/when の内部では === が使われる。Range・Regexp・Proc などはそれぞれ意味のある === を実装している。
# Range#=== は include? と同義
p (1..10) === 5 # => true
# Regexp#=== は =~ と同義
p /\d+/ === "abc123" # => true
# Proc#=== は call と同義
even = ->(n) { n.even? }
p even === 4 # => true
# case/when で活用
def classify(x)
case x
when String then "文字列"
when (1..9) then "1〜9"
when Integer then "その他整数"
end
end
p classify("hi") # => "文字列"
p classify(5) # => "1〜9"Enumerable
基礎Enumerable は each を実装したクラスに include するだけで 50 以上のコレクション操作メソッドを提供する。Array・Hash・Range などに組み込み済み。
class NumberSet
include Enumerable
def initialize(*nums) = (@data = nums)
def each(&block) = @data.each(&block)
end
ns = NumberSet.new(3, 1, 4, 1, 5, 9)
p ns.sort # => [1, 1, 3, 4, 5, 9]
p ns.min # => 1
p ns.max # => 9
p ns.select(&:odd?) # => [3, 1, 1, 5, 9]each_with_object は累積オブジェクトを持ち回る、each_with_index はインデックス付き反復、each_slice / each_cons はウィンドウ処理に使う。
# each_with_object: 累積オブジェクト
result = [1, 2, 3].each_with_object({}) do |n, h|
h[n] = n ** 2
end
p result # => {1=>1, 2=>4, 3=>9}
# each_with_index: インデックス付き
%w[a b c].each_with_index { |v, i| puts "#{i}: #{v}" }
# each_slice: n 個ずつ
(1..6).each_slice(2) { |s| p s } # [1,2] [3,4] [5,6]
# each_cons: スライドウィンドウ
(1..5).each_cons(3) { |c| p c } # [1,2,3] [2,3,4] [3,4,5]group_by はキーでグルーピング、chunk は連続する同値要素をまとめる、tally は Ruby 2.7+ で各要素の出現回数を数える。
words = %w[one two three four five six]
p words.group_by { |w| w.length }
# => {3=>["one","two","six"], 4=>["four","five"], 5=>["three"]}
p [1, 1, 2, 2, 1, 3].chunk { |n| n }.map { |k, v| [k, v.size] }
# => [[1,2],[2,2],[1,1],[3,1]]
p %w[a b a a c b].tally
# => {"a"=>3, "b"=>2, "c"=>1}lazy は遅延評価の Enumerator を返す。無限シーケンスに対して map/select を適用し、必要な分だけ first や take で取り出せる。
# 無限の自然数から偶数の先頭 5 つ
result = (1..Float::INFINITY)
.lazy
.select { |n| n.even? }
.map { |n| n ** 2 }
.first(5)
p result # => [4, 16, 36, 64, 100]Proc・lambda・クロージャ
基礎主な違いは 2 点。(1) 引数の数: lambda は厳密チェック、Proc は余剰・不足を許容。(2) return の挙動: lambda はラムダ内のみ、Proc は呼び出し元メソッドごとリターン。
lam = lambda { |x, y| x + y }
prc = Proc.new { |x, y| [x, y].inspect }
p lam.call(1, 2) # => 3
# lam.call(1) # ArgumentError
p prc.call(1, 2) # => "[1, 2]"
p prc.call(1) # => "[1, nil]" ← 不足は nil 扱い
p prc.call(1, 2, 3) # => "[1, 2]" ← 余剰は無視& でブロックを Proc に変換、または Proc/Method をブロックとして渡せる。method(:name) でメソッドを Method オブジェクトとして取り出せる。
# Proc をブロックとして渡す
double = ->(n) { n * 2 }
p [1, 2, 3].map(&double) # => [2, 4, 6]
# Symbol#to_proc を利用(&:メソッド名)
p ["hello", "world"].map(&:upcase) # => ["HELLO", "WORLD"]
p [1, 2, 3].select(&:odd?) # => [1, 3]
# method() でメソッド参照
p [1, -2, 3].map(&method(:puts))curry で Proc/lambda をカリー化する。引数を 1 つずつ渡して部分適用でき、全引数が揃った時点で評価される。
add = ->(a, b) { a + b }
add5 = add.curry.(5) # 第1引数に 5 を固定
p add5.(3) # => 8
p add5.(10) # => 15
multiply = ->(a, b, c) { a * b * c }.curry
double_then = multiply.(2).(3) # a=2, b=3 で固定
p double_then.(4) # => 24ブロック・Proc・lambda はすべてクロージャ。定義された時点の変数スコープを「キャプチャ」し、後から参照・変更できる。
def make_counter(start = 0)
count = start
inc = -> { count += 1; count }
dec = -> { count -= 1; count }
get = -> { count }
[inc, dec, get]
end
inc, dec, get = make_counter(10)
inc.()
inc.()
dec.()
p get.() # => 11 (外の count を共有している)ファイル・IO
基礎File.open にブロックを渡すとブロック終了時に自動でクローズする(ensure 不要)。File.read / File.write は一括読み書きの短縮形。
# 書き込み
File.write("hello.txt", "Hello, Ruby!\n")
# 読み取り(全体)
content = File.read("hello.txt")
puts content # => Hello, Ruby!
# 行ごとに読み取り(自動クローズ)
File.open("hello.txt") do |f|
f.each_line { |line| puts line.chomp }
end
# 追記
File.open("hello.txt", "a") { |f| f.puts "2行目" }$stdin・$stdout・$stderr はグローバルな IO オブジェクト。テストやリダイレクトで差し替えられる。STDIN は定数でリダイレクト不可。
# 標準出力への書き込み
$stdout.puts "標準出力"
$stderr.puts "エラー出力"
# StringIO でキャプチャ(テスト等)
require 'stringio'
buf = StringIO.new
$stdout = buf
puts "captured"
$stdout = STDOUT
p buf.string # => "captured\n"csv・json は標準ライブラリ(gem 不要)。CSV.foreach は大きなファイルを行ごとにストリーム処理できる。
require 'csv'
require 'json'
# JSON
data = { name: "Alice", scores: [90, 85] }
json_str = JSON.generate(data)
p JSON.parse(json_str, symbolize_names: true)
# => {name:"Alice", scores:[90,85]}
# CSV(文字列から)
csv_str = "name,age\nAlice,30\nBob,25"
CSV.parse(csv_str, headers: true) do |row|
puts "#{row['name']}: #{row['age']}"
endDir.glob でファイルパターン検索、Pathname はパスを OOP 的に操作できるラッパー。FileUtils と組み合わせてディレクトリ操作に使う。
require 'pathname'
# Dir.glob でファイル列挙
Dir.glob("**/*.rb").first(3).each { |f| puts f }
# Pathname で OOP 的なパス操作
base = Pathname.new("/tmp/myapp")
config = base / "config" / "app.yml"
p config.to_s # => "/tmp/myapp/config/app.yml"
p config.dirname # => #<Pathname:/tmp/myapp/config>
p config.extname # => ".yml"
p config.exist? # => false(ファイルがなければ)正規表現
基礎正規表現は /pattern/ リテラルで書くのが基本。i で大小文字無視、m で .が改行にもマッチ、x で空白・コメントを無視できる。
p /hello/i === "Hello World" # => true
# Regexp.new(動的パターン)
word = "ruby"
re = Regexp.new(word, Regexp::IGNORECASE)
p re === "I love Ruby" # => true
# x オプションで可読性向上
tel = /
\A # 先頭
(\d{2,4}) # 市外局番
-
(\d{2,4}) # 市内局番
-
(\d{4}) # 番号
\z # 末尾
/x
p tel.match?("03-1234-5678") # => true=~ はマッチした位置(整数)または nil を返す。match は MatchData、scan は全マッチを配列で返す。gsub はブロックで動的置換が可能。
str = "Ruby 3.3 released in 2024"
p str =~ /\d+/ # => 5(最初のマッチ位置)
p str.match(/\d+/)[0] # => "3"
p str.scan(/\d+/) # => ["3", "3", "2024"]
# gsub でブロック置換
p str.gsub(/\d+/) { |m| "(#{m})" }
# => "Ruby (3).(3) released in (2024)"() でグループを作りキャプチャする。=~ でマッチ後は $1・$2 などのグローバル変数でアクセスできる。match の戻り値からも取得可能。
if "2024-05-13" =~ /(\d{4})-(\d{2})-(\d{2})/
puts "年: #{$1}, 月: #{$2}, 日: #{$3}"
end
m = "John Doe".match(/(\w+)\s(\w+)/)
p m[0] # => "John Doe"
p m[1] # => "John"
p m[2] # => "Doe"(? で名前付きキャプチャを定義できる。インデックスより意図が明確になる。match の MatchData から m[:name] でアクセス。
pattern = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
m = "2024-05-13".match(pattern)
p m[:year] # => "2024"
p m[:month] # => "05"
p m[:day] # => "13"
p m.named_captures
# => {"year"=>"2024", "month"=>"05", "day"=>"13"}標準ライブラリ・Gem
基礎Bundler は gem の依存管理ツール。Gemfile に依存を記述し bundle install で Gemfile.lock を生成する。bundle exec でロックされたバージョンを使用。
# Gemfile
source "https://rubygems.org"
ruby "3.3.0"
gem "rails", "~> 7.1"
gem "pg", "~> 1.5"
gem "puma", "~> 6.0"
group :development, :test do
gem "rspec-rails"
gem "factory_bot_rails"
endRuby の標準的なタスクランナー。Rakefile に task を定義し rake タスク名 で実行する。namespace でタスクをグループ化できる。
# Rakefile
require 'rake'
desc "データベースをリセットする"
task :db_reset do
puts "DB をリセット中..."
end
namespace :assets do
desc "アセットをビルドする"
task :build do
puts "アセットをビルド中..."
end
end
# $ rake db_reset
# $ rake assets:build標準ライブラリの Net::HTTP で HTTP リクエストを送れる。単純な GET には Net::HTTP.get が便利。より複雑な用途には Faraday や HTTParty gem が一般的。
require 'net/http'
require 'uri'
require 'json'
uri = URI("https://api.github.com/users/ruby")
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
req = Net::HTTP::Get.new(uri)
req["Accept"] = "application/json"
http.request(req)
end
p response.code # => "200"
user = JSON.parse(response.body)
p user["public_repos"]pp は構造をわかりやすく出力するデバッグ用メソッド(print pretty)。Benchmark は実行時間計測、OptionParser はコマンドライン引数解析に使う。
require 'benchmark'
require 'optparse'
# Benchmark
Benchmark.bm(10) do |x|
x.report("Array#sum:") { (1..100_000).to_a.sum }
x.report("inject: ") { (1..100_000).inject(:+) }
end
# OptionParser
options = {}
OptionParser.new do |opts|
opts.on("-n NAME", "名前") { |v| options[:name] = v }
opts.on("-v", "--verbose") { options[:verbose] = true }
end.parse!(ARGV)
pp optionsメタプログラミング
応用method_missing は存在しないメソッドが呼ばれた際に呼び出される。必ず respond_to_missing? もオーバーライドして respond_to? の整合性を保つ。
class DynamicProxy
def initialize(target) = (@target = target)
def method_missing(name, *args, &block)
if @target.respond_to?(name)
puts "[呼び出し] #{name}(#{args.join(', ')})"
@target.send(name, *args, &block)
else
super
end
end
def respond_to_missing?(name, include_private = false)
@target.respond_to?(name) || super
end
end
proxy = DynamicProxy.new([1, 2, 3])
p proxy.length # [呼び出し] length => 3
p proxy.respond_to?(:length) # => truemethod_missing は NoMethodError をキャッチするため、タイポによるエラーが隠れやすい。可能なら define_method で明示的に定義する方が安全。define_method は実行時にメソッドを動的に定義する。class_eval はクラスのコンテキストでコードを評価、instance_eval はオブジェクトのコンテキストで評価する。
class Report
%w[pdf csv html].each do |fmt|
define_method("export_#{fmt}") do
"Exporting as #{fmt.upcase}"
end
end
end
r = Report.new
p r.export_pdf # => "Exporting as PDF"
p r.export_csv # => "Exporting as CSV"
# class_eval で既存クラスに後付け
String.class_eval do
def palindrome? = self == self.reverse
end
p "racecar".palindrome? # => truesend は private メソッドも呼べる動的ディスパッチ。public_send は public のみ。tap はデバッグや中間処理に、then(yield_self)はパイプライン的な変換に使う。
"hello".send(:upcase) # => "HELLO"
"hello".public_send(:upcase) # => "HELLO"
# tap: 値を変えずに副作用(デバッグ等)
result = [1, 2, 3]
.tap { |a| p "before: #{a}" }
.map { |n| n * 2 }
.tap { |a| p "after: #{a}" }
# then(yield_self): パイプライン変換
" 42 "
.then { |s| s.strip }
.then { |s| Integer(s) }
.then { |n| n * 2 }
.tap { |n| p n } # => 84freeze でオブジェクトを凍結(変更不可)。dup は freeze を解除したコピーを作る(シャローコピー)。clone は freeze 状態を保持したコピーを作る。
str = "hello".freeze
p str.frozen? # => true
# str << " world" # FrozenError
copy_dup = str.dup
copy_clone = str.clone
p copy_dup.frozen? # => false(freeze 解除)
p copy_clone.frozen? # => true(freeze 保持)
copy_dup << " world"
p copy_dup # => "hello world"Ruby の型とパフォーマンス
応用「アヒルのように歩き、アヒルのように鳴くなら、それはアヒルだ」。型ではなくメソッドの存在でオブジェクトを扱う。respond_to? で能力の確認が可能。
def print_length(obj)
if obj.respond_to?(:length)
puts "長さ: #{obj.length}"
else
puts "length をサポートしていません"
end
end
print_length("hello") # => 長さ: 5
print_length([1, 2, 3]) # => 長さ: 3
print_length({ a: 1 }) # => 長さ: 1
print_length(42) # => length をサポートしていませんRuby 3.0 以降は RBS(Ruby Signature)で型定義を別ファイルに記述できる。steep ツールで静的型チェックを実施する。
# person.rbs(型定義ファイル)
# class Person
# attr_reader name: String
# attr_reader age: Integer
# def initialize: (name: String, age: Integer) -> void
# def greet: () -> String
# end
# person.rb(実装)
class Person
attr_reader :name, :age
def initialize(name:, age:)
@name = name
@age = age
end
def greet = "Hello, I'm #{name}!"
end
# $ steep check で型エラーを検出ファイル先頭に # frozen_string_literal: true を付けると全文字列リテラルが自動 freeze される。文字列オブジェクトの生成が減りメモリ効率が向上する。
# frozen_string_literal: true
s = "hello"
p s.frozen? # => true
# s << " world" # FrozenError
# ミュータブルな文字列が必要なら String.new か +""
mutable = +"hello"
mutable << " world"
p mutable # => "hello world"Ruby の GC は世代別 GC(Minor: 若い世代、Major: 全世代)と Incremental GC(Ruby 2.2+)を採用。長いポーズを分割して STW(Stop-The-World)を最小化する。
# GC の統計情報
p GC.stat.slice(:count, :major_gc_count, :minor_gc_count)
# 手動 GC(通常は不要)
GC.start
# ObjectSpace で生きているオブジェクト数を確認
require 'objspace'
p ObjectSpace.count_objects.slice(:T_STRING, :T_ARRAY, :T_HASH)
# GC.compact(Ruby 3.0+)でヒープをコンパクション
GC.compactGC.compact はオブジェクトのアドレスが変わるため、C 拡張が Raw ポインタを保持していると問題が起きる場合がある。本番投入前に十分なテストを行うこと。