skip to content

In Ruby, what happens step by step when you call BankAccount.new("Ana", 100), and what does that call return?

level: juniorimportance: must knowfreq 72%

answer

  1. two steps inside Class#new
  2. allocate: blank object, no ivars
  3. initialize is private automatically
  4. args, keywords and block forwarded
  5. initialize's return value discarded

basics

~10 s

Class#new first calls allocate to get a blank BankAccount, then calls that object's private initialize with the same arguments and block, and finally returns the new object, whatever initialize itself returned.

solid answer

~30 s

`new` is an ordinary instance method of `Class`. `BankAccount.new("Ana", 100)` first calls `BankAccount.allocate`, which creates an instance with no instance variables, then invokes `initialize("Ana", 100)` on it, passing positional arguments, keyword arguments and any block through unchanged. When `initialize` finishes, `new` returns the object it allocated; the value of `initialize`'s last expression, or of an explicit `return` in it, is thrown away. Ruby makes any method named `initialize` private automatically, so `account.initialize(0)` from outside raises `NoMethodError` ("private method 'initialize' called"). If `initialize` raises, `new` raises too and the caller never receives a half-built object.

code

ruby · 16 lines
ruby
class BankAccount
  def initialize(owner, balance)
    @owner = owner
    @balance = balance
    nil                          # return value is discarded
  end
end

account = BankAccount.new("Ana", 100)
account.class                    # => BankAccount

account.initialize("Bo", 0)
# NoMethodError: private method 'initialize' called for an instance of BankAccount

blank = BankAccount.allocate     # initialize never runs
blank.instance_variables         # => []

go deeper

for a junior

Recall the order: allocate, then initialize, then the new object comes back. Say that initialize is private and that its return value does not matter.

for a middle

Explain that new is Class#new found by normal lookup, that it forwards arguments, keywords and the block, and that allocate alone skips initialize, leaving every instance variable nil.

for a senior

Show judgment about construction: validate in initialize so a raise means no object escapes, avoid allocate outside deserialization code, and reach for a named reset method rather than re-running initialize.

for a principal

Frame construction as a contract the whole codebase leans on: when a class overrides self.new or skips initialize, every caller's assumptions change, so such overrides need a strong reason and clear documentation.

## Construction is two method calls In Ruby there is no constructor keyword. Writing `BankAccount.new("Ana", 100)` sends the message `new` to the object `BankAccount`, and because `BankAccount` is an instance of `Class`, the method that runs is **`Class#new`**. Its documented job is short: 1. **Allocate.** It calls `allocate` on the class. `Class#allocate` asks the class's allocator for memory and returns a fresh instance whose class is `BankAccount`. The object has no instance variables yet. 2. **Initialize.** It calls `initialize` on that new object, forwarding every argument it received: positional arguments, keyword arguments and the block. 3. **Return.** It returns the allocated object. ```ruby class BankAccount def initialize(owner, balance) @owner = owner @balance = balance :ready # ignored by new end end BankAccount.new("Ana", 100) # => #<BankAccount:0x... @owner="Ana", @balance=100> ``` ## What initialize is and is not `initialize` is a normal instance method with a special name. A few rules follow from that: - **It is private automatically.** When a class defines a method named `initialize` (the same applies to `initialize_copy`, `initialize_clone`, `initialize_dup` and `respond_to_missing?`), CRuby gives it private visibility even if the `def` sits under a `public` section. Calling `account.initialize(0)` from outside raises `NoMethodError` with a message like `private method 'initialize' called for an instance of BankAccount`. - **Its return value is ignored.** `new` returns the allocated object no matter what `initialize` evaluates to, so returning `nil`, `false` or a different object from `initialize` has no effect on the caller. - **It receives the block.** A block given to `new` is passed on, so `BankAccount.new("Ana", 100) { |a| ... }` can `yield` inside `initialize`. - **It is inherited.** A class without its own `initialize` uses the nearest ancestor's; `BasicObject#initialize` accepts no arguments, which is why `Object.new(1)` raises `ArgumentError`. - **Raising aborts construction.** If `initialize` raises, `new` propagates the exception and the caller gets no reference to the object. ## allocate on its own `Class#allocate` is public, and it skips `initialize` entirely: | Call | Runs `initialize`? | Instance variables afterwards | |---|---|---| | `BankAccount.new("Ana", 100)` | yes | `@owner`, `@balance` set | | `BankAccount.allocate` | no | none; `@balance` reads `nil` | Libraries that rebuild objects from stored data use this split: they allocate a blank instance and then fill it in without re-running validation. Application code almost never needs it, and an allocated-but-uninitialized `BankAccount` breaks every invariant `initialize` was meant to establish. Some core classes have no allocator at all. `Integer`, `Float` and `Symbol` undefine theirs, so `Integer.allocate` raises `TypeError` (`allocator undefined for Integer`), and `Integer.new` is undefined, raising `NoMethodError`. ## Where interviewers push - **"Is `new` a keyword?"** No. It is `Class#new`, found through normal method lookup on the class object. A class can define its own `self.new`, and core classes such as `Struct` do exactly that. - **"Why can't I call `initialize` again to reset an object?"** Because it is private. If an object needs a reset, give it a public method with a meaningful name, such as `reset!`, and share code with `initialize` through a private helper. - **"Does `new` run `initialize` of the superclass?"** Only if the subclass's `initialize` calls `super`, or if the subclass defines no `initialize` of its own. - **Ruby 4.0 note.** Ruby 4.0 made `Class#new` faster, especially with keyword arguments, without changing its behaviour; the allocate-then-initialize contract is the same one Ruby has always had. ## A mental checklist When you read `SomeClass.new(args)`, ask four questions: which `new` runs (usually `Class#new`), which `initialize` runs (the nearest one in the ancestors), what arguments and block reach it, and what happens if it raises. The answer to "what does it return" is almost always the fresh instance.

  • In Ruby, if BankAccount#initialize raises ArgumentError for a negative balance, what does the caller of new receive?
    The `ArgumentError` propagates out of `new`, so the assignment `account = BankAccount.new("Ana", -5)` never happens. Unless `initialize` had already stored `self` somewhere, the allocated object becomes unreachable and is garbage collected. That is why validating in `initialize` is the usual way to guarantee that every instance a caller holds passed its checks.
  • In Ruby, why does Integer.new raise NoMethodError while Integer.allocate raises TypeError?
    CRuby undefines `new` on `Integer`'s singleton class, so the method is simply not there and lookup fails with `NoMethodError`. `allocate` still exists, inherited from `Class`, but `Integer` has no allocator function, so it raises `TypeError` with `allocator undefined for Integer`. Integers are created by literals and arithmetic, never by allocation.
  • In Ruby, does a block passed to BankAccount.new reach initialize?
    Yes. `Class#new` passes the block it received on to `initialize`, so `initialize` can call `yield` or capture it with a `&block` parameter. A common use is a configuration block: `initialize` sets defaults and then yields `self` so the caller can adjust them before the object is returned.

saying these in an interview costs you the question

  • new is a keyword that builds the object before calling any method
  • new returns whatever the last expression of initialize evaluates to
  • initialize is public, so you can call it again to reset an object
  • allocate also runs initialize, just without arguments
  • A block given to new is dropped before initialize runs