函数的定义、调用、执行

一、函数定义规则详解

1.1 函数定义的基本语法

在仓颉编程语言中,函数定义使用func关键字,基本语法格式为:

func 函数名(参数列表): 返回值类型 {
    函数体
}

各部分说明

  • func:函数定义的关键字,必须放在最前面
  • 函数名:合法的标识符,可以是任意有效的变量名
  • 参数列表:定义在圆括号内,多个参数用逗号分隔
  • 返回值类型:可选,定义函数返回的类型,冒号后声明
  • 函数体:定义在花括号内,包含函数执行的具体逻辑

示例

func add(a: Int64, b: Int64): Int64 {
    return a + b
}

这个例子定义了一个名为add的函数,接收两个Int64类型的参数a和b,返回类型为Int64,函数体将两个参数相加并返回结果。

1.2 无参函数的定义

无参函数是指不接收任何参数的函数,其参数列表为空。在仓颉中定义无参函数的格式为:

func 函数名(): 返回值类型 {
    函数体
}

示例 1:无参无返回值函数

func sayHello(): Unit {
    print("Hello, World!")
    print("Welcome to Cangjie Programming!")
}

示例 2:无参有返回值函数

func getPi(): Float64 {
    return 3.1415926535
}

特殊说明

  • 当函数没有返回值时,返回类型应声明为Unit
  • 函数体的最后一个表达式会被视为返回值,因此也可以省略return语句
  • 例如:
func add(a: Int64, b: Int64): Int64 {
    a + b  // 直接返回表达式结果
}

1.3 有参函数的定义

有参函数可以接收一个或多个参数,参数分为非命名参数命名参数两种类型。

1.3.1 非命名参数(位置参数)

非命名参数的定义方式为:参数名: 类型,调用时按位置传递参数。

定义示例

func multiply(a: Int64, b: Int64): Int64 {
    return a * b
}

调用示例

let result = multiply(3, 5)  // 3和5按位置传递给a和b
print(result)  // 输出:15
1.3.2 命名参数

命名参数在定义时需要在参数名后加!,调用时必须使用参数名: 值的形式。

定义示例

func divide(dividend!: Int64, divisor!: Int64): Float64 {
    return dividend.toFloat() / divisor.toFloat()
}

调用示例

let result = divide(dividend: 10, divisor: 3)  // 必须指定参数名
print(result)  // 输出:3.333333...

命名参数的优势

  • 提高代码可读性,明确参数含义
  • 调用时可以改变参数顺序
  • 例如:
let result = divide(divisor: 3, dividend: 10)  // 参数顺序可以调换
1.3.3 混合参数列表

在一个函数中可以同时包含非命名参数和命名参数,但必须遵循规则:非命名参数必须放在命名参数之前

正确示例

func calculate(a: Int64, b: Int64, operation!: String): Int64 {
    if operation == "add" {
        return a + b
    } else if operation == "subtract" {
        return a - b
    }
    return 0
}

调用示例

let result = calculate(5, 3, operation: "add")  // 前两个是位置参数,第三个是命名参数
print(result)  // 输出:8

1.4 默认参数的定义

仓颉语言支持为命名参数设置默认值,语法为:参数名!: 类型 = 默认值。

定义示例

func greet(name!: String = "Guest", age!: Int64 = 18): Unit {
    print(f"Hello, {name}! You are {age} years old.")
}

调用示例

greet()  // 使用默认参数:Hello, Guest! You are 18 years old.
greet(name: "Alice")  // 只指定name:Hello, Alice! You are 18 years old.
greet(age: 25)  // 只指定age:Hello, Guest! You are 25 years old.
greet("Bob", 30)  // 按顺序传递:Hello, Bob! You are 30 years old.
greet(name: "Charlie", age: 35)  // 明确指定参数名

注意事项

  • 只能为命名参数设置默认值,不能为非命名参数设置
  • 函数重载时,默认参数不会产生新的重载
  • 例如:
func add(a: Int64, b: Int64 = 10): Int64 {  // 错误,b是非命名参数
    return a + b
}

1.5 可变参数的定义

当函数需要接收不定数量的参数时,可以使用可变参数。在仓颉中,可变参数必须是函数的最后一个参数,且只能有一个。

定义语法

func 函数名(参数名: 类型...): 返回类型 {
    函数体
}

示例 1:计算任意个数的和

func sum(numbers: Int63...): Int64 {
    var total = 0
    for number in numbers {
        total += number
    }
    return total
}

调用示例

let result1 = sum(1, 2, 3, 4, 5)  // 传递5个参数
print(result1)  // 输出:15
let result2 = sum(10, 20, 30)    // 传递3个参数
print(result2)  // 输出:60
let numbers = [1, 2, 3, 4, 5]
let result3 = sum(numbers...)    // 传递数组,使用...展开
print(result3)  // 输出:15

示例 2:格式化日志信息

func log(message: String, args: Any...): Unit {
    let formattedArgs = args.map { it.toString() }.join(", ")
    print(f"[Log] {message}: {formattedArgs}")
}

调用示例

log("User login", "Alice", 12345, true)  // 输出:[Log] User login: Alice, 12345, true

1.6 函数重载

函数重载是指在同一作用域内定义多个同名函数,但参数列表必须不同(参数个数或类型不同)。

重载示例

func abs(x: Int64): Int64 {
    return x < 0 ? -x : x
}
func abs(x: Int32): Int32 {
    return x < 0 ? -x : x
}
func abs(x: Float64): Float64 {
    return x < 0 ? -x : x
}

调用示例

let a = abs(-10)      // 调用abs(Int64)
let b = abs(-3.14)    // 调用abs(Float64)
let c = abs(-123)     // 调用abs(Int64)

重载决议规则

  1. 编译器根据参数的个数和类型自动选择匹配的函数
  2. 优先选择作用域级别高的函数
  3. 对于泛型函数,需满足非泛型部分参数不同才能构成重载

1.7 函数返回值

1.7.1 单一返回值

函数可以返回一个值,使用return语句,语法为:return 表达式。

示例

func calculateArea(radius: Float64): Float64 {
    return 3.14159 * radius * radius
}
1.7.2 多返回值(元组)

仓颉支持函数返回多个值,通过元组(Tuple)实现。

定义示例

func minMax(array: Array<Int64>): (min: Int64, max: Int64) {
    var min = array[0]
    var max = array[0]
    
    for value in array {
        if value < min {
            min = value
        }
        if value > max {
            max = value
        }
    }
    
    return (min, max)  // 返回元组
}

调用示例

let numbers = [1, 3, 5, 2, 4]
let (min, max) = minMax(numbers)  // 解构元组
print(f"Min: {min}, Max: {max}")  // 输出:Min: 1, Max: 5
// 也可以只获取部分值
let (_, maxOnly) = minMax(numbers)  // 使用_忽略第一个值
print(f"Max only: {maxOnly}")      // 输出:Max only: 5
1.7.3 无返回值(Unit 类型)

当函数不需要返回值时,应声明返回类型为Unit。在这种情况下,return语句可以省略或使用return。

示例

func printInfo(name: String, age: Int64): Unit {
    print(f"Name: {name}")
    print(f"Age: {age}")
    // 可以省略return语句
}

1.8 函数参数的不可变性

在仓颉语言中,所有函数参数都是不可变的(immutable)。这意味着:

  1. 不能在函数体内对参数进行赋值
  2. 如果参数是值类型,也不能修改其成员

错误示例

func increment(x: Int64): Int64 {
    x = x + 1  // 错误:无法对参数x赋值
    return x
}
func modifyPoint(point: Point): Unit {
    point.x = 10  // 错误:无法修改值类型的成员
}

正确做法

func increment(x: Int64): Int64 {
    return x + 1  // 直接返回计算结果
}
func modifyPoint(point: Point): Point {
    return Point(x: 10, y: point.y)  // 返回新的Point对象
}

1.9 特殊函数:main 函数

在仓颉程序中,main函数是程序的入口,具有特殊地位:

定义要求

  • 必须定义在顶层作用域
  • 可以没有参数,也可以有Array<String>类型的参数
  • 返回值类型可以是整数类型或Unit类型

示例 1:无参数 main 函数

func main(): Int64 {
    print("Program started.")
    let result = add(3, 5)
    print(f"3 + 5 = {result}")
    print("Program ended.")
    return 0
}

示例 2:带参数 main 函数

func main(args: Array<String>): Unit {
    print("Arguments:")
    for arg in args {
        print(f"- {arg}")
    }
}

二、函数调用技巧

2.1 基础调用方式

函数调用的基本语法为:函数名(参数列表)。根据函数参数类型的不同,调用方式分为以下几种:

2.1.1 非命名参数调用

调用非命名参数函数时,直接按顺序传递参数值。

示例

func add(a: Int64, b: Int64): Int64 {
    return a + b
}
let result = add(3, 5)  // 3传递给a,5传递给b
print(result)  // 输出:8
2.1.2 命名参数调用

调用命名参数函数时,必须使用参数名: 值的形式。

示例

func greet(name!: String, age!: Int64): Unit {
    print(f"Hello, {name}! You are {age} years old.")
}
greet(name: "Alice", age: 25)  // 必须指定参数名

命名参数的优势

  • 参数顺序可以改变:
greet(age: 25, name: "Alice")  // 顺序不影响结果
  • 提高代码可读性,明确参数含义
2.1.3 混合调用

当函数同时包含非命名参数和命名参数时,可以混合使用两种调用方式。但需要注意:非命名参数必须按照定义顺序传递,命名参数可以任意顺序。

示例

func calculate(a: Int64, b: Int64, operation!: String): Int64 {
    if operation == "add" {
        return a + b
    } else if operation == "subtract" {
        return a - b
    }
    return 0
}
// 混合调用示例
let result1 = calculate(5, 3, operation: "add")  // 前两个是位置参数,第三个是命名参数
let result2 = calculate(10, b: 4, operation: "subtract")  // 第一个是位置参数,后两个是命名参数

2.2 函数作为参数(高阶函数)

在仓颉语言中,函数是 "一等公民",可以像普通值一样作为参数传递给其他函数。

2.2.1 函数类型

函数类型的表示方式为:(参数类型1, 参数类型2, ...) -> 返回类型。

示例

// 定义一个函数类型别名
type MathOperation = (Int64, Int64) -> Int64
// 或者直接使用函数类型
func applyOperation(f: (Int64, Int64) -> Int64, a: Int64, b: Int64): Int64 {
    return f(a, b)
}
2.2.2 函数作为参数的应用

示例 1:通用计算函数

func calculate(a: Int64, b: Int64, operation: (Int64, Int64) -> Int64): Int64 {
    return operation(a, b)
}
// 定义具体的运算函数
func add(a: Int64, b: Int64): Int64 { return a + b }
func subtract(a: Int64, b: Int64): Int64 { return a - b }
func multiply(a: Int64, b: Int64): Int64 { return a * b }
// 调用示例
let result1 = calculate(5, 3, add)        // 加法运算
let result2 = calculate(10, 4, subtract)  // 减法运算
let result3 = calculate(6, 2, multiply)   // 乘法运算

示例 2:数组映射(map)函数

func map(array: Array<Int64>, transform: (Int64) -> Int64): Array<Int64> {
    var result = Array<Int64>()
    for element in array {
        result.append(transform(element))
    }
    return result
}
// 使用示例
let numbers = [1, 2, 3, 4, 5]
// 计算平方
let squares = map(numbers, { x in x * x })
print(squares)  // 输出:[1, 4, 9, 16, 25]
// 计算立方
let cubes = map(numbers, { x in x * x * x })
print(cubes)    // 输出:[1, 8, 27, 64, 125]
// 翻倍
let doubled = map(numbers, { x in x * 2 })
print(doubled)  // 输出:[2, 4, 6, 8, 10]
2.2.3 匿名函数(Lambda 表达式)

在传递函数参数时,可以直接使用匿名函数(Lambda 表达式),使代码更加简洁。

语法格式

{ 参数列表 in 表达式 }

示例

// 计算1到10的和
let sum = reduce(1..10, 0, { acc, x in acc + x })
print(sum)  // 输出:55
// 找出数组中的偶数
let numbers = [1, 2, 3, 4, 5, 6]
let evens = filter(numbers, { x in x % 2 == 0 })
print(evens)  // 输出:[2, 4, 6]

2.3 函数作为返回值

函数不仅可以作为参数,还可以作为返回值。

示例 1:创建计数器函数

func createCounter(initialValue: Int64): () -> Int64 {
    var count = initialValue
    return { 
        count += 1
        return count
    }
}
// 使用示例
let counter = createCounter(0)  // 创建一个从0开始的计数器
print(counter())  // 输出:1
print(counter())  // 输出:2
print(counter())  // 输出:3

示例 2:根据条件返回不同的运算函数

func getOperation(operator: String): (Int64, Int64) -> Int64 {
    if operator == "+" {
        return { a, b in a + b }
    } else if operator == "-" {
        return { a, b in a - b }
    } else if operator == "*" {
        return { a, b in a * b }
    } else if operator == "/" {
        return { a, b in a / b }
    }
    return { a, b in 0 }  // 默认返回0
}
// 使用示例
let addFunc = getOperation("+")
print(addFunc(5, 3))  // 输出:8
let multiplyFunc = getOperation("*")
print(multiplyFunc(4, 6))  // 输出:24

2.4 闭包的应用

闭包是指函数能够捕获并记住其定义时的外部变量,即使在脱离原作用域后仍能访问这些变量。

2.3.1 闭包的基本概念

示例

func outer(): () -> Int64 {
    let x = 10  // 外部变量
    return {  // 内部函数(闭包)
        return x + 5  // 访问外部变量x
    }
}
let closure = outer()  // closure捕获了x=10
print(closure())  // 输出:15

在这个例子中,outer函数返回的匿名函数形成了一个闭包,它捕获并记住了x=10的值。

2.3.2 闭包的变量捕获规则

仓颉语言对闭包的变量捕获有以下重要规则:

  1. 捕获的变量必须在闭包定义时已经声明
  2. 捕获var声明的可变变量时,闭包只能被调用,不能作为一等公民使用(不能赋值给变量、不能作为实参或返回值等)

错误示例

func createCounter(): () -> Int64 {
    var count = 0
    return {  // 错误:捕获了var声明的可变变量
        count += 1
        return count
    }
}

正确做法

func createCounter(): (() -> Int64, () -> Unit) {
    var count = 0
    
    let increment = {  // 闭包1:递增计数器
        count += 1
        return count
    }
    
    let reset = {  // 闭包2:重置计数器
        count = 0
    }
    
    return (increment, reset)  // 返回两个函数
}
// 使用示例
let (increment, reset) = createCounter()
print(increment())  // 输出:1
print(increment())  // 输出:2
reset()
print(increment())  // 输出:1
2.3.3 闭包的实际应用

示例 1:缓存计算结果

func memoize(f: (Int64) -> Int64): (Int64) -> Int64 {
    var cache = Dictionary<Int64, Int64>()
    
    return { x in
        if let result = cache[x] {
            print(f"Using cached result for {x}")
            return result
        }
        
        let result = f(x)
        cache[x] = result
        print(f"Calculated and cached result for {x}")
        return result
    }
}
// 定义一个耗时的计算函数
func expensiveCalculation(x: Int64): Int64 {
    // 模拟耗时操作
    sleep(1 * Duration.Second)
    return x * x * x
}
// 创建记忆化版本
let memoizedCalculation = memoize(expensiveCalculation)
// 第一次调用,计算并缓存
print(memoizedCalculation(5))  // 输出:125
// 第二次调用,直接使用缓存
print(memoizedCalculation(5))  // 输出:125

示例 2:事件处理器

func createButtonClickListener(onClick: () -> Unit): () -> Unit {
    var isClicked = false
    
    return {
        if !isClicked {
            isClicked = true
            onClick()
        }
    }
}
// 使用示例
let clickHandler = createButtonClickListener {
    print("Button clicked!")
}
clickHandler()  // 输出:Button clicked!
clickHandler()  // 无输出(防止重复点击)

2.5 尾随 Lambda 语法

当函数的最后一个参数是函数类型时,可以将 Lambda 表达式移到函数调用括号外面,形成尾随 Lambda 语法。

普通调用方式

filter(numbers, { x in x % 2 == 0 })

尾随 Lambda 调用方式

filter(numbers) { x in x % 2 == 0 }

更简洁的形式:当函数只有一个参数且为函数类型时,可以省略括号:

runLater {
    print("This will run later.")
}

示例应用

// 定义一个条件执行函数
func unless(condition: Bool, action: () -> Unit) {
    if !condition {
        action()
    }
}
// 使用尾随Lambda
unless(5 > 10) {
    print("5 is not greater than 10")
}

2.6 管道操作符

仓颉引入了管道操作符|>,用于简化嵌套函数调用,更直观地表达数据流向。

语法格式

值 |> 函数1 |> 函数2 |> ...

示例 1:计算流程

func double(x: Int64): Int64 { return x * 2 }
func increment(x: Int64): Int64 { return x + 1 }
// 传统方式
let result1 = double(increment(double(double(5))))  // 42
// 管道方式
let result2 = 5 |> double |> double |> increment |> double
print(result2)  // 输出:42

示例 2:数据处理流程

let numbers = [1, 2, 3, 4, 5]
// 传统方式
let processed1 = map(filter(numbers, { x in x % 2 == 0 }), { x in x * x })
// 管道方式
let processed2 = numbers 
    |> filter({ x in x % 2 == 0 })
    |> map({ x in x * x })
print(processed2)  // 输出:[4, 16]

三、函数执行原理

3.1 函数调用栈机制

当函数被调用时,系统会在内存中创建一个栈帧(Stack Frame)来管理函数的执行。栈帧中包含以下信息:

  1. 返回地址:函数执行完毕后应返回的代码位置
  2. 调用者的栈帧基址:用于恢复调用者的栈环境
  3. 函数参数:传递给函数的参数值
  4. 局部变量:函数内部定义的变量
  5. 临时数据:表达式计算的中间结果

示例:函数调用过程

func main(): Int64 {
    let a = 10
    let b = 20
    let c = add(a, b)  // 调用add函数
    print(c)
    return 0
}
func add(x: Int64, y: Int64): Int64 {
    return x + y
}

当main函数调用add函数时,栈的变化过程如下:

  1. main函数正在执行,栈中保存了a=10、b=20等局部变量
  2. 执行add(a, b)时,将a和b的值压入栈
  3. 将返回地址(add函数调用后的下一条指令)压入栈
  4. 跳转到add函数执行
  5. add函数创建自己的栈帧,读取参数x=10、y=20
  6. 执行加法运算,返回结果
  7. 恢复main函数的栈帧,从返回地址继续执行

3.2 作用域与变量生命周期

在仓颉语言中,变量的作用域由花括号{}定义。函数体、条件语句、循环语句等都会创建新的作用域。

3.2.1 作用域规则
  1. 全局作用域:定义在所有函数外部的变量
  2. 函数作用域:函数体内定义的变量
  3. 块作用域:if、for、while等代码块内定义的变量

示例

let globalVar = 100  // 全局变量
func outer(): Unit {
    let outerVar = 200  // outer函数作用域
    
    if true {
        let blockVar = 300  // 块作用域
        print(globalVar)  // 可以访问
        print(outerVar)   // 可以访问
        print(blockVar)   // 可以访问
    }
    
    // print(blockVar)  // 错误:blockVar超出作用域
}
3.2.2 变量遮蔽

当内部作用域定义了与外部作用域同名的变量时,内部变量会遮蔽外部变量。

示例

let x = 10  // 全局变量
func f(): Unit {
    let x = 20  // 遮蔽全局变量x
    print(x)  // 输出:20
}
func g(): Unit {
    print(x)  // 输出:10(访问全局变量)
}

3.3 递归函数的执行过程

递归函数是指在函数内部调用自身的函数。

3.3.1 递归的基本要素
  1. 递归终止条件:避免无限递归
  2. 递归调用:调用自身,但参数逐渐接近终止条件
  3. 递归处理:将问题分解为更小的子问题

示例:计算阶乘

func factorial(n: Int64): Int64 {
    if n == 0 {  // 终止条件
        return 1
    } else {
        return n * factorial(n - 1)  // 递归调用
    }
}
3.3.2 递归执行过程分析

以factorial(3)为例,执行过程如下:

  1. 调用factorial(3)
  2. 由于3 != 0,执行3 * factorial(2)
  3. 调用factorial(2)
  4. 由于2 != 0,执行2 * factorial(1)
  5. 调用factorial(1)
  6. 由于1 != 0,执行1 * factorial(0)
  7. 调用factorial(0)
  8. 由于0 == 0,返回 1
  9. 返回1 * 1 = 1(回到factorial(1))
  10. 返回2 * 1 = 2(回到factorial(2))
  11. 返回3 * 2 = 6(回到factorial(3))
3.3.3 尾递归优化

尾递归是指递归调用是函数的最后一条语句。现代编译器可以对尾递归进行优化,避免栈溢出。

示例:尾递归实现阶乘

func factorialTailRecursive(n: Int64, accumulator: Int64 = 1): Int64 {
    if n == 0 {
        return accumulator
    } else {
        return factorialTailRecursive(n - 1, n * accumulator)  // 尾递归调用
    }
}

调用示例

let result = factorialTailRecursive(5)  // 计算5! = 120

3.4 函数调用的性能开销

函数调用会带来以下开销:

  1. 栈操作开销:创建和销毁栈帧
  2. 参数传递开销:复制参数值
  3. 指令跳转开销:CPU 缓存失效
  4. 返回开销:恢复调用者环境

在竞赛中,了解这些开销有助于优化代码:

优化建议

  1. 对于频繁调用的简单函数,可以考虑内联
  2. 使用值类型传递小型数据
  3. 避免在循环中进行复杂的函数调用
  4. 使用尾递归优化替代普通递归

3.5 内存管理与垃圾回收

仓颉语言使用自动垃圾回收机制,开发者不需要手动管理内存。但理解内存管理机制对优化程序很重要。

3.5.1 栈内存与堆内存
  1. 栈内存
    • 自动分配和释放
    • 存储函数参数、局部变量
    • 速度快,但容量有限
  1. 堆内存
    • 需要手动分配(通过new等操作)
    • 存储动态创建的对象
    • 容量大,但分配速度慢
3.5.2 引用计数与垃圾回收

仓颉使用引用计数机制管理堆内存:

  • 每个对象都有一个引用计数
  • 当引用计数为 0 时,对象被自动回收
  • 循环引用会导致内存泄漏

示例

class Node {
    var next: Node?
}
func createCycle(): Unit {
    let a = Node()
    let b = Node()
    a.next = b
    b.next = a  // 循环引用,导致内存泄漏
}

3.6 并发执行模型

仓颉语言主打 "轻松并发",支持协程和通道等并发机制。

3.6.1 协程(Coroutine)

协程是一种轻量级的线程,可以在函数内部暂停和恢复执行。

示例

func asyncPrint(): Unit {
    go {  // 在新协程中执行
        sleep(1 * Duration.Second)
        print("Printed after 1 second")
    }
}
func main(): Unit {
    asyncPrint()
    print("Main function continues")
    sleep(2 * Duration.Second)  // 等待协程完成
}
3.6.2 通道(Channel)

通道用于协程间的通信和同步。

示例

func producer(channel: Channel<Int64>): Unit {
    for i in 0..<5 {
        channel.send(i)
        sleep(1 * Duration.Second)
    }
    channel.close()
}
func consumer(channel: Channel<Int64>): Unit {
    for item in channel {
        print(f"Received: {item}")
    }
}
func main(): Unit {
    let channel = Channel<Int64>()
    go producer(channel)
    go consumer(channel)
    sleep(6 * Duration.Second)
}

3.7 错误处理机制

在函数执行过程中,可能会发生各种错误。仓颉提供了多种错误处理方式:

3.7.1 错误码返回

简单的错误可以通过返回错误码或Result类型处理。

示例

enum ErrorCode {
    Success,
    DivideByZero,
    InvalidInput
}
func safeDivide(a: Int64, b: Int64, result: inout Int64): ErrorCode {
    if b == 0 {
        return .DivideByZero
    }
    result = a / b
    return .Success
}
// 使用示例
var result = 0
let code = safeDivide(10, 3, &result)
if code == .Success {
    print(result)
} else {
    print("Error occurred")
}
3.7.2 异常处理

对于更复杂的错误,可以使用异常处理机制。

示例

func divide(a: Int64, b: Int64): Int64 {
    if b == 0 {
        throw "Division by zero"
    }
    return a / b
}
func main(): Unit {
    try {
        let result = divide(10, 0)
        print(result)
    } catch let error {
        print(f"Error: {error}")
    }
}
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐