BC00
BC00

Reputation: 1639

Ruby/Rails: Understanding ruby getter-setter methods and instances

I am new to ruby and programming in general and am trying to grasp a few key concepts. Given I have a class Dog, with the below characteristics.

class Dog
  attr_accessor :type, :popularity, :total

  def initialize(type = nil)
    @type = type
  end

  def total_dogs
    Dog.count
  end

  def total
    Dog.where(:type => self.type).size
  end

  def popularity
    total.to_f/total_dogs
  end
end

What I am trying to understand, is how ruby persists attributes to an instance via getter/setter methods. Its clear to me that if I instantiate a new instance and then save attributes to that instance, those attributes are tied to that instance because if I look at the object the attributes appear as such:

 @dog = Dog.new
 @dog
 => #<Dog:0x007fa8689ea238 @type=nil> 

Its easy for me to understand that as I pass the @dog object around its always going to have the @type attribute as nil. However, the situation I am having trouble understanding is if I pass this @dog object to another class. Like if I did:

 Owner.new(@dog)

When I am in the owner class and I call @dog.popularity how does it know the value of popularity for that instance? At runtime are all methods processed and then that instance just always is tied to the value at the time? Apologies if this makes no sense or I am way off.

Upvotes: 1

Views: 8158

Answers (4)

Tilo
Tilo

Reputation: 33732

Niel has a great answer for this, I just want to add something to it.

Counting Dogs :)

You need a class variable to do this..

class Dog
  @@count = 0     # this is a class variable; all objects created by this class share it

  def initialize
    @@count += 1  # when we create a new Dog, we increment the count
  end
  def total
    @@count
  end
end

There is another way to do this with "instance variables of the Class object" but that's a bit of an advanced topic.

Accessing Instance Variables

In Ruby, variables are really just references to objects / instances.

 > x = 1
 => 1 
 > x.class
 => Fixnum
  > 1.instance_variables
 => [] 

x is a reference to the object '1', which is an instance of class Fixnum. The '1' object is an instance of Fixnum which does not contain any instance variables. It is not different in any way from a reference to a new "Dog" instance.

Similarly, you can say x = Dog.new , then x is a reference to an instance of class Dog.

class Dog
  attr_accessor :legs   # this defines the 'legs' and 'legs=' methods!
end

x = Dog.new
x.instance_variables
=> []     # if you would assign legs=4 during "initialize", then it would show up here
x.legs = 4      # this is really a method call(!) to the 'legs' method
x.instance_variables   # get created when they are first assigned a value
 => [:legs] 

It does not matter if you pass such a reference to a method call, or to another class or just evaluate it by itself - Ruby knows it's an object reference, and looks inside the object and it's inheritance chain on how to resolve things.

Resolving Method Names

That was only the partial truth :) When interpreting x.legs , Ruby checks if there is a method in the class-inheritance chain of the object, which responds to that name 'legs'. It is not magically accessing the instance variable with the same name!

We can define a method 'legs' by doing "attr_reader :legs" or "attr_accessor :legs", or by defining the method ourselves.

class Dog
  def legs
     4     # most dogs have 4 legs, we don't need a variable for that
  end
end

x.legs     # this is a method call! it is not directly accessing a :legs instance variable!
 => 4
x.instance_variables
 => []     # there is no instance variable with name ":legs"

and if we try to implement it as a method and an instance variable, this happens: :)

class Dog
   attr_accessor :legs  # this creates "def legs" and "def legs=" methods behind the scenes
   def legs        # here we explicitly override the "def legs" method from the line above.
      4
   end
end

x = Dog.new
x.legs       # that's the method call we implemented explicitly
 => 4
x.legs = 3   # we can still assign something to the instance_variable via legs=
 => 3
x.legs       # the last definition of a method overrides previous definitions
             # e.g. it overrides the automatically generated "legs" method
 => 4 

attr_accessor :legs is just a short hand notation for doing this:

class Dog
  def legs
    @legs
  end 
  def legs=(value)
    @legs = value
  end
end

there is no magic way instance variable get automatically accessed. They are always accessed through a method, which can be overridden later.

I hope that makes sense to you

Upvotes: 2

Simon Perepelitsa
Simon Perepelitsa

Reputation: 20639

Both "type" and "popularity" are methods on "dog" instances. Their definition is as follows:

class Dog
  # getter
  def type
    @type
  end

  def popularity
    total.to_f/total_dogs
  end
end

This is roughly equivalent to:

class Dog
  attr_accessor :type

  def popularity
    total.to_f/total_dogs
  end
end

Note that attr_accessor is just a shortcut for defining a getter method. If you define a method yourself it is pointless to use attr_accessor:

class Dog
  attr_accessor :popularity

  # this will override getter defined by attr_accessor
  def popularity
    total.to_f/total_dogs
  end
end

Back to your question: @dog.type invokes type method on @dog which returns its instance variable; @dog.popularity invokes popularity method on @dog which does calculations (as defined by you) on the fly and returns the result. No magic here!

Upvotes: 0

BlackHatSamurai
BlackHatSamurai

Reputation: 23483

When you create an object, you don't need to use the @ symbol. The variable is the object. So if you have multiple dogs, you'd do:

myDog = Dog.new(brown)
yourDog = Dog.new(white)

From there, you can say:

yourDog.type #white
myDog.type #brown

What you would NOT do is:

@dog = Dog.new #myDog
@dog = Dog.new #yourDog

If you need multiple versions of an object, you just give them different names. So if you create multiple dogs and pass them to other objects, they will work. For example:

Say your owner class is:

Class Owner
def initialize(pet)
    puts "my pet is #{pet.type}"
end

Then using the instance variable would be:

me = Owner.new(myDog) #my pet is brown
you = Owner.new(yourDog) #my pet is white

Upvotes: 0

Neil Slater
Neil Slater

Reputation: 27207

When you do

@dog = Dog.new

You do two spearate things

1) Create an instance variable @dog for whatever object your code is currently inside

2) Instantiate a new instance of Dog (with all its methods and attributes) and assign a reference to it to @dog

@dog is a variable, that just happens to point at the Dog instance ("instance of class" generally same meaning as "object") you created at that point. You can set other variables to point to the same instance, and in Ruby this is generally how you pass data around. Objects contain instance variables, and those instance variables point to yet more objects.

Using the assignment operator (i.e "=") you can point a variable at any other object.

To answer your questions in turn:

When I am in the owner class and I call @dog.popularity how does it know the value of popularity for that instance?

You have to be careful in Ruby (and OO languages in general) to differentiate between class and object in your descriptions and questions. Ruby I'm assuming you are referring to a line of code in the Owner class, and that you intend it to work with an owner object. I'd also assume that @dog is an attribute you have added to Owner.

In which case, Ruby knows because @dog points to the Dog object that you added to owner. Each Dog object has its own copy of all of Dog's instance variables. You do need to take care in Ruby though, because variables point to objects, that you aren't simply passing in the same Dog object to all the owners (i.e. they all effectively share a single dog). So you need to understand when you are creating new instances (via new) and when you are simply handling existing references.

At runtime are all methods processed and then that instance just always is tied to the value at the time?

No. At runtime, basic Ruby will only perform the assignments that you have coded. Instance variables may not even exist until the code that assigns them has been run. If you use attr_reader etc methods, then the variables will at least exist (but will be nil unless you assign something during initialize)

Upvotes: 6

Related Questions