理解 Ruby - 三等号介绍 三等号总结

2025-06-08

理解 Ruby - 三等号

介绍

三等分

总结

介绍

Ruby 中的三等号 ( ===) 是整个语言中最强大的功能之一,但你可能也不知道它正在被使用。事实上,它是最不为人知的秘密之一!

今天我们将了解一些关于这些秘密的知识,并探索===它是如何定义的、它的作用是什么以及它隐藏在你的 Ruby 代码中的什么位置。

注意:这是对我 2017 年的一篇旧帖子《Triple Equals Black Magic》的重写和扩展,并包含更新的语法(包括示例的模式匹配)。

困难

基础

无需任何先修知识。本文重点介绍 Ruby 程序员的基础知识。

三等分

那么它是什么?它是如何定义的?

有些 JavaScript 使用者可能会认为===是一个比 更严格的相等运算符==,但在 Ruby 中,它的作用却截然不同。默认情况下,它是 的别名==,但有些类的作用更有趣。

它有几个名称:大小写相等运算符、成员资格运算符、三重等于。

它的功能非常类似于检查右边的值是否是左边的值的成员。

Ruby 是如何实现的

这是什么意思呢?好吧,让我们快速浏览几个类,看看它是如何工作的。

警告:请勿===在代码中显式地使用此类方法,建议使用名称更清晰的方法。我们很快就会看到它的实际使用情况,而且通常情况下,我们会以隐式的方式使用它,而不是显式地使用它。

范围

Ruby 中的范围有起点和终点,例如是从到 的1..10范围。它的工作原理如下:检查右侧的值是否恰好包含在范围内,或者是范围的成员:110===include?

(1..10) === 1
# => true

(1..10).include?(1)
# => true
Enter fullscreen mode Exit fullscreen mode

范围的有趣之处在于它不仅限于数字。字符串也可以,这使得范围包含更加有趣:

SUPPORTS_PATTERN_MATCH_VERSIONS = '2.7.0'..'3.0.0'
SUPPORTS_PATTERN_MATCH_VERSIONS === '2.7.5'
# => true
Enter fullscreen mode Exit fullscreen mode

当然,一旦 Ruby 超出范围,该示例就会中断3.0.0,但重点是范围还可以识别整数以外的类型,有时还会以有趣的方式识别,但这是另一篇文章的主题。

正则表达式

正则表达式是一种用于匹配文本中的模式的语言,===它的工作方式非常类似于match?

/abc/ === 'abcdef'
# => true

/abc/.match?('abcdef')
# => true
Enter fullscreen mode Exit fullscreen mode

===在这种情况下,它表示存在匹配项,或者说我们的字符串是此正则表达式所引用的匹配项集合的成员。注意到了吗?

课程

Ruby 中有诸如 、 等类IntegerString通常,你可以使用 来检查某个对象是否属于特定类型is_a?。不出所料,===这里的工作方式大致相同:

String === 'foo'
# => true

'foo'.is_a?(String)
# => true
Enter fullscreen mode Exit fullscreen mode

===这里说的是,它'foo'是类的一个成员String,或者它包含在我们所说的字符串中。

需要注意的是,这适用于标准库中的每个 Ruby 核心类,但由于它需要自定义实现,因此可能存在一些不适用的特殊情况。

函数(Proc 和 Lambda)

Ruby 有几种表达匿名函数的方式,包括 procs 和 lambdas。此外还有 block,但我们暂时只讨论这两种。它们可以这样表达:

add_one_lambda = -> x { x + 1 }
add_one_proc   = proc { |x| x + 1 }
Enter fullscreen mode Exit fullscreen mode

在这一轮中我们不会讨论它们之间的差异,但请注意,我总体上更喜欢 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
Enter fullscreen mode Exit fullscreen mode

这个问题有点让人摸不着头脑。1函数的成员怎么会是函数呢?这说不通,它实际上又不是任何类型的集合或集合,对吧?其实在数学里,它叫做函数的定义域,或者说所有有效输入都落入的集合,所以这听起来也很像成员资格!

IP地址

Ruby 还为我们所有操作和网络类型的人提供了这个可爱的功能IPAddr

require 'ipaddr'

IPAddr.new('10.0.0.1')
Enter fullscreen mode Exit fullscreen mode

您甚至可以使用它来表达子网:

local_network = IPAddr.new('192.168.1.0/24')
local_network.include?('192.168.1.1')
# => true
Enter fullscreen mode Exit fullscreen mode

你的直觉是不是有点不对劲?因为我们还有另一个例子===

local_network === '192.168.1.1'
# => true
Enter fullscreen mode Exit fullscreen mode

这次我们检查一个 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
Enter fullscreen mode Exit fullscreen mode

您甚至可以使用逗号来检查多种可能性:

case 'foobar'
when String, Integer then :one
when Float, NilClass then :two
else
  :three
end
Enter fullscreen mode Exit fullscreen mode

检查一个值是否在预期集合内有很多可能性,并且使用函数会变得更加有趣:

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
# ]
Enter fullscreen mode Exit fullscreen mode

有趣不是吗?

这里面有个技巧叫做闭包,也就是一个函数返回另一个函数。返回的函数会记住 的值divisor,这样我们就可以检查某个值是否能被它整除。这是函数式编程中一个非常有用的技巧,它真正展现了 Ruby 中函数的强大功能,尤其是在case语句之​​类的用法中。

列举我们能享受的所有乐趣

Enumerable还有一些方法可以接受响应的值===。我们来看一些例子。

谓词方法

谓词方法(any?,,,)都all?可以很好地发挥作用none?one?===

['1', 2, :a].any?(Integer)
# => true

Enter fullscreen mode Exit fullscreen mode

搜索已开始

grep诸如(查找所有与模式匹配的)和grep_v(查找所有与模式匹配的)之类的搜索方法也实现了一个===接口:

%w(The rain in spain falls mainly on the plain).grep(/the/i)
# => ["The", "the"]
Enter fullscreen mode Exit fullscreen mode

生活片段

还有 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]]
Enter fullscreen mode Exit fullscreen mode

模式匹配

现在,如果你对语句感到兴奋,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
Enter fullscreen mode Exit fullscreen mode

如果我们愿意,我们甚至可以通过名称或其他一些有趣的项目来捕获这些值,但我们会将其保存到另一篇文章中。

类似哈希

接下来是类似哈希的方法,这里事情变得有趣了。假设我们有一个 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
Enter fullscreen mode Exit fullscreen mode

我们首先对其进行解析,但我们希望确保这些键Symbols使用以下语法JSON.parse

require 'json'

json_data = JSON.parse(raw_json, symbolize_names: true)
Enter fullscreen mode Exit fullscreen mode

现在我们可以用模式匹配做一些非常有趣的事情:

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"]
Enter fullscreen mode Exit fullscreen mode

好多啊。它到底在干什么?

首先,我们要选出所有年龄大于 ,20且眼睛颜色以字母 开头b,名字以T或 开头的人I。这样表达效果怎么样?

接下来,我们使用一种叫做“右赋值”的方法(=>)来提取人物的姓和名,并返回他们的姓名。在模式匹配中,如果某个键没有值,它会被放入一个局部变量中,这样firstlast可以在其下一行访问它。

再次强调,我们不会在本文中深入探讨模式匹配的所有细节,但你很快就会发现它是多么有趣。我计划很快写一篇更全面的模式匹配概念介绍,敬请期待。

总结

真是太多了。Ruby===中处处隐藏着各种特性,一旦你对它有了一定的了解,就会经常注意到它。更棒的是,这种了解意味着,===如果有一天你需要它,你现在知道如何创建自己的特性了。

不过,这是一个被定义为方法的运算符:

# Example implementation
class String
  def self.===(other)
    other.is_a?(self)
  end
end
Enter fullscreen mode Exit fullscreen mode

Ruby 拥有众多引人入胜的方面,并拥有强大的功能。本系列将继续介绍 Ruby 的一些基础知识以及一些最实用的工具和功能。

在此之前,请尽情享受您新获得的知识===

想了解我的写作和工作进展吗?快来看看我的新通讯:《宝石狐猴》

鏂囩珷鏉ユ簮锛�https://dev.to/baweaver/understanding-ruby-triple-equals-2p9c
PREV
在寻找开发工作之前
NEXT
理解 Ruby - Blocks、Procs 和 Lambdas 简介 Blocks、Procs 和 Lambdas 总结