💎

【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"]

注意点とベストプラクティス

注意点

  1. 元のデータを保持したい場合は非破壊的メソッドを使う
# 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]
  1. freeze されたオブジェクトは変更できない
str = "hello".freeze
# str.upcase!  # FrozenError: can't modify frozen String

# freezeを確認してから操作
unless str.frozen?
  str.upcase!
end
  1. nilが返される場合がある
# 変更がない場合はnilを返す
str = "HELLO"
result = str.upcase!
puts result.inspect  #=> nil

# nilチェックが必要な場合
if result
  puts "Changed"
else
  puts "No change"
end

ベストプラクティス

  1. 破壊的メソッドは慎重に使う
# 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]
  1. メソッドチェーンで使う場合は戻り値に注意
# 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
  1. パフォーマンスが重要な場合に使う
# メモリ効率を重視する場合
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