How to make instance variables private in Ruby?
Asked Answered
C

7

45

Is there any way to make instance variables "private"(C++ or Java definition) in ruby? In other words I want following code to result in an error.

class Base
  def initialize()
    @x = 10
  end
end

class Derived < Base
  def x
    @x = 20
  end
end

d = Derived.new
Clone answered 25/1, 2010 at 11:34 Comment(4)
This seems like a slightly unusual request, what's the use case for such a pattern? Perhaps you know something I don't, that would be useful in the future.Attractant
Coming from C++ world it just looks natural for me to have private variables in base class which can't be accessed in the derived class and gives me good confidence that it won't be modified in the derived class. In the above example I can be sure that the only place where @x will be modified is in class "Base" if it is possible to make it private instance variable.Clone
I think you shouldn't be trying to code C++ in Ruby. Since Ruby's a very dynamic & powerful language, there will always be a way to get to the private data.Degeneration
Could you give me a more specific use case for it? It doesn't have to be a complicated one. I feel like if I understood one problem for which you wouldn't want an object to be able to access it's own slots it might help the discussion.Attractant
N
40

Like most things in Ruby, instance variables aren't truly "private" and can be accessed by anyone with d.instance_variable_get :@x.

Unlike in Java/C++, though, instance variables in Ruby are always private. They are never part of the public API like methods are, since they can only be accessed with that verbose getter. So if there's any sanity in your API, you don't have to worry about someone abusing your instance variables, since they'll be using the methods instead. (Of course, if someone wants to go wild and access private methods or instance variables, there isn’t a way to stop them.)

The only concern is if someone accidentally overwrites an instance variable when they extend your class. That can be avoided by using unlikely names, perhaps calling it @base_x in your example.

Nimwegen answered 25/1, 2010 at 13:1 Comment(5)
isn't the issue here that in his code he can modify the @x variable from the derived class? That is contrary to what it is in C++ where a derived class cannot access private data members. So, while it's true that 'instance variables in ruby are private' -- the important point is that it's a different kind of private to the meaning of private in C++Kodak
I think in C++ parlance, one would say 'instance variables in ruby are always protected'. While not a perfect analog, it is more accurate than the C++ meaning of private.Zeke
sigh, yep... join the club of scripting languages which fail to properly implement OOP support.Centrobaric
As horseguy already mentioned in a previous comment, in the "instance variables in Ruby are always private" statement, private means "not directly accessible using <obj_name>.<attrib_name> outside the class". However, you can use the instance_variable_get() method to access the attribute from outside the class and children classes can access the attributes. In OOP (and C++) terminology, attributes in Ruby would be protected (if you ignore the instance_variable_get() accessor) or public (if you do not).Zygo
been using for a while, now sharing .. gist.github.com/amolpujari/ad1f4b61a3ffc50ab4e90dfe9b0dbac1Gretagretal
P
32

Never use instance variables directly. Only ever use accessors. You can define the reader as public and the writer private by:

class Foo
  attr_reader :bar

  private

  attr_writer :bar
end

However, keep in mind that private and protected do not mean what you think they mean. Public methods can be called against any receiver: named, self, or implicit (x.baz, self.baz, or baz). Protected methods may only be called with a receiver of self or implicitly (self.baz, baz). Private methods may only be called with an implicit receiver (baz).

Long story short, you're approaching the problem from a non-Ruby point of view. Always use accessors instead of instance variables. Use public/protected/private to document your intent, and assume consumers of your API are responsible adults.

Pastelist answered 24/1, 2012 at 17:30 Comment(3)
The part about accessibility and receivers really helped clarify some issues I've had in the past.Whyte
"Never use instance variables directly..." Why not? They're a central part of the language. I would say it depends on your situation and the problem you're trying to solve.Kling
It's a rule of thumb. Of course attr_reader and attr_writer use instance variables behind the scenes. And you might want use them directly for transparent memoization (@_foo ||= begin; # slow operation; end). But if you use instance variables directly, you can't hook into their behavior when getting or setting their values without changing code everywhere else (including code that subclasses them). You also don't get an exception if you misspell an @isntance_variable whereas you do for a self.mtehod(). They're no more "central" than @@class_variables, which are likewise verboten.Pastelist
L
15

It is possible (but inadvisable) to do exactly what you are asking.

There are two different elements of the desired behavior. The first is storing x in a read-only value, and the second is protecting the getter from being altered in subclasses.


Read-only value

It is possible in Ruby to store read-only values at initialization time. To do this, we use the closure behavior of Ruby blocks.

class Foo
  def initialize (x)
    define_singleton_method(:x) { x }
  end
end

The initial value of x is now locked up inside the block we used to define the getter #x and can never be accessed except by calling foo.x, and it can never be altered.

foo = Foo.new(2)
foo.x  # => 2
foo.instance_variable_get(:@x)  # => nil

Note that it is not stored as the instance variable @x, yet it is still available via the getter we created using define_singleton_method.


Protecting the getter

In Ruby, almost any method of any class can be overwritten at runtime. There is a way to prevent this using the method_added hook.

class Foo
  def self.method_added (name)
    raise(NameError, "cannot change x getter") if name == :x
  end
end

class Bar < Foo
  def x
    20
  end
end

# => NameError: cannot change x getter

This is a very heavy-handed method of protecting the getter.

It requires that we add each protected getter to the method_added hook individually, and even then, you will need to add another level of method_added protection to Foo and its subclasses to prevent a coder from overwriting the method_added method itself.

Better to come to terms with the fact that code replacement at runtime is a fact of life when using Ruby.

Lorilee answered 18/2, 2014 at 3:24 Comment(2)
Keep in mind that defining a method will invalidate ruby's method cache. If you're creating a lot of these, it could adversely affect performance.Maroon
@Kelvin, that is a really great point, thanks. Anyone interested in learning more about this performance penalty in Ruby should check out this great writeup: github.com/charliesome/charlie.bz/blob/master/posts/…Lorilee
L
5

Unlike methods having different levels of visibility, Ruby instance variables are always private (from outside of objects). However, inside objects instance variables are always accessible, either from parent, child class, or included modules.

Since there probably is no way to alter how Ruby access @x, I don't think you could have any control over it. Writing @x would just directly pick that instance variable, and since Ruby doesn't provide visibility control over variables, live with it I guess.

As @marcgg says, if you don't want derived classes to touch your instance variables, don't use it at all or find a clever way to hide it from seeing by derived classes.

Lutes answered 26/1, 2010 at 2:47 Comment(0)
B
2

It isn't possible to do what you want, because instance variables aren't defined by the class, but by the object.

If you use composition rather than inheritance, then you won't have to worry about overwriting instance variables.

Berwick answered 24/1, 2012 at 22:1 Comment(2)
+1. in most cases composition provides a more flexible solution. It would be nice if the derived class didn't get access to private member variables to protect against the case where the developer accidentally reuses a variable name, but then again, variable predeclaration isn't required in ruby anyhow.Ammadas
Andrew's first statement is so true and one that programmers coming from Java/C++ should tattoo on their hands! Classes do not 'declare' instance variables. Instance variables are added to objects as the program executes. If the method(s) that create an instance variable are not invoked, the object will never have that instance variable.Zr
S
0

If you want protection against accidental modification. I think attr_accessor can be a good fit.

class Data
 attr_accessor :id
 
 private :id
end

That will disable writing of id but would be readable. You can however use public attr_reader and private attr_writer syntax as well. Like so:

class Data
 attr_reader :id
 
 private
 
 attr_writer :id
end
Spent answered 9/9, 2022 at 18:52 Comment(1)
It should read private :id=; if you do private :id, the reading will be disabled, not writing.Miscible
U
-1

I know this is old, but I ran into a case where I didn't as much want to prevent access to @x, I did want to exclude it from any methods that use reflection for serialization. Specifically I use YAML::dump often for debug purposes, and in my case @x was of class Class, which YAML::dump refuses to dump.

In this case I had considered several options

  1. Addressing this just for yaml by redefining "to_yaml_properties"

    def to_yaml_properties
      super-["@x"]
    end
    

    but this would have worked just for yaml and if other dumpers (to_xml ?) would not be happy

  2. Addressing for all reflection users by redefining "instance_variables"

    def instance_variables
      super-["@x"]
    end
    
  3. Also, I found this in one of my searches, but have not tested it as the above seem simpler for my needs

So while these may not be exactly what the OP said he needed, if others find this posting while looking for the variable to be excluded from listing, rather than access - then these options may be of value.

Uitlander answered 8/5, 2012 at 23:16 Comment(2)
I suggest asking this as a separate question and answering it yourself. Answering here creates extra noise.Maroon
@Maroon I answered here because it wasn't quite clear WHY the OP wanted to do this but this would have helped him if his reasons were similar to mine. He never stated his reasons, if he did and his full goal was different then I would have removed it. As it is it would help anyone reaching this question trying to solve a specific use-case. I dont think its right for me to ask a question to which I already know the answer (tho answering own questions is obviously fine)Uitlander

© 2022 - 2024 — McMap. All rights reserved.