const_defined? vs. defined? in Ruby

Viewed 1027

To test, whether a constant (say: a class) is known at a certain point in the code, I can write for instance:

if defined? :String

or I can write

if self.class.const_defined? :String

Is there a situation where I these two ways of testing would make a difference? Note that I don't ask about the case where I have an explicit receiver, such as MyModule.const_defined? :Something, but only for the case where I want to test whether a certain constant (which in my case happens to be a constant denoting a class) is already defined.

5 Answers

First things first, defined? is a keyword which behaves a bit similar similar to a method. It receives the name of the thing (variable, constant, ...) to check. What makes this method different from all others is that the parser will not resolve the value of the given name but rather check directly for whether it is defined (hence the keyword property). To check if a constant is defined, you thus have to pass the actual name (rather than a Symbol):

if defined?(String)

The const_defined? on the oither hand is more regular. It expects a Symbol or String with the name of a constant and checks whether it is defined on the receiver.

Now as for the differences between the two (when used correctly): if you use them both within the context of an instance method to check for the existence of a constant, they work the same.

When running e.g. in a class definition (such that self is e.g. a class), you need to make sure to use the correct receiver for your const_defined method, e.g. if self.const_defined? :String.

Also, defined? can check for a lot more than just constants (e.g. methods, expressions, variables, ...)

If you want to use this to make sure you actually have the name of a constant at hand in a given variable, you need to use const_defined?. If you want to "statically" check whether an constant was defined, you can use defined?.

defined? is a keyword that will check if an expression exists in the current scope.

const_defined? is a method that will check if a constant exists through the ancestor chain of the receiver.

planet = "Earth"

class Experiment
  def diff
    ""
  end

  def show
    puts "defined" if defined?(diff)
    puts "Earth not found" if !defined?(planet)
    puts "String constant defined" if self.class.const_defined?(:String)
  end
end

Experiment.new.show
#=> defined
#=> Earth not found
#=> String constant defined

p Experiment.ancestors #=> [Experiment, Object, Kernel, BasicObject]
p String.superclass #=> Object

Here's an example of situations where this will make a difference:

Using defined?(Nothing's printed)

class Lab
  class Coco
  end
end

class Experiment

  def diff
    ""
  end

  def show
    puts "defined" if defined?(Coco) #=> Nothing printed
  end
end

Experiment.new.show

Using self.class.const_defined? (Something's printed)

class Lab
  class Coco
  end
end

class Experiment < Lab

  def diff
    ""
  end

  def show
    puts "defined" if self.class.const_defined? :Coco #=> defined
  end
end

Experiment.new.show
p Experiment.ancestors #=> [Experiment, Lab, Object, Kernel, BasicObject] We find 'Lab' class in the ancestor chain.

To test, whether a constant (say: a class) is known at a certain point in the code, I can write for instance:

if defined? :String

or I can write

if self.class.const_defined? :String

Is there a situation where I these two ways of testing would make a difference?

These two really do two completely different things. The first tests whether the Symbol literal :String is defined. Obviously, a literal will always be defined, so this expression will always be true.

The second will check whether the constant String is defined, but not starting at the current constant lookup scope, instead starting at the class of self.

TL;DR

There may be cases where you can use them interchangeably, but one is a keyword and the other a method. In addition, the semantics and return values of the two are quite different.

Keywords vs. Methods

Among other things, one key difference is that Module#const_defined? is a method on a class or module that looks up constants in a class and its ancestors, while defined? is a keyword that determines whether its argument is currently known at the calling point in your code.

For example:

char = 'a'

char.const_defined?
#=> NoMethodError (undefined method `const_defined?' for "a":String)

defined? char
#=> "local-variable"

Exceptions vs. Return Values

If you're only concerned about constants, then the main difference is that you can use defined? to determine whether a constant is currently in scope without triggering a NoMethodError exception. For example:

defined? String
#=> "constant"

defined? FooBarBaz
#=> nil

As a bonus, defined? will tell what type of object you're passing as an argument (e.g. "constant"), while #const_defined? returns a truthy value.

Float.constants
#=> [:ROUNDS, :RADIX, :MANT_DIG, :DIG, :MIN_EXP, :MAX_EXP, :MIN_10_EXP, :MAX_10_EXP, :MIN, :MAX, :EPSILON, :INFINITY, :NAN]

defined? Float::NAN
#=> "constant"

Float.const_defined? :NAN
#=> true

As a rule of thumb, it's often considered best practice to reserve exceptions for handling something unexpected that may require your application to halt. Introspection or branching should generally rely on return values or Booleans, so defined? is usually a better choice if you aren't already expecting a given class to already be defined and available within the current scope.

Is there a situation where I these two ways of testing would make a difference?

const_defined? only checks the receiver and its ancestors, but it doesn't take the current module nesting into account:

module Foo
  ABC = 123

  class Bar
    def self.test
      p defined?(ABC)        #=> "constant"
      p const_defined?(:ABC) #=> false
    end
  end
end

In order to do so, you have to traverse Module.nesting:

module Foo
  ABC = 123

  class Bar
    def self.test
      p defined?(ABC)                                      #=> "constant"
      p Module.nesting.any? { |m| m.const_defined?(:ABC) } #=> true
    end
  end
end
Related