Skip to the content.

Initializing Instance Variables: Self and Attributes

Recently, I learned that you can initialize an instance variable in two different ways. I already knew that instance variables were commonly initialized with an @ sign (as in, @instance_variable), but what’s new to me is that you can also do so with the prefix self. (as in, self.instance_variable).

For example, say I want to write a new class, one called Books that will store titles for each instance.

Of course, we start with a test:

require 'books'

describe Books do
  let(:new_book) {described_class.new("Nineteen Eighty-Four")}

  it "begins with a title" do
    expect(new_book.title).to eq("Nineteen Eighty-Four")
  end
end

In the past, I would have tried to make the test pass by writing something like this:

class Books
  attr_reader :title

  def initialize(title)
    @title = title
  end
end

And that’s fine. The tests pass.

But what if I wanted to try out this new naming convention? Well, I would start by replacing the @ with self.:

class Books
  attr_reader :title

  def initialize(title)
    self.title = title
  end
end

But this on its own fails the tests:

=> NoMethodError: undefined method for 'title='. 

So what’s going wrong? Is this whole alternative naming convention just a sham? Nope.

First of all - special thanks to Diana and my other fellow student apprentices for helping me figure this out.

It turns out that initializing instance variables with self. requires more than just an attr_reader. It also needs an attr_writer or, more elegantly, just an attr_accessor (which combines attr_reader and attr_writer).

Using attr_writer lets you set the value of an attribute (in this case, the instance variable ‘title’), and attr_reader lets you call the value of that attribute elsewhere.

To help flesh this out, say we wanted an option to add an author to our instances. We don’t want to require that new instances pass in an author argument when they’re initialized (since having an author is optional), so we create a separate method to set the value of author. Test first:

require 'books'

describe Books do
  let(:new_book) {described_class.new("1984")}

  it "begins with a title" do
    expect(new_book.title).to eq("1984")
  end

  it "allows authors to be added" do
    expect(new_book.author=("George Orwell")).to eq("George Orwell")
  end
end

Put in the method to write our author attribute:

class Books
  attr_accessor :title

  def initialize(title)
    self.title = title
  end

  def author=(author)
    @author = author
  end
end

The tests pass. Now create tests for a method to read that attribute:

require 'books'

describe Books do
  let(:new_book) {described_class.new("1984")}

  it "begins with a title" do
    expect(new_book.title).to eq("1984")
  end

  it "allows authors to be added" do
    expect(new_book.author=("George Orwell")).to eq("George Orwell")
  end

  it "allows authors to be called" do
    new_book.author=("George Orwell")
    expect(new_book.author).to eq("George Orwell")
  end
end

Make it pass:

class Books
  attr_accessor :title

  def initialize(title)
    self.title = title
  end

  def author=(name)
    @author = name
  end

  def author
    @author
  end
end

Everything works.

But, as I’m sure you might have already guessed, there is a better way! What we’re doing here is writing and reading attributes for instances of our Books class. And, wouldn’t you know it, that’s exactly where attr_accessor is most helpful. We could replace the whole class we just completed like this:

class Books
  attr_accessor :title, :author

  def initialize(title)
    self.title = title
  end
end

All the tests still pass.

When you use the self. technique of initializing an instance variable with attr_accessor, you’re carrying out a very similar process. The key diffence is that attr_writer is using the argument you pass in when you initialize a new instance of the class.

Bonus: self.instance_variable will return an undefined method error for the instance variable if you include a typo, while @instance_variable with a typo will return nil (but run just fine). Maybe this approach can help isolate and rectify errors more quickly by making them more explicit.


[See my newer post about how to use the @instance_variable naming format with attr_reader to avoid the risks associated with typos, and also avoid unwanted code leaks]