Mojo 学习笔记(六)

在(三)中已经提到了,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 ]
标签: 技术 Mojo