Is it possible to compare private attributes in Ru

2019-02-18 12:51发布

I'm thinking in:

class X
    def new()
        @a = 1
    end
    def m( other ) 
         @a == other.@a
    end
end

x = X.new()
y = X.new()
x.m( y ) 

But it doesn't works.

The error message is:

syntax error, unexpected tIVAR

How can I compare two private attributes from the same class then?

4条回答
来,给爷笑一个
2楼-- · 2019-02-18 12:55

There are several methods

Getter:

class X
  attr_reader :a
  def m( other )
    a == other.a
  end
end

instance_eval:

class X
  def m( other )
    @a == other.instance_eval { @a }
  end
end

instance_variable_get:

class X
  def m( other )
    @a == other.instance_variable_get :@a
  end
end

I don't think ruby has a concept of "friend" or "protected" access, and even "private" is easily hacked around. Using a getter creates a read-only property, and instance_eval means you have to know the name of the instance variable, so the connotation is similar.

查看更多
够拽才男人
3楼-- · 2019-02-18 13:09

Not sure, but this might help:

Outside of the class, it's a little bit harder:

# Doesn't work:
irb -> a.@foo
SyntaxError: compile error
(irb):9: syntax error, unexpected tIVAR
        from (irb):9

# But you can access it this way:
irb -> a.instance_variable_get(:@foo)
    => []

http://whynotwiki.com/Ruby_/_Variables_and_constants#Variable_scope.2Faccessibility

查看更多
地球回转人心会变
4楼-- · 2019-02-18 13:12

If you don't use the instance_eval option (as @jleedev posted), and choose to use a getter method, you can still keep it protected

If you want a protected method in Ruby, just do the following to create a getter that can only be read from objects of the same class:

class X
    def new()
        @a = 1
    end
    def m( other ) 
        @a == other.a
    end

    protected
    def a 
      @a
    end
end

x = X.new()
y = X.new()
x.m( y ) # Returns true
x.a      # Throws error
查看更多
劫难
5楼-- · 2019-02-18 13:18

There have already been several good answers to your immediate problem, but I have noticed some other pieces of your code that warrant a comment. (Most of them trivial, though.)

Here's four trivial ones, all of them related to coding style:

  1. Indentation: you are mixing 4 spaces for indentation and 5 spaces. It is generally better to stick to just one style of indentation, and in Ruby that is generally 2 spaces.
  2. If a method doesn't take any parameters, it is customary to leave off the parantheses in the method definition.
  3. Likewise, if you send a message without arguments, the parantheses are left off.
  4. No whitespace after an opening paranthesis and before a closing one, except in blocks.

Anyway, that's just the small stuff. The big stuff is this:

def new
  @a = 1
end

This does not do what you think it does! This defines an instance method called X#new and not a class method called X.new!

What you are calling here:

x = X.new

is a class method called new, which you have inherited from the Class class. So, you never call your new method, which means @a = 1 never gets executed, which means @a is always undefined, which means it will always evaluate to nil which means the @a of self and the @a of other will always be the same which means m will always be true!

What you probably want to do is provide a constructor, except Ruby doesn't have constructors. Ruby only uses factory methods.

The method you really wanted to override is the instance method initialize. Now you are probably asking yourself: "why do I have to override an instance method called initialize when I'm actually calling a class method called new?"

Well, object construction in Ruby works like this: object construction is split into two phases, allocation and initialization. Allocation is done by a public class method called allocate, which is defined as an instance method of class Class and is generally never overriden. It just allocates the memory space for the object and sets up a few pointers, however, the object is not really usable at this point.

That's where the initializer comes in: it is an instance method called initialize, which sets up the object's internal state and brings it into a consistent, fully defined state which can be used by other objects.

So, in order to fully create a new object, what you need to do is this:

x = X.allocate
x.initialize

[Note: Objective-C programmers may recognize this.]

However, because it is too easy to forget to call initialize and as a general rule an object should be fully valid after construction, there is a convenience factory method called Class#new, which does all that work for you and looks something like this:

class Class
  def new(*args, &block)
    obj = alloc
    obj.initialize(*args, &block)

    return obj
  end
end

[Note: actually, initialize is private, so reflection has to be used to circumvent the access restrictions like this: obj.send(:initialize, *args, &block)]

Lastly, let me explain what's going wrong in your m method. (The others have already explained how to solve it.)

In Ruby, there is no way (note: in Ruby, "there is no way" actually translates to "there is always a way involving reflection") to access an instance variable from outside the instance. That's why it's called an instance variable after all, because it belongs to the instance. This is a legacy from Smalltalk: in Smalltalk there are no visibility restrictions, all methods are public. Thus, instance variables are the only way to do encapsulation in Smalltalk, and, after all, encapsulation is one of the pillars of OO. In Ruby, there are visibility restrictions (as we have seen above, for example), so it is not strictly necessary to hide instance variables for that reason. There is another reason, however: the Uniform Access Principle.

The UAP states that how to use a feature should be independent from how the feature is implemented. So, accessing a feature should always be the same, i.e. uniform. The reason for this is that the author of the feature is free to change how the feature works internally, without breaking the users of the feature. In other words, it's basic modularity.

This means for example that getting the size of a collection should always be the same, regardless of whether the size is stored in a variable, computed dynamically every time, lazily computed the first time and then stored in a variable, memoized or whatever. Sounds obvious, but e.g. Java gets this wrong:

obj.size # stored in a field

vs.

obj.getSize() # computed

Ruby takes the easy way out. In Ruby, there is only one way to use a feature: sending a message. Since there is only one way, access is trivially uniform.

So, to make a long story short: you simply can't access another instance's instance variable. you can only interact with that instance via message sending. Which means that the other object has to either provide you with a method (in this case at least of protected visibility) to access its instance variable, or you have to violate that object's encapsulation (and thus lose Uniform Access, increase coupling and risk future breakage) by using reflection (in this case instance_variable_get).

Here it is, in all its glory:

#!/usr/bin/env ruby

class X
  def initialize(a=1)
    @a = a
  end

  def m(other) 
    @a == other.a
  end

  protected

  attr_reader :a
end

require 'test/unit'
class TestX < Test::Unit::TestCase
  def test_that_m_evaluates_to_true_when_passed_two_empty_xs
    x, y = X.new, X.new
    assert x.m(y)
  end
  def test_that_m_evaluates_to_true_when_passed_two_xs_with_equal_attributes
    assert X.new('foo').m(X.new('foo'))
  end
end

Or alternatively:

class X
  def m(other) 
    @a == other.instance_variable_get(:@a)
  end
end

Which one of those two you chose is a matter of personly taste, I would say. The Set class in the standard library uses the reflection version, although it uses instance_eval instead:

class X
  def m(other) 
    @a == other.instance_eval { @a }
  end
end

(I have no idea why. Maybe instance_variable_get simply didn't exist when Set was written. Ruby is going to be 17 years old in February, some of the stuff in the stdlib is from the very early days.)

查看更多
登录 后发表回答