在(三)中已经提到了,Mojo 里面的函数有 def 和 fn 两种,今天进一步说一说关于函数参数的细节问题。
参数类型
def 不需要定义参数类型和返回值类型,但如果想定义也是可以的。当不定义参数类型,其实都是被当成一个 object 类型来传递的。object 类型可以让函数接受任意类型的值,Mojo会在运行的时候进行推导,得到实际的类型。不定义返回值类型时,其实也是返回了一个 object 类型的值。
fn 则必须定义参数类型和返回值类型,因此它可以提供严格的类型检查和提高内存安全性。从调用者角度,def 和 fn 能完成的功能都是一样的,没有什么 def 可以做的事情是 fn 做不到的,反之亦然。通过强制进行这些类型检查, fn 可以帮助开发者避免很多运行时错误。同时,由于不需要在运行时刻来检查实际的类型是什么(所有变量的类型在编译的时候就已经决定了),因而降低了很多开销,对比使用动态类型的 def,性能也会有相应的提升。
参数传递方式
那么函数的参数是以什么方式传递的呢,值或者引用?这可能是学习过其它编程语言的开发者一上来就会想到的问题。让我们看看其他语言中的情况:
- C++ 中,默认情况是按值传递的,也就是说,当函数被调用时,实参的值会被复制到形参中,因此函数内部无法改变实参的值,如果要按照引用传递,则需要显式使用引用类型(&),此时就可以改变实参的值了。但如果只是为了减少复制的消耗而传递引用,并不希望修改实参的值,可以使用const 修饰。当然 C/C++中还有万恶的指针,也可以使用指针来进行参数的传递,此时和引用是类似的。
- python中,万物皆对象,函数参数统一按照对象引用传递。对于可变对象,函数内部可以修改实参的值,而对于不可变对象(如数值、字符串、元组),函数内部无法改变实参的值,这使得在外面看起来也像是值传递;
- Java中,也是万物皆对象,但函数参数是按照值的方式来传递对象的引用。对于基本数据类型,传递的是实参的值,而对于其它对象类型,传递的是实参引用的拷贝,因此可以在函数中改变实参所指对象的状态,但不能修改实参对象引用。
- C# 中,函数参数默认也是按值传递的,如果要按照引用传递,则需要在形参声明时使用 ref 或者 out 关键字修饰。
- Rust中,基本数据类型以及实现了Copy trait的类型是按照值传递的,如果需要使用引用传递来避免数据拷贝提高性能,则需要指明(&T, &mut T)。但对于实现了 Move trait的类型,如果直接传给函数,会发生所有权转移,此时函数内部拥有该值的使用权,调用者不能再使用改值。
好吧,有够乱的,那么Mojo里面是什么样的呢?
Mojo 允许开发者指定每个参数是如何传递的,包括如下几种方式:
- 按照值传递(使用 owned 关键字),此时函数就拿到了这个参数的所有权,
- 按照不可变引用传递(使用borrowed 关键字),此时函数可以读到这个参数的值,但不能够修改它
- 按照可变引用传递(使用 inout 关键字),此时函数既可以读到这个参数的值,也可以修改它
当不指定这些关键词的时候:
- 在 def 中,参数缺省是按值传递的(也就是缺省是 owned)。但有一个例外,当参数没有指定类型时,也即参数是 object 类型的时候,那么它缺省是按照引用传递,这是为了实现和 Python 的兼容性。
- 在 fn 中,参数缺省是按不可变的引用传递的(也就是缺省是 borrowed)。
一些例子
def 中在指定类型的情况下,缺省按值传递(owned):
def test(a: Int):
a = 1
print("in test, a:", a)
def main():
x = 10
print("before test, x:", x)
test(x)
print("after test, x:", x)
输出如下,可以看到 main 中的 x 变量没有被修改,仍然是10:
before test, x: 10
in test, a: 1
after test, x: 10
fn 中缺省按不可变引用传递(borrowed),将上面的函数只把 def 改为 fn:
fn test(a: Int):
a = 1
print("in test, a:", a)
编译就会报错,因为不可变引用参数只能读取不能修改:
error: expression must be mutable in assignment
a = 1
^
可以指定 owned 关键字,表示按值传递:
fn test(owned a: Int):
a = 1
print("in test, a:", a)
这样就和 def 缺省时一样,输出也一样。此时参数可以在函数内部被修改,但不会影响到调用者传入的实参。
但如果指定 inout 关键字,表示按照可变引用传递:
fn test(inout a: Int):
a = 1
print("in test, a:", a)
则会输出:
before test, x: 10
in test, a: 1
after test, x: 1
x 的值在 test 函数中被修改了。
再用List来一个长一些的完整的例子:
from collections import List
fn print_list(prompt:String, a : List[Int]):
print(prompt, '[', end='')
for i in a:
print(i[], end='')
print(' ', end='')
print(']')
fn test_owned(owned a: List[Int]):
# a 是一个值,是一份拷贝,可以修改,但不会改变传入的实参
a.append(10)
print_list("in test owned, a:", a)
# 这个也是 fn 的缺省方式,不定义任何传递方式时,就是 borrowed
fn test_borrowed(borrowed a: List[Int]):
# a 是一个不可变引用,因此是只读但不可修改的
# invalid call to 'append': invalid use of mutating method on rvalue of type 'List[Int]'
# a.append(10)
var b = a
b.append(10)
print_list("in test borrowed, a:", a)
print_list("in test borrowed, b:", b)
fn test_inout(inout a: List[Int]):
# a 是一个可变引用,因此会改变传入的实参
a.append(10)
print_list("in test inout, a:", a)
def main():
x = List(1, 2, 3)
print_list("before test owned, x:", x)
test_owned(x)
print_list("after test owned, x:", x)
print("-----------------------")
y = List(1, 2, 3)
print_list("before test borrowed, y:", y)
test_borrowed(y)
print_list("after test borrowed, y:", y)
print("-----------------------")
z = List(1, 2, 3)
print_list("before test inout, z:", z)
test_inout(z)
print_list("after test inout, z:", z)
会输出:
before test owned, x: [1 2 3 ]
in test owned, a: [1 2 3 10 ]
after test owned, x: [1 2 3 ]
-----------------------
before test borrowed, y: [1 2 3 ]
in test borrowed, a: [1 2 3 ]
in test borrowed, b: [1 2 3 10 ]
after test borrowed, y: [1 2 3 ]
-----------------------
before test inout, z: [1 2 3 ]
in test inout, a: [1 2 3 10 ]
after test inout, z: [1 2 3 10 ]