可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I'm playing with Ruby on Rails and I'm trying to create a method with optional parameters. Apparently there are many ways to do it. I trying naming the optional parameters as hashes, and without defining them. The output is different. Take a look:
# This functions works fine! def my_info(name, options = {}) age = options[:age] || 27 weight = options[:weight] || 160 city = options[:city] || "New York" puts "My name is #{name}, my age is #{age}, my weight is #{weight} and I live in {city}" end my_info "Bill" -> My name is Bill, my age is 27, my weight is 160 and I live in New York -> OK! my_info "Bill", age: 28 -> My name is Bill, my age is 28, my weight is 160 and I live in New York -> OK! my_info "Bill", weight: 200 -> My name is Bill, my age is 27, my weight is 200 and I live in New York -> OK! my_info "Bill", city: "Scottsdale" -> My name is Bill, my age is 27, my weight is 160 and I live in Scottsdale -> OK! my_info "Bill", age: 99, weight: 300, city: "Sao Paulo" -> My name is Bill, my age is 99, my weight is 300 and I live in Sao Paulo -> OK! **************************** # This functions doesn't work when I don't pass all the parameters def my_info2(name, options = {age: 27, weight: 160, city: "New York"}) age = options[:age] weight = options[:weight] city = options[:city] puts "My name is #{name}, my age is #{age}, my weight is #{weight} and I live in #{city}" end my_info2 "Bill" -> My name is Bill, my age is 27, my weight is 160 and I live in New York -> OK! my_info2 "Bill", age: 28 -> My name is Bill, my age is 28, my weight is and I live in -> NOT OK! Where is my weight and the city?? my_info2 "Bill", weight: 200 -> My name is Bill, my age is , my weight is 200 and I live in -> NOT OK! Where is my age and the city?? my_info2 "Bill", city: "Scottsdale" -> My name is Bill, my age is , my weight is and I live in Scottsdale -> NOT OK! Where is my age and my weight? my_info2 "Bill", age: 99, weight: 300, city: "Sao Paulo" -> My name is Bill, my age is 99, my weight is 300 and I live in Sao Paulo -> OK!
What's wrong with the second approach for optional parameters? The second method only works if I don't pass any optional parameter or if I pass them all.
What am I missing?
回答1:
The way optional arguments work in ruby is that you specify an equal sign, and if no argument is passed then what you specified is used. So, if no second argument is passed in the second example, then
{age: 27, weight: 160, city: "New York"}
is used. If you do use the hash syntax after the first argument, then that exact hash is passed.
The best you can do is
def my_info2(name, options = {}) options = {age: 27, weight: 160, city: "New York"}.merge(options) ...
回答2:
The problem is the default value of options
is the entire Hash
in the second version you posted. So, the default value, the entire Hash
, gets overridden. That's why passing nothing works, because this activates the default value which is the Hash
and entering all of them also works, because it is overwriting the default value with a Hash
of identical keys.
I highly suggest using an Array
to capture all additional objects that are at the end of your method call.
def my_info(name, *args) options = args.extract_options! age = options[:age] || 27 end
I learned this trick from reading through the source for Rails. However, note that this only works if you include ActiveSupport. Or, if you don't want the overhead of the entire ActiveSupport gem, just use the two methods added to Hash
and Array
that make this possible.
rails / activesupport / lib / active_support / core_ext / array / extract_options.rb
So when you call your method, call it much like you would any other Rails helper method with additional options.
my_info "Ned Stark", "Winter is coming", :city => "Winterfell"
回答3:
If you want to default the values in your options hash, you want to merge the defaults in your function. If you put the defaults in the default parameter itself, it'll be over-written:
def my_info(name, options = {}) options.reverse_merge!(age: 27, weight: 160, city: "New York") ... end
回答4:
In second approach, when you say,
my_info2 "Bill", age: 28
It will pass {age: 28}, and entire original default hash {age: 27, weight: 160, city: "New York"} will be overridden. That's why it does not show properly.
回答5:
For the default values in your hash you should use this
def some_method(required_1, required_2, options={}) defaults = { :option_1 => "option 1 default", :option_2 => "option 2 default", :option_3 => "option 3 default", :option_4 => "option 4 default" } options = defaults.merge(options) # Do something awesome! end
回答6:
You can also define method signatures with keyword arguments (New since, Ruby 2.0, since this question is old):
def my_info2(name, age: 27, weight: 160, city: "New York", **rest_of_options) p [name, age, weight, city, rest_of_options] end my_info2('Joe Lightweight', weight: 120, age: 24, favorite_food: 'crackers')
This allows for the following:
- Optional parameters (
:weight
and :age
) - Default values
- Arbitrary order of parameters
- Extra values collected in a hash using double splat (
:favorite_food
collected in rest_of_options
)
回答7:
To answer the question of "why?": the way you're calling your function,
my_info "Bill", age: 99, weight: 300, city: "Sao Paulo"
is actually doing
my_info "Bill", {:age => 99, :weight => 300, :city => "Sao Paulo"}
Notice you are passing two parameters, "Bill"
and a hash object, which will cause the default hash value you've provided in my_info2
to be completely ignored.
You should use the default value approach that the other answerers have mentioned.
回答8:
#fetch
is your friend!
class Example attr_reader :age def my_info(name, options = {}) @age = options.fetch(:age, 27) self end end person = Example.new.my_info("Fred") puts person.age #27
回答9:
I don't see anything wrong with using an or operator to set defaults. Here's a real life example (note, uses rails' image_tag
method):
file:
def gravatar_for(user, options = {} ) height = options[:height] || 90 width = options[:width] || 90 alt = options[:alt] || user.name + "'s gravatar" gravatar_address = 'http://1.gravatar.com/avatar/' clean_email = user.email.strip.downcase hash = Digest::MD5.hexdigest(clean_email) image_tag gravatar_address + hash, height: height, width: width, alt: alt end
console:
2.0.0-p247 :049 > gravatar_for(user) => "<img alt=\"jim's gravatar\" height=\"90\" src=\"http://1.gravatar.com/avatar/<hash>\" width=\"90\" />" 2.0.0-p247 :049 > gravatar_for(user, height: 123456, width: 654321) => "<img alt=\"jim's gravatar\" height=\"123456\" src=\"http://1.gravatar.com/avatar/<hash>\" width=\"654321\" />" 2.0.0-p247 :049 > gravatar_for(user, height: 123456, width: 654321, alt: %[dogs, cats, mice]) => "<img alt=\"dogs cats mice\" height=\"123456\" src=\"http://1.gravatar.com/avatar/<hash>\" width=\"654321\" />"
It feels similar to using the initialize method when calling a class.
回答10:
There is a method_missing method on ActiveRecord models that you can override to have your class dynamically respond to calls directly. Here's a nice blog post on it.
回答11:
Why not just use nil?
def method(required_arg, option1 = nill, option2 = nil) ... end