Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to correct `Style/ClassVars` in RuboCop?

Tags:

ruby

rubocop

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?

like image 427
RWDJ Avatar asked Dec 31 '18 06:12

RWDJ


2 Answers

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
like image 138
Aleksei Matiushkin Avatar answered Sep 23 '22 17:09

Aleksei Matiushkin


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

What rubocop really wanted to tell us.

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.

And what's the big deal? How can this harm?

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.

like image 27
Zlatov Avatar answered Sep 19 '22 17:09

Zlatov