Dynamische Klassendefinition MIT einem Klassennamen

74

Wie definiere ich dynamisch eine Klasse in Ruby MIT einem Namen?

Ich weiß, wie man eine Klasse dynamisch ohne Namen erstellt, indem man Folgendes verwendet:

dynamic_class = Class.new do
  def method1
  end
end

Sie können jedoch keinen Klassennamen angeben. Ich möchte eine Klasse dynamisch mit einem Namen erstellen .

Hier ist ein Beispiel dafür, was ich tun möchte, aber natürlich funktioniert es nicht wirklich.
(Beachten Sie, dass ich keine Instanz einer Klasse erstelle, sondern eine Klassendefinition.)

class TestEval
  def method1
    puts "name: #{self.name}"
  end
end

class_name = "TestEval"
dummy = eval("#{class_name}")

puts "dummy: #{dummy}"

dynamic_name = "TestEval2"
class_string = """
class #{dynamic_name}
  def method1
  end
end
"""
dummy2 = eval(class_string)
puts "dummy2: #{dummy2}" # doesn't work

Tatsächliche Ausgabe:

dummy: TestEval
dummy2: 

Gewünschte Ausgabe:

dummy: TestEval
dummy2: TestEval2

================================================== ====

Antwort: Eine völlig dynamische Lösung mit der Methode von sepp2k

dynamic_name = "TestEval2"

Object.const_set(dynamic_name, Class.new) # If inheriting, use Class.new( superclass )
dummy2 = eval("#{dynamic_name}")
puts "dummy2: #{dummy2}"
Joshua Pinter
quelle
1
Ich verstehe nicht wirklich, was Sie erreichen wollen. Es gibt eine Klasse TestEval2, danach können Sie test_eval2 = TestEval2.new ausführen. Und: Klasse A ... Ende ergibt immer Null, also ist deine Ausgabe in Ordnung, denke ich ;-)
Philip
Es ist für einen TDD-Testschritt. Ich muss eine Testklasse dynamisch erstellen und dann auf ihren Namen verweisen, da sie auf diese Weise in freier Wildbahn verwendet wird. sepp2K hat es richtig gemacht.
2
@Philip: class A ... endist nicht auswerten zu nil, wertet sie aus dem Wert des letzten Ausdrucks in ihm ausgewertet, genau wie jede andere Verbindung , die Expression (Blöcke, Methoden Moduldefinitionen, die Expression Gruppen) in Ruby. Es kommt einfach so vor, dass in vielen Klassendefinitionskörpern der letzte Ausdruck ein Methodendefinitionsausdruck ist, der ausgewertet wird nil. Manchmal ist es jedoch nützlich, einen Klassendefinitionskörper auf einen bestimmten Wert auswerten zu lassen, z class << self; self end. B. in der Redewendung.
Jörg W Mittag

Antworten:

135

Der Name einer Klasse ist einfach der Name der ersten Konstante, die darauf verweist.

Dh wenn ich das tue myclass = Class.newund dann MyClass = myclasswird der Name der Klasse MyClass. Ich kann es jedoch nicht tun, MyClass =wenn ich den Namen der Klasse erst zur Laufzeit kenne.

Sie können also stattdessen verwenden Module#const_set, wodurch der Wert einer Konstante dynamisch festgelegt wird. Beispiel:

dynamic_name = "ClassName"
Object.const_set(dynamic_name, Class.new { def method1() 42 end })
ClassName.new.method1 #=> 42
sepp2k
quelle
Ausgezeichnet! Vielen Dank! Genau das brauchte ich.
1
Vielen Dank. Dies hat mir hier geholfen: github.com/validates-email-format-of/validates_email_format_of/…
Isaac Betesh
3
Beeindruckend. Scheint mir sehr seltsam, dass (konstante) Zuordnung diesen Nebeneffekt hat.
Daniel Lubarov
Aus irgendeinem Grund funktioniert dies in der Entwicklung für mich, aber nicht in der Produktion
sandre89
Ich habe versucht, die Konstante für die neue Klasse selbst festzulegen, ohne Erfolg. Mit Object funktioniert es. Vielen Dank.
John Dvorak
34

Ich habe auch damit rumgespielt. In meinem Fall habe ich versucht, Erweiterungen für ActiveRecord :: Base zu testen. Ich musste in der Lage sein, eine Klasse dynamisch zu erstellen, und da der aktive Datensatz eine Tabelle basierend auf einem Klassennamen nachschlägt, konnte diese Klasse nicht anonym sein.

Ich bin mir nicht sicher, ob dies Ihrem Fall hilft, aber ich habe mir Folgendes ausgedacht:

test_model_class = Class.new(ActiveRecord::Base) do
  def self.name
    'TestModel'
  end

  attr_accessor :foo, :bar
end

Für ActiveRecord hat es gereicht, self.name zu definieren. Ich vermute, dass dies tatsächlich in allen Fällen funktioniert, in denen eine Klasse nicht anonym sein kann.

(Ich habe gerade die Antwort von sepp2k gelesen und denke, seine ist besser. Ich werde das trotzdem hier lassen.)

Will Tomlins
quelle
2
Übrigens KÖNNTEN Sie den Tabellennamen für die Klasse explizit wie folgt festlegen:self.table_name = "my_things"
Earl Jenkins
@EarlJenkins Wenn innerhalb einer solchen Klasse Assoziationen definiert sind (z. B. Gehört zu), self.nameist dies erforderlich, andernfalls schlägt Null#demodulize fehl, wenn versucht wird, der Assoziation zu folgen :( Zumindest ist mir das mit Rails 5.2.3 passiert.
mlt
3

Ich weiß, dass dies eine wirklich alte Frage ist, und einige andere Rubyisten meiden mich möglicherweise aus der Community, aber ich arbeite daran, ein sehr dünnes Wrapper-Juwel zu erstellen, das ein beliebtes Java-Projekt mit Ruby-Klassen umschließt. Basierend auf der Antwort von @ sepp2k habe ich einige Hilfsmethoden erstellt, da ich dies viele, viele Male in einem Projekt tun musste. Beachten Sie, dass ich diese Methoden mit einem Namespace versehen habe, damit sie keinen Namespace der obersten Ebene wie Object oder Kernel verschmutzen.

module Redbeam
  # helper method to create thin class wrappers easily within the given namespace
  # 
  # @param  parent_klass [Class] parent class of the klasses
  # @param  klasses [Array[String, Class]] 2D array of [class, superclass]
  #   where each class is a String name of the class to create and superclass
  #   is the class the new class will inherit from
  def self.create_klasses(parent_klass, klasses)
    parent_klass.instance_eval do
      klasses.each do |klass, superklass|
        parent_klass.const_set klass, Class.new(superklass)
      end
    end
  end

  # helper method to create thin module wrappers easily within the given namespace
  # 
  # @param parent_klass [Class] parent class of the modules
  # @param modules [Array[String, Module]] 2D array of [module, supermodule]
  #   where each module is a String name of the module to create and supermodule
  #   is the module the new module will extend
  def self.create_modules(parent_klass, modules)
    parent_klass.instance_eval do
      modules.each do |new_module, supermodule|
        parent_klass.const_set new_module, Module.new { extend supermodule }
      end
    end
  end
end

So verwenden Sie diese Methoden (beachten Sie, dass dies JRuby ist):

module Redbeam::Options
  Redbeam.create_klasses(self, [
    ['PipelineOptionsFactory', org.apache.beam.sdk.options.PipelineOptionsFactory]
  ])
  Redbeam.create_modules(self, [
    ['PipelineOptions', org.apache.beam.sdk.options.PipelineOptions]
  ])
end

WARUM??

Auf diese Weise kann ich ein JRuby-Juwel erstellen, das das Java-Projekt verwendet und es der Open Source-Community und mir ermöglicht, diese Klassen in Zukunft nach Bedarf zu dekorieren. Es wird auch ein benutzerfreundlicherer Namespace erstellt, in dem die Klassen verwendet werden können. Da mein Juwel ein sehr, sehr dünner Wrapper ist, musste ich viele, viele Unterklassen und Module erstellen, um andere Module zu erweitern.

Wie wir bei JD Power sagen, "ist dies eine entschuldigungsgetriebene Entwicklung: Es tut mir leid".

WattsInABox
quelle
1

Wie wäre es mit dem folgenden Code:

dynamic_name = "TestEval2"
class_string = """
class #{dynamic_name}
  def method1
  end
end
"""
eval(class_string)
dummy2 = Object.const_get(dynamic_name)
puts "dummy2: #{dummy2}"

Eval führt das Laufzeitklassenobjekt nicht erneut aus, zumindest auf meinem PC nicht. Verwenden Sie Object.const_get, um das Class-Objekt abzurufen.

Wuyunxie
quelle
1
Es kann jedoch auch ohne evalerreicht werden. Mit evalmüssen Sie die Eingabe bereinigen, um sich gegen die Ausführung von bösartigem Code abzusichern.
Pistos