Wie überprüfe ich, ob eine Klasse definiert ist?

74

Wie verwandle ich eine Zeichenfolge in einen Klassennamen, aber nur, wenn diese Klasse bereits vorhanden ist?

Wenn Amber bereits eine Klasse ist, kann ich von einem String zur Klasse gelangen über:

Object.const_get("Amber")

oder (in Schienen)

"Amber".constantize

Aber beides wird scheitern, NameError: uninitialized constant Amberwenn Amber noch keine Klasse ist.

Mein erster Gedanke ist, die defined?Methode zu verwenden, aber sie unterscheidet nicht zwischen bereits existierenden und nicht existierenden Klassen:

>> defined?("Object".constantize)
=> "method"
>> defined?("AClassNameThatCouldNotPossiblyExist".constantize)
=> "method"

Wie teste ich, ob eine Zeichenfolge eine Klasse benennt, bevor ich versuche, sie zu konvertieren? (Okay, wie wäre es mit einem begin/ rescueBlock, um NameError-Fehler abzufangen? Zu hässlich? Ich stimme zu ...)

furchtloser Dummkopf
quelle
2
defined?Im Beispiel wird genau das getan, was es tun soll: Es wird überprüft, ob die constantizeMethode für ein String-Objekt definiert ist. Es ist egal, ob die Zeichenfolge "Object" oder "AClassNameThatCouldNotPossiblyExist" enthält.
ToniTornado

Antworten:

129

Wie wäre es const_defined??

Denken Sie daran, dass in Rails das automatische Laden im Entwicklungsmodus erfolgt. Daher kann es beim Testen schwierig sein:

>> Object.const_defined?('Account')
=> false
>> Account
=> Account(id: integer, username: string, google_api_key: string, created_at: datetime, updated_at: datetime, is_active: boolean, randomize_search_results: boolean, contact_url: string, hide_featured_results: boolean, paginate_search_results: boolean)
>> Object.const_defined?('Account')
=> true
ctcherry
quelle
2
perfekt danke. Für den Auto-Loader IIRC gibt es eine Möglichkeit, herauszufinden, was auf der Autoloader-Liste steht. Ich werde das ausgraben, wenn es sich als Problem herausstellt.
fearless_fool
4
Dies entspricht jedoch auch Dingen, die keine Klassen sind.
Mahemoff
19

In Schienen ist es wirklich einfach:

amber = "Amber".constantize rescue nil
if amber # nil result in false
    # your code here
end
Eiji
quelle
Das rescuewar hilfreich, weil manchmal Konstanten entladen werden können und die Überprüfung mit const_defined?falsch ist.
Spencer
1
Das Unterdrücken von Ausnahmen wird nicht empfohlen. Lesen Sie hier mehr: github.com/bbatsov/ruby-style-guide#dont-hide-exceptions
Andrew K
@ AndrewK: In Ruby wird Rettung sehr oft eingesetzt - ofc Ich stimme zu, dass es nicht gut ist; In der Elixier-Welt versuchen wir, dies nicht zu tun, wenn dies nicht erforderlich ist, aber ich habe gesehen, dass viele Menschen in Ruby
Eiji
1
@ Eiji, vereinbart. Ich wollte es nur erwähnen, da Leute, die neu bei Ruby sind, nicht wissen, dass es ein Anti-Muster ist und vermieden werden sollte.
Andrew K
13

Inspiriert von der obigen Antwort von @ ctcherry, ist hier eine 'sichere Klassenmethode send', bei der class_namees sich um eine Zeichenfolge handelt. Wenn class_namekeine Klasse benannt wird, wird null zurückgegeben.

def class_send(class_name, method, *args)
  Object.const_defined?(class_name) ? Object.const_get(class_name).send(method, *args) : nil
end

Eine noch sicherere Version, die methodnur aufruft , wenn sie darauf class_namereagiert:

def class_send(class_name, method, *args)
  return nil unless Object.const_defined?(class_name)
  c = Object.const_get(class_name)
  c.respond_to?(method) ? c.send(method, *args) : nil
end
furchtloser Dummkopf
quelle
2
ps: Wenn dir diese Antwort gefällt, stimme bitte der Antwort von ctcherry zu, da dies mich in die richtige Richtung gelenkt hat.
fearless_fool
5

Es scheint, dass alle Antworten, die die Object.const_defined?Methode verwenden, fehlerhaft sind. Wenn die betreffende Klasse aufgrund des verzögerten Ladens noch nicht geladen wurde, schlägt die Zusicherung fehl. Der einzige Weg, dies definitiv zu erreichen, ist folgender:

  validate :adapter_exists

  def adapter_exists
    # cannot use const_defined because of lazy loading it seems
    Object.const_get("Irs::#{adapter_name}")
  rescue NameError => e
    errors.add(:adapter_name, 'does not have an IrsAdapter')
  end
TomDunning
quelle
2

Ich habe einen Validator erstellt, um zu testen, ob eine Zeichenfolge ein gültiger Klassenname (oder eine durch Kommas getrennte Liste gültiger Klassennamen) ist:

class ClassValidator < ActiveModel::EachValidator
  def validate_each(record,attribute,value)
    unless value.split(',').map { |s| s.strip.constantize.is_a?(Class) rescue false }.all?
      record.errors.add attribute, 'must be a valid Ruby class name (comma-separated list allowed)'
    end
  end
end
Fred Willmore
quelle
1

Ein anderer Ansatz, falls Sie die Klasse auch erhalten möchten. Gibt null zurück, wenn die Klasse nicht definiert ist, sodass Sie keine Ausnahme abfangen müssen.

class String
  def to_class(class_name)
    begin
      class_name = class_name.classify (optional bonus feature if using Rails)
      Object.const_get(class_name)
    rescue
      # swallow as we want to return nil
    end
  end
end

> 'Article'.to_class
class Article

> 'NoSuchThing'.to_class
nil

# use it to check if defined
> puts 'Hello yes this is class' if 'Article'.to_class
Hello yes this is class
Mahemoff
quelle