2.5 函数
函数是什么?
我们会把代码分组,每个组里有一行或多行代码,而函数则是这些代码组的名字。函数与变量很相似,函数也有类型和名字,只不过它不只是存放信息,还会处理信息。这跟基础代数的概念很接近,变量可以用一个字母x表示,而函数则是一个指令,向指令输入一些内容,期待它返还一个结果。如果用简单的文字描述一个函数,大概会像这样:“收到一笔钱,把收到的金额加在原来的金额上,最后把总金额告诉我。”
这件事可以分解成三部分:
- 收到一笔钱
- 累加金额
- 报告总金额
以上三部分可以这样来考虑:函数收到什么,做了什么,返回什么。函数是程序中的指定行为,接收和处理特定类型的数据,完成后返回结果。
2.5.1 定义函数
我们就把上文提到的收钱的例子用代码实现一下。定义一个变量放你所有的钱:
int myBank = 0;
创建一个用来收钱的函数,把钱放到myBank里,返回钱的总数:
int moneyReceived(int money){
myBank += money;
return myBank;
}
现在你已经创建了一个名叫moneyReceived()的函数。伪代码和文字描述都有助于我们理解函数。“取得一个整数,加到银行里的现有金额中,报告总金额。”从图2-6可以看到函数做了些什么。
图2-6:函数的声明
注意,return语句所返回的内容要和标在函数名前面的数据类型一致,这里函数返回的myBank就是一个整型变量。
2.5.2 向函数传递参数
函数定义好之后,就可以开始调用了。要调用函数,需要向它传递类型匹配的参数。在函数moneyReceived()的例子里,函数指定输入一个整型参数。下面的语句都是正确的:
moneyReceived(4);
或
int priceOfDinner = 17;
moneyReceived(priceOfDinner);
但是这样就不对了:
float f = 1.32;
moneyReceived(f);
错误在于传递给函数的参数类型不对,所以要看清楚函数的声明,包括返回值的类型、名字和参数。
return语句指出这个方法函数返回的值的类型,就跟数据类型指定了变量可以存放什么数据的道理一样。不返回任何值的函数会被声明为void类型,其他情况下函数返回值的类型需要明确声明。例如,创建一个返回一个字符的函数,可以这样写:
char myFunction()
函数肯定是这样一种格式:类型、函数名、括号以及被传入的参数,如下所示。
int multiplyByTwo(int value){
return value * 2;
}
这个函数接收了一个整型值,把这个值和2相乘,返回相乘的结果。我们可以用带有返回值的函数去设置变量的值:
int x = 5;
int twoTimesX = multiplyByTwo(x); // twoTimesX 等于10
而下面这个例子则是调用了一个返回字符的函数,返回的结果取决于输入的参数:
char convertIntToChar(int i){
char ch = char(i);
return ch;
}
新变量的值可由函数的返回值设置:
string addExclamationPoints(string s) {
return s+"!!!";
}
string myStr = addExclamationPoints("hello"); // myStr被设置为'hello!!!'
由此可见函数类型是多么重要。任何属于整型的东西都可以为整型变量赋值。
int squareOfEight = square(8);
其中square()定义如下:
int square(int val) {
return val*val;
}
square()返回一个整型值,你可以用它来设置一个整型变量。如果它返回的是浮点型或是其他类型的结果,那就不可以用来设置整型变量了。再强调一次,函数返回值的类型很重要。
2.5.3 有关写函数的一些建议
给函数起个好名字,名字最好能指明函数的功能。上面例子里的square就是个好名字,指明了这个函数是用来做平方运算的。一般情况下,把函数命名为动词大有好处,因为这能从一开始就让你去考虑这个函数的功能,而此后回过头再次看这个函数的时候,函数名也能提醒你这个函数是做什么用的。
函数规模要适中。如果一个函数包含了两三百行的代码,那么我劝你还是把它分解为几个小函数。这样便于你把不同部分的代码用在别的地方,也便于定位问题。把一个大函数分解为几个小函数之后,原来两三百行代码的问题就可以精确定位为某几行代码的问题,从而能节省不少时间。
当你又要解决以前遇到过并解决好的问题时,就把以前使用过的代码包装成一个函数吧。例如你要经常调整图片尺寸,并把调好的图片存放到线上某个目录下,你就可以写一个函数来完成这些重复的操作:
resizeAndSave(int picHeight, int picWidth, String urlToSave)
这样看起来简洁、省事又方便调试。代码越短小,越容易查找内容和定位问题。
2.5.4 重载函数
函数声明之所以重要是因为两个原因。第一,函数声明告诉你向函数传递什么参数,函数对数据作一些什么处理,以及返回什么结果。第二,在编译器看来,函数声明以及它所接收的参数是独一无二的。就算两个函数拥有相同的名字,但如果其中一个接收的参数是2个字符串,而另一个接收3个字符串,则它们仍然是两个不同的函数。一系列名字相同但参数不同的函数,可以让你把同一个功能用在不同的场景。这种做法叫“重载”,同名函数拥有不同的参数。
我们拿动词“draw”(画)来打个比方。显然“drawing a picture”(画一幅画)和“drawing a card”(画一张卡片)是不一样的。编译器也是这样看函数的。我们大可放心地定义同名的函数,编译器能够根据传递参数的不同分辨出它们。例如我们可以让检测视频大小的函数接收整型或浮点型参数,编译器会把它看成两个独立的函数,一个接收整型参数,另一个接收浮点型参数,就算它们都是同一个名字。调用这个函数的时候,如果传入的参数是浮点型的,那么就是调用浮点型参数对应的那一个函数。
这里是Processing里重载函数的一个例子:
char multiplyByTwo(char value){
return char(int(value) * 2);
}
String multiplyByTwo(String value) {
return value+value;
}
int multiplyByTwo(int value){
return value * 2;
}
int[] multiplyByTwo(int value[]){
for(int i = 0; i<value.length; i++) {
value[i] *= 2;
}
return value;
}
这个函数接收整型、字符串、字符以及整型数组。哪个版本的函数被调用,取决于传入的参数的类型。
Processing里的可以这样做:
println(multiplyByTwo('z')); // 打印出
println(multiplyByTwo(5)); // 打印出10
println(multiplyByTwo("string")); // 打印出stringstring
int[] foo = {1, 2, 3, 4};
println(multiplyByTwo(foo)); //打印出2, 4, 6, 8
警告:重载函数是非常强大的工具,一种方法只需要经过轻微的改动(通过接收不同类型的参数)就能用在不同的场合。不过要注意,指定不同类型的参数,函数重载有效,例如:
int function(int i) {
}
int function(float f) {
}
int function(char c) {
}
而指定了不同类型的返回值,函数重载却未必总能凑效:
int function(int i) {
}
float function(float f) {
}
char function(char c) {
}
上面三个声明在Arduino和C++里都会报错,Processing则不会。一般不建议用这种写法,但如果你坚持要这样做,相信肯定有你的原因。