Comment puis-je 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?
méthode, mais elle ne fait pas de distinction entre les classes qui existent déjà et celles qui ne le font pas:
>> defined?("Object".constantize) => "method" >> defined?("AClassNameThatCouldNotPossiblyExist".constantize) => "method"
Alors, comment tester si une chaîne désigne une classe avant d’essayer de la convertir? (Ok, que diriez-vous d’un bloc begin
/ rescue
pour détecter les erreurs NameError? Trop moche? Je suis d’accord …)
Qu’en est-il de const_defined?
?
Rappelez-vous dans Rails, il y a un chargement automatique en mode développement, il peut donc être difficile de le tester:
>> Object.const_defined?('Account') => false >> Account => Account(id: integer, username: ssortingng, google_api_key: ssortingng, created_at: datetime, updated_at: datetime, is_active: boolean, randomize_search_results: boolean, contact_url: ssortingng, hide_featured_results: boolean, paginate_search_results: boolean) >> Object.const_defined?('Account') => true
Dans les rails, c’est vraiment facile:
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écurisée send», où class_name
est une chaîne. Si class_name
ne class_name
pas une 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 la method
uniquement si class_name
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 Object.const_defined?
la méthode est imparfaite. Si la classe en question n’a pas encore été chargée en raison d’un chargement différé, l’assertion échoue. 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
Une autre approche, au cas où vous voudriez obtenir la classe aussi. Renvoie nil si la classe n’est pas définie, vous n’avez donc pas à intercepter une exception.
class Ssortingng 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
J’ai créé un validateur pour tester si une chaîne est un nom de classe valide (ou une liste de noms de classes 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