Ruby黑魔法

Ruby之父父松本行弘说过:“Ruby是为了让程序员快乐而设计的语言。” 快乐来自于自由和优雅的表达,所以他为Ruby设计了非常多的语法糖,初见Ruby让人惊叹于它的简洁和优雅,让我们一起领略一下Ruby的黑魔法吧。

输出方法

Ruby提供了多种输出方法,每种都有自己的特色,选对工具很重要!

print方法:不自动换行

1
print "hi,","ruby!"
输出:hi,ruby!

puts方法:自动换行

1
puts "hi,","ruby!"
输出:hi,
     ruby!

p方法:区分数值与字符串,\n、\t输出时不会转义

1
2
p 1
p "1"
输出:1
     "1"

p方法在调试时特别好用,它会调用对象的inspect方法,能清楚地看到对象的真实类型!

pp方法:输出格式化文本

1
2
3
4
5
6
require "pp"
v = [{
word: "Hi",
name: "Mark"
}]
pp v
输出:[{:word=>"Hi", 
       :name=>"Mark"}] #sublime实际输出是一行!!

pp方法处理复杂的嵌套结构时简直是神器,比p方法更易读!

中文编码

首行代码添加注释#encoding:编码方式

1
2
#encoding: UTF-8
print "你好,Ruby!"

在Ruby 2.0之后,UTF-8成为默认编码,但加上这行注释是个好习惯,能避免很多莫名其妙的编码问题!

字符串插值

使用#{变量}的方式将变量输出到字符串

1
2
3
4
#encoding: UTF-8
language = "Ruby"
print "我在学习#{language}!"
#输出:我在学习Ruby!

字符串插值可以执行任意Ruby代码,不仅仅是变量:

1
2
3
4
5
puts "1 + 1 = #{1 + 1}"
#输出:1 + 1 = 2

puts "当前时间:#{Time.now}"
#输出:当前时间:2015-07-18 18:08:03 +0800

注意:只有双引号字符串支持插值,单引号不行!这是Ruby的一个小陷阱。

times方法

数字也是对象,可以直接调用方法!这就是Ruby的魅力!

1
2
3
4
5
6
7
8
def func_say()
puts "重要的话说三遍!"
end

3.times do |i|
puts "第#{i+1}遍"
func_say()
end
输出:第1遍
     重要的话说三遍!
     第2遍
     重要的话说三遍!
     第3遍
     重要的话说三遍!

times方法还可以用更简洁的大括号语法:

1
2
5.times { |i| puts "#{i}" }
#输出:0 1 2 3 4

类似的还有upto和downto方法:

1
2
3
4
5
1.upto(3) { |i| puts "向上数:#{i}" }
#输出:向上数:1 向上数:2 向上数:3

3.downto(1) { |i| puts "向下数:#{i}" }
#输出:向下数:3 向下数:2 向下数:1

符号(Symbol)

在字符串前加:符号是Ruby中独特的数据类型

1
sym = :foo

符号和字符串看起来相似,但本质不同:符号是不可变的,相同的符号在内存中只有一份拷贝,而字符串每次创建都会分配新内存。这使得符号在用作hash的键时性能更好!

1
2
3
4
5
6
7
# 字符串每次都创建新对象
p "hello".object_id #输出不同的id
p "hello".object_id #输出不同的id

# 符号永远是同一个对象
p :hello.object_id #输出相同的id
p :hello.object_id #输出相同的id

在Rails中,符号随处可见,是Ruby的标志性特性之一!

正则匹配

/模式/ =~ 字符串,/模式/i =~ 字符串(加i表示不区分大小写)

1
2
p /Ruby/ =~ "Hi,Ruby!"
#输出是3,匹配成功返回首字符的位置,匹配失败返回nil

Ruby的正则表达式非常强大,支持各种花式玩法:

1
2
3
4
5
6
7
8
9
10
11
12
# 不区分大小写
p /ruby/i =~ "Hi,RUBY!" #输出:3

# 使用match方法获取更多信息
result = "Email: mark@example.com".match(/(\w+)@(\w+)\.(\w+)/)
p result[0] #输出:"mark@example.com"
p result[1] #输出:"mark"
p result[2] #输出:"example"

# 字符串的gsub方法配合正则,强大到哭!
text = "Phone: 123-456-7890"
p text.gsub(/\d/, "*") #输出:"Phone: ***-***-****"

ARGV

ARGV读取命令行参数

1
2
3
4
5
6
#add.rb 
num_one = ARGV[0].to_i
num_two = ARGV[1].to_i
puts "#{num_one+num_two}"
#命令行输入:ruby ./Desktop/add.rb 1 2
#输出:3

ARGV是一个数组,可以用各种数组方法操作它:

1
2
3
4
5
6
7
8
9
# args.rb
if ARGV.empty?
puts "请提供参数!"
exit
end

ARGV.each_with_index do |arg, index|
puts "参数#{index}: #{arg}"
end

多重赋值

Ruby的多重赋值简直是语法糖中的战斗机!

变量前加*,表示将未分配的值封装成数组赋值给该变量

1
2
3
4
5
6
7
a,*b = 1,2,3
p a
p b
=begin
输出:1
[2, 3]
=end

交换变量的值

1
2
3
4
5
a = 10
b = 20
a,b = b,a
puts "a=#{a},b=#{b}"
#输出:a=20,b=10

不需要临时变量,一行搞定!这在其他语言里可做不到。

数组赋值

1
2
3
4
arr = [10,20]
a,b = arr
puts "a=#{a},b=#{b}"
#输出:a=10,b=20

嵌套数组赋值

1
2
3
4
arr = [10,[20,30]]
a,(b,c) = arr
puts "a=#{a},b=#{b},c=#{c}"
#输出:a=10,b=20,c=30

忽略不需要的值

1
2
3
4
5
6
7
first, *, last = [1, 2, 3, 4, 5]
puts "首尾:#{first}, #{last}"
#输出:首尾:1, 5

# 用下划线表示刻意忽略的值
name, _, age = ["Mark", "中间不重要", 25]
puts "#{name}今年#{age}岁"

范围(Range)

Ruby的范围操作符太优雅了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 两个点表示包含结尾
(1..5).each { |i| print i }
#输出:12345

# 三个点表示不包含结尾
(1...5).each { |i| print i }
#输出:1234

# 范围可以用于数组切片
arr = [10, 20, 30, 40, 50]
p arr[1..3] #输出:[20, 30, 40]
p arr[1...3] #输出:[20, 30]

# 范围甚至可以用于字符串!
p ('a'..'e').to_a #输出:["a", "b", "c", "d", "e"]

后置条件

Ruby允许把if/unless放在语句后面,读起来像英语!

1
2
3
4
5
6
7
8
9
puts "太热了!" if temperature > 30
puts "该睡觉了" if Time.now.hour > 22

# unless是if的反义词,表示"除非"
puts "天气不错" unless raining

# 这种写法简洁又优雅
user.save! if user.valid?
return nil unless user

安全导航操作符(&.)

Ruby 2.3引入的神器,再也不怕nil错误了!

1
2
3
4
5
6
7
8
9
10
# 传统写法:啰嗦
if user && user.profile && user.profile.avatar
avatar = user.profile.avatar
end

# 安全导航:优雅!
avatar = user&.profile&.avatar

# 如果user是nil,整个表达式返回nil而不会报错
p nil&.upcase #输出:nil,而不是NoMethodError!

默认值操作符(||=)

这个操作符太常用了,堪称Ruby的标志性语法!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 只在变量为nil或false时赋值
count ||= 0
count += 1

# 等价于
count = count || 0

# 常用于缓存
@users ||= User.all # 只查询一次数据库

# hash的默认值也可以这样玩
hash = {}
hash[:key] ||= []
hash[:key] << "value"

方法定义的灵活性

方法名可以带问号或感叹号,这是Ruby的命名约定!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 问号表示返回布尔值
def valid?
true
end

# 感叹号表示"危险"操作(会修改原对象或可能抛出异常)
def save!
# 保存失败会抛出异常
end

# Ruby的方法参数超级灵活
def greet(name, age: 18, city: "北京") # 关键字参数有默认值
puts "#{name}, #{age}岁, 来自#{city}"
end

greet("Mark") #输出:Mark, 18岁, 来自北京
greet("Mark", age: 25, city: "上海")

# 方法的括号是可选的!
puts "Hello" # 等价于 puts("Hello")

块(Block)、Proc和Lambda

Ruby的闭包三剑客,各有千秋!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Block:不是对象,不能保存
[1,2,3].each { |n| puts n }

# Proc:Block的对象化
my_proc = Proc.new { |n| puts n * 2 }
my_proc.call(5) #输出:10

# Lambda:更严格的Proc
my_lambda = ->(n) { n * 2 } # 箭头语法,炫酷!
p my_lambda.call(5) #输出:10

# Lambda和Proc的区别:
# 1. Lambda检查参数数量,Proc不检查
# 2. Lambda的return只退出lambda,Proc的return退出外层方法

tap方法

tap方法简直是调试和链式调用的神器!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 传统写法
result = [1,2,3].map { |n| n * 2 }
p result
result = result.select { |n| n > 2 }
p result

# 使用tap:不打断链式调用
[1,2,3]
.map { |n| n * 2 }
.tap { |arr| p "映射后:#{arr}" }
.select { |n| n > 2 }
.tap { |arr| p "过滤后:#{arr}" }

# tap常用于对象初始化
user = User.new.tap do |u|
u.name = "Mark"
u.age = 25
end

case when的魔法

Ruby的case语句比其他语言强大得多!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 基础用法
grade = 'B'
case grade
when 'A'
puts "优秀"
when 'B'
puts "良好"
else
puts "加油"
end

# 多个值匹配
case day
when 'Monday', 'Tuesday', 'Wednesday'
puts "工作日前半周"
when 'Thursday', 'Friday'
puts "工作日后半周"
end

# 范围匹配
case score
when 90..100
puts "A"
when 80...90
puts "B"
when 60...80
puts "C"
end

# 正则匹配
case input
when /^\d+$/
puts "纯数字"
when /^[a-z]+$/i
puts "纯字母"
end

# 类型匹配
case obj
when String
puts "这是字符串"
when Array
puts "这是数组"
end

猴子补丁(Monkey Patching)

Ruby允许你随意修改已有的类,这就是”开放类”的威力!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 给String类添加新方法
class String
def shout
self.upcase + "!!!"
end
end

p "hello".shout #输出:"HELLO!!!"

# 给Integer类添加新方法
class Integer
def seconds
self
end

def minutes
self * 60
end

def hours
self * 60 * 60
end
end

p 5.minutes #输出:300
p 2.hours #输出:7200

# Rails就是这样实现 5.days.ago 这种魔法的!

这个特性超级强大但也很危险,用不好会导致难以调试的bug。在生产环境要谨慎使用!

method_missing

Ruby的终极黑魔法:拦截不存在的方法调用!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class DynamicMethod
def method_missing(method_name, *args)
puts "你调用了不存在的方法:#{method_name}"
puts "参数是:#{args.inspect}"
end
end

obj = DynamicMethod.new
obj.hello("world")
#输出:你调用了不存在的方法:hello
# 参数是:["world"]

# ActiveRecord就是用这个实现动态查询的:
# User.find_by_name("Mark")
# User.find_by_age(25)

行内rescue

异常处理也可以写成一行!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 传统写法
begin
result = 10 / 0
rescue
result = nil
end

# 行内写法:简洁!
result = 10 / 0 rescue nil

# 常用于给默认值
value = some_risky_method rescue "默认值"

# 从文件读取,失败则返回空字符串
content = File.read("config.txt") rescue ""

其他有趣的语法糖

无限循环

1
2
3
4
loop do
puts "永远执行"
break if condition # 用break退出
end

until循环

1
2
3
4
5
i = 0
until i > 5 # 直到条件为真才停止
puts i
i += 1
end

数组和哈希的花式创建

1
2
3
4
5
6
7
8
9
10
11
12
# %w创建字符串数组,省去引号和逗号
languages = %w[Ruby Python JavaScript]
p languages #输出:["Ruby", "Python", "JavaScript"]

# %i创建符号数组
symbols = %i[foo bar baz]
p symbols #输出:[:foo, :bar, :baz]

# Hash的多种创建方式
hash1 = { :name => "Mark", :age => 25 } # 传统箭头语法
hash2 = { name: "Mark", age: 25 } # 符号键的简写,推荐!
hash3 = Hash[name: "Mark", age: 25] # 使用Hash类

双星号()操作符**

1
2
3
4
5
6
7
8
9
10
11
12
13
# 收集多余的关键字参数
def method(name:, **others)
p name
p others
end

method(name: "Mark", age: 25, city: "北京")
#输出:"Mark"
# {:age=>25, :city=>"北京"}

# 展开hash作为参数
params = { name: "Mark", age: 25 }
method(**params)

BEGIN和END

1
2
3
4
5
6
7
8
9
END { puts "程序结束时执行" }
BEGIN { puts "程序开始前执行" }

puts "主程序"

#输出顺序:
#程序开始前执行
#主程序
#程序结束时执行

%Q和%q

1
2
3
4
5
6
7
8
# %Q等价于双引号,支持插值
name = "Ruby"
str1 = %Q[Hello, #{name}!] # 可以用任意符号做定界符
p str1 #输出:"Hello, Ruby!"

# %q等价于单引号,不支持插值
str2 = %q[Hello, #{name}!]
p str2 #输出:"Hello, \#{name}!"

赶快拿起你的机械键盘吧,Happy Hacking with Ruby (: