🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
8-模块 ====== [编译](#81-%E7%BC%96%E8%AF%91) [脚本模式](#82-%E8%84%9A%E6%9C%AC%E6%A8%A1%E5%BC%8F) [命名函数](#83-%E5%91%BD%E5%90%8D%E5%87%BD%E6%95%B0) [函数捕捉](#84-%E5%87%BD%E6%95%B0%E6%8D%95%E6%8D%89) [默认参数](#85-%E9%BB%98%E8%AE%A4%E5%8F%82%E6%95%B0) Elixir中我们把许多函数组织成一个模块。我们在前几章已经提到了许多模块, 如[String模块](http://elixir-lang.org/docs/stable/elixir/String.html): ```elixir iex> String.length "hello" 5 ``` 创建自己的模块,用```defmodule```宏。用```def```宏在其中定义函数: ```elixir iex> defmodule Math do ...> def sum(a, b) do ...> a + b ...> end ...> end iex> Math.sum(1, 2) 3 ``` >像ruby一样,模块名大写起头 ## 8.1-编译 通常把模块写进文件,这样可以编译和重用。假如文件```math.ex```有如下内容: ```elixir defmodule Math do def sum(a, b) do a + b end end ``` 这个文件可以用```elixirc```进行编译: ```elixir $ elixirc math.ex ``` 这将生成名为```Elixir.Math.beam```的bytecode文件。 如果这时再启动iex,那么这个模块就已经可以用了(假如在含有该编译文件的目录启动iex): ```elixir iex> Math.sum(1, 2) 3 ``` Elixir工程通常组织在三个文件夹里: - ebin,包括编译后的字节码 - lib,包括Elixir代码(.ex文件) - test,测试代码(.exs文件) 实际项目中,构建工具Mix会负责编译,并且设置好正确的路径。 而为了学习方便,Elixir也提供了脚本模式,可以更灵活而不用编译。 ## 8.2-脚本模式 除了.ex文件,Elixir还支持.exs脚本文件。 Elixir对两种文件一视同仁,唯一区别是.ex文件会保留编译执行后产出的比特码文件, 而.exs文件用来作脚本执行,不会留下比特码文件。例如,如下创建名为math.exs的文件: ```elixir defmodule Math do def sum(a, b) do a + b end end IO.puts Math.sum(1, 2) ``` 执行之: ```sh $ elixir math.exs ``` 像这样执行脚本文件时,将在内存中编译和执行,打印出“3”作为结果。没有比特码文件生成。 后文中(为了学习和练习方便),推荐使用脚本模式执行学到的代码。 ## 8.3-命名函数 在某模块中,我们可以用```def/2```宏定义函数,用```defp/2```定义私有函数。 用```def/2```定义的函数可以被其它模块中的代码使用,而私有函数仅在定义它的模块内使用。 ```elixir defmodule Math do def sum(a, b) do do_sum(a, b) end defp do_sum(a, b) do a + b end end Math.sum(1, 2) #=> 3 Math.do_sum(1, 2) #=> ** (UndefinedFunctionError) ``` 函数声明也支持使用卫兵或多个子句。 如果一个函数有好多子句,Elixir会匹配每一个子句直到找到一个匹配的。 下面例子检查参数是否是数字: ```elixir defmodule Math do def zero?(0) do true end def zero?(x) when is_number(x) do false end end Math.zero?(0) #=> true Math.zero?(1) #=> false Math.zero?([1,2,3]) #=> ** (FunctionClauseError) ``` 如果没有一个子句能匹配参数,会报错。 ## 8.4-函数捕捉 本教程中提到函数,都是用```name/arity```的形式描述。 这种表示方法可以被用来获取一个命名函数(赋给一个函数型变量)。 下面用iex执行一下上文定义的math.exs文件: ```elixir $ iex math.exs ``` ```elixir iex> Math.zero?(0) true iex> fun = &Math.zero?/1 &Math.zero?/1 iex> is_function fun true iex> fun.(0) true ``` 用```&<function notation>```通过函数名捕捉一个函数,它本身代表该函数值(函数类型的值)。 它可以不必赋给一个变量,直接用括号来使用该函数。 本地定义的,或者已导入的函数,比如```is_function/1```,可以不用前缀模模块名: ```elixir iex> &is_function/1 &:erlang.is_function/1 iex> (&is_function/1).(fun) true ``` 这种语法还可以作为快捷方式来创建和使用函数: ```elixir iex> fun = &(&1 + 1) #Function<6.71889879/1 in :erl_eval.expr/5> iex> fun.(1) 2 ``` 代码中```&1``` 表示传给该函数的第一个参数。 因此,```&(&1+1)```其实等同于```fn x->x+1 end```。在创建短小函数时,这个很方便。 想要了解更多关于```&```捕捉操作符,参考[Kernel.SpecialForms文档](http://elixir-lang.org/docs/stable/elixir/Kernel.SpecialForms.html)。 ## 8.5-默认参数 Elixir中,命名函数也支持默认参数: ```elixir defmodule Concat do def join(a, b, sep \\ " ") do a <> sep <> b end end IO.puts Concat.join("Hello", "world") #=> Hello world IO.puts Concat.join("Hello", "world", "_") #=> Hello_world ``` 任何表达式都可以作为默认参数,但是只在函数调用时 **用到了** 才被执行。 (函数定义时,那些表达式只是存在那儿,不执行;函数调用时,没有用到默认值,也不执行)。 ```elixir defmodule DefaultTest do def dowork(x \\ IO.puts "hello") do x end end ``` ```elixir iex> DefaultTest.dowork 123 123 iex> DefaultTest.dowork hello :ok ``` 如果有默认参数值的函数有了多条子句,推荐先定义一个函数头(无具体函数体)声明默认参数: ```elixir defmodule Concat do def join(a, b \\ nil, sep \\ " ") def join(a, b, _sep) when is_nil(b) do a end def join(a, b, sep) do a <> sep <> b end end IO.puts Concat.join("Hello", "world") #=> Hello world IO.puts Concat.join("Hello", "world", "_") #=> Hello_world IO.puts Concat.join("Hello") #=> Hello ``` 使用默认值时,注意对函数重载会有一定影响。考虑下面例子: ```elixir defmodule Concat do def join(a, b) do IO.puts "***First join" a <> b end def join(a, b, sep \\ " ") do IO.puts "***Second join" a <> sep <> b end end ``` 如果将以上代码保存在文件“concat.ex”中并编译,Elixir会报出以下警告: ```elixir concat.ex:7: this clause cannot match because a previous clause at line 2 always matches ``` 编译器是在警告我们,在使用两个参数调用```join```函数时,总使用第一个函数定义。 只有使用三个参数调用时,才会使用第二个定义: ```elixir $ iex concat.exs ``` ```elixir iex> Concat.join "Hello", "world" ***First join "Helloworld" iex> Concat.join "Hello", "world", "_" ***Second join "Hello_world" ``` 后面几章将介绍使用命名函数来做循环,如何从别的模块中导入函数,以及模块的属性等。