为什么在Ruby中符号不被认为是一种变量类型?

11

我刚开始学习编程和Ruby语言,希望这个关于符号(symbols)的问题在范围内。我知道Ruby中符号的用途(例如:book:price),特别是作为哈希键值,以及轻量级、特定子集的字符串功能。

但是,有一个方面让我感到困惑,即当它们用于attr_accessor类型方法时,它们似乎更像一个变量。例如:attr_reader :book, :price

如果它们在这种用法中是变量,那么这有点令人困惑,因为它们通常不被列为变量类型(例如全局变量、实例变量、本地变量、类变量以及有时常量变量类型)之一。

如果符号在这种情况下也是轻量级字符串,那么应该期望什么作用域呢?或者它们是否仍然是轻量级字符串?(或者更广义地说,符号、字符串和变量是否都共享着一种基本的鸭子特征?)感谢您提前的见解和建议。


有趣的内容:http://railstips.org/blog/archives/2006/11/18/class-and-instance-variables-in-ruby/ - BernardK
8个回答

7
符号并不是变量,而是一种字面值类型,类似于数字和引用字符串。重要的是,符号被用来表示Ruby运行时中的变量和其他命名值。因此,当Ruby解释器将名称foo用作变量或方法名称时,在运行时值的哈希表中查找的是符号:foo,而不是字符串"foo"。实际上,这是编程语言术语中“符号”的最初用法;变量、函数、常量、方法等都被称为存储在编译器或解释器的“符号表”中。
在Ruby中,几乎任何时候传递某个名称,你都会使用符号。如果您使用method_missing作为catch-all,在对象类上实现任意方法,那么作为参数传递给它的就是一个符号,告诉它实际调用的方法名称。如果您使用.methods或.instance_variables来检查对象,则返回的是符号数组。等等。

谢谢!了解编译器如何思考符号是有帮助的! - allenemilyh

6

它们不是变量,因为它们不保存值,它们是不可变的。事实上,值本身就是一个变量。这类似于数字。你不能设置1的值。 1 = 2 不起作用。


很有道理,谢谢!太好了,简单的例子。它们保持一个值,不可变,听起来是这样的。 - allenemilyh

3

访问器方法中使用的符号不是变量。它们只是代表变量的名称。变量保存某些引用,因此不能在定义访问器方法时使用变量本身。例如,假设您想要在其值为"bar"的上下文中为变量@foo定义访问器方法。如果Ruby的语法是这样的,会发生什么:

attr_accessor @foo

这与写下以下内容没有任何区别:

这将不会有任何不同:

attr_accessor "bar"

当你无法访问你感兴趣的名称@foo时,你需要设计这样的构造来引用元级别的变量名。为此,使用符号。它们本身不是变量,而是代表变量的名称。

与访问器方法相关的变量是实例变量。


1
谢谢,非常有帮助!使用实例变量太具体了,Ruby会将其解释为该变量的当前值,因此通常使用符号来表示变量。 - allenemilyh
所有的回答都很有帮助,但是你的回答对我来说最清晰,并且最好地解决了“为什么”(即符号的需求——不仅仅是“什么”或“如何”)。非常感谢! - allenemilyh
请注意,如果您也有我的问题,请务必查看下面BernardK的答案,其中有一个有用的声明:“为了回答您的'如果它们是变量'和'范围'问题,回答说访问者符号与实例变量无关会更简单,即使听起来像破坏者。他们不指向实例变量。访问者只定义getter和setter方法。”事实上,访问器方法符号定义方法,如本帖子的输出2所示,它为nil而不是NoMethodError。 - allenemilyh

2

attr_accessor等方法都属于Class类,它们期望作为参数的是符号。如果你想要,你可以编写自己版本的attr_,使用字符串作为参数。这只是Ruby的一种习惯用法。下面是我为一个作业任务制作的attr_accessor示例,它存储了先前所有attr_accessor的值。

class Class
  def attr_accessor_with_history(attr_name)
    attr_name = attr_name.to_s   # make sure it's a string
    attr_reader attr_name        # create the attribute's getter
    attr_reader attr_name+"_history" # create bar_history getter
    class_eval %Q"
      def #{attr_name}=(value)
        if !defined? @#{attr_name}_history 
          @#{attr_name}_history = [nil]
        end
        @#{attr_name} = value
        @#{attr_name}_history << value
      end
    "
  end
end

1
谢谢!很高兴知道你可以更改类,但听起来有些危险!我看到你的头像似乎在做一些猴子补丁操作... - allenemilyh
1
实际上,attr_reader和它的同类已经支持字符串参数了。但符号看起来更酷 :-) - AlexChaffee

1
为了回答你的“如果它们是变量”和“作用域”问题,更简单的回答是访问符号与实例变量无关,即使这听起来有些异端邪说。它们不指向实例变量。访问器只定义getter和setter方法。在Object#instance_variables下,Pickaxe(*)指出:请注意,仅定义访问器并不会创建相应的实例变量。
在Ruby中,变量在赋值之前不存在。以下代码演示了这一点。
class MyClass
    attr_accessor :name
    attr_reader   :book
end

obj = MyClass.new # line 6
print '1) obj.instance_variables : '; p obj.instance_variables
print '2) obj.name : '; p obj.name

obj.name = 'xyz'
print '3) obj.instance_variables : '; p obj.instance_variables
print '4) obj.name : '; p obj.name
print '5) obj.book : '; p obj.book

class MyClass
    def initialize(p_book)
        @book = p_book
    end
end

obj = MyClass.new('The Pickaxe') # line 21
print '6) [new] obj.book : '; p obj.book

class MyClass
    method_name = 'title'
    attr_accessor method_name # line 26
end

obj.title = 'Programming Ruby'
print '7) obj.instance_variables : '; p obj.instance_variables
print '8) obj.title : '; p obj.title

输出:

$ ruby -w t.rb 
1) obj.instance_variables : []
2) obj.name : nil
3) obj.instance_variables : ["@name"]
4) obj.name : "xyz"
5) obj.book : nil
6) [new] obj.book : "The Pickaxe"
7) obj.instance_variables : ["@title", "@book"]
8) obj.title : "Programming Ruby"

1) 空数组:访问器未定义实例变量
2) 请求实例变量@name的答案为nil:它不存在
3) 分配一个值已创建实例变量。
请注意,name =是使用带参数的setter作为普通方法的语法糖:obj.name=('xyz')
4) getter方法name回答@name的值
5) getter方法book回答nil,因为实例变量@book不存在。定义访问器attr_reader :book未定义相应的实例变量
6) getter方法book回答在initialize中分配的值,由第21行的new调用。实例变量@book已由@book = p_book创建
第26行)我一直认为访问器只接受符号。我发现变量是可能的,但兴趣有限。
7) setter方法title=已创建@title。这也表明实例变量属于单个对象。我们经常认为它们属于类的所有实例,就像其他语言一样。在这种情况下,@name仅属于第6行创建的对象。
8) getter方法title回答@title的值

class MyClass
    def title # line 34
        @book + ' (cheating !)'
    end
end

print '9) obj.title : '; p obj.title  

输出:

t.rb:34: warning: method redefined; discarding old title  
9) obj.title : "The Pickaxe (cheating !)"  

9) 当然,访问器符号和相应的实例变量之间存在紧密的关联,因为在后台,Ruby创建了引用同名实例变量的方法。您可以定义自己的getter并欺骗它。


请注意,除了类变量(@@var,有些人认为它们像全局变量一样丑陋)外,类还可以具有实例变量。我称它们为类实例变量:)。
class MyClass :Ruby分配一个新的类Class的实例,定义一个常量MyClass,并将新实例分配给该常量。因此,MyClass是一个普通对象(Class的实例),因此可以具有实例变量。

if RUBY_VERSION[0..2] == '1.8'
    class Object
        def singleton_class
            class << self
                self
            end
        end
    end
end

class MyClass
    singleton_class.instance_eval do
        attr_accessor :counter
    end
    @counter = 0

    def initialize(p_book)
        @book = p_book
        self.class.counter += 1
    end
end

print '10) MyClass.singleton_methods  : '; p MyClass.singleton_methods
print '11) MyClass.instance_variables : '; p MyClass.instance_variables
obj = MyClass.new('Ruby')
print '12) [new] obj.book ', MyClass.counter, ': '; p obj.book
obj = MyClass.new('Metaprogramming')
print '13) [new] obj.book ', MyClass.counter, ': '; p obj.book  

输出:

t.rb:55: warning: method redefined; discarding old initialize
10) MyClass.singleton_methods  : ["counter", "counter="]
11) MyClass.instance_variables : ["@counter"]
12) [new] obj.book 1: "Ruby"
13) [new] obj.book 2: "Metaprogramming"  

关于单例模式,可以在这里了解更多:什么是def `self.function`名称?

(*) http://pragprog.com/book/ruby3/programming-ruby-1-9


谢谢!啊哈,访问器方法定义的是方法,而不是实例变量,正如您在第一个代码块中演示的那样。(而且,您可以导致方法的重新定义/重命名。)从您的最后两个代码块中我看到,您可以创建一次性单对象方法(也称为单例方法),并且这些方法也可以使用访问器方法。如果大声朗读,“class << self”应该怎么读?也许像这样:“从class,用以下一次性/特殊指令实例化单例对象self”?总之,再次感谢您提供了清晰明了的示例代码! - allenemilyh

1

(回复您的评论)

dog = 'dog' 或者 String.new("dog")

在执行 dog = String.new 后,实例 dog 的类指向了 String 类。

class << dog
    puts "inside #{self}" #=> inside #<Class:#<String:0x007fb38a83a820>>
    def bark
        puts 'woof'
    end
end
dog.bark #=> "woof" 
p dog.singleton_methods #=> ["bark"]

使用class << dogdef dog.bark,Ruby创建一个匿名类,实例dog的字段类现在指向这个匿名类,然后指向String。在此上下文中使用def或define_method定义的方法进入匿名类的方法表。

Ruby 1.9.2引入了Object#singleton_class。[The Pickaxe]返回obj的单例类,必要时创建一个。(我添加)它等同于class << self; self end

The Ruby Programming Language (O'Reiily)简单地说:要打开对象o的特殊类[单例类],请使用class << o。

所以我不知道如何大声朗读。我已经读到有些人更喜欢o >> class。直到最近我才发现如何弄清楚这个奇怪的表达式的含义。我发音:从o到它的匿名类。

class << MyClass
    def dog
        puts 'dog as class method'
    end
end
MyClass.dog #=> dog as class method

同样的道理也适用于类。使用class MyClass,MyClass作为Class的实例,是一个具有指向其类Class的指针的对象。使用def MyClass.some_methodclass << MyClass,Ruby创建了一个匿名类,该类被插入在MyClass和Class之间,并且类方法进入其中。

可能会像这样:"从类中,实例化单例对象self"

是的,对于"从类/对象"到匿名单例类/特殊类/元类。但我们不会实例化self。Self(在Smaltalk中,在C++/Java中)是一种保留字,用于指定消息的接收者。 dog.bark:在面向对象的语言中,我们说将bark消息发送给dog对象。在bark方法内部,self将被设置为dog,以便我们可以引用dog。这在下面的示例中更加明显。

o1 = MyClass.new; o2 = MyClass.new
o1.some_method; o2.some_method

some_method必须以通用的方式引用接收器,是o1还是o2,这就是self的作用。


1
谢谢!单例类的内容很有趣。我也喜欢这个链接上关于这个主题的内容:http://www.devalot.com/articles/2008/09/ruby-singleton。 - allenemilyh
另外,我喜欢你建议的class << MyClass符号的简单术语化,即“从类中进入其匿名类”。(我发现找到一种发音方式非常有帮助!) - allenemilyh
@allenemilyh,感谢你提供的链接,解释得非常好,我已经收藏了它。 - BernardK

1

谢谢!根据链接和其他示例,使用访问器,符号通常指向实例变量。(即使这些符号不是变量本身,而只是指向变量,也有助于解决我上面提到的“作用域”问题。) - allenemilyh
我不知道我是否会走得那么远 - 符号本身并没有特定的“指向”。它们只是一种指定某个名称的方式,而不使用字符串。在这种情况下,由类方法(如attr_accessor)来解释符号表示实例变量名称的方式。 - Greg Hewgill

0

很酷,我猜你现在已经理解它们了。 但是为什么它们如此重要呢?

Ruby中的符号是不可变的,而字符串是可变的。 你会想,好吧,那又怎样?

假设你有一个字符串数组,就像这样:

    [ "a", "b", "a", "b", "a", "b", "c" ]

每次创建新字符串时,Ruby都会创建一个包含“a”值的字符串/对象,因为字符串是可变的东西,Ruby会为每个字符串分配不同的ID。 如果您改用符号:
[ :a, :b, :a, :b, :a, :b, :c ]

Ruby现在将指向这些符号,并且它只会创建一次。

让我们进行一些基准测试:

require 'benchmark'

Benchmark.bm do |x|
  x.report("Symbols") do
    a = :a
    1000_000.times do
      b = :a
    end
  end

  x.report("Strings") do
    a = "a"
    1000_000.times do
      b = "a"
    end
  end

end

ruby -w symbols.rb

Symbols  0.220000   0.000000   0.220000 (  0.215795)
Strings  0.460000   0.000000   0.460000 (  0.452653)

如果您想查看已创建的所有符号,可以执行以下操作:

Symbol.all_symbols

您也可以发送一条消息询问他们的ID:

:a.object_id #=> 123
:a.object_id #=> 123

"a".id #=> 23323232
"a".id #=> some_blob_number

这是因为 Ruby 中的字符串是可变的,而符号则不是。

Ruby 符号代表 Ruby 解释器内部的名称。

这个视频真的帮了我很多: Ruby 符号的解释

希望它也能帮到你们。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接