理解 Ruby - 三等号
介绍
三等分
总结
介绍
Ruby 中的三等号 ( ===
) 是整个语言中最强大的功能之一,但你可能也不知道它正在被使用。事实上,它是最不为人知的秘密之一!
今天我们将了解一些关于这些秘密的知识,并探索===
它是如何定义的、它的作用是什么以及它隐藏在你的 Ruby 代码中的什么位置。
注意:这是对我 2017 年的一篇旧帖子《Triple Equals Black Magic》的重写和扩展,并包含更新的语法(包括示例的模式匹配)。
困难
基础
无需任何先修知识。本文重点介绍 Ruby 程序员的基础知识。
三等分
那么它是什么?它是如何定义的?
有些 JavaScript 使用者可能会认为===
是一个比 更严格的相等运算符==
,但在 Ruby 中,它的作用却截然不同。默认情况下,它是 的别名==
,但有些类的作用更有趣。
它有几个名称:大小写相等运算符、成员资格运算符、三重等于。
它的功能非常类似于检查右边的值是否是左边的值的成员。
Ruby 是如何实现的
这是什么意思呢?好吧,让我们快速浏览几个类,看看它是如何工作的。
警告:请勿
===
在代码中显式地使用此类方法,建议使用名称更清晰的方法。我们很快就会看到它的实际使用情况,而且通常情况下,我们会以隐式的方式使用它,而不是显式地使用它。
范围
Ruby 中的范围有起点和终点,例如是从到 的1..10
范围。它的工作原理如下:检查右侧的值是否恰好包含在范围内,或者是范围的成员:1
10
===
include?
(1..10) === 1
# => true
(1..10).include?(1)
# => true
范围的有趣之处在于它不仅限于数字。字符串也可以,这使得范围包含更加有趣:
SUPPORTS_PATTERN_MATCH_VERSIONS = '2.7.0'..'3.0.0'
SUPPORTS_PATTERN_MATCH_VERSIONS === '2.7.5'
# => true
当然,一旦 Ruby 超出范围,该示例就会中断3.0.0
,但重点是范围还可以识别整数以外的类型,有时还会以有趣的方式识别,但这是另一篇文章的主题。
正则表达式
正则表达式是一种用于匹配文本中的模式的语言,===
它的工作方式非常类似于match?
:
/abc/ === 'abcdef'
# => true
/abc/.match?('abcdef')
# => true
===
在这种情况下,它表示存在匹配项,或者说我们的字符串是此正则表达式所引用的匹配项集合的成员。注意到了吗?
课程
Ruby 中有诸如 、 等类Integer
。String
通常,你可以使用 来检查某个对象是否属于特定类型is_a?
。不出所料,===
这里的工作方式大致相同:
String === 'foo'
# => true
'foo'.is_a?(String)
# => true
===
这里说的是,它'foo'
是类的一个成员String
,或者它包含在我们所说的字符串中。
需要注意的是,这适用于标准库中的每个 Ruby 核心类,但由于它需要自定义实现,因此可能存在一些不适用的特殊情况。
函数(Proc 和 Lambda)
Ruby 有几种表达匿名函数的方式,包括 procs 和 lambdas。此外还有 block,但我们暂时只讨论这两种。它们可以这样表达:
add_one_lambda = -> x { x + 1 }
add_one_proc = proc { |x| x + 1 }
在这一轮中我们不会讨论它们之间的差异,但请注意,我总体上更喜欢 lambdas 而不是 procs。
要使用这些功能,您需要使用.call
(或[]
或.()
),您可能不会惊讶地发现它们===
也是如此:
add_one_lambda = -> x { x + 1 }
add_one_lambda === 1
# => 2
add_one_lambda.call(1)
# => 2
add_one_lambda.(1)
# => 2
add_one_lambda[1]
# => 2
这个问题有点让人摸不着头脑。1
函数的成员怎么会是函数呢?这说不通,它实际上又不是任何类型的集合或集合,对吧?其实在数学里,它叫做函数的定义域,或者说所有有效输入都落入的集合,所以这听起来也很像成员资格!
IP地址
Ruby 还为我们所有操作和网络类型的人提供了这个可爱的功能IPAddr
:
require 'ipaddr'
IPAddr.new('10.0.0.1')
您甚至可以使用它来表达子网:
local_network = IPAddr.new('192.168.1.0/24')
local_network.include?('192.168.1.1')
# => true
你的直觉是不是有点不对劲?因为我们还有另一个例子===
:
local_network === '192.168.1.1'
# => true
这次我们检查一个 IP 地址是否属于某个子网,进一步完善了这个有趣的模式。Ruby 喜欢模式,数学和编程通常也对模式有着一定的亲和力。
案例===
现在一切都很好,但是上面的警告说不要===
显式使用,那么为什么要花那么多时间来描述它的工作原理呢?因为我们即将看到隐式的 throughcase
语句。
你看,语句when
中的每个分支都case
通过以下方式进行比较===
:
case 1990
when ..1899 then :too_early
when 1900..1924 then :gi
when 1925..1945 then :silent
when 1946..1964 then :baby_boomers
when 1965..1979 then :generation_x
when 1980..2000 then :millenials
when 2000..2010 then :generation_z
when 2010.. then :generation_alpha
else
:who_knows
end
# => :millenials
您甚至可以使用逗号来检查多种可能性:
case 'foobar'
when String, Integer then :one
when Float, NilClass then :two
else
:three
end
检查一个值是否在预期集合内有很多可能性,并且使用函数会变得更加有趣:
divisible_by = -> divisor { -> n { n % divisor == 0 } }
(1..15).map do |n|
case n
when divisible_by[15] then :fizzbuzz
when divisible_by[5] then :buzz
when divisible_by[3] then :fizz
else
n
end
end
# => [
# 1, 2, :fizz, 4, :buzz, :fizz, 7, 8, :fizz, :buzz,
# 11, :fizz, 13, 14, :fizzbuzz
# ]
有趣不是吗?
这里面有个技巧叫做闭包,也就是一个函数返回另一个函数。返回的函数会记住 的值divisor
,这样我们就可以检查某个值是否能被它整除。这是函数式编程中一个非常有用的技巧,它真正展现了 Ruby 中函数的强大功能,尤其是在case
语句之类的用法中。
列举我们能享受的所有乐趣
Enumerable
还有一些方法可以接受响应的值===
。我们来看一些例子。
谓词方法
谓词方法(any?
,,,)都all?
可以很好地发挥作用:none?
one?
===
['1', 2, :a].any?(Integer)
# => true
搜索已开始
grep
诸如(查找所有与模式匹配的)和grep_v
(查找所有与模式不匹配的)之类的搜索方法也实现了一个===
接口:
%w(The rain in spain falls mainly on the plain).grep(/the/i)
# => ["The", "the"]
生活片段
还有 Slice 方法允许我们按照类似slice_before
和 的模式对元素进行分组slice_after
:
array = [7, 9, 4, 1, 14, 5, 13, 8, 2, 6, 3, 12, 15, 11, 10]
array.slice_before(10..).to_a
# => [[7, 9, 4, 1], [14, 5], [13, 8, 2, 6, 3], [12], [15], [11], [10]]
array.slice_after(..5).to_a
# => [[7, 9, 4], [1], [14, 5], [13, 8, 2], [6, 3], [12, 15, 11, 10]]
模式匹配
现在,如果你对语句感到兴奋,case
让我告诉你,模式匹配就像一个case
内置了许多额外乐趣的语句。我们不会深入探讨它的所有细微差别,但每个值都使用 进行匹配===
。它使用in
而不是when
,这解锁了一些额外的功能。
类数组
它支持两种语法:类数组匹配和类哈希匹配。我们先从类数组开始:
case [0, 1]
in [..10, ..10] then :close_to_base
in [..100, ..100] then :venturing_out
in [..1000, ..1000] then :pretty_far_out
else :way_out_there
end
# => :close_to_base
如果我们愿意,我们甚至可以通过名称或其他一些有趣的项目来捕获这些值,但我们会将其保存到另一篇文章中。
类似哈希
接下来是类似哈希的方法,这里事情变得有趣了。假设我们有一个 JSON API,其中包含一些数据,我们取回了这些数据,这些数据位于一个名为 的变量中raw_json
:
raw_json = <<~JSON
[{
"age": 22,
"eyeColor": "blue",
"name": { "first": "Trina", "last": "Chang" },
"friends": ["Browning Marsh", "Keisha Abbott", "Shawn Callahan"]
}, {
"age": 32,
"eyeColor": "brown",
"name": { "first": "Irma", "last": "Petersen" },
"friends": ["Koch Ballard", "Chandra Rodriquez", "Carmen Avery"]
}, {
"age": 27,
"eyeColor": "hazel",
"name": { "first": "Madeleine", "last": "Blake" },
"friends": ["Tina Massey", "Annette Yates", "Zelma Brennan"]
}, {
"age": 20,
"eyeColor": "green",
"name": { "first": "Horton", "last": "Haynes" },
"friends": ["Sophia Oconnor", "Sheila Wilkins", "Mia Molina"]
}, {
"age": 12,
"eyeColor": "brown",
"name": { "first": "Hull", "last": "Benson" },
"friends": ["Teresa Mack", "Mcfadden Conley", "Juanita Rollins"]
}]
JSON
我们首先对其进行解析,但我们希望确保这些键Symbols
使用以下语法JSON.parse
:
require 'json'
json_data = JSON.parse(raw_json, symbolize_names: true)
现在我们可以用模式匹配做一些非常有趣的事情:
selected_people = json_data.select do |person|
case person
in age: 20.., eyeColor: /^b/, name: { first: /^[TI]/ }
person
else
false
end
end
selected_people.map do |person|
person => { name: { first:, last: } }
"#{first} #{last}"
end
# => ["Trina Chang", "Irma Petersen"]
好多啊。它到底在干什么?
首先,我们要选出所有年龄大于 ,20
且眼睛颜色以字母 开头b
,名字以T
或 开头的人I
。这样表达效果怎么样?
接下来,我们使用一种叫做“右赋值”的方法(=>
)来提取人物的姓和名,并返回他们的姓名。在模式匹配中,如果某个键没有值,它会被放入一个局部变量中,这样first
就last
可以在其下一行访问它。
再次强调,我们不会在本文中深入探讨模式匹配的所有细节,但你很快就会发现它是多么有趣。我计划很快写一篇更全面的模式匹配概念介绍,敬请期待。
总结
真是太多了。Ruby===
中处处隐藏着各种特性,一旦你对它有了一定的了解,就会经常注意到它。更棒的是,这种了解意味着,===
如果有一天你需要它,你现在知道如何创建自己的特性了。
不过,这是一个被定义为方法的运算符:
# Example implementation
class String
def self.===(other)
other.is_a?(self)
end
end
Ruby 拥有众多引人入胜的方面,并拥有强大的功能。本系列将继续介绍 Ruby 的一些基础知识以及一些最实用的工具和功能。
在此之前,请尽情享受您新获得的知识===
!
想了解我的写作和工作进展吗?快来看看我的新通讯:《宝石狐猴》
鏂囩珷鏉ユ簮锛�https://dev.to/baweaver/understanding-ruby-triple-equals-2p9c