最近在做重构的时候干了这样的一件事情,把
def resource_not_found
...
end
改成了
def resource_not_found message: nil, id: nil
...
end
然后像下面的这样的代码就会出现异常了
rescue_from Exception, with: :rescue_not_found
从图片可以看出,报错的代码是在 Rails 的源码中,也就是说我的代码违背了 Rails 的 convention。
直接原因是handler.call(exception)
误传了参数,但是根据handler.arity != 0
说明是需要参数的。奇怪?
其实对比一下我重构之后的方法也就比较明显了,重构之后resource_not_found
需要接受 optional 的 keyword 参数。而源码中明显是在按照 positional params 的方式在调用。
所以,出现了ArgumentError
。
这里引伸出一个问题,像上面重构的方法,不仅仅是用在rescue_from
,也会在别的场景下单独调用。也就是说我们暂时认为这个重构是 ok 的,那么我们应该怎么处理rescue_from
带来的问题呢?
根据源码来看
# Exceptions raised inside exception handlers are not propagated up.
def rescue_from(*klasses, &block)
options = klasses.extract_options!
unless options.has_key?(:with)
if block_given?
options[:with] = block
else
raise ArgumentError, "Need a handler. Supply an options hash that has a :with key as the last argument."
end
end
klasses.each do |klass|
key = if klass.is_a?(Class) && klass <= Exception
klass.name
elsif klass.is_a?(String)
klass
else
raise ArgumentError, "#{klass} is neither an Exception nor a String"
end
# put the new handler at the end because the list is read in reverse
self.rescue_handlers += [[key, options[:with]]]
end
end
# Tries to rescue the exception by looking up and calling a registered handler.
def rescue_with_handler(exception)
if handler = handler_for_rescue(exception)
handler.arity != 0 ? handler.call(exception) : handler.call
true # don't rely on the return value of the handler
end
end
def handler_for_rescue(exception)
# We go from right to left because pairs are pushed onto rescue_handlers
# as rescue_from declarations are found.
_, rescuer = self.class.rescue_handlers.reverse.detect do |klass_name, handler|
# The purpose of allowing strings in rescue_from is to support the
# declaration of handler associations for exception classes whose
# definition is yet unknown.
#
# Since this loop needs the constants it would be inconsistent to
# assume they should exist at this point. An early raised exception
# could trigger some other handler and the array could include
# precisely a string whose corresponding constant has not yet been
# seen. This is why we are tolerant to unknown constants.
#
# Note that this tolerance only matters if the exception was given as
# a string, otherwise a NameError will be raised by the interpreter
# itself when rescue_from CONSTANT is executed.
klass = self.class.const_get(klass_name) rescue nil
klass ||= klass_name.constantize rescue nil
exception.is_a?(klass) if klass
end
case rescuer
when Symbol
method(rescuer)
when Proc
if rescuer.arity == 0
Proc.new { instance_exec(&rescuer) }
else
Proc.new { |_exception| instance_exec(_exception, &rescuer) }
end
end
end
end
根据rescue_from
的源码来看,我们会得到一个数组,[[key, options[:with]]。这里的key
是 Exception 的名字,如“ArgumentError”;options[:with]
就是我们在调用rescue_from
的时候传给:with
的值,如果是 block 的话那么就是 block 本身。
所以在handler_for_rescue
中,根据options[:with]
是 Symbol 还是 Block 的不同,会有不同的处理方式来得到 handler。如果是 Symbol,那么直接返回了method(rescuer)
,如果是 Block 那么回重新用 Proc 来包装。
在rescue_with_handler
中会根据handler.arity != 0
来决定 handler 的调用方式。根据上面的分析,当我们以 Symbol 的方式来调用,我们会得到这样的 handler,method(:resource_not_found)
, 而method(:resource_not_found).arity = -1
, 所以会采用`handler.call(exception) 的调用方式,那么错误也就必然了。
但是如果我们用 Block 的方式呢,像
rescue_from Exception, with: -> { resource_not_found }
#这里的rescuer
就是:with
后面的 block
那么我们得到的 handler 会是, Proc.new { instance_exec(&rescuer) }
,并且(Proc.new { instance_exec(&rescuer) }).arity == 0
,所以会采用handler.call
的方式来调用,那么就可以避免这个错误了。
上面的这些分析我是基于说我的这个重构是可以接受的,那么可以用 Block 的方式来避免报错。但是这个重构真的是否能接受,还希望大家给点意见!