Working on a Ruby program I was looking to move some state data from instance variables to class variables, it dawned on me that while instance variables are auto-vivified (if you try to read them "without initializing" them, they are automatically initialized to nil), class variables are not - and this looks very inconsistent to me (compared to most Ruby syntax which is very consistent).
Sample program:
class Test
def id
#id.to_i
end
def id=(i)
#id = i
end
def nextid
self.id = id + 1
end
end
t = Test.new
puts t.nextid #=> 1
puts t.nextid #=> 2
In which case, when calling Test::id, if #id was not initialized, Ruby will auto-vivify it to nil (after which I to_i it to get 0).
Now I decide that I want the running ID to be shared across Test instance, so I rewrite it like this:
class Test
def id
##id.to_i
end
def id=(i)
##id = i
end
def nextid
self.id = id + 1
end
end
t = Test.new
puts t.nextid
puts t.nextid
Should work the same, I thought, but no:
NameError: uninitialized class variable ##id in Test
But this workaround works (!?) :
class Test
def id
(##id ||= 0).to_i
end
def id=(i)
##id = i
end
def nextid
self.id = id + 1
end
end
t = Test.new
puts t.nextid #=> 1
puts t.nextid #=> 2
(granted, after doing lazy init to 0 I can drop the to_i, but I left it for consistency).
It looks like Ruby understands "lazy initialization" and treats it as the magic needed to not throw NameError - even though ||= is supposedly just syntactic sugar to x = x || val (which BTW doesn't work for initing class variables, thanks for asking).
How come?
Class variables initialization
Here's a possible explanation why #a is nil but ##a is a NameError.
But if you want to use class variables, you should initialize them inside the class, not inside instance methods :
class Test
##id = 0
def id
##id
end
def id=(i)
##id = i
end
def nextid
self.id = id + 1
end
end
t = Test.new
puts t.nextid
puts t.nextid
Please note that it doesn't make much sense to have an instance setter method for a class variable.
Class instance variables
To avoid mixing instance methods and class variables, you could define everything at the class level with a "class instance variable". It's an instance variable defined at the class level:
class Test
#id = 0
class << self
def id
#id
end
def id=(i)
#id = i
end
def nextid
self.id = id + 1
end
end
end
puts Test.id
# 0
puts Test.nextid
# 1
puts Test.nextid
# 2
It means you could just use attr_accesor:
class Test
#id = 0
class << self
attr_accessor :id
def nextid
self.id = id + 1
end
end
end
Related
I'm trying to access a class variable from a method outside of the class.
This is my class:
class Book
##bookCount = 0
##allBooks = []
def self.allBooks
##allBooks
end
def self.bookCount
##bookCount
end
attr_accessor :name,:author,:date,:genre,:rating
def initialize(name, author, date, genre, rating)
#name = name
#author = author
#date = date
#genre = genre
#rating = rating
##bookCount += 1
##allBooks << self
end
end
This is the method trying to access the class variable ##bookCount
def seeBookShelf
if ##bookCount == 0
puts "Your bookshelf is empty."
else
puts "You have " + #bookCount + " books in your bookshelf:"
puts allBooks
end
end
When I try to execute the method I get this:
undefined local variable or method `bookCount' for main:Object (NameError)
How can I access bookCount from the outside?
Use class_variable_get to access a class variable outside of a class:
class Foo
##a = 1
end
Foo.class_variable_get(:##a)
=> 1
For most cases, class instance variables are preferred to class variables. The latter are prone to all manner of strange behaviour when used with inheritance.
Consider:
class Book
#book_count = 0
#all_books = []
class << self
attr_reader :book_count
attr_reader :all_books
end
# further code omitted.
end
With this code Book.book_count and Book.all_books get the expected data.
You can use class_eval to evaluate a block of code within the scope of a specific class:
class Book
##bookCount = 1
end
Book.class_eval '##bookCount'
# => 1
And just for fun... you can actually do all kinds of trickery with class_eval such as define a new method in the class without monkey patching:
Book.class_eval { ##bookCount = 5 }
Book.class_eval '##bookCount'
# => 5
Book.class_eval do
def self.hey_look_a_new_method
return "wow"
end
end
Book.hey_look_a_new_method
# => "wow"
You need a getter to access the class variable, try this code.
See http://www.railstips.org/blog/archives/2006/11/18/class-and-instance-variables-in-ruby/ for an explanation.
You are also better to use string interpolation otherwise you get a Type error, also it is more Rubyesque.
class Book
##bookCount = 0
def self.bookCount
##bookCount
end
end
def seeBookShelf
if Book.bookCount == 0
puts "Your bookshelf is empty."
else
puts "You have #{Book.bookCount} books in your bookshelf:"
end
end
seeBookShelf # Your bookshelf is empty.
You have to specify the Class of the variable :
def seeBookShelf
if Book.bookCount == 0
puts "Your bookshelf is empty."
else
puts "You have " + Book.bookCount + " books in your bookshelf:"
puts Book.allBooks
end
end
class Books
attr_accessor :name, :book_id
def initialize(name, book_id)
#name = name,
#book_id = book_id
end
end
class BookCollection
def intialize
#book_names = []
end
def add_to_books(book_name)
book_name.push(book_names)
end
end
book1 = Books.new("catch22", "12345")
book_collection1 = BookCollection.new
book_collection1.add_to_books(book1.name)
puts book_collection1
end
That is my code and the error I'm getting is "undefined local variable or method `book_names'". I tried adding " attr_accessor :book_names" and when I do that the printed output doesn't make sense.
There are a few mistakes in your code:
line 4 should not end with a comma.
initialize in class BookCollection is misspelled, resulting in #book_names not being initialized. #book_names therefore equals nil when you attempt to add an element to it with push. nil does not have a method push; hence the exception, and the message printed with the exception.
book_name.push(book_names) should be #book_name.push(book_name). (#book_name must be an instance_variable, as opposed to a local variable, to be visible outside a method, within the class definition.
puts book_collection1 prints the class instance; you want to print #book_names.
Here I've fixed your code. I've used << instead of push. Either is OK, but the former seems to be favored my most.
class Books
attr_accessor :name, :book_id
def initialize(name, book_id)
puts "name = #{name}, book_id = #{book_id}"
#name = name
#book_id = book_id
end
end
class BookCollection
attr :book_names
def initialize
#book_names = []
end
def add_to_books(book_name)
#book_names << book_name
end
end
book_collection1 = BookCollection.new
book1 = Books.new("Catch22", "12345")
book2 = Books.new("Hawaii", "67890")
book_collection1.add_to_books(book1.name)
book_collection1.add_to_books(book2.name)
book_collection1.book_names # => ["Catch22", "Hawaii"]
Probably just a typo at
book_name.push(book_names)
Should have been
book_names.push(book_name)
With attr_accessor :book_names
I'm new to Ruby and confused about how I can make a method in Class similar to :attr_accessor, in that it adds methods to a user class, but so these added methods have access to a pre-initialized instance variable. It's difficult for me to explain so here is a greatly simplified sample of my efforts:
class Class
def super_accessor_wow(attr_name)
attr_name = attr_name.to_s
new_var_name = "#crazy_var_name"
instance_variable_set(new_var_name, ["hi", "everyone"])
module_eval(%Q/
def super_#{attr_name}()
return ##{attr_name}
end
def super_#{attr_name}=(value)
##{attr_name} = value
end
def greetings
return #{new_var_name}
end
/)
end
end
This is how I'm trying to use the new method on Class to modify my own class:
class Foo
super_accessor_wow(:bar)
end
foo1 = Foo.new()
foo1.super_bar = 1000
puts foo1.super_bar
puts foo1.greetings.inspect
The first puts prints '1000'
The second puts prints 'nil', so my instance_variable_set call in super_accessor_wow seemingly has no effect.
I expected that the second puts would print '['hi', 'everyone']' by the way. All of this code is contained in a single Ruby file.
Your instance_variable_set is called when you call super_accessor_wow during the class definition. No instance of the class exists yet. You create an instance of the class when you call new. You could add your #crazy_var_name initialization to the constructor, or you could define it in the greetings method:
Put the default in a class variable, and initialize the instance variable in the constructor (be aware that this creates a constructor for your class, and if you then create your own constructor, it will override this one):
class Class
def super_accessor_wow(attr_name)
attr_name = attr_name.to_s
new_var_name = "#crazy_var_name"
new_var_name_default = "##{new_var_name}"
module_eval(%Q/
#{new_var_name_default} = ["hi", "everyone"]
def initialize()
#{new_var_name} = #{new_var_name_default}
end
def super_#{attr_name}()
return ##{attr_name}
end
def super_#{attr_name}=(value)
##{attr_name} = value
end
def greetings
return #{new_var_name}
end
/)
end
end
class Foo
super_accessor_wow(:bar)
end
foo1 = Foo.new()
foo1.super_bar = 1000
puts foo1.super_bar
puts foo1.greetings.inspect
puts Foo.class_variable_get('##crazy_var_name').inspect
puts foo1.instance_variable_get('#crazy_var_name').inspect
Outputs:
1000
["hi", "everyone"]
["hi", "everyone"]
["hi", "everyone"]
Define it in the greetings method:
class Class
def super_accessor_wow(attr_name)
attr_name = attr_name.to_s
new_var_name = "#crazy_var_name"
module_eval(%Q/
def super_#{attr_name}()
return ##{attr_name}
end
def super_#{attr_name}=(value)
##{attr_name} = value
end
def greetings
#{new_var_name} = ["hi", "everyone"] unless #{new_var_name}
return #{new_var_name}
end
/)
end
end
class Foo
super_accessor_wow(:bar)
end
foo1 = Foo.new()
foo1.super_bar = 1000
puts foo1.super_bar
puts foo1.greetings.inspect
Outputs
1000
["hi", "everyone"]
As noted in the comment, instance_variable_set takes a symbol, not a string, so we'll fix that up first.
instance_variable_set(new_var_name.to_sym, ["hi", "everyone"])
But the big issue is that instance_variable_set isn't being called by an instance of Foo, it's being called by the Foo class itself. So, an instance variable is being set, but not on what you expected.
Foo.instance_variable_get(:#crazy_var_name).inspect
# ["hi", "everyone"]
In Ruby, when defining the contents of a class with class_exec, I am getting unexpected results. When I define a class variable in the block sent to class_exec, the class variable is being defined on Object instead of the class on which class_exec is being called:
class X; end
X.class_exec do
##inner_value = "123"
def inner_value
##inner_value
end
def inner_value=(arg)
##inner_value = arg
end
end
obj1 = X.new
puts obj1.inner_value
puts ##inner_value
puts Object.class_variables
Produces:
123
123
##inner_value
This does not happen when using class_eval:
X.class_eval(<<-RUBY)
##inner_value = "123"
def inner_value
##inner_value
end
def inner_value=(arg)
##inner_value = arg
end
RUBY
obj1 = X.new
puts obj1.inner_value
puts ##inner_value
puts Object.class_variables
Produces:
123
and an error:
uninitialized class variable ##inner_value in Object (NameError)
The results with class_eval are what I would expect to happen in both cases. I have tried this with both MRI 1.8.7 and MRI 1.9.3 and got the same results running on Windows XP.
Is this expected behavior? If so, why? If not, bug?
class variables are bound to the class in which they are declared at compile time. The block passed to class_exec is compiled before it is passed to class_exec, so the class variables are bound to Object.
I guess your class_exec is at the top level, which is in Object, so that's where they go. To demonstrate:
public
class Object
##x = "ribbit"
end
def foo
puts "test: #{##x}"
end
x = Object.new
x.foo
This is why when you use class vars in a module, all classes that include that module (through the included methods) will see the same class variables. The class variables are bound to the module. If you run this:
class WithClassVars
def self.classvars
#classvars ||= {}
end
def classvars
self.class.classvars
end
end
class A < WithClassVars;end
class B < WithClassVars;end
a = A.new
b = B.new
a.classvars[:a] = 1
b.classvars[:a] = 2
puts a.classvars
puts b.classvars
a and b will end up with the same data.
If you pass your code as a string to class_eval, the string is compiled in class_eval, so you can make sure they are in the right class then.
So if you want to store per-class data, you have to either go with class_eval, or use some mechanism to use a class's instance variables. Say:
class WithClassVars
def self.classvars
#classvars ||= {}
end
def classvars
self.class.classvars
end
end
class A < WithClassVars;end
class B < WithClassVars;end
a = A.new
b = B.new
a.classvars[:a] = 1
b.classvars[:a] = 2
puts a.classvars
puts b.classvars
If creating a class variable is often dangerous and unpredictable why do we need them?
If solution is just to use class instance variable with the class level accessors:
class Foo
#variable = :something
def self.getvariable
#variable
end
def self.setvariable(value)
#variable = value
end
end
Then why do we need class variables???
Class variables have their use on occasion, but I agree that using the eigenclass is frequently more useful:
class Foo
#bar = 'bar'
class << self
attr_accessor :bar
end
end
puts Foo.bar # bar
puts Foo.bar = 'baz' # baz
The above is safe with inheritance, because it sets a variable in the Foo constant, rather than a class variable.
Foo.new.instance_eval { puts ##bar } # error
This has several causes:
It's syntactic sugar. You can always get a class variable (whether you are in class or instance scope) using ##var. This won't work for instance variables of the class.
Class variables persist for the singleton classes of the instances of this class. Example:
class Test
#instance_var = 0
##class_var = 0
def self.instance_var
#instance_var
end
def self.class_var
##class_var
end
end
Test.instance_var #=> 0
Test.class_var #=> 0
Test.new.singleton_class.instance_var #=> nil
Test.new.singleton_class.class_var #=> 0
Here is an example (think ActiveRecord):
class Base
def connect(connection)
##connection = connection
end
def connection
##connection
end
end
class User < Base
end
class SuperUser < User
end
Base.new.connect("A connection")
puts User.new.connection #=> A connection
puts SuperUser.new.connection #=> A connection
The trick here is that class variable is accessible from an instance method and is inherited. Try this:
class Base
def self.connect(connection)
#connection = connection
end
def self.connection
#connection
end
def connection
self.class.connection
end
end
class User < Base
end
Base.connect("A connection")
puts User.new.connection #=> nil
You will get nil as self.connection tries to access it's own class instance variable (from User class), and it is not inherited.
Added: And yes, it can be dangerous if you misuse it:
##a = "A"
class A
def self.a
##a
end
def a
##a
end
end
puts A.a #=> A
puts A.new.a #=> A