💎
【Ruby 37日目】基本文法 - 破壊的メソッド
はじめに
Rubyの破壊的メソッド(Destructive Methods)について、Ruby 3.4の仕様に基づいて詳しく解説します。
この記事では、基本的な概念から実践的な使い方まで、具体的なコード例を交えて説明します。
基本概念
破壊的メソッドは、レシーバ(呼び出し元のオブジェクト)自身を変更するメソッドです:
-
命名規則 - メソッド名が
!で終わる(慣習的) - 元のオブジェクトを変更 - 新しいオブジェクトを作らず、既存のオブジェクトを直接変更
- selfを返す - 通常はselfを返すが、nilを返す場合もある
- パフォーマンス - オブジェクトのコピーを作らないため、メモリ効率が良い
非破壊的メソッドと破壊的メソッドを使い分けることで、効率的で安全なコードを書けます。
基本的な使い方
文字列の破壊的メソッド
# 非破壊的メソッド(元の文字列は変更されない)
str = "hello"
result = str.upcase
puts result #=> HELLO
puts str #=> hello(元の文字列はそのまま)
# 破壊的メソッド(元の文字列が変更される)
str = "hello"
result = str.upcase!
puts result #=> HELLO
puts str #=> HELLO(元の文字列が変更されている)
配列の破壊的メソッド
# 非破壊的
arr = [3, 1, 2]
sorted = arr.sort
puts sorted.inspect #=> [1, 2, 3]
puts arr.inspect #=> [3, 1, 2]
# 破壊的
arr = [3, 1, 2]
arr.sort!
puts arr.inspect #=> [1, 2, 3]
よく使われる破壊的メソッド
str = " hello world "
# gsub! - 文字列の置換
str.gsub!("world", "ruby")
puts str #=> " hello ruby "
# strip! - 前後の空白削除
str.strip!
puts str #=> "hello ruby"
# upcase! - 大文字化
str.upcase!
puts str #=> "HELLO RUBY"
# 配列の操作
arr = [1, 2, 3, 4, 5]
# delete! - 要素の削除
arr.delete(3)
puts arr.inspect #=> [1, 2, 4, 5]
# compact! - nilの削除
arr2 = [1, nil, 2, nil, 3]
arr2.compact!
puts arr2.inspect #=> [1, 2, 3]
# uniq! - 重複の削除
arr3 = [1, 2, 2, 3, 3, 3]
arr3.uniq!
puts arr3.inspect #=> [1, 2, 3]
変更がない場合の戻り値
# 変更がない場合はnilを返す
str = "HELLO"
result = str.upcase!
puts result.inspect #=> nil(既に大文字なので変更なし)
# 変更があった場合はselfを返す
str = "hello"
result = str.upcase!
puts result.inspect #=> "HELLO"
ハッシュの破壊的メソッド
hash = { a: 1, b: 2, c: 3 }
# delete - キーと値の削除
hash.delete(:b)
puts hash.inspect #=> {:a=>1, :c=>3}
# keep_if - 条件に合うものだけ残す
hash.keep_if { |k, v| v > 1 }
puts hash.inspect #=> {:c=>3}
# merge! - ハッシュのマージ
hash.merge!({ d: 4, e: 5 })
puts hash.inspect #=> {:c=>3, :d=>4, :e=>5}
よくあるユースケース
ケース1: 大量データの効率的な処理
メモリを節約するために破壊的メソッドを使用します。
class DataProcessor
def normalize_data(items)
# 破壊的メソッドでメモリを節約
items.each do |item|
item[:name]&.strip!
item[:name]&.downcase!
item[:description]&.gsub!(/\s+/, " ")
item[:description]&.strip!
end
items.compact!
items.uniq!
items
end
end
processor = DataProcessor.new
data = [
{ name: " PRODUCT A ", description: " Great product " },
{ name: " product b ", description: " Nice item " },
{ name: " PRODUCT A ", description: " Great product " },
nil
]
result = processor.normalize_data(data)
puts result.inspect
#=> [{:name=>"product a", :description=>"Great product"}, {:name=>"product b", :description=>"Nice item"}]
ケース2: テキストのクリーンアップ
複数の破壊的操作を連続して適用します。
class TextCleaner
def clean!(text)
return if text.nil?
# 連続して破壊的メソッドを適用
text.strip!
text.gsub!(/\s+/, " ") # 複数の空白を1つに
text.gsub!(/[^\w\s]/, "") # 特殊文字を削除
text.downcase!
text
end
def clean_multiple!(texts)
texts.each { |text| clean!(text) }
texts.compact!
texts
end
end
cleaner = TextCleaner.new
text = " Hello, WORLD!!! How are you? "
cleaner.clean!(text)
puts text #=> "hello world how are you"
texts = [
" TEXT 1!! ",
" TEXT 2?? ",
nil,
" TEXT 3... "
]
cleaner.clean_multiple!(texts)
puts texts.inspect
#=> ["text 1", "text 2", "text 3"]
ケース3: 配列の条件付きフィルタリング
条件に合わない要素を削除します。
class TaskManager
def initialize(tasks)
@tasks = tasks
end
def remove_completed!
@tasks.delete_if { |task| task[:completed] }
self
end
def remove_low_priority!
@tasks.delete_if { |task| task[:priority] < 3 }
self
end
def remove_old_tasks!(days)
cutoff = Time.now - (days * 24 * 60 * 60)
@tasks.delete_if { |task| task[:created_at] < cutoff }
self
end
def tasks
@tasks
end
end
tasks = [
{ id: 1, title: "Task 1", priority: 5, completed: false, created_at: Time.now - 100000 },
{ id: 2, title: "Task 2", priority: 2, completed: true, created_at: Time.now },
{ id: 3, title: "Task 3", priority: 4, completed: false, created_at: Time.now },
]
manager = TaskManager.new(tasks)
manager.remove_completed!.remove_low_priority!
puts manager.tasks.inspect
#=> [{:id=>3, :title=>"Task 3", :priority=>4, :completed=>false, :created_at=>...}]
ケース4: ハッシュの設定管理
設定の更新や正規化を行います。
class Configuration
def initialize(config = {})
@config = config
end
def set!(key, value)
@config[key] = value
self
end
def merge!(other_config)
@config.merge!(other_config)
self
end
def normalize_keys!
# キーをシンボルに統一
@config.transform_keys! { |k| k.to_sym }
self
end
def remove_nil_values!
@config.delete_if { |k, v| v.nil? }
self
end
def apply_defaults!(defaults)
defaults.each do |key, value|
@config[key] ||= value
end
self
end
def config
@config
end
end
config = Configuration.new({
"host" => "localhost",
"port" => 3000,
"timeout" => nil
})
defaults = { timeout: 30, retry_count: 3 }
config
.normalize_keys!
.remove_nil_values!
.apply_defaults!(defaults)
puts config.config.inspect
#=> {:host=>"localhost", :port=>3000, :timeout=>30, :retry_count=>3}
ケース5: インプレース変換
リスト内の各要素を変換します。
class ListTransformer
def uppercase_all!(items)
items.map!(&:upcase)
end
def apply_discount!(products, discount_rate)
products.each do |product|
product[:price] = (product[:price] * (1 - discount_rate)).round(2)
end
products
end
def normalize_urls!(urls)
urls.map! do |url|
url.strip!
url.downcase!
url.gsub!(/^https?:\/\//, "")
url.gsub!(/\/$/, "")
url
end
urls.compact!
urls.uniq!
urls
end
end
transformer = ListTransformer.new
# 大文字変換
names = ["alice", "bob", "charlie"]
transformer.uppercase_all!(names)
puts names.inspect #=> ["ALICE", "BOB", "CHARLIE"]
# 価格の割引適用
products = [
{ name: "Product A", price: 100.0 },
{ name: "Product B", price: 200.0 }
]
transformer.apply_discount!(products, 0.1)
puts products.inspect
#=> [{:name=>"Product A", :price=>90.0}, {:name=>"Product B", :price=>180.0}]
# URL正規化
urls = [
" HTTPS://Example.com/ ",
" http://example.com ",
" https://another.com/ "
]
transformer.normalize_urls!(urls)
puts urls.inspect
#=> ["example.com", "another.com"]
注意点とベストプラクティス
注意点
- 元のデータを保持したい場合は非破壊的メソッドを使う
# BAD: 元のデータが必要なのに破壊的メソッドを使用
original = [3, 1, 2]
sorted = original.sort!
# originalも変更されてしまう
# GOOD: 非破壊的メソッドを使用
original = [3, 1, 2]
sorted = original.sort
puts original.inspect #=> [3, 1, 2]
puts sorted.inspect #=> [1, 2, 3]
- freeze されたオブジェクトは変更できない
str = "hello".freeze
# str.upcase! # FrozenError: can't modify frozen String
# freezeを確認してから操作
unless str.frozen?
str.upcase!
end
- nilが返される場合がある
# 変更がない場合はnilを返す
str = "HELLO"
result = str.upcase!
puts result.inspect #=> nil
# nilチェックが必要な場合
if result
puts "Changed"
else
puts "No change"
end
ベストプラクティス
- 破壊的メソッドは慎重に使う
# GOOD: 意図が明確な場合に使用
def process_items(items)
# 明示的にコピーを作成
working_copy = items.dup
working_copy.sort!
working_copy.uniq!
working_copy
end
# 元の配列は変更されない
original = [3, 1, 2, 1]
result = process_items(original)
puts original.inspect #=> [3, 1, 2, 1]
puts result.inspect #=> [1, 2, 3]
- メソッドチェーンで使う場合は戻り値に注意
# BAD: nilが返される可能性がある
text = "HELLO"
# result = text.upcase!.reverse! # NoMethodError (upcase!がnilを返す)
# GOOD: 非破壊的メソッドを使用
text = "hello"
result = text.upcase.reverse
puts result #=> OLLEH
# ALSO GOOD: 破壊的メソッドを個別に呼ぶ
text = "hello"
text.upcase!
text.reverse!
puts text #=> OLLEH
- パフォーマンスが重要な場合に使う
# メモリ効率を重視する場合
def process_large_dataset(items)
items.map! { |item| expensive_transformation(item) }
items.select! { |item| item[:valid] }
items.sort_by! { |item| item[:score] }
items
end
def expensive_transformation(item)
# 重い処理
item.merge(processed: true, valid: item[:score] > 50)
end
Ruby 3.4での改善点
- Prismパーサーによる最適化 - 破壊的メソッドの解析が高速化
- YJITの最適化 - 破壊的メソッドのパフォーマンスが向上
- Frozen String Literalsのデフォルト化 - 文字列リテラルがデフォルトでfreezeされるため、破壊的メソッドの使用時に注意が必要
- メモリ管理の改善 - GCの最適化により、破壊的メソッドのメモリ効率がさらに向上
# Ruby 3.4ではfrozen string literalsがデフォルト
# "hello".upcase! # FrozenError
# 変更可能な文字列を作成する場合
str = +"hello" # +プレフィックスで変更可能に
str.upcase!
puts str #=> HELLO
まとめ
この記事では、破壊的メソッドについて以下の内容を学びました:
- 基本概念と重要性 -
!で終わる命名規則、元のオブジェクトの変更 - 基本的な使い方と構文 - 文字列、配列、ハッシュの破壊的メソッド
- 実践的なユースケース - データ処理、テキストクリーンアップ、フィルタリング、設定管理、インプレース変換
- 注意点とベストプラクティス - freezeの確認、nilの扱い、パフォーマンスとの兼ね合い
破壊的メソッドを適切に使用することで、メモリ効率の良いコードを書くことができます。
Discussion