Comment transformer une chaîne en nom de classe, mais uniquement si cette classe existe déjà?
Si Amber est déjà une classe, je peux passer d'une chaîne à la classe via:
Object.const_get("Amber")
ou (en Rails)
"Amber".constantize
Mais l'un ou l'autre échouera avec NameError: uninitialized constant Amber
si Amber n'est pas déjà une classe.
Ma première pensée est d'utiliser le defined?
, mais elle ne fait pas de distinction entre les classes qui existent déjà et celles qui n'existent pas:
>> defined?("Object".constantize)
=> "method"
>> defined?("AClassNameThatCouldNotPossiblyExist".constantize)
=> "method"
Alors, comment puis-je tester si une chaîne nomme une classe avant d'essayer de la convertir? (D'accord, que diriez-vous d'un bloc begin
/rescue
pour détecter les erreurs NameError? Trop moche? Je suis d'accord ...)
Que diriez-vous const_defined?
?
Rappelez-vous que dans Rails, il y a un chargement automatique en mode développement, il peut donc être difficile lorsque vous le testez:
>> 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
En Rails c'est très simple:
amber = "Amber".constantize rescue nil
if amber # nil result in false
# your code here
end
Inspiré par la réponse de @ ctcherry ci-dessus, voici une 'méthode de classe sûre envoyer', où class_name
est une chaîne. Si class_name
ne nomme pas de classe, il renvoie nil.
def class_send(class_name, method, *args)
Object.const_defined?(class_name) ? Object.const_get(class_name).send(method, *args) : nil
end
Une version encore plus sûre qui invoque method
uniquement si class_name
y répond:
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
Il semblerait que toutes les réponses utilisant le Object.const_defined?
la méthode est défectueuse. Si la classe en question n'a pas encore été chargée, en raison d'un chargement paresseux, l'assertion échouera. La seule façon d'y parvenir définitivement est la suivante:
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
J'ai créé un validateur pour tester si une chaîne est un nom de classe valide (ou une liste de noms de classe valides séparés par des virgules):
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
Une autre approche, au cas où vous souhaiteriez également obtenir le cours. Renvoie nil si la classe n'est pas définie, vous n'avez donc pas à intercepter une exception.
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