Class method vs constant in Ruby/Rails
The latter is better. If it were a method, a new array and new strings will be created every time it is called, which is a waste of resource.
Ruby Class method or Constant, best practice
This is the most common idiom:
class Product
def xml_doc
@@xml_doc ||= Nokogiri::XML(open("#{Rails.root}/myxmlfile.xml"))
return @@xml_doc
end
end
The ||=
operator says "if the variable is nil
, calculate the result of the expresion and store it, otherwise do nothing". This idiom is called "memoization".
Do no think of constants as a way to optimize your code, in Ruby they are not really constant anyway.
Constants or class variables in ruby?
The main thing is that by using the CONSTANT notation, you're making it clear to the reader. the lower case, frozen string gives the impression is might be settable, forcing someone to go back and read the RDoc.
In Ruby, in a method defined in class self, why can't a constant defined on the superclass be access without self?
You have encountered a common Ruby gotcha - constant lookup.
The most important concept in constant lookup is Module.nesting
(unlike in method lookup, where the primary starting point is self
). This method gives you the current module nesting which is directly used by the Ruby interpreter when resolving the constant token. The only way to modify the nesting is to use keywords class
and module
and it only includes modules and classes for which you used that keyword:
class A
Module.nesting #=> [A]
class B
Module.nesting #=> [A::B, A]
end
end
class A::B
Module.nesting #=> [A::B] sic! no A
end
In meta programming, a module or class can be defined dynamically using Class.new
or Module.new
- this does not affect nesting and is an extremely common cause of bugs (ah, also worth mentioning - constants are defined on the first module of Module.nesting):
module A
B = Class.new do
VALUE = 1
end
C = Class.new do
VALUE = 2
end
end
A::B::VALUE #=> uninitialized constant A::B::VALUE
A::VALUE #=> 2
The above code will generate two warnings: one for double initialization of constant A::VALUE and a second for reassigning the constant.
If it looks like "I'd never do that" - this also applies to all the constants defined within RSpec.describe
(which internally calls Class.new), so if you define a constant within your rspec tests, they are most certainly global (unless you explicitly stated the module it is to be defined in with self::
)
Now let's get back to your code:
class SubExample < SuperExample
puts Module.nesting.inspect #=> [SubExample]
class << self
puts Module.nesting.inspect #=> [#<Class:SubExample>, SubExample]
end
end
When resolving the constant, the interpreter first iterates over all the modules in Module.nesting
and searches this constant within that module. So if nesting is [A::B, A]
and we're looking for the constant with token C
, the interpreter will look for A::B::C
first and then A::C
.
However, in your example, that will fail in both cases :). Then the interpreter starts searching ancestors of the first (and only first) module in Module.nesting. SubrExample.singleton_class.ancestors
gives you:
[
#<Class:SubExample>,
#<Class:SuperExample>,
#<Class:Object>,
#<Class:BasicObject>,
Class,
Module,
Object,
Kernel,
BasicObject
]
As you can see - there is no SuperExample
module, only its singleton class - which is why constant lookup within class << self
fails (print_constant_fails
).
The ancestors of Subclass
are:
[
SubExample,
SuperExample,
Object,
Kernel,
BasicObject
]
We have SuperExample
there, so the interpreter will manage to find SuperExample::A_CONSTANT
within this nesting.
We're left with print_constant_works_2
. This is an instance method on a singleton class, so self
within this method is just SubExample
. So, we're looking for SubExample::A_CONSTANT
- constant lookup firstly searches on SubExample
and, when that fails, on all its ancestors, including SuperExample
.
Constant vs. instance variable vs. instance method
Last time I was evil to a female newbie. This time over, I'll try to be nice.
Way one, using a constant:
A = Class.new
class B < A
FOO = [ :hello, :world ] # this is your array
end
# Access different constants defined in different descendants is tricky, like this:
class A
def foo_user
puts self.class.const_get :FOO
end
end
B.new.foo_user #=> [ :hello, :world ]
# Note theat you can't do this:
class A
def foo_user
puts FOO # this would look for FOO in A mother class only
end
end
B.new.foo_user #=> error
Way two, using an instance variable belonging to the subclasses of A:
A = Class.new
class B < A
@foo = [ "hello", "world" ]
end
# This way is more usable. I would go about it like this:
class A
class << self
attr_reader :foo # defining a reader class method
end
def foo
self.class.foo # delegating foo calls on the instances to the class
end
def foo_user
puts foo
end
end
B.new.foo_user #=> [ :hello, :world ]
Way three, using an instance method defined on the descendants:
A = Class.new
class B < A
def foo
[ "hello", "world" ]
end
end
# This way is also usable.
class A
def foo_user
puts foo
end
end
The choice between the way 2 (instance variable belonging to a descendant class) and 3 (method defined on a descendant class) depends on how flexible the value (the array) needs to be. Way 2 is most flexible, but way 3 takes less code.
Ruby constant within a class method
Not exactly what you wanted, but you simply haven't defined CONST inside class A but in its metaclass, which I have therefore saved a reference to...
class A
class << self
::AA = self
CONST = 1
end
end
puts AA::CONST
Related Topics
Access Instance Variable from Outside the Class
How to Specify a Required Switch (Not Argument) with Ruby Optionparser
How to Convert a String to a Class Method
Ruby: Sum Corresponding Members of Two or More Arrays
Eager Loading: the Right Way to Do Things
Hot Deploy on Heroku with No Downtime
Rails 3. How to Add a Helper That Activeadmin Will Use
Rails - Multiple Top Level Domains and a Single Session/Cookie
Rails: Render View from Outside Controller
One Liner in Ruby for Displaying a Prompt, Getting Input, and Assigning to a Variable
Rbenv: Surviving Without Gemsets
Transforming Datetime into Month, Day and Year
Ruby on Rails Error "Cannot Load Such File -- Less"
Ruby: Extracting Words from String
Subtract N Hours from a Datetime in Ruby
Changing Field Separator/Delimiter in Exported CSV Using Ruby CSV