2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rubyのsortと宇宙船演算子(<=>)の裏側をC言語のソースコードから読み解く

2
Last updated at Posted at 2026-04-25

はじめに

Rubyで独自に定義したクラスのインスタンスを配列に入れてsortしようとしたとき、
ArgumentError: comparison of ... failedというエラーに遭遇したことはありませんか?

例えば、サポートチケットを管理するシステムを実装していて、チケットの「緊急度」で並べ変えたいケースを考えてみます。本記事では、このエラーが「なぜ起きるのか」、そして「どう解決するのか」を、Rubyの本体であるC言語のソースコード(Cruby)の内部挙動まで潜って解説します。

デフォルト状態でのソート

まずは、以下のような新しく作ったばかりのSupportTicketインスタンスを配列に入れてソートしてみます。

class SuportTicket
  attr_reader :id, :urgency
  
  def initialize(id, urgency)
    @id = id
    @urgency = urgency # 緊急度(数値が大きいほど優先)
  end
end

tickets = [
  SupportTicket.new(1, 50),
  SupportTicket.new(2, 100)
]

# さあ、並び替えてくれ
tickets.sort

これを実行すると、無情にも以下のエラーが発生します。

ArgumentError: comparison of SupportTicket with SupportTicket failed

なぜ、ただのソートでエラーになるのでしょうか?この時、C言語レベルでは以下のルートを辿ってクラッシュしています。

C言語の内部で起きていること(失敗ルート)

  1. 現場監督(array.c)が起動
    Array#sortの実体はC言語のarray.cにあります。クイックソートなどのアルゴリズムが起動し、要素A(1)と要素B(2)を掴みます。
  2. C言語からRubyへ問い合わせ
      C言語自身はRubyオブジェクトの大小を知らないため、rb_funcallというC言語の関数を使って、Rubyの世界の<=>(宇宙船演算子)を呼び出し、「どっちが大きいの?」と聞きます。
  3. Object#<=>に行き着く
      しかし、SupportTicketクラスには<=>が定義されていません。Rubyは親クラスを辿り、最終的にすべてのオブジェクトの頂点であるObjectクラスのデフォルト実装に到達します。

このObject#<=>の実装は、C言語のobject_cに以下のように定義されています。

static VALUE rb_obj_cmp(VALUE obj1, VALUE obj2)
{
    if (rb_equal(obj1, obj2)) // 全く同じメモリ上のインスタンスか?
        return INT2FIX(0);

    return Qnil; // 違うなら nil を返す
}

ここで使われているrb_equalは、Ruby側の==メソッドを呼び出す関数です。
今回、SupportTicketクラスには==メソッドを定義していません。そのため、親クラスであるBasicObject#==(デフォルトではオブジェクトの同一性)、つまり、「全く同じオブジェクト(オブジェクトIDが一致する)か?」 を判定する)が呼び出されます。
SupportTicket.newで作られた2つのチケットは、新しく生まれた別々のインスタンス(メモリの住所が違う)です。そのため、==の結果はfalseになり、結果としてQnil(nil)が返却されます。
これを受け取ったC言語の現場監督(array.c)は、「-1, 0, 1の数値が返ってこないと並べ替えられない!」とパニックになり、ArgumentErrorを吐いて終了してしまうのです。

補足 :もし、==をオーバーライドしていたら?

仮にSupportTicketクラスでdef ==(other); true; endのように常にtrueを返すようにオーバーライドしていた場合、rb_equalは常に真となり、<=>が未定義でも0が返るため、なんとエラーにならずにsortを完走することができます。

補足2 : Array.new(100, C.new) だとエラーにならない理由

上記を踏まえると、面白い挙動が確認できます。空っぽのクラス C を用意して配列を作った場合、配列の作り方によって sort の結果が変わります。

class C; end

# ① ブロックで生成した場合 ➔ エラーになる💥
Array.new(100) { C.new }.sort

# ② 第二引数で生成した場合 ➔ エラーにならない✨
Array.new(100, C.new).sort

①はブロックが100回実行されるため「100個の別々のインスタンス」が生成されます。そのため BasicObject#==false を返し、比較不能でエラーになります。 しかし、②は「1回だけ生成した同一インスタンス」を100個並べた配列になります。すべてが全く同じオブジェクト(オブジェクトIDが一致する)であるため、デフォルトの BasicObject#== の判定が true となり、<=> が未定義でも見事に sort を完走するのです。

書き換え : Ruby側で<=>をオーバーライド

新しいインスタンス同士を比較するには、「チケットの緊急度(urgency)で比べる」というルールを教えてあげる必要があります。

class SupportTicket
  attr_reader :id, :urgency
  
  def initialize(id, urgency)
    @id = id
    @urgency = urgency
  end
  
  # Rubyの世界でルールを上書き(オーバーライド)する!
  def <=>(other)
    self.urgency <=> other.urgency
  end
end

tickets = [
  SupportTicket.new(1, 50)
  SupportTicket.new(2, 100)
]

tickets.sort
#=> [#<SupportTicket @id=1, @urgency=50>, #<SupportTicket @id=2, @urgency=100>] # 今度は成功する!

C言語の内部で起きていること(成功ルート)

ルールを追加したことで、内部の挙動は劇的に変わります。

  1. 現場監督(array.c)が<=>を呼び出す
      先ほどと同じく、C言語から要素へ問い合わせが発生します。
  2. オーバーライドしたメソッドが反応
      今度はSupportTicketクラスに<=>があるため、親のObjectまで行かず、先ほど定義したRubyのコードが実行されます。
  3. Integer#<=>への委譲
      self.urgency(50)とother.urgency(100)が取り出されます。これは数値同士の比較になるため、今度はIntegerクラスの<=>(C言語のnumeric.c)にバトンタッチされます。
  4. ソート完了!
      C言語のnumeric.cが「50と100なら、100の方が大きい」と判断し、-1を返します。現場監督(array.c)がこの-1を受け取り、無事に並び替えが完了します。

まとめ

新しく new したばかりのオブジェクトは、メモリ上の住所が完全に異なるため、デフォルトの Object#<=> に任せると 「自分自身と全く同じインスタンスでない限り、比較不能(nil)」 という判定を下されます。(厳密にはBasicObject#==による判定)。

だからこそ、独自のオブジェクトを並べ替えるときは、必ず 「インスタンスの中のどのプロパティ(文字や数値)を、Ruby標準クラス(StringやInteger)に委譲するか」<=> を用いて明示的に書く必要があります。

  1. C言語(配列のソートアルゴリズム)

  2. Ruby(自作の <=> によるビジネスルール)

  3. C言語(Integerなどの標準クラスによる比較)

Rubyの sort は、言語の壁を行ったり来たりしながら、美しいコンテキストスイッチとオブジェクト指向のメッセージングによって成り立っています。 この記事が、Rubyの内部挙動を理解する一助になれば幸いです!

参考文献

2
2
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?