网安
  • Develop
    • JAVA学习
      • 字节码
      • API开发
      • Web开发
      • 工程结构推荐
      • 创建第一个项目
      • 权限管控
      • 配置文件
      • 日志管理
      • 数据访问
      • 性能监控
      • IoC容器
      • Spring
      • Filter与Listener
      • jsp
      • MVC
      • servlet-1
      • servlet-2
      • servlet-3
      • servlet-4
      • FreeMarker
      • Thymeleaf
      • EL
      • SpEL
      • JSTL
      • 部署
      • JDBC
      • 数据库连接池
      • fastjson
      • jackson
      • XML
      • JSON
      • 序列化
      • Maven
      • 安装与使用
      • 工具
      • 爬虫
    • GO学习
      • GO
        • flag 包
        • goland 远程调试
        • GoReleaser
        • OS 包
        • time 包
        • 格式化输出
    • Lua学习
      • Lua
      • 基础语法
      • LuaJIT
      • 与系统交互
    • Pyhon
      • 基础
      • Django
      • CLI
      • miniforge
      • MockingBird
      • pdb
      • pyc
      • 装的我脑血栓要犯了
      • Python101
      • 反序列化
      • 爬虫
      • Pillow
      • 图像识别
      • flask
    • Speed-Ruby
      • 入门1
      • 入门2 对象
      • 入门3 创建命令
      • Encoding类
      • File类与Dir类
      • IO
      • Proc类
      • Time类与Date类
      • 正则
      • 错误处理与异常
      • 对象、变量和常量
      • 方法
      • 数值
      • 数组
      • 条件判断
      • 循环
      • 运算符
      • Socket编程
      • 字符串
      • 并发与线程
      • 块
      • 类和模块
      • 散列
    • Web
      • HTTP
        • Connection
        • HTTP 报文
        • Vary
      • 笔记
        • 跳转
        • 认证 & 授权
        • 同源策略(SOP)
        • 文件
    • Git 学习笔记
    • JSON
      • JSON 学习笔记
    • HTML
      • Speed-HTML
      • 语法学习
      • HTML字符实体
    • XML
      • XML 学习笔记
    • 计算机基础
      • 操作系统
      • 计算机组成
      • 算法
      • 内存
      • 字符编码
    • gnuplot 学习笔记
    • regex
  • Integrated
    • Linux
      • God-Linux
      • Secure-Linux
      • Power-Linux
      • IO模型
      • Speed-Linux
      • 发行版
      • 工具
      • 启动过程
      • 进程
      • 认证
      • 日志
      • 守护进程
      • 文件
      • 信息
      • VSFTP 配置案例
      • auditd
      • containerd
      • DNS 配置案例
      • Docker
      • Docker-Compose
      • firewalld 实验
      • gpg
      • Iptables
      • httpd
      • LAMP
      • mysql
      • nfs 配置案例
      • openssl
      • PAM
      • samba 配置案例
      • terraform
      • ufw
      • VSFTP 配置案例
    • Network
      • Speed-Net
      • Power-Net
      • SDN 笔记
      • DNS
      • TLS
    • Windows
      • Secure-Win
      • Speed-Win
      • ACL
      • LDAP
      • IPC$(Internet Process Connection)
      • PDB符号文件
      • 工作组
      • WinRM
      • 角色权限
      • 凭据
      • 签名
      • 日志
      • 认证
      • 协议
      • 信息
      • 应用
      • 组策略
      • 域
      • asp站点搭建
      • Exchange 搭建
      • Windows 故障转移集群
      • Windows 基础服务搭建
      • Windows 域搭建
      • 本地抓包
      • PowerShell 笔记
    • 容器
      • Docker
    • 数据库
      • Speed-SQL
      • Power-SQL
      • MSSQL
      • MySQL
      • Postgresql
      • Redis
      • MySQL大小写问题
      • 主键和外键
      • MySQL快速入门
      • 虚拟化
        • ESXi
        • vCenter
  • Plan
    • Mac-Plan
    • Misc-Plan
    • Team-Plan
    • Thinking-Plan
    • VM-Plan
  • Sercurity
    • Power-PenTest
    • BlueTeam
      • 安全建设
      • 分析
      • 加固
      • 取证
      • 应急
      • USB取证
      • 磁盘取证
      • 内存取证
      • ClamAV 部署
      • yara 实验
      • 安防设施搭建使用
      • ZIP明文攻击
      • 流量分析
    • Crypto
      • Crypto
        • 2020 9 G60攻防大赛
        • CTF
        • 2020 9 中能融合杯工控CTF
        • 2020 10 全国工业互联网安全技术技能大赛江苏省选拔赛
        • 2020 10 全国网络与信息安全管理职业技能大赛江苏场
        • 2020 11 I²S峰会暨工业互联网安全大赛
        • 2021 6 第二届I²S峰会暨工业互联网安全大赛
        • 2021-9-第七届工控信息安全攻防竞赛
        • 2021 9 第七届全国职工职业技能大赛某市县选拔赛
        • 2021 9 全国网络与信息安全管理职业技能大赛江苏场
        • 2021-10-G60攻防大赛
    • CTF
      • CTF
      • writeup
        • 2020 9 中能融合杯工控CTF
        • 2020 9 G60攻防大赛
        • 2020 10 全国工业互联网安全技术技能大赛江苏省选拔赛
        • 2020 10 全国网络与信息安全管理职业技能大赛江苏场
        • 2020 11 I²S峰会暨工业互联网安全大赛
        • 2021 6 第二届I²S峰会暨工业互联网安全大赛
        • 2021-9-第七届工控信息安全攻防竞赛
        • 2021 9 第七届全国职工职业技能大赛某市县选拔赛
        • 2021 9 全国网络与信息安全管理职业技能大赛江苏场
        • 2021-10-G60攻防大赛
    • ICS
      • PLC攻击
      • S7comm 相关
      • 工控协议
      • 上位机安全
      • Modbus 仿真环境搭建
      • siemens 仿真搭建实验
      • S7-300 启停实验
    • IOT
      • 无线电安全
        • RFID复制卡
        • RFID基础知识
        • WiFikiller
      • 硬件安全
        • DIY键盘嵌入指纹识别模块实验记录
        • Device-Exploits
        • HID-Digispark
        • HID-KeyboardLogger
        • HID-USBHarpoon
        • HID-USBKeyLogger
      • 固件安全
        • 固件安全
        • Dlink_DWR-932B 路由器固件分析
    • Mobile sec
      • 小程序安全
      • Android安全
    • PWN
      • SLMail溢出案例
      • PWN
    • Red Team
      • OS安全
        • Linux 安全
        • Exploits
        • NTLM中继
        • Windows 安全
        • Responder欺骗
        • Windows-LOL
      • Web_Generic
        • Top 10
          • RCE
          • Fileread
          • SQLi
          • SSRF
          • SSTI
          • Web Generic
          • XSS
          • XXE
      • Web_Tricks
        • JWT 安全
        • HTTP_request_smuggling
        • OOB
        • 绕过访问
      • 靶场
        • Hello-Java-Sec 学习
        • DVWA-WalkThrough
        • pikachu-WalkThrough
        • upload-labs-WalkThrough
        • XVWA-WalkThrough
        • XSS挑战-WalkThrough
      • 实验
        • flask
        • fastjson
        • Log4j
        • nodejs
        • Shiro
        • Spring
        • Weblogic
      • 前端攻防
      • IDOR
    • 安防设备
      • Exploits
      • Bypass 技巧
    • 后渗透
      • 权限提升
      • 后渗透
      • 权限维持
      • 实验
        • C2 实验
        • Exchange
        • 端口转发实验
        • 代理实验
        • 免杀实验
        • 隧道实验
    • 软件服务安全
      • Exploits
      • CS Exploits
      • 实验
        • Docker
        • Kubernetes
        • Mysql
        • Oracle
        • PostgreSQL
        • Redis
        • vCenter
    • 协议安全
      • Exploits
    • 信息收集
      • 端口安全
      • 空间测绘
      • 信息收集
    • 语言安全
      • 语言安全
        • 语言安全
      • GO安全
        • GO安全
        • Go代码审计
      • JAVA安全
        • JAVA安全
        • JAVA代码审计
        • JAVA反序列化
        • SpEL 注入
      • PHP安全
        • PHP安全
        • bypass_disable_function
        • bypass_open_basedir
        • phpinfo
        • PHP代码审计
        • PHP反序列化
        • PHP回调函数
        • 变量覆盖
        • POP
        • 弱类型
        • 伪协议
        • 无字母数字Webshell
      • Python安全
        • pyc反编译
        • Python安全
        • Python 代码审计
        • 沙箱逃逸
      • dotnet安全
      • JS安全
    • 云安全
      • 公有云安全
    • Reverse
      • Reverse
      • FILE
        • ELF
        • BMP
        • JPG
        • PE
        • PNG
        • ZIP
        • 文件头
      • 实验
        • PYAble
          • 2-逆运算
          • 1-基本分析
          • 3-异或
          • 4-Base64
          • 5-Base64换表
          • 6-动态调试
        • Windows
          • condrv.sys 内存损坏漏洞
    • 工具
      • Aircrack
      • BloodHound
      • Burp Suite
      • frp
      • CobaltStrike
      • Ghidra
      • fscan
      • Hashcat
      • IDA
      • merlin
      • Kali
      • Metasploit
      • Mimikatz
      • ModSecurity
      • Nmap
      • nps
      • nuclei
      • pupy
      • RedGuard
      • SET
      • sliver
      • Snort
      • Sqlmap
      • Suricata
      • Sysmon
      • uncover
      • Volatility
      • Wfuzz
      • Wireshark
      • xray
    • 安全资源
      • 靶机
        • VulnHub
          • DC
            • DC2 WalkThrough
            • DC1 WalkThrough
            • DC3 WalkThrough
            • DC4 WalkThrough
            • DC5 WalkThrough
            • DC6 WalkThrough
            • DC9 WalkThrough
            • DC8 WalkThrough
          • It's_October
            • It’s_October1 WalkThrough
          • Kioptrix
            • Kioptrix2 WalkThrough
            • Kioptrix3 WalkThrough
            • Kioptrix4 WalkThrough
            • Kioptrix5 WalkThrough
          • Mission-Pumpkin
            • PumpkinGarden-WalkThrough
            • PumpkinFestival WalkThrough
            • PumpkinRaising WalkThrough
          • Symfonos
            • symfonos1 WalkThrough
            • symfonos2 WalkThrough
            • symfonos3 WalkThrough
            • symfonos5 WalkThrough
        • Wargames
          • Bandit
            • Bandit-WalkThrough
      • 面试问题
        • 面试问题
Powered by GitBook
On this page
  • 类和模块
  • 类是什么
  • 类的创建
  • 扩展类
  • alias 与 undef
  • 模块
  • 创建模块
  • Mix-in
  • 面向对象程序设计
  1. Develop
  2. Speed-Ruby

类和模块

类和模块


  • https://www.kancloud.cn/imxieke/ruby-base/107295


类是什么

类(class)是面向对象中一个重要的术语。

类和实例

类表示对象的种类。Ruby 中的对象都一定属于某个类。例如,我们常说的“数组对象”“数组”,实际上都是 Array 类的对象(实例)。还有字符串对象,实际上是 String 类的对象(实例)。

相同类的对象所使用的方法也相同。类就像是对象的雏形或设计图,决定了对象的行为。

我们在生成新的对象时,一般会用到各个类的 new 方法。例如,使用 Array.new 方法可以生成新的数组对象。

ary = Array.new
p ary #=> []

像数组、字符串这样的类,也可以使用字面量(像 [1, 2, 3]、"abc" 这样的写法)来生成对象。

当想知道某个对象属于哪个类时,我们可以使用 class 方法。

ary = []
str = "Hello world."
p ary.class    #=> Array
p str.class    #=> String

当判断某个对象是否属于某个类时,我们可以使用 instance_of? 方法。

ary = []
str = "Hello world."
p ary.instance_of?(Array)    #=> true
p str.instance_of?(String)   #=> true
p ary.instance_of?(String)   #=> false
p str.instance_of?(Array)    #=> false

继承

我们把通过扩展已定义的类来创建新类称为继承。

假设我们需要编写一个在屏幕中显示时间的小程序。根据用户的喜好,这个小程序能以模拟时钟或者电子时钟的方式显示。

模拟时钟与电子时钟,两者只是在时间的表现形式上不一样,获取当前时间的方法以及闹钟等基本功能都是相同的。因此,我们可以首先定义一个拥有基本功能的时钟类,然后再通过继承来分别创建模拟时钟类和电子时钟类。

继承后创建的新类称为子类(subclass),被继承的类被称为父类(superclass)。

通过继承我们可以实现以下事情:

  • 在不影响原有功能的前提下追加新功能。

  • 重定义原有功能,使名称相同的方法产生不同的效果。

  • 在已有功能的基础上追加处理,扩展已有功能。

此外,我们还可以利用继承来轻松地创建多个具有相似功能的类。

BasicObject 类是 Ruby 中所有类的父类,它定义了作为 Ruby 对象的最基本功能。

BasicObject 类是最最基础的类,甚至连一般对象需要的功能都没有定义。因此普通对象所需要的类一般都被定义为 Object 类。字符串、数组等都是 Object 类的子类。

子类与父类的关系称为“is-a 关系”。例如,String 类与它的父类 Object 就是 is-a 关系。

之前我们提到过查找对象所属的类时使用 instance_of? 方法,而根据类的继承关系反向追查对象是否属于某个类时,则可以使用 is_a? 方法。

str = "This is a String."
p str.is_a?(String)    #=> true
p str.is_a?(Object)    #=> true

顺便提一下,由于 instance_of? 方法与 is_a? 方法都已经在 Object 类中定义过了,因此普通的对象都可以使用这两个方法。

类的创建

class HelloWorld                   # class 关键字
  def initialize(myname = "Ruby")  # initialize 方法
    @name = myname # 初始化实例变量
  end

  def hello                        # 实例方法
    puts "Hello, world. I am       #{@name}."
  end
end

bob = HelloWorld.new("Bob")
alice = HelloWorld.new("Alice")
ruby = HelloWorld.new

bob.hello

class 关键字

class 关键字在定义类时使用。以下是 class 关键字的一般用法:

class 类名
 类的定义
end

类名的首字母必须大写。

initialize 方法

在 class 关键字中定义的方法为该类的实例方法。

其中,名为 initialize 的方法比较特别。使用 new 方法生成新的对象时,initialize 方法会被调用,同时 new 方法的参数也会被原封不动地传给 initialize 方法。因此初始化对象时需要的处理一般都写在这个方法中。

def initialize(myname = "Ruby")  # initialize 方法
  @name = myname                 # 初始化实例变量
end

在这个例子中,initialize 方法接受了参数 myname。因此,

bob = HelloWorld.new("Bob")

像这样,就可以把 "Bob" 传给 initialize 方法生成对象。由于 initialize 方法的参数指定了默认值 "Ruby",因此,像下面这样没有指定参数时,

ruby = HelloWorld.new

会自动把 "Ruby" 传给 initialize 方法。

实例变量与实例方法

def initialize(myname = "Ruby")  # initialize 方法
  @name = myname                 # 初始化实例变量
end

通过 @name = myname 这行程序,作为参数传进来的对象会被赋值给变量 @name。我们把以 @ 开头的变量称为实例变量。在不同的方法中,程序会把局部变量看作是不同的变量来对待。而只要在同一个实例中,程序就可以超越方法定义,任意引用、修改实例变量的值。另外,引用未初始化的实例变量时的返回值为 nil。

不同实例的实例变量值可以不同。只要实例存在,实例变量的值就不会消失,并且可以被任意使用。而局部变量则是在调用方法时被创建,而且只能在该方法内使用。

我们来看看下面的例子:

alice = HelloWorld.new("Alice")
bob = HelloWorld.new("Bob")
ruby = helloWorld.new

alice、bob、ruby 各自拥有不同的 @name

可以在实例方法中引用实例变量,下面是 HelloWorld 类定义的 hello 方法引用 @name 的例子:

class HelloWorld
  ┊
  def hello                        # 实例方法
    puts "Hello, world. I am #{@name}."
  end
end

通过以下方式调用 HelloWolrd 类定义的 hello 方法:

bob.hello

输出结果如下所示:

Hello, world. I am Bob.

存取器

在 Ruby 中,从对象外部不能直接访问实例变量或对实例变量赋值,需要通过方法来访问对象的内部。

为了访问 HelloWorld 类的 @name 实例变量,我们需要定义以下方法:

class HelloWorld
  ┊
  def name          # 获取@name
    @name
  end

  def name=(value)  # 修改@name
    @name = value
  end
  ┊
end

第一个方法 name 只是简单地返回 @name 的值,我们可以像访问属性一样使用该方法。

p bob.name        #=> "Bob"

第二个方法的方法名为 name=,使用方法如下:

bob.name = "Robert"

乍一看,该语法很像是在给对象的属性赋值,但实际上却是在调用 name=("Robert") 这个方法。利用这样的方法,我们就可以突破 Ruby 原有的限制,从外部来自由地访问对象内部的实例变量了。

当对象的实例变量有多个时,如果逐个定义存取器,就会使程序变得难懂,而且也容易写错。为此,Ruby 为了我们提供了更简便的定义方法 attr_reader、attr_writer、attr_accessor。只要指定实例变量名的符号(symbol),Ruby 就会自动帮我们定义相应的存取器。

定义 | 意义

  • | - attr_reader :name | 只读(定义 name 方法) attr_writer :name | 只写(定义 name= 方法) attr_accessor :name | 读写(定义以上两个方法)

也可以像下面这样只写一行代码,其效果与刚才的 name 方法以及 name= 方法的效果是一样的。

class HelloWorld
  attr_accessor :name
end

Ruby 中一般把设定实例变量的方法称为 writer,读取实例变量的方法称为 reader,这两个方法合称为 accessor。另外,有时也把 reader 称为 getter,writer 称为 setter,合称为 accessor method4。

特殊变量 self

在实例方法中,可以用 self 这个特殊的变量来引用方法的接收者。接下来就让我们来看看其他的实例方法如何调用 name 方法。

class HelloWorld
  attr_accessor :name
  ┊
  def greet
    puts "Hi, I am #{self.name}."
  end
end
  ┊

greet 方法里的 self.name 引用了调用 greet 方法时的接收者。

调用方法时,如果省略了接收者,Ruby 就会默认把 self 作为该方法的接收者。因此,即使省略了 self,也还是可以调用 name 方法,如下所示:

def greet
  print "Hi, I am #{name}"
end

另外,在调用像 name= 方法这样的以 = 结束的方法时,有一点需要特别注意。即使实例方法中已经有了 name = "Ruby" 这样的定义,但如果仅在方法内部定义名为 name 的局部变量,也不能以缺省接收者的方式调用 name= 方法。这种情况下,我们需要用 self.name = "Ruby" 的形式来显式调用 name 方法。

def test_name
  name = "Ruby"         # 为局部变量赋值
  self.name = "Ruby"    # 调用name= 方法
end

虽然 self 本身与局部变量形式相同,但由于它是引用对象本身时的保留字,因此我们即使对它进行赋值,也不会对其本身的值有任何影响。像这样,已经被系统使用且不能被我们自定义的变量名还有 nil、true、false、__FILE__、__LINE__、__ENCODING__ 等。

类方法

方法的接收者就是类本身(类对象)的方法称为类方法。类方法的操作对象不是实例,而是类本身。

下面,让我们在 class << 类名 ~ end 这个特殊的类定义中,以定义实例方法的形式来定义类方法。

class << HelloWorld
  def hello(name)
    puts "#{name} said hello."
  end
end

HelloWorld.hello("John")    #=> John said hello.

在 class 上下文中使用 self 时,引用的对象是该类本身,因此,我们可以使用 class << self ~ end 这样的形式,在 class 上下文中定义类方法。

class HelloWorld
  class << self
    def hello(name)
      puts "#{name} said hello."
    end
  end
end

除此以外,我们还可以使用 def 类名 . 方法名 ~ end 这样的形式来定义类方法。

def HelloWorld.hello(name)
  puts "#{name} said hello."
end

HelloWorld.hello("John")    #=> John said hello.

同样,只要是在 class 上下文中,这种形式下也可以像下面的例子那样使用 self。

class HelloWorld
  def self.hello(name)
    puts "#{name} said hello."
  end
end

class << 类名 ~ end 这种写法的类定义称为单例类定义,单例类定义中定义的方法称为单例方法。

常量

在 class 上下文中可以定义常量。

class HelloWorld
  Version = "1.0"
  ┊
end

对于在类中定义的常量,我们可以像下面那样使用 ::,通过类名来实现外部访问。

p HelloWorld::Version    #=> "1.0"

类变量

以 @@ 开头的变量称为类变量。类变量是该类所有实例的共享变量,这一点与常量类似,不同的是我们可以多次修改类变量的值。另外,与实例变量一样,从类的外部访问类变量时也需要存取器。不过,由于 attr_accessor 等存取器都不能使用,因此需要直接定义。

class HelloCount
  @@count = 0           # 调用hello 方法的次数

  def HelloCount.count  # 读取调用次数的类方法
    @@count
  end

  def initialize(myname="Ruby")
    @name = myname
  end

  def hello
    @@count += 1        # 累加调用次数
    puts "Hello, world. I am #{@name}.\n"
  end
end

bob = HelloCount.new("Bob")
alice = HelloCount.new("Alice")
ruby = HelloCount.new

p HelloCount.count      #=> 0
bob.hello
alice.hello
ruby.hello
p HelloCount.count      #=> 3

限制方法的调用

到目前为止,我们定义的方法,都能作为实例方法被任意调用,但是有时候我们可能并不希望这样。例如,只是为了汇总多个方法的共同处理而定义的方法,一般不会公开给外部使用。

Ruby 提供了 3 种方法的访问级别,我们可以按照需要来灵活调整。

  • public ……以实例方法的形式向外部公开该方法

  • private ……在指定接收者的情况下不能调用该方法(只能使用缺省接收者的方式调用该方法,因此无法从实例的外部访问)

  • protected ……在同一个类中时可将该方法作为实例方法调用

在修改方法的访问级别时,我们会为这 3 个关键字指定表示方法名的符号。

首先来看看使用 public 和 private 的例子

class AccTest
  def pub
    puts "pub is a public method."
  end

  public :pub   # 把pub 方法设定为public(可省略)

  def priv
    puts "priv is a private method."
  end

  private :priv # 把priv 方法设定为private
end

acc = AccTest.new
acc.pub
acc.priv

AccTest 类的两个方法中,pub 方法可以正常调用,但是在调用 priv 方法时程序会发生异常,并出现以下错误信息 :

> ruby acc_test.rb
pub is a public method.
acc_test.rb:17:in `<main>': private method `priv' called for
#<AccTest:0x007fb4089293e8> (NoMethodError)

希望统一定义多个方法的访问级别时,可以使用下面的语法 :

class AccTest
  public # 不指定参数时,
         # 以下的方法都被定义为public

  def pub
    puts "pub is a public method."
  end

  private # 以下的方法都被定义为private

  def priv
    puts "priv is a private method."
  end
end

没有指定访问级别的方法默认为 public,但 initialize 方法是个例外,它通常会被定义为 private。

定义为 protected 的方法,在同一个类(及其子类)中可作为实例方法使用,而在除此以外的地方则无法使用。

class Point
  attr_accessor :x, :y   # 定义存取器
  protected :x=, :y=     # 把x= 与y= 设定为protected

  def initialize(x=0.0, y=0.0)
    @x, @y = x, y
  end

  def swap(other)        # 交换x、y 值的方法
    tmp_x, tmp_y = @x, @y
    @x, @y = other.x, other.y
    other.x, other.y = tmp_x, tmp_y   # 在同一个类中
                                      # 可以被调用
    return self
  end
end

p0 = Point.new
p1 = Point.new(1.0, 2.0)
p [ p0.x, p0.y ]         #=> [0.0, 0.0]
p [ p1.x, p1.y ]         #=> [1.0, 2.0]

p0.swap(p1)
p [ p0.x, p0.y ]         #=> [1.0, 2.0]
p [ p1.x, p1.y ]         #=> [0.0, 0.0]

p0.x = 10.0              #=> 错误(NoMethodError)

定义了拥有 X、Y 坐标的 Point 类。在这个类中,实例中的坐标可以被外部读取,但不能被修改。为此,我们可以利用 protected 来实现交换两个坐标值的方法 swap。

扩展类

在原有类的基础上添加方法

Ruby 允许我们在已经定义好的类中添加方法。下面,我们来试试给 String 类添加一个计算字符串单词数的实例方法 count_word

class String
  def count_word
    ary = self.split(/\s+/) # 用空格分割接收者
    return ary.size         # 返回分割后的数组的元素总数
  end
end

str = "Just Another Ruby Newbie"
p str.count_word            #=> 4

继承

利用继承,我们可以在不对已有的类进行修改的前提下,通过增加新功能或重定义已有功能等手段来创建新的类。

定义继承时,在使用 class 关键字指定类名的同时指定父类名。

class 类名< 父类名
 类定义
end
class RingArray < Array  # 指定父类
  def [](i)              # 重定义运算符[]
    idx = i % size       # 计算新索引值
    super(idx)           # 调用父类中同名的方法
  end
end

wday = RingArray["日", "月", "火", "水", "木", "金", "土"]
p wday[6]   #=> "土"
p wday[11]  #=> "木"
p wday[15]  #=> "月"
p wday[-1]  #=> "土"

创建一个继承了 Array 类的 RingArray 类。RingArray 类只是重定义了读取数组内容时使用的 [] 运算符。该程序通过 super 关键字调用父类中同名的方法(在本例中也就是 Array#[])。

对 RingArray 类指定了超过数组长度的索引时,结果就会从溢出部分的开头开始重新计算索引

利用继承,我们可以把共同的功能定义在父类,把各自独有的功能定义在子类。

定义类时没有指定父类的情况下,Ruby 会默认该类为 Object 类的子类。

Object 类提供了许多便于实际编程的方法。但在某些情况下,我们也有可能会希望使用更轻量级的类,而这时就可以使用 BasicObject 类。

BasicObject 类只提供了组成 Ruby 对象所需的最低限度的方法。类对象调用 instance_methods 方法后,就会以符号的形式返回该类的实例方法列表。下面我们就用这个方法来对比一下 Object 类和 BasicObject 类的实例方法。

> irb --simple-prompt
>> Object.instance_methods
=> [:nil?, :===, :=~, :!~, :eql?, :hash, :<=>, :class, :singleton_class, :clone,
:dup, :taint, :tainted?, :untaint, :untrust, :untrusted?, :trust, :freeze,
:frozen?, :to_s, ...... 等众多方法名......]
>> BasicObject.instance_methods
=> [:==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__, :__id__]

虽然大部分方法我们都还没有接触到,但据此也可以看出,相对于 Object 类持有多种方法,BacsicObject 类所拥有的功能都是最基本的。 定义 BasicObject 的子类时,与 Object 类不同,需要明确指定 BasicObject 类为父类,如下所示 :

class MySimpleClass < BasicObject
  ┊
end

alias 与 undef

alias

有时我们会希望给已经存在的方法设置别名。这种情况下就需要使用 alias 方法。alias 方法的参数为方法名或者符号名。

alias 别名 原名   # 直接使用方法名
alias``: 别名 : 原名  # 使用符号名

像 Array#size 与 Array#length 这样,为同一种功能设置多个名称时,我们会使用到 alias。

另外,除了为方法设置别名外,在重定义已经存在的方法时,为了能用别名调用原来的方法,我们也需要用到 alias。

下面的例子中定义了类 C1 及其子类 C2。在类 C2 中,对 hello 方法设置别名 old_hello 后,重定义了 hello 方法。

class C1                    # 定义C1
  def hello                 # 定义hello
    "Hello"
  end
end

class C2 < C1               # 定义继承了C1 的子类C2
  alias old_hello hello     # 设定别名old_hello
    def hello               # 重定义hello
  "#{old_hello}, again"
  end
end

obj = C2.new
p obj.old_hello             #=> "Hello"
p obj.hello                 #=> "Hello, again

undef

undef 用于删除已有方法的定义。与 alias 一样,参数可以指定方法名或者符号名。

undef 方法名    # 直接使用方法名
undef : 方法名   # 使用符号名

例如,在子类中希望删除父类定义的方法时可以使用 undef。

单例类

通过利用单例类定义,就可以给对象添加方法(单例方法)。单例类定义被用于定义对象的专属实例方法。在下面的例子中,我们分别将 "Ruby" 赋值给 str1 对象和 str2 对象,然后只对 str1 对象添加 hello 方法。这样一来,两个对象分别调用 hello 方法时,str1 对象可以正常调用,但 str2 对象调用时程序就会发生错误。

str1 = "Ruby"
str2 = "Ruby"
 
class << str1
 def hello
   "Hello, #{self}!"
 end
end
 
p str1.hello    #=> "Hello, Ruby!"
p str2.hello    #=> 错误(NoMethodError)

Ruby 中所有的类都是 Class 类的实例,对 Class 类添加实例方法,就等于给所有的类都添加了该类方法。因此,只希望对某个实例添加方法时,就需要利用单例方法。

模块

模块是 Ruby 的特色功能之一。如果说类表现的是事物的实体(数据)及其行为(处理),那么模块表现的就只是事物的行为部分。模块与类有以下两点不同:

  • 模块不能拥有实例

  • 模块不能被继承

提供命名空间

所谓命名空间(namespace),就是对方法、常量、类等名称进行区分及管理的单位。由于模块提供各自独立的命名空间,因此 A 模块中的 foo 方法与 B 模块中的 foo 方法,就会被程序认为是两个不同的方法。同样,A 模块中的 FOO 常量与 B 模块的 FOO 常量,也是两个不同的常量。

无论是方法名还是类名,当然都是越简洁越好,但是像 size、start 等这种普通的名称,可能在很多地方都会使用到。因此,通过在模块内定义名称,就可以解决命名冲突的问题。

例如,在 FileTest 模块中存在与获取文件信息相关的方法。我们使用 “模块名 . 方法名” 的形式来调用在模块中定义的方法,这样的方法称为模块函数。

# 检查文件是否存在
p FileTest.exist?("/usr/bin/ruby")  #=> true
# 文件大小
p FileTest.size("/usr/bin/ruby")    #=> 1374684

如果没有定义与模块内的方法、常量等同名的名称,那么引用时就可以省略模块名。通过 include 可以把模块内的方法名、常量名合并到当前的命名空间。下面是与数学运算有关的 Math 模块的例子。

# 圆周率(常量)
p Math::PI      #=> 3.141592653589793
# 2 的平方根
p Math.sqrt(2)  #=> 1.4142135623730951

include Math    # 包含Math 模块
p PI            #=> 3.141592653589793
p sqrt(2)       #=> 1.4142135623730951

像这样,通过把一系列相关的功能汇总在一个模块中,就可以集中管理相关的命名。

利用 Mix-in 扩展功能

Mix-in 就是将模块混合到类中。在定义类时使用 include,模块里的方法、常量就都能被类使用。

我们可以把 MyClass1 和 MyClass2 中两者共通的功能定义在 MyModule 中。虽然有点类似于类的继承,但 Mix-in 可以更加灵活地解决下面的问题。

  • 虽然两个类拥有相似的功能,但是不希望把它们作为相同的种类(Class)来考虑的时候

  • Ruby 不支持父类的多重继承,因此无法对已经继承的类添加共通的功能的时候

module MyModule
  # 共通的方法等
end

class MyClass1
  include MyModule
  # MyClass1 中独有的方法
end

class MyClass2
  include MyModule
  # MyClass2 中独有的方法
end

创建模块

我们使用 module 关键字来创建模块。

语法与创建类时几乎相同。模块名的首字母必须大写。

module 模块名
 模块定义
end
module HelloModule          # module 关键字
  Version = "1.0"           # 定义常量

  def hello(name)           # 定义方法
    puts "Hello, #{name}."
  end

  module_function :hello    # 指定hello 方法为模块函数
end

p HelloModule::Version      #=> "1.0"
HelloModule.hello("Alice")  #=> Hello, Alice.

include HelloModule         # 包含模块
p Version                   #=> "1.0"
hello("Alice")              #=> Hello, Alice.

常量

和类一样,在模块中定义的常量可以通过模块名访问。

p HelloModule::Version      #=> "1.0"

方法的定义

和类一样,我们也可以在 module 上下文中定义方法。

然而,如果仅仅定义了方法,虽然在模块内部与包含此模块的上文中都可以直接调用,但却不能以“模块名 . 方法名”的形式调用。如果希望把方法作为模块函数公开给外部使用,就需要用到 module_function 方法。module_function 的参数是表示方法名的符号。

def hello(name)
  puts "Hello, #{name}."
end

module_function :hello

以 “模块名 . 方法名” 的形式调用时,如果在方法中调用 self(接收者),就会获得该模块的对象。

module FooMoudle
  def foo
    p self
  end
  module_function :foo
end

FooMoudle.foo   #=> FooMoudle

此外,如果类 Mix-in 了模块,就相当于为该类添加了实例方法。在这种情况下,self 代表的就是被 Mix-in 的类的对象。

即使是相同的方法,在不同的上下文调用时,其含义也会不一样,因此对于 Mix-in 的模块,我们要注意根据实际情况判断是否使用模块函数功能。一般不建议在定义为模块函数的方法中使用 self。

Mix-in

module M
  def meth
    "meth"
  end
end

class C
  include M  # 包含M 模块
end

c = C.new
p c.meth     #=> meth

类 C 包含模块 M 后,模块 M 中定义的方法就可以作为类 C 的实例方法供程序调用。

另外,如果想知道类是否包含某个模块,可以使用 include? 方法。

C.include?(M)   #=> true

类 C 的实例在调用方法时,Ruby 会按类 C、模块 M、类 C 的父类 Object 这个顺序查找该方法,并执行第一个找到的方法。被包含的模块的作用就类似于虚拟的父类。

我们用 ancestors 方法和 superclass 方法调查类的继承关系

追加以下代码并执行,我们就可以通过 ancestors 取得继承关系的列表。进而也就可以看出,被包含的模块 M 也被认为是类 C 的一个“祖先”。而 superclass 方法则直接返回类 C 的父类。

p C.ancestors       #=> [C, M, Object, Kernel, BasicObject]
p C.superclass      #=> Object

ancestors 方法的返回值中的 Kernel 是 Ruby 内部的一个核心模块,Ruby 程序运行时所需的共通函数都封装在此模块中。例如 p 方法、raise 方法等都是由 Kernel 模块提供的模块函数。

虽然 Ruby 采用的是不允许多个父类的单一继承模型,但是通过利用 Mix-in,我们就既可以保持单一继承的关系,又可以同时让多个类共享其他功能。

在 Ruby 标准类库中,Enumerable 模块就是利用 Mix-in 扩展功能的一个典型例子。使用 each 方法的类中包含 Enumerable 模块后,就可以使用 each_with_index 方法、collect 方法等对元素进行排序处理的方法。Array、Hash、IO 类等都包含了 Enumerable 模块。这些类虽然没有继承这样的血缘关系,但是从“可以使用 each 方法遍历元素”这一点来看,可以说它们都拥有了某种相似甚至相同的属性。

单一继承的优点就是简单,不会因为过多的继承而导致类之间的关系变得复杂。但是另一方面,有时我们又会希望更加积极地重用已有的类,或者把多个类的特性合并为更高级的类,在那样的情况下,灵活使用单一继承和 Mix-in,既能使类结构简单易懂,又能灵活地应对各种需求。

查找方法的规则

首先,我们来了解一下使用 Mix-in 时方法的查找顺序。

  • 同继承关系一样,原类中已经定义了同名的方法时,优先使用该方法。

    module M
    def meth
        "M#meth"
    end
    end
    
    class C
    include M     # 包含M
    def meth
        "C#meth"
    end
    end
    
    c = C.new
    p c.meth        #=> C#meth
  • 在同一个类中包含多个模块时,优先使用最后一个包含的模块。

    module M1
    ┊
    end
    
    module M2
    ┊
    end
    
    class C
    include M1        #=> 包含M1
    include M2        #=> 包含M2
    end
    
    p C.ancestors       #=> [C, M2, M1, Object, Kernel]
  • 嵌套 include 时,查找顺序也是线性的。

    module M1
    ┊
    end
    
    module M2
    ┊
    end
    
    module M3
    include M2        #=> 包含M2
    end
    
    class C
    include M1        #=> 包含M1
    include M3        #=> 包含M3
    end
    
    p C.ancestors       #=> [C, M3, M2, M1, Object, Kernel]
  • 相同的模块被包含两次以上时,第 2 次以后的会被省略。

    module M1
    ┊
    end
    
    module M2
    ┊
    end
    
    class C
    include M1        #=> 包含M1
    include M2        #=> 包含M2
    include M1        #=> 包含M1
    end
    
    p C.ancestors       #=> [C, M2, M1, Object, Kernel, BasicObject]

extend 方法

利用 Object#extend 方法,我们可以实现批量定义单例方法。extend 方法可以使单例类包含模块,并把模块的功能扩展到对象中。

module Edition
  def edition(n)
    "#{self} 第#{n} 版"
  end
end

str = "Ruby 基础教程"
str.extend(Edition)     #=> 将模块Mix-in 进对象

p str.edition(4)        #=> "Ruby 基础教程第4 版"

include 可以帮助我们突破继承的限制,通过模块扩展类的功能;而 extend 则可以帮助我们跨过类,直接通过模块扩展对象的功能。

类与 Mix-in

在 Ruby 中,所有类本身都是 Class 类的对象。我们之前也介绍过接收者为类本身的方法就是类方法。也就是说,类方法就是类对象的实例方法。我们可以把类方法理解为:

  • Class 类的实例方法

  • 类对象的单例方法

继承类后,这些方法就会作为类方法被子类继承。对子类定义单例方法,实际上也就是定义新的类方法。

除了之前介绍的定义类方法的语法外,使用 extend 方法也同样能为类对象追加类方法。下面是使用 extend 方法追加类方法,并使用 include 方法追加实例方法的一个例子。

module ClassMethods    # 定义类方法的模块
  def cmethod
    "class method"
  end
end

module InstanceMethods # 定义实例方法的模块
  def imethod
    "instance method"
  end
end

class MyClass
  # 使用extend 方法定义类方法
  extend ClassMethods
  # 使用include 定义实例方法
  include InstanceMethods
end

p MyClass.cmethod        #=> "class method"
p Myclass.new.imethod    #=> "instance method"

在 Ruby 中,所有方法的执行,都需要通过作为接收者的某个对象的调用。换句话说,Ruby 的方法(包括单例方法)都一定属于某个类,并且作为接收者对象的实例方法被程序调用。从这个角度来说,人们只是为了便于识别接收者的类型,才分别使用了“实例方法”和 “类方法”这样的说法。

面向对象程序设计

“面向对象”这个概念,被广泛地应用在问题分析、系统设计或程序设计等系统和程序开发领域中。虽然这个概念目前被用在了各种各样的领域中,但首先使用这个概念的是与程序设计相关的领域。

由于本书是程序设计语言的入门书,因此这里并不会向其他领域过多延伸,而只会介绍与程序设计语言(当然就是 Ruby 了)相关的对象和面向对象方面的基础知识。

下面,我们暂且不讨论具体如何编写程序,而是先来了解一下编写程序时的一些思考方法。

对象是什么

包括 Ruby 在内,世界上有多种面向对象的程序设计语言。不同的语言,不仅语法不一样,功能也千差万别,但它们几乎都有一个共通点,就是将程序处理的主体作为“对象”来考虑。

一般情况下,程序语言的处理主体是数据。之前提到的数值、字符串、数组等都是简单的数据。

而面向对象的语言中的“对象”就是指数据(或者说数据的集合)及操作该数据的方法的组合。之前我们提到过 Ruby 里的数值 3.14 是 Float 类的实例。这个 3.14 不仅是表示 3.14 这个数值的数据,还包括与数值相关的操作方法。

f = 3.14
p f.round   #=> 3 (四舍五入)
p f.ceil    #=> 4 (进位)
p f.to_i    #=> 3 (整数变换)

像这样,把数据以及处理数据的操作方法作为对象合并在一起贯穿整体,在面向对象程序设计中是很常见的。例如,将浮点数做四舍五入处理的 round 方法是可以被作为 Float 类的一部分来提供的,这样一来,数据以及处理数据的方法的组合也不会出现错误。

如果只是简单的数值处理,并不会有太大的问题,但大部分程序都需要更复杂的数据构成。例如,在一个处理图片的程序中,图片的长宽、颜色、包括图片本身的内容都需要转换为二进制数据。如果能把一个图片作为一个零部件来处理,那么像图册这样复杂的应用程序也就变得容易编写了

在开发大型程序的时候,若不将大量的数据整合到一起并根据一定的规则进行整理,程序处理本身的统一性会荡然无存。面向对象程序设计会把这种归类统一的数据作为各种各样的对象来看待。在对象中,数据以及处理数据的方法也是成套存在的,而且还负有处理数据的责任。

另外,就像网络上的服务器管理的文件一样,远程数据也可以作为程序的处理对象来考虑。在网络程序设计里,Web、邮件等不同的应用程序,都需要遵守各自不同的规范(也称为协议)。用程序来实现协议的情况下,一般会把管理消息格式、规范等的程序抽象成库(library)。Ruby 的库里就有现成的 Net::HTTP、Net::POP 等类,可以非常轻松地编写网络程序。

面向对象的特征

  • 封装

    所谓封装(encapsulation),就是指使对象管理的数据不能直接从外部进行操作,数据的更新、查询等操作都必须通过调用对象的方法来完成。通过封装,可以防止因把非法数据设置给对象而使程序产生异常的情况发生。

    为此,就需要编写不会让对象内部产生异常的方法。最理想的做法是,在定义方法时就考虑如何避免错误的发生,而不是在使用方法编写程序时才开始注意。

    Ruby 对象在默认情况下是强制被封装的,因此无法从外部直接访问 Ruby 对象的实例变量。虽然有像 attr_accessor 这样简单定义访问级别的方法,但也不要过度使用,建议只在需要公开时才使用。

    封装的另外一个好处就是,可以隐藏对象内部数据处理的具体细节,把内部逻辑抽象地表现出来。例如,通过使用 Time 类,就可以进行从系统获取当前时间、从时间里提取年月日等操作。

    t = Time.now    # 从系统获取当前时间
    p t.year        #=> 2013(从时间里提取年)

    从系统获取当前时间时是如何处理的、Time 对象内部是以什么形式管理时间的、以及从时间中提取年时要进行何种运算等等,以上这些事情都由 Time 类的方法来实现。就算对象内部的数据结构改变了,只要公开给外部的方法名、功能等没有改变,类的使用者就完全不需要理会内部逻辑作出了怎样的修改,照常使用即可。相反,类的编写者只要提供的方法恰当,就可以直接修改类的内部逻辑,而不需要在意类的使用者。可见,封装对类的编写者和使用者来说都非常有好处。

  • 多态

    对象是数据及其处理的组合。对象知道数据是怎样被处理的。换句话说,各个对象都有自己独有的消息解释权。一个方法名属于多个对象(不同对象的处理结果也不一样)这种现象,用面向对象的术语来说,就是多态(polymorphism)。

    例如,我们可以观察一下对 Object 类、String 类和 Float 类的各对象调用 to_s 方法的运行结果,可以看出,不同的类得到的结果是不一样的。

    obj = Object.new    # 对象(Object)
    str = "Ruby"        # 字符串(String)
    num = Math::PI      # 数值(Float)
     
    p obj.to_s          #=> "#<Object:0x07fa1d6bd1008>"
    p str.to_s          #=> "Ruby"
    p num.to_s          #=> "3.141592653589793"

    三者的 to_s 方法名一样,含义也都是“以可以显示的形式把数据转换为字符串”,但实际的字符串转换方式却因对象而异。String 类和 Float 类都是继承自 Object 类,也都重新定义了从 Object 类继承的 to_s 方法,并提供了更适合自己语义的 to_s 方法。

鸭子类型

下面,我们来看一种结合对象特征,灵活运用多态的思考方法——鸭子类型(duck typing)。鸭子类型的说法来自于“能像鸭子那样走路,能像鸭子一样啼叫的,那一定就是鸭子”这句话。这句话的意思是,对象的特征并不是由其种类(类及其继承关系)决定的,而是由对象本身具有什么样的行为(拥有什么方法)决定的。例如,假设我们希望从字符串数组中取出元素,并将字母转换成小写后返回结果。

def fetch_and_downcase(ary, index)
  if str = ary[index]
    return str.downcase
  end
end

ary = ["Boo", "Foo", "Woo"]
p fetch_and_downcase(ary, 1)  #=> "foo"

实际上,除了数组外,我们也可以像下面那样,把散列传给该方法处理。

hash = {0 => "Boo", 1 => "Foo", 2 => "Woo"}
p fetch_and_downcase(hash, 1)  #=> "foo"

fetch_and_downcase 方法对传进来的参数只有两个要求:

  • 能以 ary[index] 形式获取元素

  • 获取的元素可以执行 downcase 方法

只要参数符合这两个要求,fetch_and_downcase 方法并不关心传进来的到底是数组还是散列。

Ruby 中的变量没有限制类型,所以不会出现不是某个特定的类的对象,就不能给变量赋值的情况。因此,在程序开始运行之前,我们都无法知道变量指定的对象的方法调用是否正确。

这样的做法有个缺点,就是增加了程序运行前检查错误的难度。但是,从另外一个角度来看,则可以非常简单地使没有明确继承关系的对象之间的处理变得通用。只要能执行相同的操作,我们并不介意执行者是否一样;相反,虽然实际上是不同的执行者,但通过定义相同名称的方法,也可以实现处理通用化。这就是鸭子类型思考问题的方法。

利用鸭子类型实现处理通用化,并不要求对象之间有明确的继承关系,因此,要想灵活运用,可能还需要花不少功夫。例如刚才介绍的 obj[index] 的形式,就被众多的类作为访问内部元素的手段而使用。刚开始时,我们可以先有意识地留意这种简单易懂的方法,然后再由浅入深,慢慢地就可以抓住窍门了。

面向对象的例子

接下来让我们通过一个实际的例子,来看看对象是如何被构造的。

利用 Net::HTTP 类取得 Ruby 官网首页的 HTML,并将其输出到控制台的脚本。

require "net/http"
require "uri"
url = URI.parse("http://www.ruby-lang.org/ja/")
http = Net::HTTP.start(url.host, url.port)
doc = http.get(url.path)
puts doc

程序的第 1、2 行中引用了 net/http 库以及 uri 库。这样,我们就可以使用 Net::HTTP 类和 URI 模块了。第 3 行使用了 URI 模块的 parse 方法来解析 URL 的字符串,返回的结果是字符串解析后的 URI::HTTP 类的对象。根据 URL 的编写规则,URL 会被分解成多个属性。

require "uri"
url = URI.parse("http://www.ruby-lang.org/ja/")
p url.scheme    #=> "http"                 (体系:URL 的种类)
p url.host      #=> "www.ruby-lang.org"    (主机名)
p url.port      #=> "80"                   (端口号)
p url.path      #=> "/ja/"                 (路径)
p url.to_s      #=> "http://www.ruby-lang.org/ja/"

体系(scheme)是指使用哪种通信协议。连接网络上的服务器时,需要知道服务器的主机名以及端口号。路径用于定位服务器上管理的文件。URI::HTTP 类的作用就是,把 URL 字符串解析后分解出来的信息,以对象的形式再次整合在一起。

需要注意的是,模块名是 URI 而不是 URL。URL 指的是 URI 标识符中某种特定种类的东西。关于两者的关系,这里不再详细介绍,现阶段我们只需要知道 URL 是 URI 的一种就可以了。

在程序第 4 行,把主机名和端口号传给 Net::HTTP 类的 start 方法,并创建 Net::HTTP 对象。在程序第 5 行,对 Net::HTTP 的 get 方法指定路径,获取文档内容。最后,在程序第 6 行,把得到的文档内容输出到控制台。由于得到的文档内容是 String 对象,因此后续处理与 Net::HTTP 类没有关系。

调用 Net::HTTP#get 方法的时候,对象内部会做以下处理:

  1. 使用主机名和端口号,与服务器建立通信(叫做 socket,套接字)

  2. 使用路径,创建代表请求信息的 Net::HTTPRequest 对象

  3. 对套接字写入请求信息

  4. 从套接字中读取数据,并将其保存到代表响应信息的 Net::HTTPResponse 对象中

  5. 利用 Net::HTTPResponse 本身提供的功能,解析响应信息,提取文档部分并返回。

流程图如下所示。

在这个例子中,URL 解析由 URI::HTTP 负责,网络连接由套接字负责,与信息交换相关的操作由 Net::HTTPRequest 和 Net::HTTPResponse 负责,通信中必要的套接字、请求、响应等相关操作由 Net::HTTP 负责。像这样,不同的对象各司其职,决定应该如何配置参数、该执行什么样的处理等事项。

这些事项不仅在新建程序时有用,在扩展、修改已有程序时也非常有用。对象之间通过方法交换信息,而至于这些信息在彼此内部是如何被处理的,则并不需要关心。在生成类时,我们只要牢记把适当的信息交给适当的方法处理,就可以设计出易于读写的程序。

重要的是如何自然地写出程序 这除了需要丰富的程序设计经验外,还需要拥有设计模式等类结构相关的知识。“自然”这样的说法可能有点夸张,但通过指把事物的外部特征作为参考依据,我们就可以使用与实际事物相近的模型去组织、构建程序。

Previous块Next散列