3.函数
在编程的上下文中,函数是执行计算的命名语句序列。定义函数时,指定名称和语句序列。之后,您可以按名称“调用”该功能。
函数调用
我们已经看到了一个函数调用的例子:
julia> typeof(42)
Int64
该函数的名称是typeof。括号中的表达式称为函数的参数。对于此函数,结果是参数的类型。
通常会说函数“接受”一个参数并“返回”一个结果。结果也称为返回值。
Julia提供了将值从一种类型转换为另一种类型的函数。该parse函数接受一个字符串并将其转换为任何数字类型(如果可以),否则会抱怨:
julia> parse(Int64, "32")
32
julia> parse(Float64, "3.14159")
3.14159
julia> parse(Int64, "Hello")
ERROR: ArgumentError: invalid base 10 digit 'H' in "Hello"
trunc可以将浮点值转换为整数,但它不会舍入; 它切掉了小部分:
julia> trunc(Int64, 3.99999)
3
julia> trunc(Int64, -2.3)
-2
float 将整数转换为浮点数:
julia> float(32)
32.0
最后,string将其参数转换为字符串:
julia> string(32)
"32"
julia> string(3.14159)
"3.14159"
数学函数
在Julia中,大多数熟悉的数学函数都是直接可用的:
ratio = signal_power / noise_power
decibels = 10 * log10(ratio)
第一个例子使用log10,以计算在分贝信噪比(假设signal_power和noise_power定义)。log,它计算基数e的对数,也提供。
radians = 0.7
height = sin(radians)
第二个例子找到弧度的正弦值。变量的名称是一个暗示,sin而另一三角函数(cos,tan等)采取参数以弧度表示。要将度数转换为弧度,请除以180并乘以π:
julia> degrees = 45
45
julia> radians = degrees / 180 * π
0.7853981633974483
julia> sin(radians)
0.7071067811865475
变量的值π是π的浮点近似值,精确到约21位数。
如果你知道三角学,你可以通过将它与2的平方根除以2来检查以前的结果:
julia> sqrt(2) / 2
0.7071067811865476
组成
到目前为止,我们已经单独查看了程序变量,表达式和语句的元素,而没有讨论如何组合它们。
编程语言最有用的功能之一是它们能够构建小型构建块并构建它们。例如,函数的参数可以是任何类型的表达式,包括算术运算符:
x = sin(degrees / 360 * 2 * π)
甚至函数调用:
x = exp(log(x+1))
几乎在任何可以放置值的地方,都可以放置一个任意表达式,但有一个例外:赋值语句的左侧必须是变量名。左侧的任何其他表达式都是语法错误(我们稍后会看到此规则的例外情况)。
julia> minutes = hours * 60 # right
120
julia> hours * 60 = minutes # wrong!
ERROR: syntax: "60" is not a valid function argument name
添加函数
到目前为止,我们只使用了Julia附带的功能,但也可以添加新功能。一个函数定义指定一个新的函数的名称和运行时函数被调用语句序列。这是一个例子:
function printlyrics()
println("I'm a lumberjack, and I'm okay.")
println("I sleep all night and I work all day.")
end
function是一个关键字,表示这是一个函数定义。该函数的名称是printlyrics。函数名称的规则与变量名称的规则相同:它们几乎可以包含所有Unicode字符,但第一个字符不能是数字。您不能使用关键字作为函数的名称,并且应该避免使用具有相同名称的变量和函数。
名称后面的空括号表示此函数不带任何参数。
函数定义的第一行称为标题 ; 其余的被称为身体。正文以关键字结尾,end它可以包含任意数量的语句。为了便于阅读,函数体可以缩进。所以就这么做吧。
引号必须是“直引号”,通常位于键盘上的Enter旁边。像“句子中的那些”一样,“卷曲引用”在julia中是不合法的。
julia> function printlyrics()
println("I'm a lumberjack, and I'm okay.")
要结束此功能,您必须输入end。
定义函数会创建一个函数对象,其类型为Function:
julia> printlyrics isa Function
true
调用新函数的语法与内置函数的语法相同:
julia> printlyrics()
I'm a lumberjack, and I'm okay.
I sleep all night and I work all day.
定义函数后,可以在其他函数中使用它。例如,要重复前面的副词,我们可以编写一个名为的函数repeatlyrics:
function repeatlyrics()
printlyrics()
printlyrics()
end
然后调用epeatlyrics:
julia> repeatlyrics()
I'm a lumberjack, and I'm okay.
I sleep all night and I work all day.
I'm a lumberjack, and I'm okay.
I sleep all night and I work all day.
定义和用途
将上一节中的代码片段整合在一起,整个程序如下所示:
function printlyrics()
println("I'm a lumberjack, and I'm okay.")
println("I sleep all night and I work all day.")
end
function repeatlyrics()
printlyrics()
printlyrics()
end
repeatlyrics()
该程序包含两个函数定义:printlyrics和repeatlyrics。函数定义与其他语句一样执行,但效果是创建函数对象。函数内部的语句在调用函数之前不会运行,函数定义不会生成输出。
正如您所料,您必须先创建一个函数,然后才能运行它。换句话说,函数定义必须在调用函数之前运行。
作为练习,将此程序的最后一行移到顶部,因此函数调用出现在定义之前。运行该程序,看看你得到了什么错误信息。
现在将函数调用移回到底部并移动定义printlyrics后的定义repeatlyrics。运行此程序会发生什么?
执行流程
要确保在第一次使用函数之前定义函数,您必须知道运行的订单语句,这称为执行流程。
执行总是从程序的第一个语句开始。报表按从上到下的顺序一次运行一个。
函数定义不会改变程序的执行流程,但请记住函数内部的语句在调用函数之前不会运行。
函数调用就像是执行流程中的绕道而已。流不会转到下一个语句,而是跳转到函数体,在那里运行语句,然后返回到它停止的位置。
这听起来很简单,直到你记得一个函数可以调用另一个函数。在一个函数的中间,程序可能必须在另一个函数中运行语句。然后,在运行该新函数时,程序可能必须运行另一个函数!
幸运的是,Julia擅长跟踪它的位置,因此每次函数完成时,程序都会在调用它的函数中从中断处继续。当它到达程序结束时,它终止。
总之,当您阅读程序时,并不总是想要从上到下阅读。如果您遵循执行流程,有时会更有意义。
参数和参数
我们看到的一些功能需要参数。例如,当调用时,sin你传递一个数字作为参数。有些函数需要多个参数:parse取两个,数字类型和字符串。
在函数内部,参数被分配给称为参数的变量。以下是带参数的函数的定义:
function printtwice(bruce)
println(bruce)
println(bruce)
end
此函数将参数分配给名为的参数bruce。调用该函数时,它会打印参数的值(无论是什么)两次。
此功能适用于任何可以打印的值。
julia> printtwice("Spam")
Spam
Spam
julia> printtwice(42)
42
42
julia> printtwice(π)
π = 3.1415926535897...
π = 3.1415926535897...
适用于内置函数的相同组合规则也适用于程序员定义的函数,因此我们可以使用任何类型的表达式作为参数printtwice:
julia> printtwice("Spam "^4)
Spam Spam Spam Spam
Spam Spam Spam Spam
被调用的函数前面的参数进行评估,因此,在实施例中的表达式”Spam “^4和cos(π)只计算一次。
您还可以使用变量作为参数:
julia> michael = "Eric, the half a bee."
"Eric, the half a bee."
julia> printtwice(michael)
Eric, the half a bee.
Eric, the half a bee.
作为参数传递的变量名(michael)与参数名(bruce)没有任何关系。在调用者中调用的值是什么并不重要;在printtwice,我们叫每个人bruce。
变量和参数是局部的
在函数内部创建变量时,它是本地的,这意味着它只存在于函数内部。例如:
function cattwice(part1, part2)
concat = part1 * part2
printtwice(concat)
end
此函数接受两个参数,连接它们,并打印两次结果。以下是使用它的示例:
julia> line1 = "Bing tiddle "
"Bing tiddle "
julia> line2 = "tiddle bang."
"tiddle bang."
julia> cattwice(line1, line2)
Bing tiddle tiddle bang.
Bing tiddle tiddle bang.
当cattwice终止时,可变concat被破坏。如果我们尝试打印它,我们会得到一个例外:
julia> println(concat)
ERROR: UndefVarError: concat not defined
参数也是本地的。例如,在外面printtwice,没有这样的东西bruce。
堆栈图
为了跟踪哪些变量可以在哪里使用,绘制堆栈图有时很有用。与状态图一样,堆栈图显示每个变量的值,但它们也显示每个变量所属的函数。
每个功能由一个框架表示。框架是一个框,旁边有一个函数的名称,以及它内部函数的参数和变量。前一个示例的堆栈图显示在堆栈图中。
帧被排列成堆栈,指示哪个函数被称为哪个,依此类推。在这个例子中,printtwice被调用cattwice,cattwice并被调用main,这是最顶层框架的特殊名称。当您在任何函数之外创建变量时,它属于main。
每个参数引用与其对应参数相同的值。因此,part1具有相同的值line1,part2具有相同的值line2,并且bruce具有相同的值concat。
如果一个函数调用过程中发生错误,朱莉娅打印功能的名称,调用它的函数的名称,函数的调用的名字是,一路回main。
例如,如果您尝试concat从内部访问printtwice,则会得到UndefVarError:
错误:UndefVarError:concat未定义
Stacktrace:
[1] printtwice at ./REPL [1] :2 [inlined]
[2] cattwice(:: String,:: String)at ./REPL[2]:3
这个函数列表称为堆栈跟踪。它告诉您错误发生在哪个程序文件中,以及当时正在执行的行和内容。它还显示导致错误的代码行。
堆栈跟踪中函数的顺序是堆栈图中帧的顺序的倒数。当前运行的功能位于顶部。
卓有成效的功能和无效功能
我们使用的一些函数,如数学函数,返回结果; 由于缺乏一个更好的名字,我称之为富有成效的功能。其他功能,例如printtwice,执行操作但不返回值。它们被称为void函数。
当你调用一个富有成效的函数时,你几乎总是想对结果做些什么; 例如,您可以将其分配给变量或将其用作表达式的一部分:
x = cos(radians)
golden = (sqrt(5) + 1) / 2
当您以交互模式调用函数时,Julia会显示结果:
julia> sqrt(5)
2.23606797749979
但是在脚本中,如果你自己调用一个富有成效的函数,那么返回值将永远丢失!
sqrt(5)
输出:
2.23606797749979
此脚本计算5的平方根,但由于它不存储或显示结果,因此它不是很有用。
Void函数可能会在屏幕上显示某些内容或产生其他影响,但它们没有返回值。如果将结果赋给变量,则会得到一个名为的特殊值nothing。
julia> result = printtwice("Bing")
Bing
Bing
julia> show(result)
nothing
要打印该值nothing,您必须使用show类似print但可以处理该值的函数nothing。
该值nothing与字符串不同”nothing”。它是一个具有自己类型的特殊值:
julia> typeof(nothing)
Nothing
到目前为止我们写的函数都是无效的。我们将在几章中开始编写富有成效的函数。
为什么功能?
可能还不清楚为什么将程序划分为函数是值得的。有几个原因:
- 创建新函数使您有机会命名一组语句,这使您的程序更易于阅读和调试。
- 函数可以通过消除重复代码使程序更小。之后,如果您进行更改,则只需在一个地方进行更改。
- 将长程序划分为函数允许您一次调试一个部件,然后将它们组装成一个整体。
- 精心设计的功能通常对许多程序有用。一旦编写并调试一个,就可以重用它。
调试
您将获得的最重要技能之一是调试。虽然它可能令人沮丧,但调试是编程中最具智力,最具挑战性和最有趣的部分之一。
在某些方面,调试就像侦探工作。您面临线索,您必须推断导致您看到的结果的过程和事件。
调试也像一门实验科学。一旦你知道出了什么问题,你就修改你的程序然后再试一次。如果您的假设是正确的,您可以预测修改的结果,并向工作程序迈进一步。如果你的假设是错误的,你必须想出一个新的假设。正如夏洛克·福尔摩斯所指出的那样,“当你消除了不可能的事物,无论剩下什么,无论多么不可能,都必须是真理。”(柯南道尔,四人的标志)
对于某些人来说,编程和调试是一回事。也就是说,编程是逐步调试程序直到它完成你想要的程序。我们的想法是,你应该从一个工作程序开始,进行小的修改,然后调试它们。
例如,Linux是一个包含数百万行代码的操作系统,但它最初是作为Linus Torvalds用于探索Intel 80386芯片的简单程序开始的。根据Larry Greenfield的说法,“Linus之前的一个项目是一个可以在打印”AAAA和“BBBB”之间切换的程序。后来演变为Linux。“(Linux用户指南测试版1)。
词汇表
功能
一个命名的语句序列,执行一些有用的操作。函数可能会也可能不会参数,可能会也可能不会产生结果。
功能定义
创建新函数的语句,指定其名称,参数及其包含的语句。
功能对象
由函数定义创建的值。函数的名称是一个引用函数对象的变量。
头
函数定义的第一行。
身体
函数定义中的语句序列。
参数
函数内部使用的名称,用于引用作为参数传递的值。
功能调用
运行函数的语句。它由函数名称后跟括号中的参数列表组成。
论据
调用函数时为函数提供的值。该值分配给函数中的相应参数。
局部变量
函数内定义的变量。局部变量只能在其函数内使用。
返回值
功能的结果。如果函数调用用作表达式,则返回值是表达式的值。
富有成效的功能
一个返回值的函数。
无效功能
一个始终返回的函数nothing。
nothing
void函数返回的特殊值。
组成
将表达式用作较大表达式的一部分,或将语句用作较大语句的一部分。
执行流程
订单声明在。
堆栈图
一组函数,它们的变量以及它们引用的值的图形表示。
帧
堆栈图中表示函数调用的框。它包含函数的局部变量和参数。
堆栈跟踪
发生异常时正在执行的函数列表。
演习
这些练习应该仅使用我们迄今为止学到的陈述和其他特征来完成。
练习3-1
编写一个名为的函数rightjustify,它接受一个名为s参数的字符串,并打印带有足够前导空格的字符串,以便字符串的最后一个字母位于显示的第70列。
julia> rightjustify("monty")
monty
使用字符串连接和重复。此外,Julia提供了一个内置函数length,它返回一个字符串的长度,因此值为length(“monty”)5。
练习3-2
函数对象是可以分配给变量或作为参数传递的值。例如,dotwice是一个将函数对象作为参数并将其调用两次的函数:
function dotwice(f)
f()
f()
end
这是一个dotwice用于调用printspam两次名为的函数的示例。
function printspam()
println("spam")
end
dotwice(printspam)
- 在脚本中键入此示例并对其进行测试。
- 修改dotwice以便它接受两个参数,一个函数对象和一个值,并调用该函数两次,将该值作为参数传递。
- 将printtwice本章前面的定义复制到您的脚本中。
- 使用修改后的版本dotwice调用printtwice两次,”spam”作为参数传递。
- 定义一个名为的新函数dofour,它接受一个函数对象和一个值,并调用该函数四次,将该值作为参数传递。这个函数的主体应该只有两个语句,而不是四个。
练习3-3
- 编写一个printgrid绘制如下网格的函数:(((“function”,“programmer-defined”,“printgrid”,see
=“printgrid”)
julia> printgrid()
+ - - - - + - - - - +
| | |
| | |
| | |
| | |
+ - - - - + - - - - +
| | |
| | |
| | |
| | |
+ - - - - + - - - - +
编写一个绘制具有四行四列的类似网格的函数。
图片来源:本练习基于Oualline的练习,实用C编程,第三版,O’Reilly Media,1997。
小费
要在一行上打印多个值,可以打印以逗号分隔的值序列:
println("+", "-")
该功能print不会前进到下一行:
print("+ ")
println("-")
这些陈述的输出”+ -“在同一行。下一个print语句的输出将在下一行开始。