Ruby 方法

方法是把"一段可复用逻辑"打包起来的基本单位。Ruby 的方法非常灵活——支持默认参数、可变参数、关键字参数,还有独特的问号/叹号命名约定。本章把方法的所有用法讲清楚。

1. 最基本的方法定义

def 关键字定义,用 end 结束:

# def 关键字定义方法,end 结束
def greet
  puts "Hello!"
end

# 调用:方法有参数时括号可省
greet          # 输出 Hello!
greet()        # 等价

Ruby 调用方法时括号可以省略(只要不引起歧义),这是社区风格。所以 puts 后面直接写字符串、link_to 不带括号传参都很常见。

2. 参数与默认值

方法和大多数语言一样可以接受参数。一个关键点:Ruby 方法默认返回最后一行表达式的值,不需要写 return

# 必选参数
def add(a, b)
  a + b           # 最后一行自动作为返回值(隐式返回)
end

puts add(3, 5)    # 8

# 调用时括号可省(Ruby 风格)
puts add 3, 5

# 默认值:从右往左给
def greet(name, greeting = "Hello")
  "#{greeting}, #{name}!"
end

puts greet("Ruby")            # Hello, Ruby!
puts greet("Ruby", "Hi")      # Hi, Ruby!

默认参数的规则是从右往左给——有默认值的参数必须放在没有默认值的右边,否则编译器无法判断哪个参数被省略了。

3. 可变参数:splat

* 收集多余参数成数组,这是写"工具方法"的常用技巧。反向用在数组前,又能把数组拆开当多个参数传:

# * 收集多余参数为数组(splat)
def sum(*nums)
  nums.reduce(0, :+)
end

puts sum(1, 2, 3, 4)     # 10

# 还能拆开数组传参(反向 splat)
nums = [1, 2, 3]
puts sum(*nums)          # 等价 sum(1, 2, 3)

4. 关键字参数(Ruby 最优雅特性之一)

把 hash 当成"命名参数"用,代码可读性极高,尤其适合参数多的方法:

# 关键字参数:把 hash 当成"命名参数"
def create_user(name:, age:, role: "guest")
  puts "#{name}, #{age} 岁, 角色 #{role}"
end

create_user(name: "Alice", age: 30)
# Alice, 30 岁, 角色 guest

create_user(name: "Bob", age: 25, role: "admin")
# Bob, 25 岁, 角色 admin

# ** 收集未声明的关键字参数(double splat)
def log(message, **opts)
  puts message
  p opts              # {:user=>"alice", :ip=>"1.2.3.4"}
end

log("登录成功", user: "alice", ip: "1.2.3.4")

关键字参数的最大好处是调用时不需要记参数顺序,这让它成为构造器、配置方法、Rails controller 参数的标配。双 splat **opts 则用于收集"额外"的关键字参数,常用于日志、转发。

5. 返回值与多返回值

Ruby 没有"多返回值"语法,但通过返回数组 + 多重赋值实现等价效果,写起来和 Python 几乎一样:

# Ruby 方法默认返回最后一行表达式的值(隐式返回)
def square(x)
  x * x
end

# 显式 return 用于提前返回
def safe_divide(a, b)
  return nil if b.zero?
  a / b
end

# 多返回值用数组 + 多重赋值
def min_max(arr)
  [arr.min, arr.max]
end

lo, hi = min_max([3, 1, 4, 1, 5])
puts lo, hi           # 1 5

建议只在最后一行用隐式返回,需要"提前返回"时再写 return。这样代码风格统一,不会出现到处都是 return 的混乱场面。

6. 问号与叹号方法(Ruby 特色)

Ruby 方法名可以以 ?! 结尾,这是社区命名约定,不是语法关键字:

# 问号方法:返回 true/false,读起来像提问
"".empty?               # true
[1, 2].include?(2)      # true
"abc".start_with?("a")  # true

# 叹号方法:危险版——通常原地修改 receiver
name = " Alice "
name.strip            # "Alice"(返回新串,name 不变)
name.strip!           # "Alice"(原地改,name 现在变了)
name.strip!           # nil(已经干净,无改动可做)

# 约定:? 与 ! 是名字的一部分,不是语法
#   empty?  是完整方法名
#   empty   是另一个方法(可能不存在)

新手口诀:看到 问号 想到布尔判断,看到 叹号 想到"会改动原对象或抛异常"。Rails 里 save 返回 true/false,而 save! 失败直接抛 ActiveRecord::RecordInvalid——一个安静一个吵闹。

7. self 与类方法

方法内部的 self 指向当前 receiver(调用方法的对象)。在 def 前加 self. 定义的是类方法(类似其他语言的静态方法):

# 类方法:在 def 前加 self.
class MathHelper
  def self.square(x)
    x * x
  end
end

MathHelper.square(5)    # 25

# 实例方法内部的 self 指向 receiver
class User
  attr_accessor :name

  def initialize(name)
    @name = name
  end

  def say_hi
    "Hi, I'm #{self.name}"   # self 可省略,这里写出来更明确
  end
end

8. 方法作为对象

Ruby 里方法不是一等公民(不能直接当作值传递),但可以通过 method 把它变成可传递的对象:

# method(:名字) 把方法变成可传递的对象
def double(x)
  x * 2
end

m = method(:double)
puts m.call(5)        # 10
puts m.(5)            # 10(简写)
puts m[5]             # 10(再简写)

# 传给其他方法做回调
[1, 2, 3].map(&m)     # [2, 4, 6]

如果需要更灵活地"把代码当数据",就要用下一章的主角——块、Proc、Lambda

小结

本章你掌握了方法的全部用法。重点记住:隐式返回是 Ruby 风格、关键字参数最优雅、?! 是约定、self 在定义类方法时表示"类自己"。下一篇我们看 Ruby 的灵魂特性——块与 yield,它是迭代器和 DSL 的基础。

← 上一篇 Ruby 控制流

下一篇 Ruby 块与 yield

✈️💬