我应该使用哪种数据结构?Elixir 速查表
Elixir 数据类型速查表
作为一名 Elixir 新手,我认为 Elixir 入门最难的事情之一就是搞清楚要使用哪种数据结构。我的团队最近开始全力投入 Elixir,所以我一直在努力认真复习。然而,我经常会读一些团队的代码,甚至很难理解我所看到的。它的语法看起来与 Ruby 非常相似(我对 Ruby 非常了解),但模式、约定和数据结构略有不同。在我看来,这很合理,因为它是一种函数式语言,而不是面向对象语言:在 Ruby 中使用对象,而在 Elixir 中,你很可能使用进程。
但无论如何,因为我现在正在学习 Elixir,所以我认为提供一份备忘单或概述来介绍我作为探索 Elixir 的 Rubyist 所注意到的数据结构差异会很有用。
数据类型
如果您之前使用过 Ruby(或大多数其他编程语言),那么整数、浮点数、范围和正则表达式可能对您来说都很熟悉。幸运的是,这些也都存在于 Elixir 中。它们之间有一些区别,但我还没有深入讨论过。
原子就像 Ruby 中的符号。它们以冒号开头,其名称即为它们的值。例如,:hello
在 Elixir 中是一个有效的原子。它们通常用于标记值。
Elixir 中也有字符串。字符串总是用双引号引起来,而字符列表则用单引号。字符串是二进制文件,而字符列表实际上只是代码点的列表。到目前为止,我很少使用字符列表。
以下是这些类型的简要介绍
iex> 2 # integer
iex> 2.0 # floating point
iex> false # boolean
iex> 1..4 # range
iex> ~r/hello/ # regular expression
iex> :hello # atom
iex> "world" # string
iex> 'world' # charlist
Elixir 具有附加数据类型Port
和PID
,用于进程通信。它们是通过 Erlang VM 提供的实体。
港口
APort
用于与应用程序外部的资源进行通信(读/写)。它们非常适合启动操作系统进程并与其通信。例如,您可能想要打开一个端口来运行某个
操作系统命令,例如echo
。
您可以打开一个端口并向其发送一条消息,如下所示:
iex> port = Port.open({:spawn, "echo sup"}, [:binary])
#Port<0.1305>
然后,您可以使用flush()
IEx 帮助程序从端口打印消息。
iex> port = Port.open({:spawn, "echo sup"}, [:binary])
#Port<0.1305>
iex> flush()
iex> {#Port<0.1305>, {:data, "sup\n"}}
iex> :ok
你可以将任何想要执行的二进制文件的名称发送到端口。例如,我从我的 Jekyll 博客目录打开了一个iex
会话,打开了一个端口,然后发送了bundle install
安装所有 Ruby gem 依赖项的命令。以下是输出片段。
iex> port = Port.open({:spawn, "bundle install"}, [:binary])
#Port<0.1306>
iex> flush()
{#Port<0.1306>, {:data, "Using concurrent-ruby 1.0.5\n"}}
{#Port<0.1306>, {:data, "Using i18n 0.9.5\n"}}
{#Port<0.1306>, {:data, "Using minitest 5.11.3\n"}}
{#Port<0.1306>, {:data, "Using thread_safe 0.3.6\n"}}
{#Port<0.1306>, {:data, "Using tzinfo 1.2.5\n"}}
{#Port<0.1306>, {:data, "Using activesupport 4.2.10\n"}}
{#Port<0.1306>, {:data, "Using public_suffix 2.0.5\n"}}
{#Port<0.1306>, {:data, "Using addressable 2.5.2\n"}}
{#Port<0.1306>, {:data, "Using bundler 1.16.2\n"}}
{#Port<0.1306>, {:data, "Using coffee-script-source 1.11.1\n"}}
{#Port<0.1306>, {:data, "Using execjs 2.7.0\n"}}
{#Port<0.1306>,
{:data,
"Bundle complete! 4 Gemfile dependencies, 85 gems now installed.\nUse `bundle info [gemname]` to see where a bundled gem is installed.\n"}}
:ok
PID
APID
是对进程的引用。每当你创建一个新进程,你都会获得一个新的 PID。后面会讲很多关于 PID 的内容。你可能需要保留 PID,以便向不同的进程发送消息。
下面是生成进程并获取 PID 的示例。
iex> pid = spawn fn -> IO.puts("hello world") end
iex> hello world
iex> #PID<0.123.0>
进程完成任务后就会终止。PID 和端口值得单独写一篇文章来介绍,但目前我认为只要知道它们的存在就足够了。
所以,现在我们已经添加了新类型,这是我们的基本备忘单。
Elixir 数据类型速查表
iex> 2 # integer
iex> 2.0 # floating point
iex> false # boolean
iex> 1..4 # range
iex> ~r/hello/ # regular expression
iex> :hello # atom
iex> "world" # string
iex> 'world' # charlist
iex> #Port<0.1306> # port
iex> #PID<0.123.0> # pid
不过,在我看来,Elixir 真正的挑战在于如何将这些基本数据类型组织成可用的结构。因此,让我们来看看各种集合类型以及它们各自的用途。
集合类型
以下是您可能会遇到的集合类型:
- 元组
- 列表
- 关键字列表
- 地图
- 结构体
你可能之前听说过这些词,至少是顺便听过。但如果你熟悉 Ruby,你可能会想知道为什么需要所有这些额外的集合类型。让我们来探究一下。
元组
元组是值的有序集合。它们看起来像这样:
iex> {:hello, "world"}
iex> {1, 2}
iex> {:ok, "this is amazing!", 2}
# You can check if it's really a tuple
iex> tuple = { "hello", "world"}
iex> is_tuple tuple
iex> true
# and then you can get an element from a tuple by index
iex> elem(tuple, 1)
iex> "world"
我觉得元组有点儿怪怪的。我的意思是,它们看起来像哈希,但行为却有点像 Ruby 数组。而且它们还叫元组!不过,熟悉一下元组还是有好处的,我每次遇到困惑,都会这样告诉自己。
元组在 Elixir 中随处可见。函数的返回值通常也是可以进行模式匹配的元组,所以开始通过元组来理解世界是有意义的。元组通常包含 2 到 4 个元素,目前,它们是我最常用的数据结构。当你处理包含 4 个以上元素的数据结构时,使用 map 或 struct 可能是一个不错的选择。
列表
列表是链接的数据结构。它们看起来像这样:
iex> [1, 2, 3, 4]
iex> ["hello", "world"]
在 Ruby 中,你会认为那是数组,但在 Elixir 中,它就是列表!由于列表是作为链式数据结构实现的,所以它们适合递归,但不适合随机检索元素,甚至计算长度,因为你需要遍历整个列表才能确定大小。到目前为止,我主要使用元组而不是列表。如果必须在它们之间做出选择,我想你需要考虑集合的预期大小以及你将对其进行的操作类型。
关键字列表
更复杂的是,Elixir 中还有关键字列表之类的东西。本质上,这是一个二值元组的列表。
# keyword list
iex> [ phrase: "oh hello", name: "tracy" ]
# is actually two-value tuples
iex> [ {:phrase, "oh hello"}, {:name, "tracy"} ]
尽管我知道它的普遍存在,但这仍然让我感到困惑。关键字列表的妙处在于,您可以在一个关键字列表中拥有两个相同的键。
iex> keyword_list = [food: "peanut butter", food: "ice cream", flavor: "chocolate"] # a valid keyword list
关键字列表适用于命令行参数和选项。
地图
接下来是 Map。如果你想要一个真正的键值存储,而不是一个键值列表,那么 Map 就是你想要的。它们看起来有点像 Ruby 中的哈希。
iex> %{"greeting" => "hello", "noun" => "world"}
iex> %{:greeting => "hello", :noun => "world"}
iex> %{greeting: "hello", noun: "world"} # if the keys are atoms, you can skip the hash rockets.
iex> greeting = %{spanish: "hola", chinese: "ni hao", english: "hello"}
iex> greeting[:spanish]
iex> "hola"
iex> greeting.chinese
iex> "ni hao"
地图适合传递关联数据,以及几乎所有大于元组大小的其他数据。
结构体
结构体就像是增强型的 Map。它们只允许使用特定的键,并且这些键必须是原子。结构体需要在模块中定义,并设置合理的默认值。它们是带有规则的 Map。
iex> defmodule IceCream do
.... defstruct flavor: "", quantity: 0
.... end
iex> chocolate = %IceCream{flavor: "chocolate"}
iex> chocolate.flavor
iex> "chocolate"
你会看到,这个结构体的定义使用了与%
map 相同的百分号,但它后面跟着的是模块的名称。我就是这样提醒自己,它们只是更严格的 map。
Elixir 的旧版本还包含用于HashDict
处理具有超过几百个值的地图的模块,但该模块已被弃用,取而代之的是老式的Map
。
至此,Elixir 中常见的数据类型和集合类型介绍就结束了。虽然两种语言之间存在诸多差异,但也存在一些相似之处。当然,关于 Elixir、它的约定以及一些很酷的功能,还有很多东西需要学习,但我认为这篇文章是熟悉这门语言的一个良好开端。希望这篇文章能成为你很快理解任何 Elixir 的指南!
Elixir 收藏速查表
iex> {:ok, "this is amazing!", 2} # tuple
iex> ["hello", "world"] # list
iex> [ phrase: "oh hello", name: "tracy" ] # keyword list
iex> greeting = %{spanish: "hola", chinese: "ni hao", english: "hello"} # map
iex> chocolate = %IceCream{flavor: "chocolate"} # struct
资源
- Elixir Lang 上的基本类型
- 戴夫·托马斯 (Dave Thomas)编写 Elixir 程序
- 十六进制文档