Julia函数基本定义

Julia函数基本定义

常规结构

函数的常规定义语法为:

function函数名(参变量1, 参变量2, ...)
  # 实现语句
  return结果表达式
end

其中,关键字function与end界定了函数的定义范围。

参数变量(参变量)用于控制函数的行为,需在紧随函数名之后(中间不能有空格)的圆括号内给出,可以使用::操作符进行类型限定。函数定义可以不提供参数,即参变量表为空,但圆括号不能省略。参变量表可以看作有秩序的(positional)元组结构;在函数调用时,实际参数(实参)值需要按照参数的顺序逐一对应提供,否则会带来不可预料的结果。

函数内部的实现语句相当于复合表达式,默认最后一个表达式的计算结果作为函数的返回结果;也可以显式地使用return关键字在函数内的任意位置立即将结果返回,不再执行后续的语句,可结合控制逻辑使用,以避免冗余的处理过程,提高效率;如果不需要结果返回,则可以在返回的地方(如end之前)提供nothing对象,即什么都不需要。

另外还有一种简洁的函数定义方式,即将复合表达式直接赋值给待定义的函数,语法为:

函数名(参变量1, 参变量2, ...) = 复合表达式

这种方式适合于比较简短的函数定义。

如果是在REPL中定义函数,回车后便会看到类似以下的提示信息:

函数名 (generic function with 1 method)

表明函数创建成功,之后便可以调用该函数了。在Julia中,函数一般被称为“generic function”,并会提示“with n methods”,其中的数字n表示同名函数有几种实现。

函数的调用方式并不复杂,只需在函数名后提供实参值即可,形式为:

函数名(实参1, 实参2, ...)

其中的实参需是有效的、存在的对象。在调用处可以直接将返回结果赋值给某个变量或用于修改某个数据区。

下面以实例说明函数的定义方法。例如,需要一个累加两个值的函数:

julia> function addtwo(x, y)
            x + y
          end
  addtwo (generic function with 1 method)

因为简单,所以可以采用简洁的方式,如下:

julia> addtwo(x, y) = x + y
  addtwo (generic function with 1 method)

尝试调用一下:
Julia基本定义

但如果加法语句之间存在return语句且被执行,则不会得到预期的结果,例如:

julia> function notaddtwo(x, y)
      return x * y
      x + y
    end
  notaddtwo (generic function with 1 method)

julia> notaddtwo(2, 3)
6

该函数在计算x*y之后便立即将乘积返回,后续的加法语句并没有执行。

类型限定

Julia是弱类型语言,使用变量或参数是可以不限制类型的,但类型的自由选择往往会出现非常意外的结果。例如:

julia> f(x, y) = 2x + y
f (generic function with 1 method)

对于数值类型,能顺利得到想要的结果:

julia> f(2, 3.0)
7.0

但如果调用不当,传入了非数值类型的参数,则会出现奇怪的结果:
Julia基本定义

所以,在程序开发过程中,尤其是关键计算处,最好都能够明确地限定类型,以免给自己“挖坑”。所谓“坑”,是那些不会报错但还可以顺利执行的代码,而结果却不是预期的,这也是最难以发现的问题。

若要在函数定义时进行类型限定,只需在参变量后使用::操作符给出其类型条件即可。基本方式为:

function函数名(参变量1::类型1, 参变量2::类型2, ...)
   # 实现
end

或者

函数名(参变量1::类型1, 参变量2::类型2, ...) = 复合表达式

其中的类型可以是具体类型也可以是抽象类型;也可以只限定部分参数或全部参数。

例如,实现的addtwo()函数如果只支持Int64类型,则可以将其定义修改为:

julia> function addtwo(x::Int64, y::Int64)
          x + y
        end

当输入参数符合要求时,可以顺序执行:

julia> addtwo(Int64(10), Int64(5))
15

但如果输入参数不符合要求,则会报错,例如:

julia> addtwo(Int32(10), Int32(5))
ERROR: MethodError: no method matching addtwo(::Int32, ::Int32)

julia> addtwo(Float64(10.0), Int64(5))
ERROR: MethodError: no method matching addtwo(::Float64, ::Int64)

这样,在函数内部执行前我们便能够知道发生了问题,可在调用处进行恰当的调整。

如果希望该函数能够支持所有的整型,则可采用抽象类型Integer来约束参数,以放大其适用的范围,重新定义如下:

julia> function addtwo(x::Integer, y::Integer)
          x + y
        end

此时再次提供Int32类型的参数,便能顺序地执行:

julia> addtwo(Int32(10), Int32(5))
15

同时也支持了其他的整型参数,例如:

julia> addtwo(Int32(10), Int8(5))
15

julia> addtwo(UInt32(10), Int8(5))
15

但对于非整型的数值仍然是不支持的,依然会报错。

如上所述,在定义函数时,可以通过::操作符对参变量进行类型限定。显然,若限定为Any类型是毫无意义的;但若限定的都是元类型,兼容性会变差。所以在类型限定时,需要根据实际需求,选择恰当的类型节点或抽象层次对参数类型做出合理的限定。

共享传参

鉴于性能问题,为避免参数传递时带来大量的内存操作消耗,Julia采用一种被称为共享传参pass-by-sharing)的方式:传递参数时,元类型作为值传递而可变的复合类型则按引用传递。这种方式综合了按值传参(pass-by-value)和按引用传参(pass-by-reference)的优点。

先看简单的元类型参数,例如:

julia> x = 10;

julia> function change_value(y)
          y = 17
        end
change_value (generic function with 1 method)

julia> change_value(x)
17

该函数希望能改变传入参数x的值,但实际上变量x的值并没有发生变化:

julia> x
10                 # 仍是10

在上例中,变量x绑定了一个Int64类型的值,因为是元类型,所以会按值传入change_value()中。此时,参变量y被创建为新的Int64对象,并被赋值(复制)为x的内容,即y被绑定到了新的内存区且不同于x。在执行y=17语句时,y又被重新绑定到了新的值。所以自x内容在传参时被复制到y后,整个处理过程都是针对新的内容区,与实参x的关系已经不大。

但如果实参的内部结构复杂(如复合类型),便会按引用传递。此时函数内部若是改变了参变量原有的数据,变化便会传递到函数外部的实参,也会影响原数据在外部后续处理过程中的使用。例如:

julia> function change_dict(a::Dict)
          a[88] = 6.6                        # 将键88的值修改为6.6
          end
  change_dict (generic function with 1 method)

  julia> d = Dict(88=>8.8, 99=>9.9)      # 创建Dict对象,键88对应8.8
  Dict{Int64, Float64} with 2 entries:
    99 => 9.9
    88 => 8.8

julia> change_dict(d);

julia> d
Dict{Int64, Float64} with 2 entries:
  99 => 9.9
  88 => 6.6                               # 值已被更新!

其中Dict对象先被绑定到d变量,在调用时再被绑定到参变量a。因为a只不过是指向Dict对象的新引用,所以与d共享着同一个对象,函数内部对a的变更会反映到外部d变量中。上述这种区别是非常重要的,需要开发者注意。

提示
为了能让这种改变参数的函数行为更为醒目,Julia建议使用感叹号!标识这种函数。所以change_dict最好命名为change_dict!形式。这不是强制的做法,仅希望以惯例的形式提醒开发者和调用者,该函数内部更新了控制参数。

数集展开式调用

如前所述,函数的参数表可视为元组结构,所以在提供实参时,我们可以直接提供元组对象。但为了各参变量能获得对应的实参,在以元组方式提供实参时,需附加...操作符,以便将元组自动展开并提取到对应的参变量中。例如:

julia> addtwo((1,2)...)                 # x与y分别在元组展开时获得1与2
3

julia> addtwo(1, (2, )...)               # x=1, 但y在(2, )展开时获得2
3

其中,addtwo()函数有两个参变量x与y,均是在元组结构展开过程中提取了对应的实参值。

更为方便的是,基于展开操作符的实参提供方式,同样适用于其他可迭代数集,包括数组、Set等。例如:
Julia基本定义

注意
Set这种容器是无序的,虽然能正常使用,但无法保证与有序参数的对应关系,所以仍是建议采用元组或数组的方式。

使用展开方式时,提供的常规实参与元组元素的总数量必须与有实参需求的参变量数目一致,否则会出错。另外,如果使用了数集作为参数但没有采用展开式表达,则该数集会被视为普通实参,只会被传递到其中一个参变量中,不会再被提取到其他参变量中。例如:

julia> addtwo((1,2))
ERROR: MethodError: no method matching addtwo(::Tuple{Int64, Int64})

其中的元组仅是普通的实参,但该函数并不接收一个为元组的参数,所以报错。

注意
采用数集展开提供实参时,必须满足函数参数对类型的要求;而且因为展开作用,数集已被提取到基本参变量中,所以对作为实参的数集不再是引用传递;但是如果数集内部被提取的元素是复杂结构,仍存在引用关系。

多返回值

在Julia的函数中同时返回多个结果是比较简单的:将需要的结果以逗号方式连接,便能够以元组结构的形式同时将它们返回。例如:

julia> function addmul(a, b)
    a+b, a*b
  end

调用后,便可以元组的方式提取结果:

julia> x, y = addmul(5, 10)
(15, 50)

julia> result = addmul(5,10)
(15, 50)

即:

julia> result[1] == x == 15
true

显然,两种方式获得的结果是一致的。

如果有必要,在结果返回处,可使用圆括号显式地以元组形式对多个结果进行封装。

酷客网相关文章:

赞(0)

评论 抢沙发

评论前必须登录!