Move语言 函数

Move语言中代码的执行是通过调用函数实现的。

Move语言函数以 fun 关键字开头,后跟函数名称、扩在括号中的参数,以及扩在花括号中的函数体。

fun function_name(arg1: u64, arg2: bool): u64 {
    // function body
}

Move函数使用 snake_case 命名规则,也就是小写字母以及下划线作为单词分隔符。

脚本中的函数

脚本块只能包含一个被视为 main 的函数。它作为交易被执行,可以有参数,但是没有返回值。它可以操作其它已经发布的模块中的函数。

这里有一个简单的例子,用来检查地址是否存在:

script {
    use 0x1::Account;

    fun main(addr: address) {
        assert(Account::exists(addr), 1);
    }
}

脚本中的函数可以带有参数,本例中它是 address 类型的参数 addr。函数中操作了导入的模块 Account。

注意:由于只有一个函数,因此你可以按任意方式对它命名。一般情况下我们遵循惯用的编程概念将其称为 main。

模块中的函数

脚本中能使用的函数功能是相对有限的,函数的全部潜能只有在模块中才能展现。

让我们再看一遍什么是模块:模块是一组函数和结构体,它可以封装一项或多项功能。

这部分内容中,我们将创建一个简单的 Math 模块,它将为用户提供一组基本的数学函数和一些辅助方法。

当然这里面大部分操作无需使用模块即可完成,但我们的目标是通过这个例子来理解函数。

module Math {
    fun zero(): u8 {
        0
    }
}

第一步:我们定义一个 Math 模块,它有一个函数:zero(),该函数返回 u8 类型的值 0。

还记得我们之前介绍过的表达式吗?0 之后没有分号,因为它是函数的返回值。是的,就像块表达式一样,函数与块非常相似。

函数参数

关于参数其实大家都已经很清楚了,但是我们还是稍微啰嗦一下,函数可以根据需要接受任意多个参数(传递给函数的值)。

就像 Move 中的其他任何变量一样,每个参数都有两个属性:参数名,也就是参数在函数体内的名称,以及参数类型。

像作用域中定义的任何其他变量一样,函数参数仅存在于函数体内。当函数块结束时,参数也会消亡。

module Math {
    public fun sum(a: u64, b: u64): u64 {
        a + b
    }

    fun zero(): u8 {
        0
    }
}

大家发现有什么不一样了么?Math 模块新增了 sum(a,b) 函数,该函数将两个 u64 值相加并作为 u64 结果返回。

关于参数的一些语法规则:

  • 参数必须具有类型,并且必须用逗号分隔
  • 函数返回值放在括号后,并且必须在冒号后面

下面我们如何在脚本中使用此函数呢?通过"导入"!

script {
    use 0x1::Math;  // used 0x1 here; could be your address
    use 0x1::Debug; // this one will be covered later!

    fun main(first_num: u64, second_num: u64) {

        // variables names don't have to match the function's ones
        let sum = Math::sum(first_num, second_num);

        Debug::print<u64>(&sum);
    }
}

关键字 return

关键字 return 允许函数结束执行并返回结果。它可以与 if 条件一起使用,这样可以根据条件返回不同结果。

module M {
    public fun conditional_return(a: u8): bool {
        if (a == 10) {
            return true // semi is not put!
        };

        if (a < 10) {
            true
        } else {
            false
        }
    }
}

多个返回值

在前面的示例中,我们尝试了没有返回值或返回单个值的函数。但是,要想返回任何类型的多个值应该怎么办呢?

要指定多个返回值,需要使用括号:

module Math {
    // ...

    public fun max(a: u8, b: u8): (u8, bool) {
        if (a > b) {
            (a, false)
        } else if (a < b) {
            (b, false)
        } else {
            (a, true)
        }
    }
}

该函数有两个参数:a 和 b,并返回两个值:第一个是两个输入参数中的较大的值,第二个是布尔类型,表示输入的参数是否相等。请仔细看一下语法,我们没有指定单个返回值,而是添加了括号并在其中列出了返回值类型。

现在让我们看看如何在另一个脚本中使用该函数的返回值。

script {
    use 0x1::Debug;
    use 0x1::Math;

    fun main(a: u8, b: u8)  {
        let (max, is_equal) = Math::max(99, 100);

        assert(is_equal, 1)

        Debug::print<u8>(&max);
    }
}

上面例子中,我们解构了一个二元组,用函数 max 的返回值创建了两个新变量。返回值的顺序保持不变,变量 max 用来存储 u8 类型的最大值,而 is_equal 用来存储 bool 类型。

返回值数量并没有限制,你可以根据需要决定元组的元素个数。下一章,我们还会介绍返回复杂数据的另一种方法,那就是结构体。

函数可见性

定义模块时,你可能希望其他开发人员可以访问某些函数,而某些函数则保持隐藏状态。这正是函数可见性修饰符发挥作用的时候。

默认情况下,模块中定义的每个函数都是私有的,无法在其它模块或脚本中访问。可能你已经注意到了,我们在 Math 模块中定义的某些函数前有关键字 public:

module Math {
    public fun sum(a: u64, b: u64): u64 {
        a + b
    }

    fun zero(): u8 {
        0
    }
}

例子中Math模块被其它模块导入后,sum() 函数可以从外部访问,但是 zero() 不能被访问,因为默认情况下它是私有的。

关键字 public 将更改函数的默认可见性并使其公开,即可以从外部访问。

基本上,如果不将 sum() 函数设为 public,从外部访问是不可能的:

script {
    use 0x1::Math;

    fun main() {
        Math::sum(10, 100); // won't compile!
    }
}

访问私有函数

如果根本无法访问,那么私有函数就没有任何意义了。调用 public 函数的同时,可以用私有函数来执行一些内部工作。

私有函数只能在定义它们的模块中访问。

那么如何访问同一模块中的函数?通过像导入一样简单地调用此函数!

module Math {
    public fun is_zero(a: u8): bool {
        a == zero()
    }

    fun zero(): u8 {
        0
    }
}

一个模块中定义的任何函数都可以被同一模块中的任何函数访问,无论它们的可见性修饰符是什么。这样,私有函数仍然可以在内部调用,而且不会暴露某些私有操作到模块外。

本地方法

有一种特殊的函数叫做"本地方法"。本地方法实现的功能超出了 Move 的能力,它可以提供了额外的功能。本地方法由 VM 本身定义,并且在不同的VM实现中可能会有所不同。这意味着它们没有用 Move 语法实现,没有函数体,直接以分号结尾。关键字 native 用于标记本地函数,它和函数可见性修饰符不冲突,native 和 public 可以同时使用。

这是 Diem 标准库中的示例。

module Signer {
    native public fun borrow_address(s: &signer): &address;

    // ... some other functions ...
}

通过控制流表达式,我们可以选择运行某个代码块,或者跳过某段代码而运行另一个代码块。if 表达式允许我们在条件为真时运行代码块,在条件为假时运行另一个代码块。script { use 0x1::Debug; ...