13. 存取控制

物件導向的封裝特性是將物件的特徵也就是屬性與操作方法也就是函數一起放在物件中,讓外界要存取這些屬性與呼叫方法時都必須要透過物件或該物件產生的實體才能接觸到物件裡面的屬性與方法。既然要存取屬性或呼叫方法都必須透過該物件,於是我們就可以透過存取等級來設定物件中的哪些項目是外界可以接觸的。

13.1 等級說明

Swift可以針對類別、結構與列舉型態中的屬性與方法分別設定五種不同的存取等級,從最開放到最嚴謹分別為open、public、internal、fileprivate與private。預設等級為internal。要設定結構、類別或是列舉型態的存取等級,可以在struct、class或enum前面加上存取等級修飾子,以及在個別的屬性與方法前加上存取等級修飾子。

五個存取等級的功用,分述如下:

open

open等級代表可允許其他模組中的程式來存取、繼承與覆寫。例如我們可以呼叫內建框架中許多類別的方法或存取他們的屬性,繼承甚至於覆寫他們,是因為他們被設定為open等級。這是最開放的一個等級。這個等級只適用於類別。模組相當於另外一個框架或函數庫,如果要使用,必須先import匯入後才可以呼叫裡面的程式碼。

若察看各種視覺化元件的原始定義,就可以發現他們的存取等級都是設定為open,例如UIWindow視窗元件、UIButton按鈕元件…等。而且不只是類別本身,甚至裡面的屬性與方法也大多定義為open等級。下面這張圖是標籤元件UILabel的原始定義,可以看到不論是類別還是屬性,以及塞不進圖裡面的其他各種方法都是open等級,代表允許任何一個專案中的任何程式存取。

public

代表同一個模組內可以存取、繼承與覆寫,模組外只能存取,不能繼承也不能覆寫。由於結構與列舉沒有繼承功能,沒有繼承功能當然也就不會有覆寫問題,因此這個等級相當於結構與列舉型態的最開放等級。只要是標準函數庫中的結構型態,存取等級都是public,例如字串String、整數Int、陣列Array…等,並且連這些結構裡面的屬性與方法也同樣設定為public等級。例如下圖中的CGPoint結構,可以看到不論是屬性還是初始化器都是public等級。

internal

代表同一個模組內可以存取、繼承與覆寫,但在模組外則完全看不到。這也是預設等級。在大部分情況下同一個專案中的各個檔案都屬於同一個模組,因此我們才能在同一個專案中存取不同檔案間的資料,而不需要特別設定存取等級。

如果建立專案時勾選了Unit Tests選項,專案建立後會產生跟單元測試有關的檔案,此時在單元測試的檔案中會發現import相關模組前加上了@testable修飾子,這個修飾子表示單元測試可以存取所匯入模組中internal等級以上的資料,如果沒有加上@testable,單元測試只能存取匯入進來模組中的open或public等級資料。但即使加上了@testable,也無法存取fileprivate與private這兩個等級的資料。

fileprivate

代表只有同一個檔案內的程式可以存取、繼承與覆寫。舉個例子,假設有兩個類別,雖然他們是同一個模組但是定義於不同檔案中,這兩個類別彼此間是互相隱藏的,都看不到對方的存在,所以自然也就無法存取對方的屬性或呼叫方法。但如果這兩個類別是在同一個檔案中,就可以互相存取對方的屬性與方法。

private

只有物件本身內部程式碼可以存取。這是最嚴格的存取等級。如果使用extension擴充型態並且想存取該型態為private等級的屬性或是方法時,只有當extension與private等級的資料位於同一檔案內時才可存取。這是為了避免第三方透過extension功能存取到private等級資料。

舉個常見的例子。在下面這個會員結構Member中將會員id屬性設定為private等級,代表這個屬性只能內部使用,外面程式碼完全無法存取也看不到有這個屬性存在。這樣設計的目的是,我們不希望id值可以被結構以外的程式碼修改或是讀取,我們希望能夠確保id屬性一旦初始化後就無法再變動。最後則是希望兩個實體可以夠過屬性id來判斷是否是同一個會員。

struct Member: Equatable {
    private let id = UUID()
    var name: String
    static func == (lhs: Member, rhs: Member) -> Bool {
        lhs.id == rhs.id
    }
}

由於結構屬於value type,因此當下面這段程式碼中m2 = m1執行後,m2與m1是兩個不同的實體,分別位於不同的記憶體區段。因此當m2修改了name屬性並不會連動更改m1的name屬性,因此如果只透過姓名來比對m2與m1是不是同一人無法得到正確結果。這時因為Member符合Equatable協定的規範,因此重新定義「==」,利用id來判斷是否是同一個人。由於id被設定為private並且宣告為常數,代表內部比對時所取到的id值沒有機會被外部或內部其他程式碼竄改,因此,最後比對結果必定是正確的。

let m1 = Member(name: "王大明")
var m2 = m1
m2.name = "王小毛"

print("m1的名字為\(m1.name)")
// Prints "m1的名字為王大明"
print("m2的名字為\(m2.name)")
// Prints "m2的名字為王小毛"
print("m1與m2是同一人嗎?\(m1 == m2)")
// Prints "m1與m2是同一人嗎?true"

13.2 類別專屬規定

由於類別具有繼承特性,因此在存取等級設定上,必須遵守「可以跟父類別一樣或比父類別更嚴謹」的方式設定,子類別不能在繼承後將存取等級改的比父類別更寬鬆。例如父類別是internal,子類別就不行改為public或open。下面這段程式碼,Subclass的存取等級設定是不被允許的,因為Superclass的存取等級為預設的internal,因此Subclass只能設定為internal、fileprivate或private,現在設定為public代表子類別比父類別寬鬆,這樣的設定不被允許。

class Superclass {
    
}

public class Subclass: Superclass {
    
}

但有一種情況是當子類別覆寫父類別中的方法或屬性時,可以將覆寫後的方法或屬性調整的比父類別寬鬆。以下面程式碼為例,Superclass為open等級,內部有一個fileprivate等級的method,因此,只有同一個檔案內的程式碼才能呼叫Superclass中的method()。我們使用Subclass繼承Superclass,由於Superclass為open等級,因此Subclass可以是任何其他的等級,這裡我們選擇降一級為public。現在在Subclass中的屬性與方法只要設定的等級為public以下(包含public)都算合法。因此,Subclass可以覆寫method()後將等級改為public,這時後相當於把method()的等級從原本嚴謹的fileprivate調到寬鬆的public了。

open class Superclass {
    fileprivate func method() {
        // 程式碼寫這
    }
}

public class Subclass: Superclass {
    override public func method() {
        super.method()
    }
}

let instance = Subclass()
instance.method()

setter 與 getter

在屬性的存取等級設定上,我們可以把setter的等級設定的比getter更嚴格,例如可以設定成internal getter與private setter。如下:

struct Student {
    private(set) var name: String
}

這段程式碼中的屬性name的getter等級為預設的internal,代表struct以外的程式可以讀取,但是如果要修改屬性值,就只能在struct內部了,因為setter被設定為private等級。

如果要設定為public getter與fileprivate setter,語法為:

struct Student {
    public fileprivate(set) var name: String
}

setter的存取等級只能跟getter一樣或是比getter嚴謹,不可以設定的比getter寬鬆。

發表迴響