Home > Mobile >  Ruby: Can not read an attribute by self.attr
Ruby: Can not read an attribute by self.attr

Time:04-04

I am learning Ruby basic, and trying to do some tests. I encounter an issue to understand the class method accesss scope.

Here is my code.

class User

    attr_accessor :name

    def initialize(name)
        @name = name
        @login_status = false
    end

    def login
        puts "user #{name} logged in"
        self.login_status = true
    end

    def logout
        puts "user #{name} logged out"
        self.login_status = false
    end

    def login?
        self.login_status
    end

    private

    attr_accessor :login_status

end

I got 'no such method error' when I had a test in irb

2.5.9 :001 > load 'user.rb'
 => true 
2.5.9 :002 > u = User.new 'Mike'
 => #<User:0x00007ff1a10eb358 @name="Mike", @login_status=false> 
2.5.9 :003 > u.login
user Mike logged in
 => true 
2.5.9 :004 > u.login?
Traceback (most recent call last):
        3: from /Users/sgao/.rvm/rubies/ruby-2.5.9/bin/irb:11:in `<main>'
        2: from (irb):3
        1: from user.rb:21:in `login?'
NoMethodError (private method `login_status' called for #<User:0x00007ff22f860938 @name="test", @login_status=false>)
2.5.9 :005 >

I am thinking why self.login_status = true works when I invoke u.login, but self.login_status fails when I invoke u.login?.

CodePudding user response:

TL;DR

The way private methods work in a dynamic language like Ruby is a common source of confusion. In Ruby, private methods primarily work by disallowing an explicit receiver. The underlying problem in your code is that you've declared your accessor methods for :login_status as private for some reason. I explain more about the problem and the possible solutions below.

How You Unintentionally Broke the Code

You did this to yourself by declaring the :login_status accessor as private. This prevents your public methods from calling it with an explicit receiver. Although you can still read the created instance variable of @login_status from your public methods as a sort of syntax sugar for the getter method—in Ruby 3.1.1, anyway; I didn't test it with anything else—you don't actually have direct access to the getter method, or an easily-accessible setter method for assigning to the instance variable.

Module#attr_accessor is just shorthand for declaring a pair of getter and setter methods on a object. When you place the accessor declaration after calling Kernel#private, Ruby defines the #login_status and #login_status= getter and setter methods as private methods. You did that here:

private

attr_accessor :login_status

In Ruby, you cannot call a private method with an explicit receiver. Your exception even tells you that this is the problem (bold and emphasis mine):

NoMethodError (private method `login_status' called for #<User:0x00007ff22f860938 @name="test", @login_status=false>)

Possible Fixes

Recommended Fixes

The best solution is probably to change your #login? method to use an implicit receiver by removing self as the explicit receiver:

def login?
  login_status
end

or make the accessor public rather than private.

Possible, Not Recommended: Fixing with Object#Send

Alternatively, you can also use Object#send to more explicitly call private methods. However, without understanding why you think an accessor (which is generally most useful when made part of the public interface) should be defined as a set of private methods, I find it hard to recommend that approach as a generic best practice.

Reasons Not to Rely on Private Methods

Unlike some other languages, Ruby private methods aren't really off-limits. As a result, the use of private or protected methods in Ruby is often more of a set of advisory usage guidelines, or an indication of semantic intent to differentiate code from the public interface, rather than a hard line that users of a class or method can't cross.

That doesn't mean you shouldn't use private methods in your code. It's just that using them, especially when declaring accessors, is a lot less common than in other languages because the use cases are often less about enforcement than they are about communicating programmer intent.

CodePudding user response:

I am thinking why self.login_status = true works when I invoke u.login, but self.login_status fails when I invoke u.login?.

For the exact reason that u.login works but u.fobnicatefoobarbazqux doesn't: one method is defined, the other isn't.

The method User#login_status= is defined (attr_writer :login_status does that for you), but User#login_status isn't. You need to define it.

Module#attr_writer essentially looks more or less like this:

class Module
  def attr_writer(*meths)
    meths.each do |meth|
      define_method(:"#{meth}=") do |val|
        instance_variable_set(:"@#{meth}", val)
      end
    end
  end
end
  • Related