對程式設計而言,所謂的「副作用(Side Effect)」,指原本預期目的之外的額外效應。這有可能是一些額外的計算、動作,或對程式狀態(變數)的改變。
有時候,副作用是無害的,它只是增加了一些惱人的行為,但是,大多數時候,副作用會造成傷害。而且此類的副作用,會產生臭蟲,影響到程式的正確行為。
副作用引發的臭蟲特別難解
正所謂「明槍易躲,暗箭難防」。顯而易見的臭蟲,容易察覺也容易解決,但隱藏於暗處,那種由程式副作用所引發的臭蟲,往往不是難以重現蹤影、就是難以發掘所在,因而不容易解決。
現今軟體開發有著高度的複雜度,而之所以會如此,錯綜複雜的各個軟體元件之間相互作用下,各自存在的副作用,是主要的影響因素之一。
想要克服軟體程式設計時的高複雜度,或者說得卑微些,想要在軟體設計的高複雜度下謀求生存,去控制、降低程式碼所衍生的副作用,肯定是重要的議題。
完美函式的標準:固定的輸入,產出固定的結果
我們所寫下的程式碼之所以可能引發副作用,其實和採用的程式設計方式有關。程式語言分為許多類型,而我們大多數程式人所慣常使用的,幾乎都是屬於「命令式程式語言(Imperative Programming Language)」。
這類的程式語言,在運作時所做的事,簡單歸納起來,是透過一組特定的指令,改變一組狀態。從現行處理器(CPU)的運作便不難理解此事,處理器提供一組指令集,允許我們操作暫存器及記憶體的值,而暫存器及記憶體中的值,便構成了程式運行時的狀態。
而倚賴一組狀態在運行的程式碼,都或多或少有引發副作用的可能。一段程式碼,倘若輸入是一組狀態,而輸出亦會更動到另一組狀態,那麼這段程式碼,便可能受到其他程式碼副作用所影響,以及可能陷入引發副作用,且影響到其他程式碼的危機當中。
例如我們所熟悉的C語言,使用了函式(Function)來指涉在其他程式語言中可能稱為子程序(Sub-Procedure)的東西。
事實上,函式是個源自於數學的名詞,而一個完美的函式,應當是在給定一組固定的傳入值得情況下,不論在各種時間點、情境下呼叫它多少次,永遠都回傳固定的結果。但是,讀者不妨捫心自問,自己所寫下的函式,又有多少個能夠符合這完美函式的標準呢?
使用全域變數的函式,將提高狀態的相依性
完美函式的確有卓越的優點存在,其中最重要的,莫過於這樣的函式不會引發副作用。因為它所回傳的值,永遠只取決於傳入函式的引數值,而不會受到其他確實存在,但又容易讓人忽略的程式狀態所影響。
對於引發副作用,有一類的函式惡名昭彰,也就是那種相依於全域變數的函式。這類的函式,有可能將全域變數值視為函式運作時輸入的一部分,也有可能在輸出時,修改到全域變數。
而它所相依的全域變數,也同樣可能做為其他函式輸入或輸出的一部分,那麼這些相依於相同全域變數的函式,便有可能因為自己對全域變數的修改方式超出其他函式的預期,因而造成了其他函式產生了不可預期的行為,引發了副作用。
這是許多程式設計守則中,都會建議程式設計者盡量減少使用全域變數,因為這項作法十分容易引發副作用。因為它具有全域的特性,一來,使得程式中的每個函式,都有權限加以存取,大大增加相互依賴的函式數目,也擴展了彼此錯綜複雜的關係。二來,因為宣告全域變數的位置,通常不在使用它的函式週遭,因而使得程式人對它失去戒心而缺乏防範。
狀態的相依性正是副作用的重要來源之一。而全域的變數(也就是全域的狀態),更將可能存在的相依性,提升到極高的程度,因為它放任所有函式無條件地可與它相依。這是Java、C#之類的程式語言,取消全域變數的關鍵原因。
降低對外在狀態的依賴,避免副作用的重要手段
從全域變數這個極端的例子切入,就不難明白,降低副作用的重要手段之一,便是降低對其他外在狀態的依賴。倘若你的函式,並不會將其他位於函式之外的變數視為計算的輸入值,僅建立區域變數(也就是內部狀態),並在運行的過程中僅修改區域變數,而不會修改到其他外在的變數,那麼它就幾乎不會受到其他函式的副作用影響,也可以大大降低引發副作用的機會。
C語言裡的靜態區域變數,雖然不像全域變數那樣,有被高度依賴的可能性,但是,由於它的生命期長,即使函式已返回,它仍舊存在。由於它是個區域的變數,只有內含該變數的函式才能加以存取,因而降低了不同函式之間互相影響的機會。
但仍然避免不掉同一個函式自我影響的情況,例如多執行緒同時呼叫同一函式時,若此函式存取靜態區域變數,仍然有可能造成問題。由此可見,變數的生命期(Life Cycle)和可視範圍(Visibility),都會影響到它被依賴的可能性。縮短變數的生命期、縮減其可視範圍(也就是盡量的區域化變數能被存取到的範圍),都可以降低變數被依賴的情況。
使用區域變數可視的範圍
命令式程式語言,在運作中免不了得操作狀態,但為了減少副作用的發生,程式人得盡力降低程式中對各個狀態的相依程度。僅使用區域變數是十分理想的情況,但現實的開發中似乎有困難。而像一些命令式的物件導向程式語言,它善用了封裝及資訊隱藏的概念,允許程式人將程式中會操作的狀態,以物件為單位凝聚在一塊。
如此一來,其可視範圍可以利用存取權限的設定(例如大家或許十分熟悉的Private、Protected關鍵字)來縮減,而其生命期則伴隨著該物件本身的生成及消滅。在許多物件導向程式設計守則中,也希望程式設計者盡量將本身的資料成員設為Private,便是希望降低這些變數的可視範圍,進而減少狀態的相依。倘若這麼做,便能將對變數的依賴區域化,控制在物件之中。
許多人在設計物件時,並未遵守這樣子的守則,受到副作用影響的機會就大很多了。此外,有些設計者會設計所謂的Getter、Setter函式,用來取得、設定物件的狀態。狀態的相依在所難免,只是要盡量減少。而且,並不是每一個資料成員都要為其設計Getter函式,只有需開放外界存取的狀態,才要這麼做,而且,應該要盡量減少對外公開的狀態,,避免增加被依賴的機會。而Setter函式,更是能免則免,因為開放了Setter函式,形同允許外界修改自己的狀態,自然提高了副作用發生的機會。
近來函數式程式語言(Functional Programming Language)似乎又有流行起來的趨勢,而函數式程式語言有個極大的優點,便是在於其中的函式並不存在對外在狀態的相依,因而避免了副作用。
實務上,或許每個人不見得採用函數式程式語言,但想要減少被副作用影響的情況,核心的精神還是一樣,便是留意、避免對函式(或物件)之外狀態的相依。若非得有相依的情況,這相依的範圍則是越局限於局部越佳。
作者簡介─王建興 |
|
清華大學資訊工程系的博士研究生,研究興趣包括電腦網路、點對點網路、分散式網路管理、以及行動式代理人,專長則是Internet應用系統的開發。曾參與過的開發專案性質十分廣泛而且不同,從ERP、PC Game到P2P網路電話都在他的涉獵範圍之內。 |
熱門新聞
2025-01-10
2025-01-10
2025-01-10
2025-01-10
2025-01-10
2025-01-10