Ben Hall
Ben Hall

Reputation: 2027

How can one set property values when initializing an object in Ruby?

Given the following class:

class Test
  attr_accessor :name
end

When I create the object, I want to do the following:

t = Test.new {name = 'Some Test Object'}

At the moment, it results in the name attribute still being nil.

Is that possible without adding an initializer?

Upvotes: 20

Views: 13950

Answers (8)

ib.
ib.

Reputation: 28954

There is a general way of doing complex object initialization by passing a block with necessary actions. This block is evaluated in the context of the object to be initialized, so you have an easy access to all instance variables and methods.

Continuing your example, we can define this generic initializer:

class Test
  attr_accessor :name

  def initialize(&block)
    instance_eval(&block)
  end 
end

and then pass it the appropriate code block:

t = Test.new { @name = 'name' }

or

t = Test.new do
  self.name = 'name'
  # Any other initialization code, if needed.
end

Note that this approach does not require adding much complexity to the initialize method, per se.

Upvotes: 8

devpuppy
devpuppy

Reputation: 832

As previously mentioned, the sensible way to do this is either with a Struct or by defining an Test#initialize method. This is exactly what structs and constructors are for. Using an options hash corresponding to attributes is the closest equivalent of your C# example, and it's a normal-looking Ruby convention:

t = Test.new({:name => "something"})
t = Test.new(name: "something") # json-style or kwargs

But in your example you are doing something that looks more like variable assignment using = so let's try using a block instead of a hash. (You're also using Name which would be a constant in Ruby, we'll change that.)

t = Test.new { @name = "something" }

Cool, now let's make that actually work:

class BlockInit
  def self.new(&block)
    super.tap { |obj| obj.instance_eval &block }
  end
end

class Test < BlockInit
  attr_accessor :name
end

t = Test.new { @name = "something" }
# => #<Test:0x007f90d38bacc0 @name="something">
t.name
# => "something"

We've created a class with a constructor that accepts a block argument, which is executed within the newly-instantiated object.

Because you said you wanted to avoid using initialize, I'm instead overriding new and calling super to get the default behavior from Object#new. Normally we would define initialize instead, this approach isn't recommended except in meeting the specific request in your question.

When we pass a block into a subclass of BlockInit we can do more than just set variable... we're essentially just injecting code into the initialize method (which we're avoiding writing). If you also wanted an initialize method that does other stuff (as you mentioned in comments) you could add it to Test and not even have to call super (since our changes aren't in BlockInit#initialize, rather BlockInit.new)

Hope that's a creative solution to a very specific and intriguing request.

Upvotes: 1

Nicolas Guillaume
Nicolas Guillaume

Reputation: 8434

ok,

I came up with a solution. It uses the initialize method but on the other hand do exactly what you want.

class Test
  attr_accessor :name

  def initialize(init)
    init.each_pair do |key, val|
      instance_variable_set('@' + key.to_s, val)
    end
  end

  def display
    puts @name
  end

end

t = Test.new :name => 'hello'
t.display

happy ? :)


Alternative solution using inheritance. Note, with this solution, you don't need to explicitly declare the attr_accessor!

class CSharpStyle
  def initialize(init)
    init.each_pair do |key, val|
      instance_variable_set('@' + key.to_s, val)
      instance_eval "class << self; attr_accessor :#{key.to_s}; end"
    end
  end
end

class Test < CSharpStyle
  def initialize(arg1, arg2, *init)
    super(init.last)
  end
end

t = Test.new 'a val 1', 'a val 2', {:left => 'gauche', :right => 'droite'}
puts "#{t.left} <=> #{t.right}"

Upvotes: 18

Zach Dennis
Zach Dennis

Reputation: 1784

If you don't want to override initialize then you'll have to move up the chain and override new. Here's an example:

class Foo
  attr_accessor :bar, :baz

  def self.new(*args, &block)
    allocate.tap do |instance|
      if args.last.is_a?(Hash)
        args.last.each_pair do |k,v|
          instance.send "#{k}=", v
        end
      else
        instance.send :initialize, *args
      end
    end
  end

  def initialize(*args)
    puts "initialize called with #{args}"
  end
end

If the last thing you pass in is a Hash it will bypass initialize and call the setters immediately. If you pass anything else in it will call initialize with those arguments.

Upvotes: 0

saihgala
saihgala

Reputation: 5774

You could do this.

class Test
   def not_called_initialize(but_act_like_one)
        but_act_like_one.each_pair do |variable,value|
            instance_variable_set('@' + variable.to_s, value)
            class << self
                    self
            end.class_eval do
                    attr_accessor variable
            end
        end
   end
end

(t = Test.new).not_called_initialize :name => "Ashish", :age => 33
puts t.name #=> Ashish
puts t.age  #=> 33

One advantage is that you don't even have to define your instance variables upfront using attr_accessor. You could pass all the instance variables you need through not_called_initialize method and let it create them besides defining the getters and setters.

Upvotes: 0

AJP
AJP

Reputation: 28483

Would need to subclass Test (here shown with own method and initializer) e.g.:

class Test
  attr_accessor :name, :some_var

  def initialize some_var
    @some_var = some_var
  end

  def some_function
    "#{some_var} calculation by #{name}"
  end
end

class SubClassedTest < Test
  def initialize some_var, attrbs
    attrbs.each_pair do |k,v|
      instance_variable_set('@' + k.to_s, v)
    end
    super(some_var)
  end
end

tester = SubClassedTest.new "some", name: "james"
puts tester.some_function

outputs: some calculation by james

Upvotes: 0

ry.
ry.

Reputation: 8055

As mentioned by others, the easiest way to do this would be to define an initialize method. If you don't want to do that, you could make your class inherit from Struct.

class Test < Struct.new(:name)
end

So now:

>> t = Test.new("Some Test Object")
=> #<struct Test name="Some Test Object">
>> t.name
=> "Some Test Object"

Upvotes: 9

Matchu
Matchu

Reputation: 85802

The code you're indicating is passing parameters into the initialize function. You will most definitely have to either use initialize, or use a more boring syntax:

test = Test.new
test.name = 'Some test object'

Upvotes: 0

Related Questions