Refinements

Aufgrund von Rubys offenen Klassen können Sie Funktionalitäten bestehender Klassen neu definieren oder hinzufügen. Dies wird als „Monkey Patch“ bezeichnet. Unglücklicherweise ist der Geltungsbereich solcher Änderungen global. Alle Benutzer der Monkey-Patched-Klasse sehen dieselben Änderungen. Dies kann zu unbeabsichtigten Nebeneffekten oder Programmabbrüchen führen.

Refinements wurden entwickelt, um die Auswirkungen von Monkey Patching auf andere Benutzer der Monkey-Patched-Klasse zu reduzieren. Refinements bieten eine Möglichkeit, eine Klasse lokal zu erweitern. Refinements können sowohl Klassen als auch Module ändern.

Hier ist ein grundlegendes Refinement

class C
  def foo
    puts "C#foo"
  end
end

module M
  refine C do
    def foo
      puts "C#foo in M"
    end
  end
end

Zuerst wird eine Klasse C definiert. Als Nächstes wird ein Refinement für C unter Verwendung von Module#refine erstellt.

Module#refine erstellt ein anonymes Modul, das die Änderungen oder Refinements an der Klasse (C im Beispiel) enthält. self im Refinement-Block ist dieses anonyme Modul, ähnlich wie bei Module#module_eval.

Aktivieren Sie das Refinement mit using

using M

c = C.new

c.foo # prints "C#foo in M"

Scope

Sie können Refinements auf oberster Ebene sowie innerhalb von Klassen und Modulen aktivieren. Sie können Refinements nicht im Methodengeltungsbereich aktivieren. Refinements sind bis zum Ende der aktuellen Klassen- oder Moduldefinition oder bis zum Ende der aktuellen Datei, wenn sie auf oberster Ebene verwendet werden, aktiv.

Sie können Refinements in einem String aktivieren, der an Kernel#eval übergeben wird. Refinements sind bis zum Ende des Eval-Strings aktiv.

Refinements sind lexikalisch im Geltungsbereich. Refinements sind nur innerhalb eines Geltungsbereichs nach dem Aufruf von using aktiv. Code vor der using-Anweisung hat das Refinement nicht aktiviert.

Wenn die Kontrolle außerhalb des Geltungsbereichs übertragen wird, wird das Refinement deaktiviert. Das bedeutet, dass, wenn Sie eine Datei requiren oder laden oder eine Methode aufrufen, die außerhalb des aktuellen Geltungsbereichs definiert ist, das Refinement deaktiviert wird.

class C
end

module M
  refine C do
    def foo
      puts "C#foo in M"
    end
  end
end

def call_foo(x)
  x.foo
end

using M

x = C.new
x.foo       # prints "C#foo in M"
call_foo(x) #=> raises NoMethodError

Wenn eine Methode in einem Geltungsbereich definiert ist, in dem ein Refinement aktiv ist, ist das Refinement aktiv, wenn die Methode aufgerufen wird. Dieses Beispiel erstreckt sich über mehrere Dateien.

c.rb

class C
end

m.rb

require "c"

module M
  refine C do
    def foo
      puts "C#foo in M"
    end
  end
end

m_user.rb

require "m"

using M

class MUser
  def call_foo(x)
    x.foo
  end
end

main.rb

require "m_user"

x = C.new
m_user = MUser.new
m_user.call_foo(x) # prints "C#foo in M"
x.foo              #=> raises NoMethodError

Da das Refinement M in m_user.rb aktiv ist, wo MUser#call_foo definiert ist, ist es auch aktiv, wenn main.rb call_foo aufruft.

Da using eine Methode ist, sind Refinements nur aktiv, wenn sie aufgerufen wird. Hier sind Beispiele dafür, wo ein Refinement M aktiv ist und wo nicht.

In einer Datei

# not activated here
using M
# activated here
class Foo
  # activated here
  def foo
    # activated here
  end
  # activated here
end
# activated here

In einer Klasse

# not activated here
class Foo
  # not activated here
  def foo
    # not activated here
  end
  using M
  # activated here
  def bar
    # activated here
  end
  # activated here
end
# not activated here

Beachten Sie, dass die Refinements in M **nicht** automatisch aktiviert werden, wenn die Klasse Foo später wieder geöffnet wird.

In eval

# not activated here
eval <<EOF
  # not activated here
  using M
  # activated here
EOF
# not activated here

Wenn nicht evaluiert

# not activated here
if false
  using M
end
# not activated here

Wenn mehrere Refinements im selben Modul innerhalb mehrerer refine-Blöcke definiert werden, sind alle Refinements desselben Moduls aktiv, wenn eine verfeinerte Methode (eine der unten aufgeführten to_json-Methoden) aufgerufen wird.

module ToJSON
  refine Integer do
    def to_json
      to_s
    end
  end

  refine Array do
    def to_json
      "[" + map { |i| i.to_json }.join(",") + "]"
    end
  end

  refine Hash do
    def to_json
      "{" + map { |k, v| k.to_s.dump + ":" + v.to_json }.join(",") + "}"
    end
  end
end

using ToJSON

p [{1=>2}, {3=>4}].to_json # prints "[{\"1\":2},{\"3\":4}]"

Methoden-Lookup

Beim Suchen nach einer Methode für eine Instanz der Klasse C prüft Ruby

Wenn an keiner Stelle eine Methode gefunden wurde, wiederholt sich dies mit der Oberklasse von C.

Beachten Sie, dass Methoden in einer Unterklasse Vorrang vor Refinements in einer Oberklasse haben. Wenn beispielsweise die Methode / in einem Refinement für Numeric definiert ist, ruft 1 / 2 die ursprüngliche Integer#/ auf, da Integer eine Unterklasse von Numeric ist und vor den Refinements für die Oberklasse Numeric durchsucht wird. Da die Methode / auch in der Kindklasse Integer vorhanden ist, bewegt sich der Methoden-Lookup nicht zur Oberklasse nach oben.

Wenn jedoch eine Methode foo auf Numeric in einem Refinement definiert ist, ruft 1.foo diese Methode auf, da foo nicht in Integer existiert.

super

Wenn super aufgerufen wird, prüft der Methoden-Lookup

Beachten Sie, dass super in einer Methode eines Refinements die Methode in der verfeinerten Klasse aufruft, auch wenn es ein weiteres Refinement gibt, das im selben Kontext aktiviert wurde. Dies gilt nur für super in einer Methode eines Refinements; es gilt nicht für super in einer Methode eines Moduls, das in ein Refinement aufgenommen wurde.

Methoden-Introspektion

Bei der Verwendung von Introspektionsmethoden wie Kernel#method oder Kernel#methods werden Refinements nicht berücksichtigt.

Dieses Verhalten kann in Zukunft geändert werden.

Refinement-Vererbung durch Module#include

Wenn ein Modul X in ein Modul Y aufgenommen wird, erbt Y Refinements von X.

Zum Beispiel erbt C Refinements von A und B im folgenden Code

module A
  refine X do ... end
  refine Y do ... end
end
module B
  refine Z do ... end
end
module C
  include A
  include B
end

using C
# Refinements in A and B are activated here.

Refinements in Nachkommen haben eine höhere Priorität als die von Vorfahren.

Weitere Lektüre

Siehe github.com/ruby/ruby/wiki/Refinements-Spec für die aktuelle Spezifikation zur Implementierung von Refinements. Die Spezifikation enthält auch weitere Details.