華為倉頡語言的函數初步
函數是一段完成特定任務的獨立代碼片段,可以通過函數名字來標識,這個名字可以被用來調用函數。
要特別注意,與C/C++、Python等語言不同,倉頡禁止參數重新賦值——函數參數均為不可變(immutable)變量,在函數定義內不能對其賦值。也就是說,倉頡函數參數與 let 聲明的變量一樣,只能讀取,不能再次賦值。例如:
func demo(x: Int64): Int64 {
x = x + 1 ? // ? 編譯錯誤
return x
}
如果需要修改,只能引入新的局部變量:
func demo(x: Int64): Int64 {
let y = x + 1 ? // ? 正確
return y
}
這說明,倉頡語言語言設計決策強化對不可變性和函數式編程范式的偏好。
函數定義
基本語法:倉頡語言使用func關鍵字來定義函數,后跟函數名、參數列表、可選的返回值類型以及函數體。其基本語法為:
func 函數名(參數列表): 返回值類型 {
??? // 函數體
??? return 返回值
}
參數列表:函數可以有 0 個或多個參數,參數分為非命名參數和命名參數。非命名參數定義為p: T,如a: Int64;命名參數定義為p!: T,如a!: Int64,且只能為命名參數設置默認值,非命名參數不能設置默認值,參數列表中非命名參數只能定義在命名參數之前。
返回值類型:必須顯式寫出返回類型,除非返回Unit(可以省略)。
函數體定義在一對花括號內。函數體中定義了函數被調用時執行的操作,通常包含一系列的變量定義和表達式,也可以包含新的函數定義(即嵌套函數)。
函數定義示例
func add(a: Int64, b: Int64): Int64 {
return a + b
}
如果函數體是單個表達式,倉頡也支持隱式返回(省略 return 關鍵字):
func add(a: Int64, b: Int64): Int64 {
a + b ?// 最后一行表達式的值即為返回值
}
函數參數示例
非命名參數 (Non-named Parameters): 即普通參數,調用時按順序傳遞。非命名參數必須定義在命名參數之前。例如:
// 定義一個包含兩個非命名參數的函數
func multiply(a: Int64, b: Int64): Int64 {return a * b
}main() {// 調用函數(必須按順序傳遞參數,無需指定參數名)let result = multiply(3, 4) // 正確調用,返回12println(result)
}
命名參數 (Named Parameters): 在參數名后加 !,調用時需指定參數名,并可提供默認值,提高了代碼可讀性和靈活性。例如:
// 定義包含命名參數的函數(帶默認值)
func calculateTotal(price: Int64, quantity!: Int64 = 1, discount!: Float64 = 0.0): Int64 {
let total = Float64(price * quantity) ? ? ?// 先轉成 Float64
let discounted = total * (1.0 - discount) ? ?// 浮點運算
return Int64(discounted) ? ? ? ? ? ? ? ? ? ? // 再轉回整數
}
注意:
只能為命名參數設置默認值,不能為非命名參數設置默認值。
參數列表中可以同時定義非命名參數和命名參數,但是需要注意的是,非命名參數只能定義在命名參數之前,也就意味著命名參數之后不能再出現非命名參數。例如,下例中 add 函數的參數列表定義是不合法的:
func add(a!: Int64, b: Int64): Int64 { // 錯誤!
return a + b
}
調用函數規則
a.非命名參數調用
調用規則:必須按定義順序傳遞,不能指定參數名。
b.命名參數調用
必須用 參數名: 值 形式傳遞,順序可任意調整,且支持使用默認值。
c.混合參數調用(非命名 + 命名)
非命名參數必須先定義、先傳遞,命名參數在后,且命名參數必須指定參數名。
示例:
// 定義包含命名參數的函數(帶默認值)
func calculateTotal(price: Int64, quantity!: Int64 = 1, discount!: Float64 = 0.0): Int64 {let total = Float64(price * quantity) // 先轉成 Float64let discounted = total * (1.0 - discount) // 浮點運算return Int64(discounted) // 再轉回整數
}main() {// 調用方式1:只傳非命名參數(price),命名參數用默認值let total1 = calculateTotal(100) // quantity默認1,discount默認0 → 結果100println(total1)// 調用方式2:指定部分命名參數(可調整順序)let total2 = calculateTotal(100, discount: 0.2, quantity: 2) // 計算:100 * 2 * (1-0.2) = 160 → 結果160println(total2)// 調用方式3:顯式指定所有參數名let total3 = calculateTotal(200, quantity: 3, discount: 0.1) // 計算:200 * 3 * 0.9 = 540 → 結果540println(total3)
}
返回值類型的說明
在 倉頡語言(Cangjie) 的正式規范中:
必須顯式寫出返回類型,當返回類型為 Unit 時可省略 -> Unit。
// ? 合法
func add(a: Int64, b: Int64): Int64 { a + b }
// ? 不合法- 函數體中有返回值的表達式時必須聲明返回類型
func add(a: Int64, b: Int64) { a + b }??
如果函數 沒有有意義的返回值(即返回 Unit),可以寫成 : Unit,也可以 直接省略 返回類型部分。
// 等價寫法
func log(msg: String) { println(msg) }??????? // 省略 : Unit
func log(msg: String): Unit { println(msg) }? // 顯式 : Unit
log("你好") //調用
單值返回和多值返回(Tuple)
單值返回示例:
// 返回單個 Int64
func square(n: Int64): Int64 {return n * n
}main(): Unit {let s = square(7) println("square = ${s}") //square = 49
}
多值返回(Tuple)示例:
// 返回一個二元組 (Int64, Int64)
func divmod(a: Int64, b: Int64): (Int64, Int64) {return (a / b, a % b)
}main(): Unit {let (q, r) = divmod(10, 3) // q = 3, r = 1println("quotient = ${q}, remainder = ${r}") //quotient = 3, remainder = 1
}
函數類型(Function Type)
倉頡編程語言中,函數是一等公民(first-class citizens),可以作為函數的參數或返回值,也可以賦值給變量。因此函數本身也有類型,稱之為函數類型。
函數類型由函數的參數類型和返回類型組成,參數類型和返回類型之間使用 -> 連接。參數類型使用圓括號 () 括起來,可以有 0 個或多個參數,如果參數超過一個,參數類型之間使用逗號(,)分隔。
函數類型是編程中一個比較抽象但極其強大的概念,是函數式編程范式(FP)的核心基石之一。
函數類型的語法非常直觀,遵循以下格式:
(參數1類型, 參數2類型, ...) -> 返回值類型
解釋一下:
? ?? 括號 ():里面放置函數的參數類型列表。如果沒有參數,就空著 ()。
? ?? 箭頭 ->:連接參數和返回值,讀作“返回”。
? ?? 返回值類型:在箭頭后面,指定函數返回的數據類型。
例如:
沒參數也要空括號:?() -> Unit
多個參數逗號隔:?(Int, String) -> Bool
返回元組括號包:?(Int, Int) -> (Int, Int)
函數類型既可以根據函數定義隱式存在,也可以由程序員在代碼中顯式地書寫出來。
1. 隱式的函數類型 (由函數定義產生)
例子:
// 【函數定義】
// 程序員寫的是具體的實現
func add(a: Int64, b: Int64): Int64 {
??? return a + b
}
// 【隱式的函數類型】
// 編譯器會自動識別出這個函數有一個類型:(Int64, Int64) -> Int64
// 這個類型是“依據函數定義存在的”,程序員沒有顯式寫出 `(Int64, Int64) -> Int64` 這幾個字。
2. 顯式的函數類型 (由程序員主動書寫)
你完全可以先寫出類型,再去找或定義一個符合該類型的函數(甚至用變量、lambda、函數值等)。
例子
// 1. 【顯式地用于變量聲明】
// 程序員主動寫下了類型注解 `: (Int64, Int64) -> Int64`
let myMathOperator: (Int64, Int64) -> Int64 = add // 將函數`add`賦值給變量
// 2. 【顯式地用于函數參數】
// 程序員定義了一個高階函數,它接受一個函數作為參數
// 參數 `operation` 的類型被顯式地定義為 `(Int64, Int64) -> Int64`
func calculate(operation: (Int64, Int64) -> Int64, x: Int64, y: Int64) -> Int64 {
??? return operation(x, y)
}
// 3. 【顯式地用于解決重載歧義】
func add(i: Int64, j: Int64) -> Int64 { i + j }
func add(i: Float64, j: Float64) -> Float64 { i + j }
// 這里直接寫 `add` 編譯器不知道選哪個,產生歧義
// let f = add // Error!
// 程序員通過【顯式地書寫類型】來告訴編譯器需要哪個函數
let f: (Int64, Int64) -> Int64 = add // OK
函數類型的核心用途
1. 聲明函數類型的變量
在倉頡中,聲明變量必須顯式或通過初始化值來表明類型。
// 定義一個函數
func add(a: Int64, b: Int64): Int64 {return a + b
}main() {// 正確聲明1: 顯式指定變量類型,再賦值函數let operation: (Int64, Int64) -> Int64 // 聲明一個函數類型的變量operation = add // 將函數賦值給變量// 正確聲明2: 聲明的同時初始化(類型由編譯器推斷)let anotherOperation = add // 編譯器能推斷出anotherOperation的類型是 (Int64, Int64) -> Int64//現在,operation或anotherOperation就代表了 add 函數let result = operation(5, 3)println(result) // 輸出 8let result2 = anotherOperation(5, 3)println(result2) // 輸出 8
}
2. 作為函數的參數(高階函數)
// 導入標準庫中的集合包
import std.collection.ArrayList // 使用ArrayList// 高階函數的參數類型使用函數類型 (Int64) -> String
// 輸入和輸出使用 ArrayList 類型
func processNumbers(numbers: ArrayList<Int64>, transform: (Int64) -> String): ArrayList<String> {// 創建一個新的 ArrayList 來存放結果let results = ArrayList<String>()// 遍歷輸入的 ArrayList - 使用 for-in 循環for(num in numbers) {// 調用傳入的 transform 函數處理每個元素let transformedValue = transform(num)// 將結果添加到新的集合中results.add(transformedValue)}return results
}// 處理行為的函數定義不變
func intToString(num: Int64): String {return "Number: ${num}"
}main() {// 使用 ArrayList 而不是原生數組let myNumbers = ArrayList<Int64>([1, 2, 3, 4, 5])// 調用方式完全一樣,傳遞函數名let resultList = processNumbers(myNumbers, intToString)// 遍歷結果 ArrayList - 使用 for-in 循環for (str in resultList) {println(str)}// 同樣可以使用 Lambda 表達式let squaredList = processNumbers(myNumbers, { n => "Squared: ${n * n}" })// 遍歷 squaredListfor (str in squaredList) {println(str)}
}
3. 作為函數的返回值
// 這個函數返回一個 () -> String 類型的函數
func getGreeter(prefix: String): () -> String {// 在內部定義一個函數,它捕獲了參數 `prefix`func greeter(): String {return "${prefix}, Hello!" }return greeter // 返回這個內部函數
}main() {// getGreeter 返回的是一個函數let casualGreet = getGreeter("Hi")let formalGreet = getGreeter("Good morning")// 調用返回的函數println(casualGreet()) // 輸出: Hi, Hello!println(formalGreet()) // 輸出: Good morning, Hello!
}
順便提示,先把函數定義好,再傳遞函數名。也可可以用Lambda 表達式(匿名函數),下面示例對比:
import std.collection.ArrayList // 使用ArrayList// 高階函數的參數類型使用函數類型 (Int64) -> String
// 輸入和輸出使用 ArrayList 類型
func processNumbers(numbers: ArrayList<Int64>, transform: (Int64) -> String): ArrayList<String> {// 創建一個新的 ArrayList 來存放結果let results = ArrayList<String>()// 遍歷輸入的 ArrayList - 使用 for-in 循環for(num in numbers) {// 調用傳入的 transform 函數處理每個元素let transformedValue = transform(num)// 將結果添加到新的集合中results.add(transformedValue)}return results
}main() {// 使用 ArrayList 而不是原生數組let myNumbers = ArrayList<Int64>([1, 2, 3, 4, 5])// ---------1.具名函數(普通寫法)------------ // 先寫一個具名函數func multiply10(num: Int64): String {return "Value is: ${num * 10}"}// 把函數名當參數傳進去let result = processNumbers(myNumbers, multiply10) // 遍歷結果for(str in result) {println(str)}println("-----------")// ---------2.Lambda 表達式(匿名寫法)------------ // Lambda 表達式寫法let result2 = processNumbers(myNumbers, { num => "Value is: ${num * 10}" }) // 遍歷結果for(str in result2) {println(str)}
}
特別提示,可給函數類型的參數標記顯式名稱(僅用于標識,不影響類型匹配),且需統一寫或統一不寫,不能混合。
示例:
// 首先定義 showFruitPrice 函數
func showFruitPrice(name: String, price: Int64): Unit {println("Fruit: ${name}, Price: ${price}")
}main() {// 1. 全部寫名字 —— 合法let handler1: (name: String, price: Int64) -> Unit = showFruitPrice// 2. 全部不寫名字 —— 合法 let handler2: (String, Int64) -> Unit = showFruitPrice// 3. 混寫 —— 非法,編譯時報錯// let handler3: (name: String, Int64) -> Unit // Error: 必須統一寫或統一不寫參數名// 調用函數handler1("apple", 5) // 正確handler2("apple", 5) // 同樣正確
}