【Lua 筆記】作用域 / 函數 - part 7

由於有款遊戲叫做 CSO(Counter-Strike Online),內建模式創世者模式(Studio)新增使用 Lua 及其遊戲的 API,所以突發奇想製作這個筆記。

這個筆記會在一開始先著重純粹的程式設計自學,在最後的章節才會與 CSO 遊戲 API 進行應用。

作用域(scope)

作用域(scope),表示一個變數或函數等能夠作用的範圍,什麼意思呢?請看以下例子:

1
2
a = 10 -- 這是全域變數
local a = 50 -- 這是局域變數

lua 當中的變數,沒有在前面加上 local 以外,全部都是「全域變數」,你可以把全域變數想像成是一個到哪裡都可以用的變數。

局域變數就只限縮於”目前”的範圍,什麼意思呢?也就是在變數前面宣告了 local 這個關鍵字,就只有這整個文件可以用這個變數而已,其他的文件不能存取到它的值。(這只是粗淺的解釋,實際上看的是他目前所在的作用域範圍)

具體是何種”目前”的範圍,我們在說明函數的時候稍待說明一下。

函數(function)

函數(function),又被稱為副程式、函式,我們在之前有稍微說明過它的概念,忘記的請至 part 1 的資料型態重看一遍。

總之函數它就是一台有功能的機器,輸入東西之後,在裡面運轉的機器(功能),運轉完後會輸出東西給我們。

以下是 lua 對函數的定義:

1
2
3
4
optional_function_scope function function_name(argument1, argument2, argument3..., argumentn)
function_body
return result_params_comma_separated
end
  • optional_function_scope : 可以指定這個函數是全域還是局域,全域的話就不用寫,局域請打上 local 關鍵字。
  • function_name : 指定函數名稱。
  • argument1, argument2, argument3…, argumentn : 表示一個函數的參數,多個參數以逗號隔開,函數也可以不帶參數。
  • function_body : 函數體,函數中需要執行的程式碼區塊。
  • result_params_comma_separated : 函數當中的回傳值,Lua 的函數可以回傳多個值,每個值以逗號隔開。

:::danger
註:return 一旦觸發後,函數裡面 return 後續的程式碼就不會繼續執行,所以 return 可以表示回傳後結束。
:::

函數當中的變數

前面我們談到作用域的部分,用函數解釋是最為明顯的,請看以下例子:

1
2
3
4
5
6
7
8
a = 10 -- 設定全域變數
function func()
local a = 15 -- 設定局域變數
return a
end

print(func()) -- 15
print(a) -- 10

在函數外面的部分,a 被設定是全域變數,a = 10。

函數裡面又設定一個局域變數,也叫做 a = 15。

我們要使用函數呢,就直接寫它的函數名稱再加上小括號就好囉,然後這個我們稱之為呼叫(call)函數。

執行結果,發現 a 的值竟然沒有被改變,理應來說兩個都要是 15 才對啊!

為什麼?這就是作用域規則啦~因為函數裡面本身也是一種局域的作用域,函數以外的就拿不到函數內的東西囉。

或是可以這樣想:函數外的全域變數,與函數內的局域變數兩者是截然不同的變數。

換個方式,如果我們這樣寫:

1
2
3
4
5
6
7
8
a = 10
function func()
a = 15
return a
end

print(func()) -- 15
print(a) -- 15

可以發現,輸出結果就變兩個都是 15 了。把 local 拿掉,此時的 a 就不再是兩個截然不同的變數了,就好像把散掉的分身結合再一起一樣。

此時函數內調用的就是 a 這個「全域變數」。

多重回傳值

範例來源:Lua 函数 | 菜鸟教程

1
2
3
4
5
6
7
8
9
10
11
12
13
function maximum (a)
local mi = 1 -- 最大值索引
local m = a[mi] -- 最大值
for i,val in ipairs(a) do
if val > m then
mi = i
m = val
end
end
return m, mi
end

print(maximum({8,10,23,12,5}))

執行結果:

1
23    3

以上的程式碼可以從一串數字當中,從陣列索引 1 開始,尋找出最大值並且回傳,還能夠回傳該最大值在陣列當中的索引值。

不定長參數(可變參數)

Lua 中使用三點 ... 表示函數有可變的參數,意思就是可以輸入很多個參數進去。

以下是一個範例(Lua 函数 | 菜鸟教程):

1
2
3
4
5
6
7
8
function add(...)  
local s = 0
for i, v in ipairs {...} do --> {...} 表示由一個所有不定長參數組成的陣列
s = s + v
end
return s
end
print(add(3,4,5,6,7)) ---> 25

上面的意思概括來說,就是呼叫 add 函數時,能夠輸入很多不限長度的參數進去,像是上面輸入 3,4,5,6,7,進入函數裡面。

這支函數主要計算由不定長參數組成的陣列中,所有元素的總和。

先用一個 temp 變數暫存(這裡稱為 s,sum 為總和的意思),之後在透過運算 s = s + v 不斷計算求得總和。

以下的範例(Lua 函数 | 菜鸟教程)當中,可以透過 select(“#”,…) 來取得不定長參數的數量:

1
2
3
4
5
6
7
8
9
10
11
function average(...)
result = 0
local arg={...}
for i,v in ipairs(arg) do
result = result + v
end
print("總共傳入 " .. select("#",...) .. " 個數")
return result/select("#",...)
end

print("平均值為",average(10,5,3,4,5,6))

輸出結果:

1
2
總共傳入 6 個數
平均值為 5.5

有時候我們需要一些固定的參數,再來加上不定長參數,以下是一個應用範例:

1
2
3
4
5
6
7
8
9
10
function func(fixedParam, ...) -- fixedParam 為固定參數, ... 是不定長參數
print("固定參數 : " .. fixedParam)

local args = { ... }
for i, v in ipairs(args) do
print("不定長參數 " .. i .. " : " .. v)
end
end

func("固定值", "參數1", "參數2", "參數3")

輸出結果:

1
2
3
4
固定參數 : 固定值
不定長參數 1 : 參數1
不定長參數 2 : 參數2
不定長參數 3 : 參數3

然後再來介紹 select 的另一個用法:

:::info
select(n, …) 用於回傳從起點 n 開始到結束位置的所有參數列表。
:::

呼叫 select 時,必須傳入一個固定實參 selector (選擇開關) 和一系列變長參數。如果 selector 為數字 n,那麼 select 傳回參數列表中從索引 n 開始到結束位置的所有參數列表,否則只能為字串 #,這樣 select 會傳回變長參數的總數。

上面這段話擷取自:Lua 函数 | 菜鸟教程

解釋一下:

  • 固定實參(Fixed Argument):固定實參是指在函數呼叫時,必須傳遞的參數。這些參數的數量和順序是固定的,函數定義時就已經確定。
  • 變長參數(Variable-Length Argument):也就是不定長參數。變長參數是指在函數呼叫時,可以傳遞任意數量的參數。這些參數的數量和順序在函數定義時是不確定的。Lua 使用三個點 (…) 來表示變長參數。

看起來很複雜,很不容易懂對吧?以下這樣解釋或許會比較清楚:

:::warning
select 函數:

它的第一個參數是固定實參 selector,後面的參數是變長參數。select( 這裡是第一個參數 , …)

如果 selector 是一個數字 n,則 select 會回傳從第 n 個變長參數開始到最後一個變長參數的列表。

如果 selector 是字串 #(”#”),那麼 select 會返回變長參數的總數。
:::

還是不清楚的話,讓我們看個具體的例子:

1
2
3
4
5
6
7
function func(...)
print(select(1, ...)) -- 打印所有變長參數
print(select(2, ...)) -- 打印從第二個變長參數開始的所有參數
print(select("#", ...)) -- 打印變長參數的總數
end

func("a", "b", "c", "d")

輸出結果:

1
2
3
a   b   c   d
b c d
4

練習一下:請用 lua 寫出一支程式碼,運用本篇的概念(不定長參數、select 函數運用),寫出如下程式碼一樣的輸出結果。

1
2
3
4
a = {1,2,3,4}
for i, v in ipairs(a) do
print("arg", v)
end

輸出結果:

1
2
3
4
arg 1
arg 2
arg 3
arg 4

:::spoiler 點左邊三角形, 看解答跟詳解

::: info
程式碼源自:Lua 函数 | 菜鸟教程

1
2
3
4
5
6
7
8
9
do
function foo(...)
for i = 1, select('#', ...) do
local arg = select(i, ...)
print("arg", arg)
end
end
foo(1, 2, 3, 4)
end

解釋:

do ... end 表示創建一個局部作用域,避免影響到外部程式碼的運作。foo 函數和任何在區域內定義的局部變數在 do ... end 區塊結束後都不會影響到外部的程式碼。

第三行:用 for 迴圈來遍歷所有傳遞給 foo 函數的參數。select(‘#’, …) 回傳傳遞給函數的參數個數。

第四行:定義局域變數 arg = select(i, …),用 select 函數來獲取第 i 個參數並將其指定給局部變量 arg。select(i, …) 回傳第 i 個參數的值。

註:直接 print 出 arg 跟用 print(select(i, …)) 是不一樣的,前者只會印出 select() 的第一個參數,後者從第 i 個參數到最後一個參數全印。
:::

總結

作用域(scope),表示一個變數或函數等能夠作用的範圍。

分為全域作用域、局域作用域,當然也分成全域變數、局域變數,抑或是全域函數、局域函數等。

使用 do ... end 可建立一個局域作用域。像這樣:

1
2
3
4
5
a = 10
do
local a = 15
end
print(a) -- 10

以下是 lua 對函數的定義:

1
2
3
4
optional_function_scope function function_name(argument1, argument2, argument3..., argumentn)
function_body
return result_params_comma_separated
end
  • optional_function_scope : 可以指定這個函數是全域還是局域,全域的話就不用寫,局域請打上 local 關鍵字。
  • function_name : 指定函數名稱。
  • argument1, argument2, argument3…, argumentn : 表示一個函數的參數,多個參數以逗號隔開,函數也可以不帶參數。
  • function_body : 函數體,函數中需要執行的程式碼區塊。
  • result_params_comma_separated : 函數當中的回傳值,Lua 的函數可以回傳多個值,每個值以逗號隔開。

:::danger
註:return 一旦觸發後,函數裡面 return 後續的程式碼就不會繼續執行,所以 return 可以表示回傳後結束。
:::

Lua 中使用三點 ... 表示函數有可變的參數,意思就是可以輸入很多個參數進去。(不定長參數或稱變長參數)

select (index, ···)

:::warning
select 函數:

它的第一個參數是固定實參 selector,後面的參數是變長參數。select( 這裡是第一個參數 , …)

如果 selector 是一個數字 n,則 select 會回傳從第 n 個變長參數開始到最後一個變長參數的列表。

如果 selector 是字串 #(”#”),那麼 select 會返回變長參數的總數。
:::

參考資料

【30天Lua重拾筆記09】基礎1: 類型 - 函數 | 又LAG隨性筆記

Lua 变量 | 菜鸟教程

Lua 函数 | 菜鸟教程