网安
  • 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
  • 块
  • 块是什么
  • 块的使用方法
  • 定义带块的方法
  • 局部变量与块变量
  1. Develop
  2. Speed-Ruby

块

块


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


块是什么

块就是在调用方法时,能与参数一起传递的多个处理的集合。之前在介绍 each 方法、time 方法等与循环有关的部分时,我们就已经接触过块。接收块的方法会执行必要次数的块。块的执行次数由方法本身决定,因此不需事前指定,甚至有可能一次都不执行。

在下面的例子中,我们使用 each 方法,把保存在 Array 对象中的各个整数依次取 2 次幂后输出。do 和 end 之间的部分就是所谓的块。在本例中,块总共被执行了 5 次。

[1, 2, 3, 4, 5].each do |i|
  puts i ** 2
end

我们把这样的方法调用称为“调用带块的方法”或者“调用块”。块的调用方法一般采用以下形式。

对象. 方法名( 参数列表) do | 块变量 |
 希望循环的处理
end

或者

对象. 方法名( 参数列表) { | 块变量 |
 希望循环的处理
}

块的开头是块变量。块变量就是在执行块的时候,从方法传进来的参数。不同方法的块变量个数也不相同。例如,在 Array#each 方法中,数组的元素会作为块变量被逐个传递到块中。而在 Array#each_with_index 方法中,则是 [ 元素 , 索引 ] 两个值被传递到块中。

块的使用方法

循环

在 Ruby 中,我们常常使用块来实现循环。在接收块的方法中,实现了循环处理的方法称为迭代器(iterator)。each 方法就是一个典型的迭代器。

在下面的例子中,我们把数组的各个元素转换为大写后输出。

alphabet = ["a", "b", "c", "d", "e"]
alphabet.each do |i|
  puts i.upcase
end

和数组一样,散列也能将元素一个个拿出来,但与数组不同的是,散列会将 [key, value] 的组合作为数组来提取元素。可以成对地提取散列的全部键、值。本例中使用 pair[1] 提取并合计了散列的值,提取散列的键时则可以使用 pair[0]。

sum = 0
outcome = {"参加费"=>1000, "挂件费用"=>1000, "联欢会费用"=>4000}
outcome.each do |pair|
  sum += pair[1] # 指定值
end
puts "合计:#{sum}"

在接收块变量时,多重赋值规则也是同样适用的。我们稍微把代码修改一下,这样一来,键、值就可以被分别赋值给不同的变量了。

sum = 0
outcome = {"参加费"=>1000, "挂件费用"=>1000, "联欢会费用"=>4000}
outcome.each do |item, price|
  sum += price
end
puts "合计:#{sum}"

File 对象被用于读写文件的内容。使用 File 对象可将文件数据从头到尾读取出来。

根据文件内容的不同,我们需要考虑是以字符为单位,还是以行为单位来做读取处理。代码里使用了 File 类的 each 方法的一个程序示例,它会把 sample.txt 文件的内容按顺序逐行读取出来并输出。

file = File.open("sample.txt")
file.each_line do |line|
  print line
end
file.close

除了 each_line 方法外,File 对象中还有以字符为单位来循环读取数据的 each_char 方法、以及以字节为单位进行循环读取的 each_byte 方法等等。而其他对象也有很多以 each_XX 命名的循环读取数据的方法。

隐藏常规处理

上文中我们介绍了将块用于循环的迭代器的例子。但正如本章开头所介绍的那样,除了迭代器以外,块还被广泛使用在其他地方。其中一个用法就是确保后处理被执行。下面我们来看一个典型的例子——File.open 方法。File.open 方法在接收块后,会将 File 对象作为块变量,并执行一次块。

File.open("sample.txt") do |file|
  file.each_line do |line|
    print line
  end
end

与改写之前的程序相比,File 对象读取数据的部分一样,不同点在于没有了最后的 close 方法的调用。如果使用完打开的文件后没有将文件关闭的话,有可能会产生其他程序无法打开该文件,或者到达一次性可打开的文件数的上限时无法再打开新文件等问题。而在上面程序中,即使遇到无法打开文件等错误也可以正常关闭文件,因为块内部进行了类似下面的异常处理。

file = File.open("sample.txt")
begin
  file.each_line do |line|
    print line
  end
ensure
  file.close
end

File.open 方法使用块时,块内部的处理完毕并跳出方法前,文件会被自动关闭,因此就不需要那样使用 File.close 方法。

文件使用完毕后,由方法执行关闭操作,而我们只需将必要的处理记述在块中即可。这样一来可以减少程序的代码量,二来可以防止忘记关闭文件等错误的发生。

替换部分算法

下面我们再来介绍一个块的常见用法。这一次我们以数组排序为例,来了解一下指定处理顺序时块的使用方法。

  • 自定义排列顺序

    Array 类的 sort 方法是对数组内元素进行排序的方法。对数组元素进行排序,可以采取多种方法。

  • 按数字的大小顺序

  • 按字母顺序

  • 按字符串的长度顺序

  • 按数组元素的合计值的大小顺序

    如果按照这样的条件分别定义相应的排序方法,就会使方法的数量过多,不便于记忆。因此,在 Array.sort 方法中,元素的排序步骤由方法决定,用户只能指定元素间关系的比较逻辑。

    Array.sort 方法没有指定块时,会使用 <=> 运算符对各个元素进行比较,并根据比较后的结果进行排序。<=> 运算符的返回值为 -1、0、1 中的一个。

    状态 | 结果

    • | - a <> 时 | -1(比 0 小) a == b 时 | 0 a > b 时 | 1(比 0 大)

    使用 <=> 运算符比较字符串时,会按照字符编码的顺序进行比较。比较字母时,会按先大写字母后小写字母的顺序排列。

    array = ["ruby", "Perl", "PHP", "Python"]
    sorted = array.sort
    p sorted    #=> ["PHP", "Perl", "Python", "ruby"]

    我们可以通过调用块来指定排列顺序。下面的例子与不使用块时的执行结果是一样的。

    array = ["ruby", "Perl", "PHP", "Python"]
    sorted = array.sort{ |a, b| a <=> b }
    p sorted    #=> ["PHP", "Perl", "Python", "ruby"]

    在 sort 方法的末尾添加了块 { |a, b| a <=> b },sort 方法会根据块的执行结果判断元素的大小关系。当需要比较元素的大小关系时,块中需要比较的两个对象就会被作为块变量调用。对块变量 a 和 b 进行比较后,数组整体就会按该顺序排列。

    在这里,我们需要注意块中最后一个表达式的值就是块的执行结果,因此 <=> 运算符必须在最后一行使用。

    备注 块的最后一个表达式不是指块的最后一行表达式,而是指在块中最后执行的表达式。

    按字符串的长度排序时,可以采用如下方法。

    array = ["ruby", "Perl", "PHP", "Python"]
    sorted = array.sort{ |a, b| a.length <=> b.length }
    p sorted    #=> ["PHP", "ruby", "Perl", "Python"]

    在之前的例子中,我们只是单纯地比较了字符串 a、b,这里我们使用 String.length 方法,来比较字符串的长度。用 <=> 运算符比较数值时,得到的是由小到大的排列顺序,因此,比较字符串长度时,结果就是按照由短到长的顺序进行排列。

    像这样,块经常被用来在 sort 方法中实现自定义排列顺序。

  • 预先取出排序所需的信息

    我们再来详细看看 sort 方法的块。每次比较元素时,sort 方法都会调用一次将两个元素作为块变量的块。这里,我们仍以刚才介绍的按字符串长度排序的程序为例,来看看程序调用了 length 方法多少次。

    ary = %w(
    Ruby is a open source programming language with a focus
    on simplicity and productivity. It has an elegant syntax
    that is natural to read and easy to write
    )
     
    call_num = 0    # 块的调用次数
    sorted = ary.sort do |a, b|
    call_num += 1 # 累加块的调用次数
    a.length &lt;=> b.length
    end
     
    puts "排序结果 #{sorted}"
    puts "数组的元素数量 #{ary.length}"
    puts "调用块的次数 #{call_num}"

    执行示例

    > ruby sort_comp_count.rb
    排序结果 ["a", "a", "on", "to", "It", "to", "is", "an", ......]
    数组的元素数量 28
    调用块的次数 91

    可以看出,在这个例子中,我们对 28 个元素进行了排序,块总共被调用了 91 次。由于每调用 1 次块,length 方法就会被调用 2 次,因此最终就会被调用 182 次。而实际上,我们只需对所有的字符串都调用 1 次 length 方法,然后再用得出的结果进行排序就可以了。像这样,在能够通过 < = > 运算符对转换后的结果进行比较的情况下,使用 sort_by 方法会使排序更加有效率。

    ary = %w(
        Ruby is a open source programming language with a focus
        on simplicity and productivity. It has an elegant syntax
        that is natural to read and easy to write
    )
    sorted = ary.sort_by{ |item| item.length }
    p sorted

    sort_by 方法会将每个元素在块中各调用一次,然后再根据这些结果做排序处理。这种情况下,虽然比较的次数不变,但获取排序所需要的信息的次数(本例中为 28 次)只需与元素个数一样就可以了。

    总结一下,元素排序算法中公共的部分由方法本身提供,我们则可以用块来替换方法中元素排列的顺序(或者取得用于比较的信息),或者根据不同的目的来替换需要更改的部分。

定义带块的方法

执行块

首先让我们重温一下 myloop 方法

def myloop
  while true
    yield               # 执行块
  end
end

num = 1                 # 初始化num
myloop do
  puts "num is #{num}"  # 输出num
  break if num > 100    # num 超过100 后跳出循环
  num *= 2              # num 乘2
end

myloop 方法在执行 while 循环的同时执行了 yield 关键字,yield 关键字的作用就是执行方法的块。因为这个 while 循环的条件固定为 true,所以会无限循环地执行下去,但只要在块里调用 break,就可以随时中断 myloop 方法,来执行后面的处理。

传递块参数,获取块的值

在刚才的例子中,块参数以及块的执行结果都没有被使用。接下来,我们会定义一个方法,该方法接收两个整数参数,并对这两个整数之间的整数做某种处理后进行合计处理,而“某种处理”则由块指定。

def total(from, to)
  result = 0                # 合计值
  from.upto(to) do |num|    # 处理从from 到to 的值
    if block_given?         #   如果有块的话
      result += yield(num)  #     累加经过块处理的值
    else                    #   如果没有块的话
      result += num         #     直接累加
    end
  end
  return result             # 返回方法的结果
end

p total(1, 10)                  # 从1 到10 的和 => 55
p total(1, 10){|num| num ** 2 } # 从1 到10 的2 次幂的和 => 385

total 方法会先使用 Integer.upto 方法把 from 到 to 之间的整数值按照从小到大的顺序取出来,然后交给块处理,最后再将块处理后的值累加到变量 result。程序第 5 行中,对 yield 传递参数后,参数值就会作为块变量传递到块中。同时,块的运行结果也会作为 yield 的结果返回。

程序第 4 行的 block_given? 方法被用来判断调用该方法时是否有块被传递给方法,如果有则返回 true,反之返回 false。如果方法没有块,则在程序第 7 行中直接把 num 相加。

在本例中,对 yield 传递 1 个参数,就有 1 个块变量接收。下面我们来看看对 yield 传递 0 个、1 个、3 个等多个参数时,对应的块变量是如何进行接收的。

def block_args_test
  yield()             # 0 个块变量
  yield(1)            # 1 个块变量
  yield(1, 2, 3)      # 3 个块变量
end

puts "通过|a| 接收块变量"
block_args_test do |a|
  p [a]
end
puts

puts "通过|a, b, c| 接收块变量"
block_args_test do |a, b, c|
  p [a, b, c]
end
puts

puts "通过|*a| 接收块变量"
block_args_test do |*a|
  p [a]
end
puts

执行示例

> ruby block_args_test.rb
通过|a| 接收块变量
[nil]
[1]
[1]

通过|a, b, c| 接收块变量
[nil, nil, nil]
[1, nil, nil]
[1, 2, 3]

通过|*a| 接收块变量
[[]]
[[1]]
[[1, 2, 3]]

首先我们注意到,yield 参数的个数与块变量的个数是不一样的。从 |a| 和 |a, b, c| 的例子中可以看出,块变量比较多时,多出来的块变量值为 nil,而块变量不足时,则不能接收参数值。

最后的通过 |*a| 接收的情况是将所有块变量整合为一个数组来接收。这与定义方法时接收可变参数的情况非常相似。

抽取嵌套数组的元素的规则,同样也适用于块变量。例如,Hash.each_with_index 方法的块变量有 2 个,并以 yield([ 键 , 值 ], 索引 ) 的形式传递。在接收块变量后,我们就可以把 [ 键 , 值 ] 部分分别赋值给不同的变量。

hash = {a: 100, b: 200, c: 300}
hash.each_with_index do |(key, value), index|
  p [key, value, index]
end

执行示例

> ruby param_grouping.rb
[:a, 100, 0]
[:b, 200, 1]
[:c, 300, 2]

控制块的执行

在调用代码清单 total 方法时,如果像下面那样在中途使用 break,total 方法的结果会变成什么样子呢?

n = total(1, 10) do |num|
  if num == 5
    break
  end
  num
end
p n     #=> ??

答案是 nil。在块中使用 break,程序会马上返回到调用块的地方,因此 total 方法中返回计算结果的处理等都会被忽略掉。但作为方法的结果,当我们希望返回某个值的时候,就可以像 break 0 这样指定 break 方法的参数,这样该值就会成为方法的返回值。

此外,如果在块中使用 next,程序就会中断当前处理,并继续执行下面的处理。使用 next 后,执行块的 yield 会返回,如果 next 没有指定任何参数则返回 nil,而如果像 next 0 这样指定了参数,那么该参数值就是返回值。

n = total(1, 10) do |num|
  if num % 2 != 0
    next 0
  end
  num
end
p n     #=> 30

最后,如果在块中使用 redo,程序就会返回到块的开头,并按照相同的块变量再次执行处理。这种情况下,块的处理结果不会返回给外部,因此需要十分小心 redo 的用法,注意不要使程序陷入死循环。

n = total(1, 10) do |num|
  if num % 2 != 0
    next 0
  end
  num
end
p n     #=> 30

最后,如果在块中使用 redo,程序就会返回到块的开头,并按照相同的块变量再次执行处理。这种情况下,块的处理结果不会返回给外部,因此需要十分小心 redo 的用法,注意不要使程序陷入死循环。

将块封装为对象

如前所述,在接收块的方法中执行块时,可以使用 yield 关键字。

而 Ruby 还能把块当作对象处理。把块当作对象处理后,就可以在接收块的方法之外的其他地方执行块,或者把块交给其他方法执行。

这种情况下需要用到 Proc 对象。Proc 对象是能让块作为对象在程序中使用的类。定义 Proc 对象的典型的方法是,调用 Proc.new 方法这个带块的方法。在调用 Proc 对象的 call 方法之前,块中定义的程序不会被执行。

在下面的代码例子中,定义一个输出信息的 Proc 对象,并调用两次。这时,程序就会把 call 方法的参数作为块参数来执行块。

hello = Proc.new do |name|
  puts "Hello, #{name}."
end

hello.call("World")
hello.call("Ruby")

执行示例

> ruby proc1.rb
Hello, World.
Hello, Ruby.

把块从一个方法传给另一个方法时,首先会通过变量将块作为 Proc 对象接收,然后再传给另一个方法。在方法定义时,如果末尾的参数使用“& 参数名”的形式,Ruby 就会自动把调用方法时传进来的块封装为 Proc 对象。

下面,我们将代码中块的接收方法加以改写,如下所示:

def total2(from, to, &block)
  result = 0               # 合计值
  from.upto(to) do |num|   # 处理从from 到to 的值
    if block               #   如果有块的话
      result +=            #     累加经过块处理的值
           block.call(num)
    else                   #   如果没有块的话
      result += num        #     直接累加
    end
 end
 return result            # 返回方法的结果
end

p total2(1, 10)                   # 从1 到10 的和 => 55
p total2(1, 10){|num| num ** 2 }  # 从1 到10 的2 次幂的和 => 385

我们在首行的方法定义中定义了 &block 参数。像这样,在变量名前添加 & 的参数被称为 Proc 参数。如果在调用方法时没有传递块,Proc 参数的值就为 nil,因此通过这个值就可以判断出是否有块被传入方法中。另外,执行块的语句不是 yield,而是 block.call(num),这一点与之前的例子也不一样。

方法可以有多个参数,而且定义参数的默认值等时都需要按照一定的顺序。而 Proc 参数则一定要在所有参数之后,也就是方法中最后一个参数。

将块封装为 Proc 对象后,我们就可以根据需要随时调用块。甚至还可以将其赋值给实例变量,让别的实例方法去任意调用。

此外,我们也能将 Proc 对象作为块传给其他方法处理。这时,只需在调用方法时,用“&Proc 对象”的形式定义参数就可以了。例如,向 Array.each 方法传递块时,可以像下面那样定义。

def call_each(ary, &block)
  ary.each(&block)
end

call_each [1, 2, 3] do |item|
  p item
end

这样一来,我们就可以非常方便地把调用 call_each 方法时接收到的块,原封不动地传给 ary.each 方法。

执行示例

> ruby call_each.rb
1
2
3

局部变量与块变量

块内部的命名空间与块外部是共享的。在块外部定义的局部变量,在块中也可以继续使用。而被作为块变量使用的变量,即使与块外部的变量同名,Ruby 也会认为它们是两个不同的变量。

x = 1            # 初始化x
y = 1            # 初始化y
ary = [1, 2, 3]

ary.each do |x|  # 将x 作为块变量使用
  y = x          # 将x 赋值给y
end

p [x, y]         # 确认x 与y 的值

执行示例

> ruby local_and_block.rb
[1, 3]

在 ary.each 方法的块中,x 的值被赋值给了局部变量 y。因此,y 保留了最后一次调用块时块变量 x 的值 3。而变量 x 的值在调用 ary.each 前后并没有发生改变。

相反,在块内部定义的变量不能被外部访问。在刚才的例子中,如果把第 2 行的代码删掉,程序就会出错。

x = 1            # 初始化 x
#y = 1           # 初始化 y
ary = [1, 2, 3]

ary.each do |x|  # 将 x 作为块变量使用
  y = x          # 将 x 赋值给y
end

p [x, y]         # 引用 y 时会出错误(NameError)

块中变量的作用域之所以这么设计,是为了通过与块外部共享局部变量,从而扩展变量的有效范围。在块内部给局部变量赋值的时候,要时刻注意它与块外部的同名变量的关系。大家一定要小心 Ruby 中的这个小陷阱。

块变量是只能在块内部使用的变量(块局部变量),它不能覆盖外部的局部变量,但 Ruby 为我们提供了定义块变量以外的块局部变量的语法。使用在块变量后使用 ; 加以区分的方式,来定义块局部变量。这里我们再稍微修改一下刚才的例子,如下所示。可以看出,块执行后 x 和 y 的值并没有变化。

x = y = z = 0       # 初始化x、y、z
ary = [1, 2, 3]
ary.each do |x; y|  # 使用块变量x,块局部变量y
  y = x             # 代入块局部变量y
  z = x             # 代入不是块局部变量的变量z
  p [x, y, z]       # 确认块内的 x、y、z 的值
end
puts
p [x, y, z]         # 确认x、y、z 的值

执行示例

> ruby local_and_block2.rb
[1, 1, 1]
[2, 2, 2]
[3, 3, 3]

[0, 0, 3]
Previous并发与线程Next类和模块