ruby中self,作用域,可见性的用法详解

作者:袖梨 2022-06-25

有些东西在任何时间任何地方表示的意思是不变的,比如整数,你看到的就是它表示的东西。关键词也一样,你不能使用 def,class 这些关键词作为变量名,所以当你看到它们的时候,你可以很容易知道它们是做什么的。不过还有很多东西的意思取决于它们所处的情境,也就是它们在不同的时间不同的地方的意思可能是会有变化的。

self 表示的是当前或者默认的对象,在程序运行的时候每次它都会表示一个特定的对象。永远都只会有一个 self ,但是它表示的东西是会变的。

作用域(scope)的规则决定了变量的可见性。你要知道所在的地方受哪个作用域的影响,这样你才能明白哪些变量表示的是什么,不致于把它们跟在其它的作用域的同名变量混淆。

知道当前是在哪个作用域,了解 self 表示的对象是谁,这样你才能明白到底发生了什么,才可以快速的分析遇到的问题。

下午 5:54 ***

self

下午6:07 ***

使用关键词 self 可以得到当前对象。在程序运行中,有且只有一个 self 。成为 self 有一些特权,一会儿我们会看到。永远都只会有一个当前对象或者叫 self 。是谁在哪里会成为 self 是有一些规则的。

在顶级的 self 对象

这里说的顶级的意思是,在任何的类或模块定义之外的地方。比如,打开一个空白的文件,输入:

x = 1
这样我们就创建了一个在顶级的本地变量 x ,下面的代码就是在顶级创建了一个方法:

def m
end
看一下在顶级的 self 是谁,执行一下:

puts self
返回的是:

main
main 是一个特别的词,它表示的就是 self 本身,比如我们可以这样试一下:

m = self
在类,模块定义里的 self

在类与模块定义里,self 指的就是类或模块对象。

下面这个实验会告诉你 self 在类的定义与模块的定义里面表示的是谁,做个实验:

class C
  puts '类:'
  # self 是 C
  puts self

  module M
    puts '模块:'
    # self 是 C::M
    puts self
  end
  puts '回到类级别:'
  # self 又是 C
  puts self
end
进入类或模块的定义区域以后,类与模块对象就变成了 self。上面的例子,最开始 self 表示的是 C 类这个对象。进入定义模块的区域的时候,self 是 C::M ,表示 C 类里面嵌套的模块 M。回到定义 C 类以后,self 表示地又会是这个类对象,也就是 C。

在定义实例方法里的 self

在实例方法的定义里的 self 很微秒,因为 Ruby 的解释器遇到 def/end 以后,它会立即定义方法。这时候方法定义里的代码还没被执行呢。你看到的屏幕上定义的方法,你只知道当方法被调用的时候,self 会是调用方法的那个对象。在定义方法的那个时候,最多你只能说的是,在这个方法里面的 self 会是未来的那个调用方法的对象。

做个实验:

class C
  def x
    puts "Class C, method x:"
    puts self
  end
end

c = C.new
c.x
puts "That was a call to x by: #{c}"
会输出:

Class C, method x:
#
That was a call to x by: #
输出的这个东西:# ,表示的就是一个 C 的实例。在执行 x 方法的时候,self 就是调用它的实例对象 c 。

独立方法与类方法中的 self

执行一个独立方法的时候,self 表示的就是拥有这个方法的那个对象。

下面这个实验,先创建了一个对象,又在这个对象里定义了一个独立方法,然后调用这个独立方法,看一下 self 表示的是谁。实验:

obj = Object.new
def obj.show_me
  puts "Inside singleton method show_me of #{self}"
end

obj.show_me
puts "Back from call to show_me by #{obj}"
会输出:

Inside singleton method show_me of #
Back from call to show_me by #
类方法基本上就是在类对象上定义的独立方法,再做个实验:

class C
  def C.x
    puts "Class method of class C"
    puts "self: #{self}"
  end
end
C.x
会输出:

Class method of class C
self: C
在类的内部我们可以使用 self 表示类的名字:

class C
  def self.x
    puts "Class method of class C"
    puts "self: #{self}"
  end
end
如果我们:

class D < C
end
D.x
输出的会是:

Class method of class C
self: D
self 作为默认的信息接收者

2016年9月8日

调用方法就是发送信息给对象,像这样:

obj.talk
ticket.venue
'abc'.capitalize
在调用方法的时候如果信息的接收者是 self,可以忽略掉接收者还有点,Ruby 会使用 self 作为默认的接收者,意思就是你发送的信息会发送给 self 。像这样:

talk
venue
capitalize
如果有个变量叫 talk,还有个方法叫 talk,调用 talk 的时候只是用了一个 talk,那么变量的优先级会更高一些。遇到这种情况你可以在调用 talk 方法的时候加上 self,像这样: self.talk,或者加上括号:talk() 。

class C
  def C.no_dot
    puts '只要 self 是 C,你就可以不用点来调用这个方法'
  end
  no_dot
end

C.no_dot
第一回调用 no_dot 的时候没有明显的接收者,Ruby 看到它以后,会判断你的意思可能是:

self.no_dot
在上面这个例子里,self.no_dot 跟 C.no_dot 是不一样的东西,因为我们在定义 C 的区块里,这样 self 就是 C。结果就是方法被调用了,然后我们就看到了输出了结果。

第二次我们使用 C.no_dot 的时候,已经是在定义类的区块以外了,所以 C 就不再是 self 了。也就是想要调用 no_dot ,我们就得指定这个信自的接收者,也就是 C。

上面这个例子输出的结果就是两次调用 no_dot 方法的结果:

只要 self 是 C,你就可以不用点来调用这个方法
只要 self 是 C,你就可以不用点来调用这个方法
最常见的使用这种无点调用方法,就是当你在另一个实例方法里去调用一个实例方法,再看个例子:

class C
  def x
    puts '这是方法 x'
  end

  def y
    puts '这是方法 y,我要无点调用 x'
    x
  end
end

c = C.new
c.y
输出的结果是:

这是方法 y,我要无点调用 x
这是方法 x
上面调用 c.y 的时候,执行了方法 y ,self 指的是 c(c 是 C 的一个实例)。在 y 里面,使用了 x,它被解释成信息要发送给 self ,这样也就会执行了方法 x 。

有一种情况你不能忽略掉对象加点的形式去调用方法,就是如果方法的名字里面带等号(设置器方法),也就是如果你想调用在 self 上的 venue= 这个方法,你需要这样做:self.venue = 'Town Hall',而不是这样:venue = 'Town Hall'。因为 Ruby 会一直认为 identifier = value 是在分配值给一个本地变量。

无点方法调用,在一个方法里使用另一个的方法的时候很有用,再来看一个例子:

class Person
  attr_accessor :first_name, :middle_name, :last_name

  def whole_name
    n = first_name + ' '
    n << "#{middle_name} " if middle_name
    n << last_name
  end
end

david = Person.new

david.first_name = 'David'
david.last_name = 'Black'

puts "David 的全名是:#{david.whole_name}"

david.middle_name = 'Alan'
puts "David 的全名现在是:#{david.whole_name}"
输出的结果会是:

David 的全名是:David Black
David 的全名现在是:David Alan Black
通过 self 解释实例变量

在 Ruby 程序里,任何的实例变量都会属于当前对象。

先做个实验,看看下面的东西会输出什么:

class C
  def show_var
    @v = 'I am an instance variable initialized to a string.'
    puts @v
  end
  @v = "instance variables can appear anywhere..."
end

C.new.show_var
会输出:

I am an instance variable initialized to a string.
在上面的代码里,有两个 @v,一个是在方法定义内,另一个是在方法定义外,它们之间没有任何联系。它们同样都是实例变量,并且名字都是 @v,不过它们不是同一个变量,它们会属于不同的对象。

第一次出现的 @v 是在方法定义区块里,也就是 C 类里面的一个实例方法,这样 C 类的每一个实例对象里面都会有它们自己的实例变量 @v 。

第二次出现的 @v 属于 C 这个类对象。类本身也是对象。

every instance variable belongs to whatever object is playing the role of self at the moment the code containing the instance variable is executed.
重新再写一下上面的例子:

class C
  puts "* 类定义区块"
  puts "  | --- self 是:#{self}"
  @v = '我是 @v'
  puts "  | --- #{@v} 实例变量,属于:#{self} nn"

  def show_var
    puts "* 实例方法定义区块"
    puts "  | --- self 是:#{self}"
    puts "  | --- @v 实例变量属于:#{self}"
    print "  | --- @v 的值是:"
    p @v
  end
end

c = C.new
c.show_var
执行代码输出的结果是:

* 类定义区块
 | --- self 是:C
 | --- 我是 @v 实例变量,属于:C

* 实例方法定义区块
 | --- self 是:#
 | --- @v 实例变量属于:#
 | --- @v 的值是:nil
作用域

上午11:15 ***

作用域指的就是标识符的可见性,特别指的是变量与常量。不同类型的标识符有不同的作用域规则。在两个方法里使用同一个名字的变量 x ,跟在两个地方使用全局变量 $x,会有不同的结果。因为本地与全局变量的作用域不一样。

全局作用域与全局变量

全局作用域覆盖了整个程序。全局变量用的是全局作用域,在任何地方你都可以使用它们。即使你开启了新的类或方法的定义,或者 self 的身份变了,初始的全局变量仍然可用。

下面这个例子,在定义类的主体里可以使用初始化的全局变量:

$gvar = "我是全局变量"
class C
  def examine_global
    puts $gvar
  end
end

c = C.new
c.examine_global
输出的结果会是:

我是全局变量
本地作用域

class,module,def 都会创建新的本地作用域。

做个实验:

class C
  a = 1

  def local_a
    a = 2
    puts a
  end

  puts a
end

c = C.new
c.local_a
输出的结果是:

1
2
第一次出现的本地变量 a ,是在类定义的本地作用域的下面。第二次出现的 a,是在方法定义的本地作用域的下面。结束了方法定义以后,本地作用域回到了类区块,这里要求输出的 a 就是在类区块这个本地作用域下面声明的本地变量 a ,它的值是 1 。然后我们创建了一个类的实例,调用了它的 local_a 方法,这个方法输出的是在方法定义作用域下面声明的本地变量 a 的值,也就是 2 。

在嵌套类与模块的时候,每次遇到新的定义区块以后就会创建一个新的本地作用域。

再试一下:

class C
  a = 5

  module M
    a = 4

    module N
      a = 3

      class D
        a = 2

        def show_a
          a = 1
          puts a
        end
        puts a
      end
      puts a
    end
    puts a
  end
  puts a
end

d = C::M::N::D.new
d.show_a
输出的结果会是:

2
3
4
5
1
任何的 class,module 或 method 都会开启一个新的本地作用域,每个作用域都可以有属于自己的全新的本地变量。

本地作用域与 self

先看个例子:

class C
  def x(value_for_a, recurse=false)
    a = value_for_a
    print "现在 self 是: "
    p self
    puts "现在 a 是:"
    puts a
    if recurse
      puts "n调用自己..."
      x("a 的第二个值")
      puts "调用自己结束以后,a 是:"
      puts a
    end
  end
end

c = C.new
c.x("a 的第一个值", true)
运行的结果会是:

现在 self 是: #
现在 a 是:
a 的第一个值

调用自己...
现在 self 是: #
现在 a 是:
a 的第二个值
调用自己结束以后,a 是:
a 的第一个值
实例方法 C#x 有两个参数,第一个参数是要分配给变量 a 的值,第二个参数是一个标记,表示是否要调用它自己。方法的第一行初始化了一个本地变量 a ,下行的几行代码就是输出了表示  self 还有 a 的值的字符串。

然后到了做决定的时候了(if recurse),调用自己不调用自己,这会由 recurse 变量决定。如果调用自己,就会调用方法 x ,调用的时候没有指定 recurse 参数的值,这个参数默认的值是 false,所以调用它自己的时候就不会再继续的调用它自己了。

调用自己的时候给 value_for_a 参数设置了一个不同的值(“a 的第二个值”),也就是会在调用的时候输出不同的信息。不过调用自己回来以后,我们发现这次运行的 x 里的 a 的值没有改变(还是 “a 的第一个值”)。也就是每一次我们调用 x 的时候,都会生成一个新的本地作用域,即使 self 并没有改变。

常量的作用域

下午1:06 ***

在类与方法定义区块里可以定义常量。如果你知道定义时使用的嵌套,你就可以在任何地方访问到常量。来看个例子:

module M
  class C
    class D
      module N
        X = 1
      end
    end
  end
end
比如我要访问在模块 N 里定义的常量 X,这样做:

M::C::D::N::X
得到常量的位置也可以是相对的,下面的例子验证了这个说法:

module M
  class C
    class D
      module N
        X = 1
      end
    end
    puts D::N::X
  end
end
在 C 类里,得到模块 N 里的 X,用的是 D::N::X 。

有时候你不想使用相对的路径得到常量。比如我们想创建的类跟 Ruby 内置的类名字一样,比如 Ruby 里面有个 String(字符串) 类,如果你创建了一个 Violin(小提琴),里面也可能会有一个 String (弦)。像这样:

class Violin
  class String
    attr_accessor :pitch
    def initialize(pitch)
      @pitch = pitch
    end
  end

  def initialize
    @e = String.new("E")
    @a = String.new("A")
    ...etc...
上面使用 String 的时候指的是我们自己定义的 String 类,如果你想使用 Ruby 内置的 String 类,可以使用常量分隔符(::,两个冒号),像这样:

::String.new('hello')
类变量,作用域,可见性

下午1:50 ***

类变量是维护类状态用的,它们的名字用两个 @ 符号开头,比如 @@var。类变量并不是类作用域,它们是类层次作用域。类变量提供了在类与类的实例对象之间共享数据的机制,也就是类变量在类方法定义与实例方法定义上都是可见的,有时候在顶级的类定义上也是。除此以外,类变量在其它的对象上是不可见的。

来看个例子,先用类方法 Car.add_make(make) 注册洗车生产商,然后用 Car.new(make) 造几辆汽车:

Car.add_make("Honda")
Car.add_make("Ford")

h = Car.new("Honda")
f = Car.new("Ford")
h2 = Car.new("Honda")
程序会告诉你创建的汽车:

创建新的 Honda!
创建新的 Ford!
创建新的 Honda!
下午2:08 **

下午2:27 **

同一个汽车生产商生产了多少个 h2?我们会使用实例方法 make_mates 查到:

puts "统计 h2 汽车的数量..."
puts "一共有 #{h2.make_mates} 辆"
一共有多少辆汽车?这需要用到类,而不是每个单独的汽车,所以我们可以问一下类:

puts "统计汽车的总数..."
puts "一共有 #{Car.total_count} 辆"
输出的应该是:

统计汽车的总数...
一共有 3 辆"
试一下创建一辆没有汽车生产商的汽车:

x = Car.new("Brand X")
会报错:

car.rb:21:in `initialize': No such make: Brand X. (RuntimeError)
代码如下:

class Car
  @@makes = []
  @@cars = {}
  @@total_count = 0
  attr_reader :make
  def self.total_count
    @@total_count
  end

  def self.add_make(make)
    unless @@makes.include?(make)
      @@makes << make
      @@cars[make] = 0
    end
  end

  def initialize(make)
    if @@makes.include?(make)
      puts "创建新的汽车生产商:#{make}"
      @make = make
      @@cars[make] += 1
      @@total_count += 1
    else
      raise "没有汽车生产商:#{make}"
    end
  end

  def make_mates
    @@cars[self.make]
  end
end
在类的顶部定义了三个类变量。@@makes 是一个数组,存储汽车生产商的名字。@@cars 是 hash,里面是名值对类型的数据。@@cars 里的数据的名字是汽车生产商汽车的汽车,对应的数据是汽车的数量。@@total_count 里面存储的是一共生产了多少辆汽车。

Car 类里还有个 make 可读属性,创建了汽车以后必须设置 make 属性的值。这里没有关于汽车生产商的可写的属性,因为我们不希望类的代码之后可以改变已有的汽车生产商。

要访问到 @@total_count 类变量,Car 类里还定义了一个 total_count 方法,它会返回类变量当前的值。还有一个类方法是 add_make,这个方法接收一个参数,会把参数的值放到表示汽车生产商的数组里,用的是 << 操作符。在这个方法里我们先要确定添加的汽车生产商还不存在,如果不存在就把它放到表示汽车生产商的类变量里 @@makes,同时也会设置一下 @@cars ,让这个汽车生产商生产的汽车等于零。意思就是还没有这个汽车生产商生产的汽车。

然后到了 initialize 方法了,在这里创建新的汽车。每辆新车都需要一个汽车生产商,如果汽车生产商不存在,也就是它不在 @@makes 数组里,就触发一个错误。如果汽车生产商存在,我们会为汽车的 make 属性设置合适的值,让这个汽车生产商生产的汽车的数量增加一(@@cars),同时也让生产的汽车总数增加一(@@total_count)。

还有一个 make_mates 方法,可以返回某个汽车生产商生产的所有的汽车。

注意上面在 initialize 方法里,还有在类方法里,比如 Car.total_count,Car.add_make 上面,都使用了类变量。类内部的实例方法 initialize,与类方法是在不同的作用域下。但它们属于同一个类,所以可以使用类变量在它们之间共享数据。

类变量与类层次

之前我们已经说过了,类变量使用的不是类作用域,也是类层次的作用域。看个例子:

class Parent
  @@value = 100
end

class Child < Parent
  @@value = 200
end

class Parent
  puts @@value
end
输出的结果会是 200。Child 是 Parent 的一个子类,也就是 Parent 与 Child 共享同样的类变量。在 Child 里面设置 @@value 的时候,你设置的是唯一的在 Parent 与 Child 上的 @@value 。

下午3:17 ***

方法访问规则

下午3:18 ***

现在我们已经知道了 Ruby 程序会发送信息给对象,对象主要干的事儿就是对这些信息做出回应。有时候,对象希望可以给自己发送信息,但是不希望别人给它们发送信息。这种情况,我们可以让方法变成私有的。

访问有几个访问级别,私有(private),保护(protected),公开(public)。public 是默认的访问级别,发送给对象的信息大部分调用的就是有公开访问级别的方法。

私有方法

把对象想成是一个你要求让他做任务的人,比如你想让某个人给你烤个蛋糕,为了给你烤这个蛋糕,烤蛋糕的人会做一系列的任务,比如打个鸡蛋,和个面什么的。烤蛋糕的人会做这些事儿,不过他可能并不想对所有这些事情做出回应。你要求的只是“请给个烤个蛋糕”。剩下的事儿交给蛋糕师就可以了。

用代码模拟一下,创建个文件名字是 baker.rb,代码如下:

class Cake
  def initialize(batter)
    @batter = batter
    @baked = true
  end
end

class Egg
end

class Flour
end

class Baker
  def bake_cake
    @batter = []
    pour_flour
    add_egg
    stir_batter
    return Cake.new(@batter)
  end

  def add_egg
    @batter.push(Egg.new)
  end

  def stir_batter
  end

  private :pour_flour, :add_egg, :stir_batter
end
上面用了一个 private 方法,你可以把想变成私有方法的名字告诉它。如果不加参数,它就像是一个开关,它下面定义的所有的实例方法都会是私有方法,直到调用 public 或 protected 。

你不能:

b = Baker.new
b.add_egg
这样调用 add_egg 会报错:

`

': private method `add_egg' called for # (NoMethodError)
因为 add_egg 是一个私有方法,调用它的时候你指定了某个具体的接收对象,这是不允许的。

如果我们不加信息的接收者:

add_egg
是否能单独调用这个方法?信息会发送到哪里?如果没有对象处理信息,那方法怎么被调用?调用方法的时候如果不指定信息的接收者,Ruby 会把信息发送给当前对象,也就是 self 表示的那个对象。

你可以推断出来,能对 add_egg 这个信息作出回应的对象,只能是 self 表示的那个可以对 add_egg 作出回应的对象。也就是我们只能在当 self 是 Baker 的实例的时候才能调用 add_egg 这个实例方法。

私有方法与独立方法

私有方法与独立方法不是一回事。独立方法只属于一个对象。私有方法可以属于多个对象,不过只有在正确的情况下才能被调用。决定可以调用私有方法的不是你发送信息到的对象,而是你发送信息的时候 self 表示的那个对象。

保护方法

保护方法是一种温柔点私有方法。规则像这样:

you can call a protected method on an object x, as long as the default object (self) is an instance of the same class as x or of an ancestor or descendant class of x’s class.
保护方法主要的目的就是,你可以在某个类的一个实例上使用这个类的另一个实例去做点事儿。来看个例子:

class C
  def initialize(n)
    @n = n
  end

  def n
    @n
  end

  def compare(c)
    if c.n > n
      puts "另一个对象的 n 更大一些"
    else
      puts "另一个对象的 n 一样或更小一些"
    end
  end
 
  protected :n
end

c1 = C.new(100)
c2 = C.new(101)
c1.compare(c2)
上面这个例子就是去拿 C 类的一个实例跟它的另一个实例比较。这个比较依赖调用方法 n 返回的结果。做比较的这个对象(例子是 c1 对象)需要让另一个对象(c2)去执行它的 n 方法。也就是 n 不能是一个私有方法。

这就需要使用一个保护方法。让 n 成为保护方法,而不是私有方法,c1 可以让 c2 去执行方法 n,因为 c1 跟 c2 是同一个类的实例对象。但是如果你试着在 C 对象上面调用 n 方法,会失败,因为 C 并不是 C 类的一个实例。

子类也会继承使用超级类上的方法访问规则,不过你可以在子类里覆盖掉这些规则。

下午4:35 ***

顶级方法

下午4:35 ***

用 Ruby 做的最自然的事情就是去设计类,模块,还有实例化类。不过有时候你想快速的写一些脚本,不想把代码放到一个类里面,你可以在顶级(top-level)去定义与使用这些方法。做这样的事儿就是在顶级默认的对象里面写代码,这个对象叫 main,它是自动生成的 Object 的一个实例,这么做主要是因为必须得有一个东西是 self,即使是在顶级。

定义顶级方法

在顶级定义个方法:

def talk
  puts 'hello'
end
在顶级上定义的方法会作为 Object 类的一个实例上的私有的方法。上面的代码就相当于是:

class Object
  private

  def talk
    puts 'hello'
  end
end
调用这些方法的时候必须使用裸字风格,也就是不能指定信息的接收者,因为它们是私有方法。Object 的私有实例方法可以在任何地方调用,因为 Object 是在所属的方法查找路径里面,所以顶级的方法会一直有效。

再看个例子:

def talk
  puts 'hello'
end

puts "不加接收者执行 talk"
talk
puts "加个接收者执行 talk"
obj = Object.new
obj.talk
第一次执行 talk 能成功,第二次执行的时候会报错,因为调用私有方法的时候不能指定接收者。

预定义的顶级方法

puts,print 都是 Kernel 的内置的私有实例方法,查看所有在 Kernel 上提供的私有方法:

ruby -e 'p Kernel.private_instance_methods.sort'

相关文章

精彩推荐