skip to content

Attribute Accessors

Ruby's attr_reader, attr_writer and attr_accessor generate getters and setters, and a setter call inside the class needs self. or it assigns a local. Interviewers test that trap.

on this pageshow

explore

questions

4

In Ruby, which methods do attr_reader, attr_writer and attr_accessor define, and what does each call return?

level: juniorimportance: must knowfreq 78%

answer

  1. name and name= methods
  2. reader returns @name, nil if unset
  3. no instance variable created
  4. returns an Array of Symbols
  5. return value changed in Ruby 3.0

basics

~10 s

attr_reader :temp defines temp, returning @temp; attr_writer :temp defines temp=, assigning @temp; attr_accessor defines both. Since Ruby 3.0 each call returns an Array of the defined method names as Symbols.

solid answer

~40 s

The three are methods on `Module` that generate instance methods from names given as Symbols or Strings. `attr_reader :target` defines `target`, which returns `@target`, or `nil` if it was never assigned. `attr_writer :target` defines `target=`, which assigns `@target`. `attr_accessor :target` defines both. Nothing is assigned at definition time: the instance variable appears only when the writer or `initialize` sets it. Since Ruby 3.0 each call returns an Array of the method names it defined, so `attr_accessor :target, :mode` returns `[:target, :target=, :mode, :mode=]`; before 3.0 it returned `nil`. The generated methods take the visibility of the section they are written in, and they do no validation: a writer stores whatever it is given.

code

ruby · 14 lines
ruby
class Thermostat
  p attr_reader(:room)             # => [:room]
  p attr_accessor(:target, :mode)  # => [:target, :target=, :mode, :mode=]

  def initialize(room)
    @room = room
  end
end

t = Thermostat.new("lab")
t.target          # => nil, never assigned
t.target = 21
t.target          # => 21
t.room = "attic"  # NoMethodError: no room= writer was defined

go deeper

for a junior

Recall the three helpers and the methods each defines: name, name=, or both, reading and writing the matching instance variable.

for a middle

Explain that nothing is assigned at definition, that readers return nil until then, and that since Ruby 3.0 the calls return the defined names as Symbols.

for a senior

Show when generated methods are the wrong tool: validation in writers, copies or computed values in readers, and names ending in a question mark.

for a principal

Weigh how freely a codebase should generate writers at all, since every public writer is a way to put an object into a state nobody validated.

## Methods that write methods Ruby objects expose state only through methods. Writing a reader and a writer by hand for every instance variable is repetitive, so `Module` provides three class-body helpers that generate them: | Call | Defines | Behaviour of the generated method | |---|---|---| | `attr_reader :target` | `target` | returns `@target`, or `nil` if it was never assigned | | `attr_writer :target` | `target=` | assigns its argument to `@target` | | `attr_accessor :target` | `target` and `target=` | both of the above | They are ordinary method calls that run while the class body is evaluated. Each accepts any number of names, as Symbols or Strings: ```ruby class Thermostat attr_reader :room attr_accessor :target, :mode end ``` ## What the generated code amounts to For a single name, `attr_accessor :target` behaves like this hand-written pair: ```ruby def target @target end def target=(value) @target = value end ``` The generated versions are not literally Ruby source: CRuby registers them as special method types that read or write the instance variable directly. For a caller there is no visible difference, and `instance_methods` lists them like any other method. Because they are ordinary methods, a subclass can override them, and a later `def target` in the same class replaces the generated reader. ## What they do not do - **They do not create the instance variable.** Defining the reader only teaches the class how to read `@target`. Until something assigns it, the reader returns `nil`. That is why the usual pattern pairs `attr_*` with assignments in `initialize`. - **They do not validate.** `attr_writer :target` stores whatever it receives, including `nil` or a String. If a value must be checked, write the writer by hand. - **They do not copy.** The reader hands back the very object stored in the variable, so a mutable object such as an Array can be changed by the caller. - **They do not accept every name.** A name must be usable as a local variable or constant name, so `attr_reader :heating?` raises `NameError`. ## The return value Since **Ruby 3.0**, each call returns an Array of the Symbols it defined: 1. `attr_reader :room` returns `[:room]`. 2. `attr_writer :target` returns `[:target=]`. 3. `attr_accessor :target, :mode` returns `[:target, :target=, :mode, :mode=]`. Before Ruby 3.0 these calls returned `nil`. The change exists so the result can be passed straight to another method that takes method names, most commonly a visibility modifier that makes the generated writer non-public. Older code that wrote the accessor and its visibility on two lines still works; the one-line form is simply available now. `attr` is a fourth, older spelling. `attr :room` is the same as `attr_reader :room`. The two-argument form `attr :room, true` still works but is deprecated, and warns in verbose mode when deprecation warnings are enabled. ## Visibility follows the section The generated methods take the default visibility in effect where the call appears. An `attr_reader` written below a bare `private` line produces a private reader, exactly as a `def` in that position would. ## When to write the methods by hand - The writer must **validate or convert** its argument, for example rejecting a target outside 5 to 30 degrees. - The reader must return a **copy** or a **computed** value rather than the stored object. - The method name should end in **`?`**, which the `attr_*` helpers cannot produce. For a plain field, the generated methods are shorter, conventional and read at a glance. RuboCop's `Style/TrivialAccessors` cop suggests replacing hand-written methods that only read or assign an instance variable of the same name with `attr_*` calls.

  • In Ruby, why did the Ruby 3.0 change to attr_accessor's return value matter?
    Because the returned Array of Symbols can be fed to any method that takes method names. The common case is a visibility modifier wrapping the call, so a class can declare a reader public and its writer non-public on one line each, instead of repeating the names in a separate statement.
  • In Ruby, does attr_accessor :target make @target appear in instance_variables right after Thermostat.new?
    No. `attr_accessor` only defines the `target` and `target=` methods. `@target` exists on an instance only after something assigns it, usually `initialize` or a call to the writer, so a fresh object lists only the variables its `initialize` actually set.

saying these in an interview costs you the question

  • attr_accessor creates the instance variable and sets it to nil
  • attr_accessor returns nil on Ruby 4.0
  • attr_writer defines both the reader and the writer
  • A generated writer validates the type of its argument
  • A reader generated by attr_reader returns a copy of the stored object
open as a page

Inside a Ruby Thermostat method, why does target = 22 leave the target unchanged even though attr_accessor :target exists?

level: juniorimportance: must knowfreq 76%

basics

~10 s

Ruby parses a bare target = 22 as assignment to a new local variable, not a call to target=. Writer methods always need a receiver, so inside the class write self.target = 22.

open as a page

In Ruby, why does attr_reader :heating? raise, and how do you give a Thermostat a heating? predicate instead?

level: middleimportance: should knowfreq 40%

basics

~10 s

attr_* names must be valid local variable or constant names, so heating? raises NameError (invalid attribute name). Write the predicate by hand, def heating? = @heating, or alias it to a generated heating reader.

open as a page

A Ruby Thermostat exposes attr_reader :schedule over an internal Array; why can callers still corrupt it, and how do you stop them?

level: seniorimportance: should knowfreq 38%

basics

~20 s

attr_reader returns the stored Array itself, so a caller's schedule << entry mutates the Thermostat's own state. Return a copy from a hand-written reader, freeze what you hand out, or expose narrower methods instead of the collection.

open as a page