问题
I want to define a class method that has access to a local variable. So this would be different for each instance of the class. I know you can make a class method dynamic with lambda like when you use it with named_scope. But can this be done for values that are specific to an instance?
In detail it is the has_attached_file method for the paperclip plugin in rails. I want to pass a lambda for the styles hash so that the image styles can be based off of attributes of the object stored in the DB. Is this possible?
回答1:
Disclaimer: First, the question (Can you pass self to lambda?) and the problem you're trying to solve (dynamic styles with paperclip) don't fully match up. I won't answer the original question because it's not entirely related to your problem, and rampion took a valiant stab at it.
I'll instead answer your paperclip question.
In detail it is the
has_attached_file
method for the paperclip plugin in rails. I want to pass a lambda for the styles hash so that the image styles can be based off of attributes of the object stored in the DB. Is this possible?
Yes, it is possible. In paperclip, the :styles
option can take a Proc. When the attachment is initialized, if a Proc was used, the attachment itself is passed to the Proc. The attachment has a reference to the associated ActiveRecord object, so you can use that to determine your dynamic styles.
For example, your has_attached_file
declaration might look something like this (assuming a User and avatar scenario where the user can customize the size of their avatar):
class User < ActiveRecord::Base
has_attached_file :avatar, :styles => lambda { |attachment|
user = attachment.instance
dimensions = "#{user.avatar_width}x#{user.avatar_height}#"
{ :custom => dimensions }
}
end
回答2:
Ok, you're being unclear.
Local variables in ruby begin with a lowercase letter (like foo
, bar
, or steve
), and are lexically scoped (like C
variables). They have nothing to do with "an instance of a class"
Instance variables in ruby begin with an @
sigil (like @foo
, @bar
, or @carl
), and are in scope whenever the current value of self
is the object they are stored in.
If you want a method that can access the instance variables of an object directly, that's called an instance method. For example, battle_cry
and initialize
are both instance methods:
class Character
def initialize(name)
@name=name
end
def battle_cry
@name.upcase + "!!!"
end
def Character.default
new("Leeroy Jenkins")
end
end
A class method, by contrast, is a method for a Class
object, and doesn't have access to any of the instance variables of that object. In the above example,
default
is a class method.
If you want a (class or instance) method that triggers a change in or gets a value from the current scope, ruby uses a type of callback called a block.
class Character
ATTACKS = [ "Ho!", "Haha!", "Guard!", "Turn!", "Parry!", "Dodge!", "Spin!", "Ha", "THRUST!" ]
def attack
ATTACKS.inject(0) { |dmg, word| dmg + yield(word) }
end
end
person = Character.default
puts person.battle_cry
num_attacks = 0;
damage = person.attack do |saying|
puts saying
num_attacks += 1
rand(3)
end
puts "#{damage} points of damage done in #{num_attacks} attacks"
In the above example, attack
uses the yield
keyword to call the block passed
to it. When we call attack
, then, the local variable num_attacks
is still
in scope in the block we pass it (delimited here by do ... end
), so we can
increment it. attack
is able to pass values into the block, here
they are captured into the saying
variable. The block also passes values
back to the method, which show up as the return value of yield
.
The word lambda
in ruby usually means the lambda
keyword, which is used
to make blocks into freestanding, function like objects (which themselves are usually
referred to as lambda
s, proc
s, or Proc
s).
bounce = lambda { |thing| puts "I'm bouncing a #{thing}" }
bounce["ball"]
bounce["frog"]
So I think what you're asking is whether you can pass a Proc
in place of a Hash
for an argument to a method. And the answer is "it depends". If the method only
ever uses the #[]
method, then yes:
class Character
attr_accessor :stats
def set_stats(stats)
@stats = stats
end
end
frank = Character.new("Victor Frankenstein")
frank.set_stats({ :str => 7, :dex => 14, :con => 9, :int => 19, :wis => 7, :cha => 11 })
monster = Character.new("Frankenstein's Monster")
monster.set_stats(lambda do |stat_name|
rand(20)
end)
However, it might use some other Hash
specific methods, or call the same key multiple times,
which can produce weird results:
monster = Character.new("Frankenstein's Monster")
monster.set_stats(lambda do |stat_name|
rand(20)
end)
monster.stats[:dex] #=> 19
monster.stats[:dex] #=> 1
In which case, you may be better off caching the requests in an intermediate hash. This is fairly easy,
since a Hash
can have an initializer block. So if we change the above to:
monster.set_stats(Hash.new do |stats_hash, stat_name|
stats_hash[stat_name] = rand(20)
end)
monster.stats[:dex] #=> 3
monster.stats[:dex] #=> 3
The results are cached in the hash
To see more about Hash
block initializers, see ri Hash::new
:
-------------------------------------------------------------- Hash::new
Hash.new => hash
Hash.new(obj) => aHash
Hash.new {|hash, key| block } => aHash
------------------------------------------------------------------------
Returns a new, empty hash. If this hash is subsequently accessed
by a key that doesn't correspond to a hash entry, the value
returned depends on the style of new used to create the hash. In
the first form, the access returns nil. If obj is specified, this
single object will be used for all default values. If a block is
specified, it will be called with the hash object and the key, and
should return the default value. It is the block's responsibility
to store the value in the hash if required.
h = Hash.new("Go Fish")
h["a"] = 100
h["b"] = 200
h["a"] #=> 100
h["c"] #=> "Go Fish"
# The following alters the single default object
h["c"].upcase! #=> "GO FISH"
h["d"] #=> "GO FISH"
h.keys #=> ["a", "b"]
# While this creates a new default object each time
h = Hash.new { |hash, key| hash[key] = "Go Fish: #{key}" }
h["c"] #=> "Go Fish: c"
h["c"].upcase! #=> "GO FISH: C"
h["d"] #=> "Go Fish: d"
h.keys #=> ["c", "d"]
来源:https://stackoverflow.com/questions/1109496/can-you-pass-self-to-lambda-in-rails