ruby类变量的陷阱与替代实现
@@是不推荐使用的
因为class variables are bound at compile-time
Like unqualified constants, class variables are bound to your current scope when the .rb file is being parsed.
This will probably trip you up when you are writing a module or trait that defines class variables for other classes.
Say you have the following code:
module Mod
def foo
@@foo
end
end
class Klass
include Mod
end
@@foo does not mean Klass::@@foo (qualifier added to make my point; it is not valid Ruby expression). It actually means Mod::@@foo and is shared between Mod, all classes including Mod as well as all their subclasses.
Also note that only the class and module keywords can be used to change the scope to which class variables are bound. You can not change scope by using class_eval or instance_eval. For instance, the code below declares a variable @@counter that is shared between all classes of your application:
ActiveRecord::Base.class_eval do
@@counter = 1
end
When encountering such code, Ruby will warn you:
warning: class variable access from toplevel
Unlike constants, which you can qualify like Klass::CONSTANT, there is no way to qualify a class variable like Klass::@@variable. When you want to tell Ruby that you mean another scope than the current scope, you need to use class_variable_get and class_variable_set.
替代1:类的实例变量
缺点就是无法让子类自动查找到
替代2:hanami util的class_attribute
缺点,它是通过inherited的hook来将当前变量值传给子类,因而如果父类本身需要另外一些inherited逻辑,就比较麻烦
摘自utils/lib/hanami/utils/class_attribute.rb(de705e02b83f52df4edb4e9e37c79d053acb99da):
require 'set'
require 'hanami/utils/duplicable'
module Hanami
module Utils
# Inheritable class level variable accessors.
# @since 0.1.0
#
# @see Hanami::Utils::ClassAttribute::ClassMethods
module ClassAttribute
def self.included(base)
base.extend ClassMethods
end
# @since 0.1.0
# @api private
module ClassMethods
# Defines a class level accessor for the given attribute(s).
#
# A value set for a superclass is automatically available by their
# subclasses, unless a different value is explicitely set within the
# inheritance chain.
#
# @param attributes [Array] a single or multiple attribute name(s)
#
# @return [void]
#
# @since 0.1.0
#
# @example
# require 'hanami/utils/class_attribute'
#
# class Vehicle
# include Hanami::Utils::ClassAttribute
# class_attribute :engines, :wheels
#
# self.engines = 0
# self.wheels = 0
# end
#
# class Car < Vehicle
# self.engines = 1
# self.wheels = 4
# end
#
# class Airplane < Vehicle
# self.engines = 4
# self.wheels = 16
# end
#
# class SmallAirplane < Airplane
# self.engines = 2
# self.wheels = 8
# end
#
# Vehicle.engines # => 0
# Vehicle.wheels # => 0
#
# Car.engines # => 1
# Car.wheels # => 4
#
# Airplane.engines # => 4
# Airplane.wheels # => 16
#
# SmallAirplane.engines # => 2
# SmallAirplane.wheels # => 8
def class_attribute(*attributes)
singleton_class.class_eval do
attr_accessor(*attributes)
end
class_attributes.merge(attributes)
end
protected
# @see Class#inherited
def inherited(subclass)
class_attributes.each do |attr|
value = send(attr)
value = Duplicable.dup(value)
subclass.class_attribute attr
subclass.send("#{attr}=", value)
end
super
end
private
# Class accessor for class attributes.
# @private
def class_attributes
@class_attributes ||= Set.new
end
end
end
end
end
替代3:active support的class_attribute
缺点:没明显缺点,就是有点复杂
摘自activesupport-5.0.0.1/lib/active_support/core_ext/class/attribute.rb:
class Class
# 因是定义类的单例类的实例方法,所以子类也能访问到
#
# class Base
# class_attribute :setting
# end
#
# class Subclass < Base
# end
#
# Base.setting = true
# Subclass.setting # => true
# Subclass.setting = false
# Subclass.setting # => false
# Base.setting # => true
#
# 修改内容的话,会影响父类,因子类的单例类继承自父类的单例类
# 而父类的单例类的实例方法的定义有闭包
#
# Base.setting = []
# Base.setting # => []
# Subclass.setting # => []
#
# # Appending in child changes both parent and child because it is the same object:
# Subclass.setting << :foo
# Base.setting # => [:foo]
# Subclass.setting # => [:foo]
#
# 除非重新赋值
#
# Base.setting = []
# Subclass.setting += [:foo]
# Base.setting # => []
# Subclass.setting # => [:foo]
#
# For convenience, an instance predicate method is defined as well.
# To skip it, pass instance_predicate: false.
#
# Subclass.setting? # => false
#
# 实例会先探测自己是否有该名字的实例变量,因此,可重写而不影响类变量
#
# Base.setting = true
# object = Base.new
# object.setting # => true
# object.setting = false
# object.setting # => false
# Base.setting # => true
#
# To opt out of the instance reader method, pass instance_reader: false.
#
# object.setting # => NoMethodError
# object.setting? # => NoMethodError
#
# To opt out of the instance writer method, pass instance_writer: false.
#
# object.setting = false # => NoMethodError
#
# To opt out of both instance methods, pass instance_accessor: false.
def class_attribute(*attrs)
options = attrs.extract_options!
# 默认带有,除非设false
instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true)
instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true)
instance_predicate = options.fetch(:instance_predicate, true)
attrs.each do |name|
# 定义class_reader
remove_possible_singleton_method(name)
define_singleton_method(name) { nil }
# 定义类class_reader?
remove_possible_singleton_method("#{name}?")
define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate
ivar = "@#{name}"
# 定义类writer,其做法是:
# 当被调用时,重定义class_reader
remove_possible_singleton_method("#{name}=")
define_singleton_method("#{name}=") do |val|
singleton_class.class_eval do
remove_possible_method(name)
define_method(name) { val }
end
# 如果是这样调用:
# class << A; class_attribute :v; end
# 则只有在:
# class << A; self.v = 123; end
# 才能从A.v访问到,因调用"#{name}="后才会定义reader
# 对于一般对象:
# class << A; class_attribute :v; end
# 也同理
# 不过,有人这样用吗……
if singleton_class?
class_eval do
remove_possible_method(name)
define_method(name) do
if instance_variable_defined? ivar
instance_variable_get ivar
else
singleton_class.send name
end
end
end
end
val
end
# 因instance_writer用的是attr_writer,修改实例变量
# 所以需先检查实例变量,没有的话,才用类变量
if instance_reader
remove_possible_method name
define_method(name) do
if instance_variable_defined?(ivar)
instance_variable_get ivar
else
self.class.public_send name
end
end
remove_possible_method "#{name}?"
define_method("#{name}?") { !!public_send(name) } if instance_predicate
end
if instance_writer
remove_possible_method "#{name}="
attr_writer name
end
end
end
end