如何在Ruby中捕获类实例赋值给常量时的常量名称?
Great question! This is a common need when building enum-like structures (or any class where instances are assigned to constants) in Ruby, and there are a couple of solid approaches to make your instances aware of the constant name they're bound to.
核心思路
We need a way to capture the name of the constant when an instance is assigned to it, then store that name on the instance itself so we can reference it in error messages or other methods.
方法1:使用TracePoint监听常量赋值(推荐,鲁棒性高)
Ruby's TracePoint API lets us hook into runtime events, including when a constant is set. We can use this to detect when our instance is assigned to a constant, then save that constant's name to the instance.
Here's how to implement this for your Enum class:
class Enum attr_reader :name, :options def initialize(&block) @options = {} instance_eval(&block) if block_given? # Set up a TracePoint to watch for constant assignments trace = TracePoint.new(:cset) do |tp| # Check if the value being assigned to the constant is this instance if tp.value == self @name = tp.name.to_s trace.disable # Stop listening once we've found our constant end end trace.enable end # Helper to define options def option(symbol) Option.new(self, symbol) end # Inner class to handle option configuration class Option def initialize(enum, symbol) @enum = enum @symbol = symbol end def set(raw_value) @enum.options[@symbol] = raw_value end end # Fetch an option with error handling def [](symbol) options[symbol] || raise("Error. enum #{name} has no option #{symbol}") end end
方法2:解析调用栈(简单但依赖代码格式)
If you prefer a lighter-weight approach without runtime hooks, you can parse the calling stack to extract the constant name from the assignment line. This works for straightforward assignments like Constant = Enum.new { ... }, but may fail if the assignment is more complex.
class Enum attr_reader :name, :options def initialize(&block) @options = {} instance_eval(&block) if block_given? # Find the line where the enum is assigned to a constant caller_line = caller.find { |line| line.include?('=') } if caller_line # Extract the constant name using a regex match = caller_line.match(/(\w+)\s*=\s*Enum\.new/) @name = match[1] if match end end # Rest of the class (option, Option inner class, [] method) is the same as above end
测试你的Enum实现
Using either method, your Pizza class will work exactly as you expect:
class Pizza attr_reader :topping Toppings = Enum.new do option(:pepperoni).set("Pepperoni") option(:sausage).set("Sausage") option(:mushrooms).set("Mushrooms") end def set_topping(symbol) @topping = Toppings[symbol] end end # Normal case works pizza = Pizza.new pizza.set_topping(:pepperoni) puts pizza.topping # Output: "Pepperoni" # Error case shows the correct constant name pizza.set_topping(:spinach) # Raises: RuntimeError (Error. enum Toppings has no option spinach)
补充:降级方案
If the automatic detection fails (e.g., for complex assignments), you can add an explicit parameter to initialize to set the name manually:
def initialize(name: nil, &block) @name = name # ... rest of initialize ... end # Usage: Toppings = Enum.new(name: "Toppings") do # ... options ... end
内容的提问来源于stack exchange,提问作者Josh Hadik

