天天看點

Swift程式設計二十七(通路控制)通路控制

案例代碼下載下傳

通路控制

通路控制限制從其他源檔案和子產品中的代碼通路部分代碼。此功能使可以隐藏代碼的實作細節,并指定一個首選接口,通過該接口可以通路和使用該代碼。

可以為各個類型(類,結構和枚舉)以及屬于這些類型的屬性,方法,initializers和下标配置設定特定的通路級别。協定可以限制在某個上下文中,全局常量,變量和函數也可以。

除了提供各種級别的通路控制外,Swift還通過為典型方案提供預設通路級别來減少顯式指定通路控制級别的需求。實際上,如果正在編寫單目标應用程式,則可能根本不需要顯式指定通路控制級别。

注意

為簡便起見,代碼中可以應用通路控制的各個方面(屬性,類型,函數等)在下面的部分中稱為“實體”。

子產品和源檔案

Swift的通路控制模型基于子產品和源檔案的概念。

子產品是代碼分布的單個單元——架構或應用程式被建構和包裝為單個單元并可以通過另一個子產品使用Swift的import關鍵字引入。

Xcode中的每個建構目标(例如應用程式包或架構)都被視為Swift中的單獨子產品。如果将應用程式代碼的各個方面組合在一起作為一個獨立的架構 - 也許是為了跨多個應用程式封裝和重用該代碼 - 當被導入和使用在應用程式中或使用在其他架構中那麼在該架構中定義的所有内容将成為單獨子產品的一部分。

源檔案是一個子產品内的單個Swift源代碼檔案(實際上,一個應用程式或架構内的一個單獨的檔案)。雖然在單獨的源檔案中定義單個類型很常見,但單個源檔案可以包含多個類型,函數等的定義。

通路級别

Swift為代碼中的實體提供了五種不同的通路級别。這些通路級别與定義實體的源檔案相關,也與源檔案所屬的子產品相關。

  • 開放通路和公共通路使實體可以在其定義子產品的任何源檔案中使用,也可以在另一個導入定義子產品的子產品的源檔案中使用。在指定架構的公共接口時,通常使用開放或公共通路。開放和公共通路之間的差別如下所述。
  • 内部通路使實體可以在其定義子產品的任何源檔案中使用,但不能在該子產品之外的任何源檔案中使用。在定義應用程式或架構的内部結構時,通常使用内部通路。
  • 檔案私有通路将實體的使用限制在其自己的定義源檔案中。當在整個檔案中使用這些詳細資訊時,使用檔案專用通路來隐藏特定功能的實作細節。
  • 私有通路将實體的使用限制為封閉聲明,以及同一檔案中該聲明的擴充。當這些詳細資訊僅在單個聲明中使用時,使用私有通路來隐藏特定功能的實作細節。

開放通路是最高(限制性最小)的通路級别,私有通路是最低(限制性最強)的通路級别。

開放通路僅适用于類和類成員,它與公共通路不同,如下所示:

  • 具有公共通路權限或任何更嚴格的通路級别的類隻能在定義它們的子產品中進行子類化。
  • 具有公共通路權限或任何更具限制性的通路級别的類成員隻能在定義它們的子產品中被子類覆寫。
  • 開放類可以在定義它們的子產品中進行子類化,也可以在導入子產品的任何子產品中進行子類化。
  • 開放類成員可以由定義它們的子產品中的子類覆寫,也可以在導入定義它們的子產品的任何子產品中覆寫。

将類标記為開放通路明确表示已考慮使用該類作為其他子產品的超類的代碼的影響,并且已相應地設計了類的代碼。

通路級别的指導原則

Swift中的通路級别遵循一個總體指導原則:沒有實體可以根據具有較低(更嚴格)通路級别的另一個實體來定義。

例如:

  • 公共變量不能定義為具有内部、檔案私有或私有類型,因為在使用公共變量的任何地方都可能無法使用該類型。
  • 函數不能具有比其參數類型和傳回類型更高的通路級别,因為該函數可用于其組成類型對周圍代碼不可用的情況。

下文詳細介紹了該指導原則對該語言不同方面的具體影響。

預設通路級别

如果沒有自己指定顯式通路級别,則代碼中的所有實體(具有一些特定的例外情況,如本章後面所述)都具有内部的預設通路級别。是以,在許多情況下,無需在代碼中指定顯式通路級别。

單目标應用的通路級别

當編寫一個簡單的單目标應用程式時,應用程式中的代碼通常是自包含在應用程式中的,并且不需要在應用程式子產品外部提供。内部的預設通路級别已比對此要求。是以,無需指定自定義通路級别。但是,可能希望将代碼的某些部分标記為私有或檔案私有,以便從應用程式子產品中的其他代碼中隐藏其實作細節。

架構的通路級别

在開發架構時,将該架構的面向公衆的接口标記為開放或公共,以便其他子產品(例如導入架構的應用程式)可以檢視和通路該架構。這個面向公衆的接口是架構的應用程式程式設計接口(或API)。

注意:

架構的任何内部實作細節仍然可以使用内部的預設通路級别,或者如果要将它們隐藏在架構内部代碼的其他部分中,則可以将其标記為私有或檔案私有。如果希望實體成為架構API的一部分,則需要将實體标記為開放或公開。

單元測試目标的通路級别

當使用單元測試目标編寫應用程式時,應用程式中的代碼需要可供該子產品使用才能進行測試。預設情況下,隻有标記為open或public的實體才可供其他子產品通路。但是,如果使用@testable屬性标記産品子產品的導入聲明并且在啟用測試的情況下編譯該産品子產品,則單元測試目标可以通路任何内部實體。

通路控制文法

定義實體的通路級别通過在實體的導入前放置一個open,public,internal,fileprivate,或private修飾語:

public class SomePublicClass {}
internal class SomeInternalClass {}
fileprivate class SomeFilePrivateClass {}
private class SomePrivateClass {}

public var somePublicVariable = 0
internal let someInternalConstant = 0
fileprivate func someFilePrivateFunction() {}
private func somePrivateFunction() {}
           

除非另行指定,否則預設通路級别為内部,如預設通路級别中所述。這意味着,SomeInternalClass和someInternalConstant能夠在沒有明确的通路級别的修改寫入,仍會有内部的通路級别:

class SomeInternalClass {}              // 預設内部通路級别
let someInternalConstant = 0            // 預設内部通路級别
           

自定義類型

如果要為自定義類型指定顯式通路級别,請在定義類型時執行此操作。然後可以在其通路級别允許的任何地方使用新類型。例如,如果定義檔案專用類,則該類隻能用作定義檔案專用類的源檔案中的屬性類型,或者作為函數參數或傳回類型。

類型的通路控制級别還會影響該類型成員的預設通路級别(其屬性,方法,初始值設定項和下标)。如果将類型的通路級别定義為私有或檔案專用,則其成員的預設通路級别也将為私有或檔案專用。如果将類型的通路級别定義為内部或公共(或使用内部的預設通路級别而未明确指定通路級别),則類型成員的預設通路級别将是内部的。

重要

公共類型預設具有内部成員,而不是公共成員。如果希望類型成員是公共的,則必須明确标記它。此要求可確定某個類型的面向公衆的API是選擇釋出的内容,并避免錯誤地将類型的内部工作方式顯示為公共API。

public class SomePublicClass {
    public var somePublicProperty = 0
    var someInternalProperty = 0
    fileprivate func someFilePrivateMethod() {}
    private func somePrivateMethod() {}
}

class SomeInternalClass {
    var someInternalProperty = 0
    fileprivate func someFilePrivateMethod() {}
    private func somePrivateMethod() {}
}

fileprivate class SomeFilePrivateClass {
    func someFilePrivateMethod() {}
    private func somePrivateMethod() {}
}

private class SomePrivateClass {
    func somePrivateMethod() {}                  
}
           

元組類型

元組類型的通路級别是元組中使用的所有類型的最嚴格的通路級别。例如,如果從兩種不同類型組成一個元組,一個具有内部通路權限,另一個具有私有通路權限,則該複合元組類型的通路級别将是私有的。

注意

元組類型沒有類,結構,枚舉和函數的獨立定義。使用元組類型時會自動推導出元組類型的通路級别,并且無法明确指定。

函數類型

函數類型的通路級别計算為函數參數類型和傳回類型的最嚴格的通路級别。如果函數的計算通路級别與上下文預設值不比對,則必須明确指定通路級别作為函數定義的一部分。

下面的示例定義了一個名為someFunction()的全局函數,但沒有為函數本身提供特定的通路級别修飾符。可能希望此函數具有預設的“内部”通路級别,但事實并非如此。實際上如下所寫的someFunction()不會:

func someFunction() -> (SomeInternalClass, SomePrivateClass) {
    // 這是函數實作
}
           

函數的傳回類型是一個元組類型,由兩個自定義類型中定義的自定義類組成。其中一個類定義為内部,另一個定義為私有。是以,複合元組類型的整體通路級别是私有的(元組的組成類型的最小通路級别)。

因為函數的傳回類型是私有的,是以必須使用private函數聲明的修飾符來标記函數的整體通路級别:

private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
    // 這是函數實作
}
           

使用public或internal修飾符或使用的預設設定internal标記someFunction()定義是無效的,因為該函數的公共或内部使用者可能對函數傳回類型中使用的私有類沒有适當通路權限。

枚舉類型

枚舉的各個案例自動獲得與其所屬枚舉相同的通路級别。無法為單個枚舉案例指定不同的通路級别。

在下面的示例中,CompassPoint枚舉具有顯式的公共通路級别。枚舉的情況north,south,east,和west是以也有公共的通路級别:

public enum CompassPoint {
    case north
    case south
    case east
    case west
}
           

原始值和關聯值

用于枚舉定義中的任何原始值或關聯值的類型必須具有至少與枚舉的通路級别一樣高的通路級别。例如,不能将私有類型用作具有内部通路級别的枚舉的原始值類型。

嵌套類型

私有類型中定義的嵌套類型具有私有的自動通路級别。在檔案專用類型中定義的嵌套類型具有檔案專用的自動通路級别。在公共類型或内部類型中定義的嵌套類型具有内部的自動通路級别。如果希望公共類型中的嵌套類型公開可用,則必須将嵌套類型顯式聲明為public。

子類

可以子類化目前通路上下文中可以通路的任何類。子類不能具有比其超類更高的通路級别 - 例如,不能編寫内部超類的公共子類。

此外,可以重寫在特定通路上下文中可見的任何類成員(方法,屬性,初始化程式或下标)。

重寫可以使繼承的類成員比其超類版本更易于通路。在下面的示例中,類A是有一個名為someMethod()的file-private方法的公共類。類B是A子類,具有更低的“内部”通路級别。盡管如此,class B提供了一個通路級别為“internal” 的someMethod()覆寫,它高于原始someMethod()實作:

public class A {
    fileprivate func someMethod() {}
}

internal class B: A {
    override internal func someMethod() {}
}
           

比超類成員具有更低通路權限的子類成員調用超類成員甚至是有效的,隻要對超類成員的調用發生在允許的通路級别上下文中(即,在與超類同一源檔案源檔案中的檔案私有成員調用,或者與超類在同一子產品中的内部成員調用):

public class A {
    fileprivate func someMethod() {}
}

internal class B: A {
    override internal func someMethod() {
        super.someMethod()
    }
}
           

因為超類A和子類B是在同一個源檔案中定義的,是以對于B的someMethod()實作調用super.someMethod()是有效的。

常量,變量,屬性和下标

常量,變量或屬性不能比其類型更公開。例如,編寫具有私有類型的公共屬性是無效的。類似地,下标不能比其索引類型或傳回類型更公開。

如果使用标記私有類型常量,變量,屬性或下标,則常量,變量,屬性或下标也必須标記為private:

private var privateInstance = SomePrivateClass()
           

Getters和Setters

常量,變量,屬性和下标的getter和setter自動獲得與它們所屬的常量,變量,屬性或下标相同的通路級别。

可以為setter提供比其對應的getter 更低的通路級别,以限制該變量,屬性或下标的讀寫範圍。通過在var或subscript引導前編寫fileprivate(set),private(set)或internal(set)配置設定更低通路級别。

注意

此規則适用于存儲的屬性以及計算的屬性。即使沒有為存儲的屬性編寫顯式的getter和setter,Swift仍然會合成一個隐式的getter和setter,以便提供對存儲屬性的後備存儲的通路。使用fileprivate(set), private(set)和internal(set)以與計算屬性中的顯式setter完全相同的方式更改此合成setter的通路級别。

下面的示例定義了一個名為TrackedString的結構,它跟蹤字元串屬性被修改的次數:

struct TrackedString {
    private(set) var numberOfEdits = 0
    var value: String = "" {
        didSet {
            numberOfEdits += 1
        }
    }
}
           

該TrackedString結構定義了一個名為value的字元串存儲屬性,其初始值為""(空字元串)。該結構還定義了一個名為numberOfEdits的整數存儲屬性,用于跟蹤value修改的次數。此修改跟蹤是通過value屬性上的屬性觀察器didSet實作的,每次将value屬性設定為新值時,屬性觀察器都會遞增numberOfEdits。

在TrackedString結構和value屬性不提供明确的通路級别的修改,是以他們都收到預設的内部通路級别。但是,numberOfEdits屬性的通路級别标有一個private(set)修飾符,表示該屬性的getter仍然具有内部的預設通路級别,但該屬性隻能從作為TrackedString結構一部分的代碼中設定。這樣TrackedString可以在内部修改numberOfEdits屬性,但在屬性在結構定義之外使用時,可以将屬性顯示為隻讀屬性。

如果建立一個TrackedString執行個體并多次修改其字元串值,則可以看到numberOfEdits屬性值更新以比對修改次數:

var stringToEdit = TrackedString()
stringToEdit.value = "This string will be tracked."
stringToEdit.value += " This edit will increment numberOfEdits."
stringToEdit.value += " So will this one."
print("The number of edits is \(stringToEdit.numberOfEdits)")

/*
列印結果:

The number of edits is 3
*/
           

雖然可以從另一個源檔案中查詢numberOfEdits屬性的目前值,但無法從其他源檔案修改該屬性。此限制可保護TrackedString編輯跟蹤功能的實作細節,同時仍可友善地通路該功能的某個方面。

請注意,如果需要,可以為getter和setter配置設定顯式通路級别。下面的示例顯示了TrackedString結構的一個版本,其中結構的顯式通路級别為public。是以,結構的成員(包括numberOfEdits屬性)預設具有内部通路級别。可以通過組合public和private(set)通路級别修飾符使結構的numberOfEdits屬性的getter為public,其屬性setter為private :

public struct TrackedString {
    public private(set) var numberOfEdits = 0
    public var value: String = "" {
        didSet {
            numberOfEdits += 1
        }
    }
    public init() {}
}
           

初始化

可以為initializers配置設定小于或等于它們初始化類型的通路級别。唯一的例外是必需的initializers(在必需的initializers中定義)。必需的initializers必須具有與其所屬類相同的通路級别。

與函數和方法參數一樣,初始化程式的參數類型不能比initializers自己的通路級别更私密。

預設initializers

如在預設initializers中描述,Swift為任何結構或所有屬性提供預設值并且本身不提供一個initializers的類自動提供了一個預設initializers而無需任何參數。

預設initializers具有與其初始化類型相同的通路級别,除非該類型定義為public。對于定義為public的類型,預設initializers被視為内部。如果希望在另一個子產品中使用無參數初始化程式時可以初始化公共類型,則必須自己明确地提供公共無參數初始化程式作為類型定義的一部分。

結構類型的預設initializers

如果結構的任何存儲屬性是私有的,則結構類型的預設initializers将被視為私有。同樣,如果結構的任何存儲屬性是檔案專用的,則initializers是檔案專用的。否則,initializers具有内部通路級别。

與上面的預設任何一樣,如果希望在另一個子產品中使用initializers時可以初始化公共結構類型,則必須提供公共成員初始化程式作為類型定義的一部分。

協定

如果要為協定類型配置設定顯式通路級别,請在定義協定時執行此操作。這使可以建立隻能在特定通路上下文中遵守的協定。

協定定義中每個需求的通路級别自動設定為與協定相同的通路級别。不能将協定要求設定為與其支援的協定不同的通路級别。這可確定在采用該協定的任何類型上都可以看到所有協定的要求。

注意

如果定義公共協定,則協定的要求在實施時需要公共通路級别。此行為與其他類型不同,其中公共類型定義意味着類型成員的内部通路級别。

協定繼承

如果定義從現有協定繼承的新協定,則新協定最多可以具有與其繼承的協定相同的通路級别。例如,無法編寫繼承内部協定的公共協定。

遵守協定

類型可以遵守通路級别低于類型本身的協定。例如,可以定義在其他子產品中可以使用的公共類型,但遵守内部協定的隻能在内部協定定義的子產品中使用。

遵守特定協定的類型上下文是類型通路級别和協定通路級别的最小值。如果類型是公共類型,但它符合的協定是内部類型,則遵守該協定的類型也是内部的。

在編寫類型的擴充以遵守協定時,必須確定每個遵守協定要求的類型實作至少具有與該協定類型一緻的通路級别。例如,如果公共類型遵守内部協定,則每個遵守協定要求的類型實作必須至少是“内部”的。

注意

在Swift中,與Objective-C一樣,遵守協定是全局的 - 類型不可能在同一程式中以兩種不同的方式遵守協定。

擴充

可以在類,結構或枚舉可用的任何通路上下文中擴充類,結構或枚舉。擴充中添加的任何類型成員具有與要擴充的原始類型中聲明的類型成員相同的預設通路級别。如果擴充公共或内部類型,則添加的任何新類型成員都具有内部的預設通路級别。如果擴充檔案專用類型,則添加的任何新類型成員都具有檔案專用的預設通路級别。如果擴充私有類型,則添加的任何新類型成員都具有私有的預設通路級别。

或者,可以使用顯式通路級别修飾符标記擴充名(例如,private extension),以便為擴充名中定義的所有成員設定新的預設通路級别。仍可以在單個類型成員的擴充中覆寫此新預設值。

如果使用擴充來添加遵守協定,則無法為擴充提供顯式通路級别修飾符。相反,協定自身的通路級别用于為擴充中的每個協定要求實作提供預設通路級别。

擴充中的私有成員

與擴充的類,結構或枚舉位于同一檔案中的擴充的行為就像擴充中的代碼已寫為原始類型聲明的一部分一樣。是以,可以:

  • 在原始聲明中聲明私有成員,并從同一檔案中的擴充通路該成員。
  • 在一個擴充中聲明一個私有成員,并從同一檔案中的另一個擴充通路該成員。
  • 在擴充中聲明私有成員,并從同一檔案中的原始聲明中通路該成員。

此行為意味着無論的類型是否具有私有實體可以使用擴充以相同的方式組織代碼。例如,給出以下簡單協定:

protocol SomeProtocol {
    func doSomething()
}
           

可以使用擴充來添加遵守的協定,如下所示:

struct SomeStruct {
    private var privateVariable = 12
}

extension SomeStruct: SomeProtocol {
    func doSomething() {
        print(privateVariable)
    }
}
           

泛型

泛型類型或泛型函數的通路級别是泛型類型或函數本身的通路級别以及對其類型參數的任何類型限制的通路級别的最小值。

類型别名

為了通路控制的目的,定義的任何類型别名都被視為不同類型。類型别名的通路級别可以小于或等于其别名類型的通路級别。例如,私有類型别名可以為私有,檔案私有,内部,公共或開放類型設定别名,但公共類型别名不能為内部,檔案私有或私有類别設定别名。

注意

此規則也适用于用于滿足遵守協定的關聯類型的類型别名。