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

前端 未结 8 2080
清歌不尽
清歌不尽 2021-01-01 16:09

Given the following class:

class Test
  attr_accessor :name
end

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

t = Test         


        
相关标签:
8条回答
  • 2021-01-01 16:14

    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.

    0 讨论(0)
  • 2021-01-01 16:16

    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'
    
    0 讨论(0)
  • 2021-01-01 16:16

    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

    0 讨论(0)
  • 2021-01-01 16:20

    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.

    0 讨论(0)
  • 2021-01-01 16:21

    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.

    0 讨论(0)
  • 2021-01-01 16:28

    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.

    0 讨论(0)
提交回复
热议问题