如何检查ruby方法中可选参数的默认值?

时间:2016-01-12 18:59:04

标签: ruby metaprogramming introspection

给出一个班级,

class MyClass
  def index(arg1, arg2="hello")
  end
end

是否可以通过arg2等某些方法获取Class#instance_method的默认值?

2 个答案:

答案 0 :(得分:5)

似乎我们只能通过访问binding方法来检查方法参数的值。使用Tracepoint类,我们可以获得这样的绑定对象,然后检查所有optional参数的值。

我们需要确保只使用必需参数调用所需方法,以便为默认参数指定默认值。

下面是我尝试这样做 - 它适用于实例方法和类方法。为了调用实例方法,我们需要实例化类 - 如果构造函数需要参数,那么创建对象会变得棘手。为了绕过这个问题,这段代码动态地创建了一个给定类的子类,并为它定义了一个无参数的构造函数。

class MyClass

  # one arg constructor to make life complex
  def initialize param
  end

  def index(arg1, arg2="hello", arg3 = 1, arg4 = {a:1}, arg5 = [1,2,3])
    raise "Hi"  # for testing purpose
  end

  def self.hi(arg6, arg7="default param")
  end
end

def opt_values(clazz, meth)
    captured_binding = nil

    TracePoint.new(:call) do |tp|
        captured_binding = tp.binding
    end.enable {
        # Dummy sub-class so that we can create instances with no-arg constructor
        obj = Class.new(clazz) do
            def initialize
            end
        end.new

        # Check if it's a class method
        meth_obj = clazz.method(meth) rescue nil

        # If not, may be an instance method.
        meth_obj = obj.method(meth) rescue nil if not meth_obj

        if meth_obj
            params = meth_obj.parameters
            optional_params = params.collect {|i| i.last if i.first == :opt}.compact
            dummy_required_params = [""] * (params.size - optional_params.size)

            # Invoke the method, and handle any errors raise            
            meth_obj.call *dummy_required_params rescue nil

            # Create a hash for storing optional argument name and its default value
            optional_params.each_with_object({}) do |i, hash|
                hash[i] = captured_binding.local_variable_get(i)
            end
        end
    }
end

p opt_values MyClass, :index
#=> {:arg2=>"hello", :arg3=>1, :arg4=>{:a=>1}, :arg5=>[1, 2, 3]}
p opt_values MyClass, :hi
#=> {:arg7=>"default param"}

答案 1 :(得分:4)

我认为没有提供此类实用程序的原因是,必须分配默认参数的值时会对其进行评估。因此,试图评估它们可能会产生额外的影响。

让我告诉你一个关于俄罗斯政府核计划的故事:

  

前一段时间,他们聘请了超级核心的俄罗斯黑客提出了一个既有防错又有大型安全的解决方案,可以启动所有可用的核武器或只是运行模拟。他们决定创建一个名为launch_all_nukes的方法,该方法可选择接受关键字参数simulation_number:。他们将实现加载到REPL并删除了代码,以便敌人的间谍永远无法知道它是如何工作的。

  

过去几年的每一天,值得信赖的专家伊万都会前往一个千兆位的秘密地点,在那里他坐在看起来像是一个普通人的前面,并评估俄罗斯联邦幸存下来的机会。

$: launch_all_nukes simulation_number: 1
     

...
  只是另一个常规日。

$: launch_all_nukes simulation_number: 2
     

...

$: launch_all_nukes simulation_number: 3
     

...
  虽然这些平均需要25分钟,但有时会感觉像是几个小时。

$: launch_all_nukes simulation_number: 4
     

...
  盯着屏幕。只是另一个常规日。另一个...定期......天...

$: launch_all_nukes simulation_number: 5
     

...
  Tik-tok,tik-tok,tik-tok ...想知道午餐可能会有什么?

$: launch_all_nukes simulation_number: 6
     

...
  最后! 7总是最有趣的。它是唯一一个有时显示有0.03% - 0.08%未完全消灭的可能性的人。伊万不知道数字7背后是什么。或者其他任何模拟。他只是运行命令并等待。但肯定的是,7号是他在其他沉闷的任务中带来一点欢乐和兴奋的一个。   Aaaaaaand,去!

$: launch_all_nukes simulation_number: 7
     

...
  0%。和所有其他人一样。多么规律。

$: launch_all_nukes simulation_number: 8
     

...
  实际上这有关系吗?为什么一个国家会优于其他国家?人类生命本身是否有价值?地球作为一个整体具有内在价值吗?只是一块漂浮在无尽宇宙中的岩石奇观......

$: launch_all_nukes simulation_number: 9
     

...
  发生了什么?伊万曾经是一个伟大的开发人员。现在他只是盯着一个控制台,不时地运行重复的命令......这是进步的感觉......

$: launch_all_nukes simulation_number: 10
     

...
  等一下...... simulation_number:的默认值是多少?它是什么?当然,实现有一些检查,如__actually_launch_nukes__ if simulation_number.nil?。但它真的是nil吗?或者是别的什么? ...

$: launch_all_nukes simulation_number: 11
     

...
  像一个重复的耳虫,这个小小的问题从未离开过他的脑海...... 它是什么? ......他从不担心意外地危害世界,因为他看到没有参数提示运行launch_all_nukes对于三种不同的访问密钥,他都不知道。

$: launch_all_nukes simulation_number: 12
     

...
  Ivan之前在控制台中运行过普通的Ruby命令。无论如何,它只是一个常规的... ...只是运行一个简单的内省方法......他知道他不被允许这样做......但没有人会知道,对吧?甚至没有人知道这个程序是如何工作的......啊......

$: launch_all_nukes simulation_number: 13
     

...
  13和14是最糟糕的! 13通常需要一个半小时。 14甚至更长。该死的,伊万渴望,只是一个微不足道的小信息,让他的思绪至少保持几分钟...让我们做到这一点!

$: method(:launch_all_nukes).default_value_for(:simulation_number)
     

...
  当突然意识到他时,伊万一动不动地冻结了。他现在知道默认值是什么。但为时已晚......

这是一个穷人的尝试:

argument_name = 'arg2'

origin_file, definition_line = MyClass.instance_method(:index).source_location
method_signature = IO.readlines(origin_file)[definition_line.pred]
eval(method_signature.match(/#{argument_name}\s*[=:]\s*\K[^\s),]*/)[0]) # => "hello"

显然非常容易出错:

  • 不适用于原生方法
  • 不适用于REPL中定义的方法
  • 您需要阅读权限
  • 正则表达式不处理很多情况(比如更复杂的默认值,其中包含空格,),),但这可以改进。

如果有人想出一个纯粹内省的解决方案,那就去吧。