I see the issue with using class variables with Ruby; however, it seems RuboCop's documentation for how to fix the issue is not sufficient.
Now, I could just ignore it. Given my project, it doesn't matter. But, I just want to know what Rubocop is trying to tell me to do, because it doesn't make sense.
Executing the provided code in irb 0.9.6
with Ruby 2.5.1
gives:
class A
@test = 10
end
#=> 10
class A
def test
@@test # you can access class variable without offense
end
end
#=> :test
A.new.test
Traceback (most recent call last):
3: from /Users/Ricky/.rbenv/versions/2.5.1/bin/irb:11:in `<main>'
2: from (irb):12
1: from (irb):9:in `test'
NameError (uninitialized class variable @@test in A)
Did you mean? @test
So, no. We obviously cannot access class variable without offense. irb was very offended. But, ruby suggests using @test
. Maybe it was just a typo? Let's try it:
class A
@test = 10
def test
@test # you can access class variable without offense
end
end
#=> :test
A.new.test
#=> nil
So, the instance variable was never defined. What is RuboCop trying to say here?
You are missing the difference between the scopes of variables.
class A
@test = 42
end
The above declares an instance variable in the class scope. It is accessible as
A.instance_variable_get(:@test)
#⇒ 42
You can define an accessor for this variable:
class A
@test = 42
def self.test
@test
end
end
A.test #⇒ 42
It is shared between instances and to access it from instances you should refer to the class:
# ⇓⇓⇓⇓⇓ HERE
A.new.class.test #⇒ 42
The following code declares an instance variable on the class instances:
class A
def initialize
@test = 42
end
end
It can be accessed from the instances of A
:
A.new.instance_variable_get(:@test)
#⇒ 42
Class variables have some drawbacks when used within the class hierarchy, that is [probably] why Rubocop suggests not to use class variables (or whatever it suggests—I honestly never used it since it brings more harm than help IMSO.)
In your first snippet you have missed the @
. The correct code would be:
class A
# ⇓⇓ HERE
@@test = 10
end
class A
def test
@@test # you can access class variable without offense
end
end
At the beginning of 2023, the problem is still relevant. Because the rubocop documentation is not the place to post information about the intricacies of OOP in ruby.
The dislike of using class variables comes from unexpected behavior when we use class inheritance. But we love to watch the code, not read the description, and the documentation clearly says:
You have to be careful when setting a value for a class variable; if a class has been inherited, changing the value of a class variable also affects the inheriting classes. This means that it's almost always better to use a class instance variable instead.
I would like to supplement Alexey Matyushkin's answer and show the behavior of class variables with simple examples. And also explain what this can lead to.
I confirm that the code from the rubocop documentation is some kind of nonsense:
# good
class A
@test = 10
end
class A
def test
@@test # you can access class variable without offense
end
end
class A
def self.test(name)
class_variable_get("@@#{name}") # you can access without offense
end
end
begin
puts A.new.test
rescue => e
puts e.message
end
begin
puts A.test 'test'
rescue => e
puts e.message
end
puts "RUBY_VERSION: #{RUBY_VERSION}"
=>>>
uninitialized class variable @@test in A
Did you mean? @test
uninitialized class variable @@test in A
Did you mean? @test
RUBY_VERSION: 2.5.3
puts 'When we use "classic" class variables:'
class A
@@var = 10
cattr_accessor :var
end
class Aa < A
end
puts Aa.var, '- the child class has inherited the methods and the value of the variable.'
Aa.var = 20
puts A.var, '- but the variable of the parent class was implicitly changed (bad)!'
puts 'When we use class instance variables:'
class B
@test = 10
class << self
attr_accessor :test
end
end
class Bb < B
end
puts Bb.test, '- the child class has inherited the methods, but not the value of the variable (this is also bad)!'
Bb.test = 20
puts B.test, '- a change in the child class does not lead to a change in the parent.'
=>>>
When we use "classic" class variables:
10
- the child class has inherited the methods and the value of the variable.
20
- but the variable of the parent class was implicitly changed (bad)!
When we use class instance variables:
- the child class has inherited the methods, but not the value of the variable (this is also bad)!
10
- a change in the child class does not lead to a change in the parent.
One way to modify BIG programs is to inherit the class and make your own changes to it. Often the project is complex, there are many implicit dependencies (let's be honest =)), and if you make changes directly to the class, the project will crash. Therefore, we use inheritance, the child class is used in a new service with its own settings, or the child class changes the behavior of one part of the program. And if, during inheritance, the child class suddenly changes the base class, then inheritance loses its meaning! Flexibility is lost.
But any question needs to be viewed in context. If you are writing a miniature project alone, then there is nothing wrong with @@ var. You just need understanding.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With