跳转到内容

Ruby 编程/语法/类

来自 Wikibooks,开放世界中的开放书籍

是用于创建 对象 实例的基本模板。类由表示内部状态的变量集合和提供操作该状态的行为的方法组成。

类定义

[编辑 | 编辑源代码]

类在 Ruby 中使用 class 关键字后跟一个名称来定义。名称必须以大写字母开头,按照惯例,包含多个单词的名称将一起运行,每个单词首字母大写,没有分隔符 (驼峰式命名法)。类定义可以包含方法、类变量和实例变量声明,以及在读取时在类上下文中执行的方法调用,例如 attr_accessor。类声明以 end 关键字结束。

示例

  class MyClass
    def some_method
    end
  end

实例变量

[编辑 | 编辑源代码]

实例变量是为每个类实例创建的,并且只能在该实例内访问。它们使用 @ 运算符访问。在类定义之外,实例变量的值只能通过该实例的公共方法读取或修改。

示例

 
  class MyClass
    @one = 1
    def do_something
      @one = 2
    end
    def output
      puts @one
    end
  end
  instance = MyClass.new
  instance.output
  instance.do_something
  instance.output

令人惊讶的是,这会输出

 nil
 2


发生这种情况(第一行输出为 nil)是因为在 class MyClass 以下定义的 @one 是属于类对象的实例变量(注意这与类变量不同,不能称为 @@one),而在 do_something 方法内部定义的 @one 是属于 MyClass 实例的实例变量。它们是两个不同的变量,第一个变量只能在类方法中访问。

访问器方法

[编辑 | 编辑源代码]

如上一节所述,实例变量只能在实例方法定义中直接访问或修改。如果您想从外部提供对其的访问权限,则需要定义公共访问器方法,例如

  class MyClass
    def initialize
      @foo = 28
    end

    def foo
      return @foo
    end

    def foo=(value)
      @foo = value
    end
  end

  instance = MyClass.new
  puts instance.foo
  instance.foo = 496
  puts instance.foo

请注意,ruby 提供了一些语法糖,使其看起来像您正在直接获取和设置变量;在幕后

  a = instance.foo
  instance.foo = b

是调用 foo 和 foo= 方法

  a = instance.foo()
  instance.foo=(b)

由于这是一个非常常见的用例,因此还有一个便利方法来自动生成这些 getter 和 setter

  class MyClass
    attr_accessor :foo

    def initialize
      @foo = 28
    end
  end

  instance = MyClass.new
  puts instance.foo
  instance.foo = 496
  puts instance.foo

与上面的程序做同样的事情。attr_accessor 方法在读取时运行,当 ruby 正在构建类对象时,它会生成 foo 和 foo= 方法。

但是,访问器方法不需要仅仅透明地访问实例变量。例如,我们可以确保所有值在存储到 foo 中之前都被四舍五入

  class MyClass
    def initialize
      @foo = 28
    end

    def foo
      return @foo
    end

    def foo=(value)
      @foo = value.round
    end
  end

  instance = MyClass.new
  puts instance.foo
  instance.foo = 496.2
  puts instance.foo #=> 496

类变量

[编辑 | 编辑源代码]

类变量使用 @@ 运算符访问。这些变量与类层次结构相关联,而不是与类的任何对象实例相关联,并且在所有对象实例中都是相同的。(这些类似于 Java 或 C++ 中的类“静态”变量)。

示例

  class MyClass
    @@value = 1
    def add_one
      @@value= @@value + 1
    end
    
    def value
      @@value
    end
  end
  instanceOne = MyClass.new
  instanceTwo = MyClass.new
  puts instanceOne.value
  instanceOne.add_one
  puts instanceOne.value
  puts instanceTwo.value

输出

 1
 2
 2

类实例变量

[编辑 | 编辑源代码]

类可以有实例变量。这使得每个类都有一个变量,该变量不会被继承链中的其他类共享。

  class Employee
    class << self; attr_accessor :instances; end
    def store
      self.class.instances ||= []
      self.class.instances << self
    end
    def initialize name
      @name = name
    end
  end
  class Overhead < Employee; end
  class Programmer < Employee; end
  Overhead.new('Martin').store
  Overhead.new('Roy').store
  Programmer.new('Erik').store
  puts Overhead.instances.size    # => 2
  puts Programmer.instances.size  # => 1

有关更多详细信息,请参见 MF Bliki:类实例变量

类方法

[编辑 | 编辑源代码]

类方法的声明方式与普通方法相同,只是它们以 self 或类名后跟一个句点为前缀。这些方法在类级别执行,可以在没有对象实例的情况下调用。它们无法访问实例变量,但可以访问类变量。

示例

  class MyClass
    def self.some_method
      puts 'something'
    end
  end
  MyClass.some_method

输出

 something

实例化

[编辑 | 编辑源代码]

对象实例是通过称为 实例化 的过程从类创建的。在 Ruby 中,这通过类方法 new 进行。

示例

  anObject = MyClass.new(parameters)

此函数在内存中设置对象,然后如果存在,将控制权委托给类的 initialize 函数。传递给 new 函数的参数将传递给 initialize 函数。

  class MyClass
    def initialize(parameters)
    end
  end

声明可见性

[编辑 | 编辑源代码]

默认情况下,Ruby 类中的所有方法都是公有的 - 任何人都可以访问。但是,对于此规则只有两个例外:在 Object 类下定义的全局方法,以及任何类的 initialize 方法。它们都是隐式私有的。

如果需要,可以通过 public、private、protected 对象方法限制方法的访问权限。

有趣的是,这些实际上不是关键字,而是真正操作类的方法,动态地改变方法的可见性,因此,这些“关键字”会影响所有后续声明的可见性,直到设置新的可见性或声明体结束为止。

简单示例

  class Example
    def methodA
    end
    
    private # all following methods in this class are private, so are inaccessible to outside objects
    
    def methodP
    end
  end

如果 private 在没有参数的情况下调用,它将为所有后续方法设置私有访问权限。它也可以使用命名参数调用。

命名私有方法示例

  class Example
    def methodA
    end
    
    def methodP
    end
    
    private :methodP
  end

这里 private 使用一个参数调用,并将 methodP 的可见性设置为私有。

请注意,类方法(使用 def ClassName.method_name 声明)必须使用 private_class_method 函数设置为私有。

private_class_method 的一个常见用法是使构造函数方法 new 不可访问,从而迫使通过某些 getter 函数访问对象。典型的 Singleton 实现就是一个明显的例子

  class SingletonLike
    private_class_method :new
    
    def SingletonLike.create(*args, &block)
      @@inst = new(*args, &block) unless @@inst
      return @@inst
    end
  end

以下是另一种常见的编写相同声明的方式

  class SingletonLike
    private_class_method :new
    
    def SingletonLike.create(*args, &block)
      @@inst ||= new(*args, &block)
    end
  end

虽然在 C++ 中 private 表示“对该类私有”,但在 Ruby 中它表示“对该实例私有”。这意味着 C++ 允许在给定类中,任何代码都可以访问该类中任何对象的私有方法。另一方面,在 Ruby 中,私有方法是它们所属的实例化的对象本地的。

private 方法不能使用显式接收器调用这一事实由以下代码说明。

  class AccessPrivate
    def a
    end
    private :a # a is private method
        
    def accessing_private
      a              # sure! 
      self.a         # nope! private methods cannot be called with an explicit receiver, even if that receiver is "self"
      other_object.a # nope, a is private, so you can't get it (but if it was protected, you could!)
    end
  end

这里,other_object 是方法 a 所调用的“接收者”。对于私有方法,它不起作用。但是,“保护”可见性将允许这样做。

方法的默认可见性级别为“公共”,可以使用 public 方法手动指定此可见性级别。

我不确定为什么指定了它——可能是为了完整性,可能是为了让你能够在某个时刻动态地将某个方法设为私有,然后在稍后时间再设为公共。

在 Ruby 中,可见性是完全动态的。你可以在运行时更改方法的可见性!

现在,“保护”值得更多讨论。那些来自 Java 或 C++ 的人已经了解到,在这些语言中,如果一个方法是“私有”,它的可见性将被限制在声明它的类中,如果方法是“保护”,它将对该类的子类(从父类继承的类)或该包中的其他类可见。

在 Ruby 中,“私有”可见性类似于 Java 中的“保护”。Ruby 中的私有方法可以从子类访问。你无法在 Ruby 中拥有真正的私有方法;你无法完全隐藏一个方法。

保护和私有之间的区别很微妙。如果一个方法是保护的,它可以被定义该类的任何实例或其子类调用。如果一个方法是私有的,它只能在调用对象的上下文中调用——永远不可能直接访问另一个对象实例的私有方法,即使该对象与调用者是同一个类。对于保护方法,它们可以从同一个类(或子类)的对象访问。

因此,从对象“a1”(类 A 的实例)内部,你只能调用实例“a1”(self)的私有方法。你不能调用对象“a2”(也是类 A 的实例)的私有方法——它们对 a2 来说是私有的。但你可以调用对象“a2”的保护方法,因为对象 a1 和 a2 都是类 A 的实例。

Ruby FAQ 给出了以下示例——实现一个运算符,将一个内部变量与同一个类中的一个变量进行比较(用于比较对象的目的)

  def <=>(other)
    self.age <=> other.age
  end

如果 age 是私有的,此方法将无法工作,因为 other.age 无法访问。如果“age”是保护的,这将正常工作,因为 self 和 other 是同一个类,可以访问彼此的保护方法。

要考虑这一点,保护实际上让我想起了 C# 中的“内部”访问修饰符或 Java 中的“默认”访问修饰符(当没有访问关键字设置在方法或变量上时):方法的访问方式与“公共”相同,但仅适用于同一个包内的类。

实例变量

[编辑 | 编辑源代码]

请注意,对象实例变量不是真正的私有的,你只是看不见它们。要访问实例变量,你需要创建一个 getter 和 setter。

像这样(不,不要手动这样做!请参见下方)

  class GotAccessor
    def initialize(size)
      @size = size
    end
    
    def size
      @size
    end
    def size=(val)
      @size = val
    end
  end
  
  # you could access the @size variable as
  # a = GotAccessor.new(5)
  # x = a.size 
  # a.size = y

幸运的是,我们有专门的函数来做这件事:attr_accessor、attr_reader、attr_writer。attr_accessor 会给你 get/set 功能,reader 仅提供 getter,writer 仅提供 setter。

现在缩减到

  class GotAccessor
    def initialize(size)
      @size = size
    end
    
    attr_accessor :size
  end
  
  # attr_accessor generates variable @size accessor methods automatically:
  # a = GotAccessor.new(5)
  # x = a.size 
  # a.size = y
super 关键字只访问直接父类的方法。不过,存在一种解决方法。

一个类可以从超类继承功能和变量,有时被称为父类基类。Ruby 不支持多重继承,因此 Ruby 中的类只能有一个超类。语法如下

  
  class ParentClass
    def a_method
      puts 'b'
    end
  end
  
  class SomeClass < ParentClass  # < means inherit (or "extends" if you are from Java background)
    def another_method
      puts 'a'
    end
  end
  
  instance = SomeClass.new
  instance.another_method
  instance.a_method

输出

  a
  b

所有非私有变量和方法都从超类继承到子类。

如果你的类覆盖了父类(超类)的方法,你仍然可以使用 'super' 关键字访问父类的方法。

  class ParentClass
    def a_method
      puts 'b'
    end
  end
  
  class SomeClass < ParentClass  
    def a_method
      super
      puts 'a'
    end
  end
  
  instance = SomeClass.new
  instance.a_method

输出

  b
  a

(因为 a_method 也调用了父类的方法)。

如果你有一条很深的继承链,并且仍然想直接访问某个父类(超类)的方法,你做不到。super 只能为你提供直接父类的方法。但存在一种解决方法!当从一个类继承时,你可以将父类方法别名为另一个名称。然后,你可以通过别名访问方法。

  class X
    def foo
      "hello"
    end
  end
  
  class Y < X
    alias xFoo foo
    def foo
      xFoo + "y"
    end
  end

  class Z < Y
    def foo
      xFoo + "z"
    end
  end
  
  puts X.new.foo
  puts Y.new.foo
  puts Z.new.foo

输出

 hello
 helloy
 helloz

混合模块

[编辑 | 编辑源代码]

首先,你需要了解模块 Ruby 模块。模块是一种将一些函数、变量和类分组在一起的方式,有点像类,但更像是命名空间。因此,模块并不是真正的类。你不能实例化模块,并且模块不能使用 Self 来引用自身。模块可以拥有模块方法(就像类可以拥有类方法)以及实例方法。

不过,我们可以将模块包含到类中。将它混合进来,可以这么说。

module A
  def a1
    puts 'a1 is called'
  end
end

module B
  def b1
    puts 'b1 is called'
  end
end

module C
  def c1
    puts 'c1 is called'
  end
end

class Test
  include A
  include B
  include C

  def display
    puts 'Modules are included'
  end
end

object=Test.new
object.display
object.a1
object.b1
object.c1

输出

Modules are included
a1 is called
b1 is called
c1 is called

模块可以通过几种不同的方式混合到类中。在深入研究本节内容之前,了解对象如何将消息解析为方法名称非常重要。

module A
  def a1
    puts "a1 from module"
  end
end

class Test
  include A

  def a1
    puts "a1 from class"
  end
end

test = Test.new
test.a1

输出

a1 from class

使用include 时,类提供的方法会在模块提供的方法之前被搜索。这意味着来自类的函数将首先被找到并执行。

module A
  def a1
    puts "a1 from module"
  end
end

class Test
  prepend A

  def a1
    puts "a1 from class"
  end
end

test = Test.new
test.a1

输出

a1 from module

使用prepend 时,模块提供的方法会在类提供的方法之前被搜索。这意味着来自插入的模块的函数将首先被找到并执行。

代码展示了使用模块的多重继承。

Ruby 类元模型

[编辑 | 编辑源代码]

为了符合 Ruby 的一切皆对象的原则,类本身是类 Class 的实例。它们存储在声明它们的模块的作用域下的常量中。对对象实例上的方法的调用被委托给对象内部的一个变量,该变量包含对该对象类的引用。方法实现存在于类实例对象本身。类方法在元类上实现,这些元类以与这些类实例链接到它们相同的方式链接到现有的类实例对象。这些元类对大多数 Ruby 函数隐藏。

华夏公益教科书