【问题标题】:How to define a Ruby Struct which accepts its initialization arguments as a hash?如何定义一个接受其初始化参数作为哈希的 Ruby 结构?
【发布时间】:2014-05-17 21:23:30
【问题描述】:

我有一种情况,我想创建一个接受许多参数的类,并在尽可能少的代码行中包含 setter 和 getter(为了可维护性)。我认为使用 Struct 会是一个好主意:

Customer = Struct.new(:id, :username, :first_name, :last_name, :address1, ...etc...)

Customer.new(123, 'joe', 'Joe', ...etc...)

但是,我不喜欢知道属性的确切顺序。我更喜欢 Ruby 2 的关键字参数功能:

class Customer
  attr_accessor :id, :username, :first_name, ...etc...
  def initialize(id:, username:, first_name:, last_name:, address1:, ...etc...)
    @id = id
    @username = username
    @first_name = first_name
    ...etc...
  end
end

Customer.new(id: 123, username: 'joe', first_name: 'Joe', ...etc...)

但是,将这些全部写出来需要更多的代码并且更加乏味。有没有办法像 Struct 这样的简写方式来实现同样的事情?

【问题讨论】:

  • 只是一个想法,您可能更喜欢 OpenStruct。

标签: ruby


【解决方案1】:

在 ruby​​ 2.5 中,您可以执行以下操作:

Customer = Struct.new(
  :id, 
  :username,
  :first_name, 
  keyword_init: true
)

Customer.new(username: "al1ce", first_name: "alice", id: 123)
=> #<struct Customer id=123, username="al1ce", first_name="alice">

参考:

【讨论】:

  • 我一直在等待这个。
【解决方案2】:

你不能这样做:

def initialize(hash)
  hash.each do |key, value|
    send("#{key}=", value)
  end
end

更新:

要指定默认值,您可以这样做:

def initialize(hash)
  default_values = {
    first_name: ''
  }
  default_values.merge(hash).each do |key, value|
    send("#{key}=", value)
  end
end

如果您想指定给定属性是必需的,但没有默认值,您可以这样做:

def initialize(hash)
  requried_keys = [:id, :username]
  default_values = {
    first_name: ''
  }
  raise 'Required param missing' unless (required_keys - hash.keys).empty?
  default_values.merge(hash).each do |key, value|
    send("#{key}=", value)
  end
end

【讨论】:

  • 一个缺点是您没有获得必需的关键字。
  • 我认为使用send 将哈希的所有键作为方法发送是危险。至少使用public_send(拒绝发送到私有方法)或根据允许的属性列表验证它们的密钥。
  • 如果你想避免使用send,你可以使用self[key] = value
  • 我发现自己在我参与的每个项目中都这样做,所以写了一个 gem (github.com/rohitpaulk/named_struct) 来让这更容易。它定义了一个继承自 Struct 的NamedStruct 类,并且只改变了初始化行为。
  • 此外,您还可以像这样调用构造函数:def initialize(*args) super(*args) end 这样可以避免命名参数的副作用。
【解决方案3】:

如果您不关心性能,可以使用OpenStruct

require 'ostruct'

user = OpenStruct.new(id: 1, username: 'joe', first_name: 'Joe', ...)
user.first_name

=> "Joe"

更多详情请见this


完全可以把它变成一个类并在上面定义方法:

class Customer < Openstruct
  def hello
    "world"
  end
end

joe = Customer.new(id: 1, username: 'joe', first_name: 'Joe', ...)
joe.hello

=> "world"

但同样,因为 OpenStructs 是使用 method_missingdefine_method 实现的,所以它们非常慢。我会接受 BroiSatse 的回答。如果你关心所需的参数,你应该这样做

def initialize(params = {})   
    if missing_required_param?(params)
      raise ArgumentError.new("Missing required parameter")   
    end   
    params.each do |k,v|
      send("#{k}=", v)   
    end 
end

def missing_required_params?(params)
  ...
end

【讨论】:

  • 这很好,不知道这个。 :)
  • 这和我想要的很接近,但是你能不能把它做成一个类并在上面定义方法?
【解决方案4】:

另外,你可以依赖Struct本身的构造函数。

def initialize(*args)
  super(*args)
  # put your magic here!
end

这样可以避免命名参数等的副作用。

【讨论】:

    【解决方案5】:

    主题的变化,但更精致,适用于任何红宝石

    class Struct
      module HashConstructable
        def from_hash hash
          rv = new
          hash.each do |k, v|
            rv.send("#{k}=", v)
          end
          rv
        end
        # alias_method :[], :from_hash
      end
    end
    

    然后

    class MyStruct < Struct.new(:foo, :boo)
      extend Struct::HashConstructable
    end
    

    这样你就两全其美了——没有有趣的名字冲突和副作用,很明显你想做的时候想做:

    MyStruct.from_hash(foo: 'foo')
    

    完全按照您的想法行事。有了更多可能的副作用,但语法更好,您可以添加 alias_method :[], :from_hash 部分,这允许您:

    MyStruct[foo: 'foo']
    

    这也很好,因为它让(我)想起了 Hash[] 构造函数,它从非散列的东西中创建散列。

    【讨论】:

      【解决方案6】:

      这是您可以使用的一种方法。

      class A
        def initialize(h)
          h.each do |k, v|
            instance_variable_set("@#{k}", v)
            create_method("#{k}=") { |v|instance_variable_set("@#{k}", v ) }
            create_method("#{k}")  { instance_variable_get("@#{k}") }
          end 
        end
      end    
      
      def create_method(name, &block)
        self.class.send(:define_method, name, &block)
      end
      
      a = A.new(apple: 1, orange: 2)
      a.apple     #=> 1
      a.apple = 3
      a.apple     #=> 3
      a.orange    #=> 2
      

      create_method 直接来自Module#define_method 的文档。

      【讨论】:

        【解决方案7】:

        您可以使用 Struct 并将代码量减少到最低限度,方法是向您的结构添加工厂方法(此处称为 build)以及必要时向您的结构添加验证方法

        Struct.new("Example",:a,:b) do
            def build(a:, b:nil)
                s = Struct::Example.new(a,b)
                s.validate
                return s
            end
            def validate
                unless a == 'stuff' || a == 'nonsense'
                    raise ValidationError, "broken"
                end
            end
         end
        
         m = Struct.Example.build(a: 'stuff')
        

        其中 validate 的目的是让诸如检查字符串之类的东西具有某些值,而不是仅仅依赖于所需的参数检查。

        现在你只需要记住一次顺序,当你编写构建方法时

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2012-03-20
          • 2015-06-08
          • 1970-01-01
          • 2019-02-17
          • 1970-01-01
          • 2011-12-17
          • 2010-10-19
          相关资源
          最近更新 更多