---
title: 'Access Control'
source: 'https://academia.sh/en/courses/swift/access-control'
course: 'The Swift Language'
language: en
updated: '2026-10-21T05:03:12+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Tell the six access levels and the `private(set)` form apart'
    - 'Show that access to a `private` member from another file is rejected'
    - 'Measure how a `private lazy` cache looks from the outside'
    - 'Show that an access level does not change the moment of evaluation'
---

# Access Control

Measures that making a cache `private` does not change the moment of evaluation, and that access control is decided at compile time.

Error handling was the way a function tells its caller that it cannot produce a result. Access control answers a different question: who can see a member? An **access level** says which pieces of code can see a declaration. Swift has six levels, from narrow to wide visibility: `private` is the declaration's own scope and its extensions in the same file, `fileprivate` is the same file, `internal` is the same module, `package` is the modules in the same package, `public` is other modules, and `open` lets other modules subclass and override as well. The default is `internal`. Because this course runs in a single module, we cannot measure the difference between `package`, `public` and `open`; knowing their names is enough.

**`private(set)`** keeps reading wide and writing narrow for a property: it can be read from outside and written only from the declaration itself. The counters of `Probe` are written this way.

## The Measurement Setup

```swift
// Probe.swift — records when an expression is evaluated
final class Probe {
    let label: String
    private(set) var evaluations = 0
    private(set) var uses = 0
    private var evaluatedBeforeUse = false

    init(_ label: String) {
        self.label = label
    }

    func eval<T>(_ value: T) -> T {
        evaluations += 1
        if uses == 0 { evaluatedBeforeUse = true }
        return value
    }

    func use<T>(_ access: () -> T) -> T {
        uses += 1
        return access()
    }

    var moment: String {
        if evaluations == 0 { return "never" }
        if evaluations > 1 { return "on-every-use" }
        return evaluatedBeforeUse ? "immediately" : "on-first-use"
    }

    func report() {
        print("\(label): \(evaluations) evaluations, \(uses) uses -> \(moment)")
    }
}

func tally(_ probes: [Probe]) {
    for moment in ["immediately", "on-first-use", "on-every-use", "never"] {
        let count = probes.filter { $0.moment == moment }.count
        print("\(moment): \(count)")
    }
}
```

**TS51.** The measurement has two helper files: `Ledger.swift`, which holds the cached and uncached totals, and `Vault.swift`, which stores a `private` member. The rejection of access to a `private` member from another file is shown not in the measured block but in a separate source fragment.

```swift
// Ledger.swift — a private lazy cache, a private computed property and an open lazy
final class Ledger {
    private let source: Probe
    private lazy var cachedTotal: Int = source.eval(10)
    private var computedTotal: Int { source.eval(10) }
    private(set) var entries = 0

    init(source: Probe) { self.source = source }
    func record() { entries += 1 }
    var total: Int { cachedTotal }
    var freshTotal: Int { computedTotal }
}

final class OpenLedger {
    let source: Probe
    lazy var cachedTotal: Int = source.eval(10)
    init(source: Probe) { self.source = source }
}
```

```swift
// Vault.swift — a private member
struct Vault {
    private var secret = 42
    var hint: Int { secret % 10 }
}
```

## The Cache Is Private, the Evaluation Is the Same

`Ledger` caches the total in `private lazy var cachedTotal` and exposes it through a computed property named `total`. Someone looking from outside sees only `total`; the existence of the cache is an implementation detail.

```swift
// main.swift — measuring a private lazy cache through outside access
let cached = Probe("private-lazy")
let ledger = Ledger(source: cached)
ledger.record()
ledger.record()
print(ledger.entries)
print(Vault().hint)

_ = cached.use { ledger.total }
_ = cached.use { ledger.total }
cached.report()
```

```
2
2
private-lazy: 1 evaluations, 2 uses -> on-first-use
```

`total` was read twice and the source expression was evaluated once: **on first use**. Being `private` did not turn the cache into anything other than a stored value; the first access did the calculation and kept the result, and the second access handed back what was kept. `Vault().hint`, on the other hand, gave out a value derived from a `private` member without showing the member. `ledger.entries` could be read; writing it needed `record()`.

We can see that the writing side is closed by trying to assign a value from outside to a property marked `private(set)`. Outside the file of the declaration, this line does not compile:

```swift
let ledger = Ledger(source: Probe("ledger"))
ledger.entries = 5
```

The same rule applies to reading a `private` member. The `secret` field of `Vault` cannot be seen from a line written in another file:

```swift
let vault = Vault()
print(vault.secret)
```

**TS52.** The compiler gave an error for both rejections; both were verified with separate temporary blocks, and the error texts are not printed here because the text depends on the version.

The difference between `private` and `fileprivate` is where the boundary of visibility is drawn. A `private` member is visible inside the scope of the declaration and its extensions in the same file; a `fileprivate` member is open to every declaration in the same file. If two types sit side by side in the same file, a `fileprivate` member can be reached from both, while a `private` one cannot. This is why `Vault` and `Ledger` were kept in separate files: so that it stays clear which boundary the rejection comes from.

Choosing an access level is a design decision, but it has no measurable effect. Making the cache `private` lets you replace the cache with another form later: the code that uses `Ledger` depends only on `total`, not on `cachedTotal`, so even if the storage form inside changes, the code outside keeps compiling. This is a contract boundary; when it will be evaluated is another axis, as we measure below.

Opening a member wider than necessary, on the other hand, is a decision that is hard to take back. Once a cache that is `internal` has been bound to from outside, pulling it back to `private` breaks those bindings. Starting narrow and widening when needed is often cheaper than the reverse, because widening does not break existing calls and narrowing does. In a library, widening has a cost too: every member you open is a new contract.

## fileprivate: The File Boundary

In the previous section we explained the difference between `private` and `fileprivate`; now we use it in a measured block. If two types sit side by side in the same file, a `fileprivate` member is visible from both. Below, the `Auditor` type in the same file reads the `fileprivate` computed property of a type named `Account`. If the same property were `private`, this access from another type would not compile even in the same file; this lesson does not measure that rejection separately.

```swift
// main.swift — a fileprivate member is read from another type in the same file
struct Account {
    fileprivate let source: Probe
    fileprivate var balance: Int { source.eval(100) }
}

struct Auditor {
    func read(_ account: Account) -> Int {
        account.balance
    }
}

let audit = Probe("fileprivate")
let account = Account(source: audit)
let auditor = Auditor()

_ = audit.use { auditor.read(account) }
_ = audit.use { auditor.read(account) }

audit.report()
```

```
fileprivate: 2 evaluations, 2 uses -> on-every-use
```

`Auditor` could read the `fileprivate` property of `Account`, and the property ran again on every read: **on every use**. The access level widened (from `private` to `fileprivate`) and the moment of evaluation stayed the same. This repeats the claim of the bounding measurement from a different direction: opening up access does not change the moment either. `fileprivate` lets you think of a file as a small unit; you put two tightly coupled types in the same file and open their internal details only to each other. **TS53.** The measurement was made in a single file; compiling the lesson blocks as separate files would be the way to try this boundary, but the `private` rejection above already shows the same boundary.

## Giving an Access Level to an Extension

An access modifier can be written not on a single member but on a whole extension: `fileprivate extension Tax { ... }`. The members inside the extension take the level of the extension if they carry no modifier of their own. This is the short way to narrow a group of helpers all at once. The extension below closes two members to the file at the same time.

```swift
// main.swift — the access level given to an extension passes to its members
struct Tax {
    let source: Probe
}

fileprivate extension Tax {
    var rate: Int { source.eval(18) }
    func applied(to amount: Int) -> Int { amount * rate / 100 }
}

let taxProbe = Probe("fileprivate-extension")
let tax = Tax(source: taxProbe)

_ = taxProbe.use { tax.applied(to: 200) }
_ = taxProbe.use { tax.rate }

taxProbe.report()
```

```
fileprivate-extension: 2 evaluations, 2 uses -> on-every-use
```

Both the method and the property were closed with a single `fileprivate`, and both could be called from the same file. The first call read `rate` from inside the method, the second read it directly; on both paths the body ran each time: **on every use**. A modifier at the extension level does not touch when the members are evaluated; it only decides who can see them. Giving an access level to an extension to hide the helpers of a type in one place removes the need to write the modifier on each member separately. **TS54.** In the measurement the members in the extension had no modifier of their own; whether a member can write a level wider than the extension was not tried.

## private(set): Reading Is a Stored Value

A property marked `private(set)` is read from outside and written from inside. The property in this measurement is a stored property; reading it triggers no calculation. `private(set)` can also be written on a computed property, but that is not what is measured here. Writing comes only from the type's own methods. In the measurement we separate the moment of writing (the initializer) from the moment of reading.

```swift
// main.swift — reading a stored value with private(set)
struct Gauge {
    private(set) var level: Int

    init(tracking tracker: Probe) {
        level = tracker.eval(3)
    }
}

let gaugeProbe = Probe("private-set")
let gauge = Gauge(tracking: gaugeProbe)

_ = gaugeProbe.use { gauge.level }
_ = gaugeProbe.use { gauge.level }

gaugeProbe.report()
```

```
private-set: 1 evaluations, 2 uses -> immediately
```

The value was written once, in the initializer, and the two reads did not produce a new evaluation: **immediately**. Having the reading side wide is free; having the writing side narrow is enforced at compile time, and the rejection was shown earlier in this lesson. `private(set)` therefore gives two things together: a property that looks constant from outside and storage that can be changed inside. **TS55.** In the measurement the value was written only in the initializer; state changed later by a method was not measured.

## public and open: The Module Boundary Cannot Be Measured

All the measurements in this lesson run in a single module. `public` and `open` change visibility from another module: a `public` type can be used from outside but cannot be subclassed, an `open` class can be subclassed from outside and its members can be overridden. Within a single module the difference between the two is invisible, and a single module cannot show the difference between `package`, `public` and `open` either; so we do not invent a measurement of the moment of evaluation for them. Still, we can measure that a `public` member compiles in a single module and that the moment of evaluation does not change.

```swift
// main.swift — a public member behaves the same in the same module
public struct Rate {
    private let reader: () -> Int
    public var current: Int { reader() }
    public init(reader: @escaping () -> Int) { self.reader = reader }
}

let publicProbe = Probe("public")
let rate = Rate(reader: { publicProbe.eval(7) })

_ = publicProbe.use { rate.current }
_ = publicProbe.use { rate.current }

publicProbe.report()
```

```
public: 2 evaluations, 2 uses -> on-every-use
```

The `public` computed property (bound through a closure because the measurement class is `internal`) ran on every read in the same way as an `internal` one: **on every use**. This result says that writing `public` did not change the evaluation; the real difference between `public` and `internal` appears between a library and the application that uses it. If you write a library, everything you mark `public` becomes a contract, and breaking that contract breaks all the code that uses it. There is also a rule: the type used in the signature of a `public` member cannot be at a narrower level than the member. Because `Probe` is `internal` in this lesson, a `public` property cannot bind to it directly; this is why we used a closure in the measurement. `open` goes one step further: allowing subclassing also brings the internal workings of the type into the contract. **TS56.** `open` was not measured in this lesson and its difference cannot be shown in a single module; only the fact that the `public` form compiles and does not change the behavior was measured.

## A private Nested Type

A type defined inside a type carries an access level too. A `private` nested type is visible only from inside the enclosing type. The code outside is not aware that the type exists, but it makes use of the behavior it offers. This is the strongest form of hiding an implementation detail completely. `Printer` below hands the formatting to a `private` nested structure.

```swift
// main.swift — a private nested type is invisible to the outside but works
struct Printer {
    private struct Formatter {
        let source: Probe
        func format(_ number: Int) -> String {
            "\(source.eval(number))"
        }
    }

    private let formatter: Formatter

    init(source: Probe) {
        formatter = Formatter(source: source)
    }

    func render(_ number: Int) -> String {
        formatter.format(number)
    }
}

let nestedProbe = Probe("private-nested")
let printer = Printer(source: nestedProbe)

print(nestedProbe.use { printer.render(5) })
_ = nestedProbe.use { printer.render(6) }

nestedProbe.report()
```

```
5
private-nested: 2 evaluations, 2 uses -> on-every-use
```

The code outside called only `render`; the name of the `Formatter` type never appeared in `main.swift`. The method of the nested type ran on every call: **on every use**. Hiding did not change the behavior; it removed the type from the outside. This completes the shared result of the previous sections: an access level is a visibility decision, not a timing decision. In short, the narrower the surface you open to the outside, the more you can change inside; in this measurement hiding did not change the moment of evaluation either. In a design, replacing `Formatter` later with another type does not affect the users of `Printer`, because the only thing they depend on is `render`. **TS57.** In the measurement the nested type was a structure; it was not separately verified for a nested class or `enum`.

## The Bounding Measurement

The previous result can be read as "a private cache is on-first-use"; this reading is misleading. An access level does not determine the moment of evaluation. The same `lazy` declaration also gives `on-first-use` when it is `internal`, while a `private` computed property gives `on-every-use` in spite of being `private`. What determines the moment is the kind of the property: stored, `lazy` or computed.

```swift
// main.swift — an access level does not change the moment
let openProbe = Probe("internal-lazy")
let openLedger = OpenLedger(source: openProbe)
_ = openProbe.use { openLedger.cachedTotal }
_ = openProbe.use { openLedger.cachedTotal }

let freshProbe = Probe("private-computed")
let freshLedger = Ledger(source: freshProbe)
_ = freshProbe.use { freshLedger.freshTotal }
_ = freshProbe.use { freshLedger.freshTotal }

openProbe.report()
freshProbe.report()
```

```
internal-lazy: 1 evaluations, 2 uses -> on-first-use
private-computed: 2 evaluations, 2 uses -> on-every-use
```

The `internal lazy` again gave **on first use**, and the `private` computed property again gave **on every use**. Access control is a **compile-time** decision: the compiler accepts or rejects a line, and does not interfere with the order of evaluation of the running program. So an access level is not one of the moments we measure, but a rule that stays outside them.

This distinction is also a signal for someone reading the code: seeing that a member is `private` tells you that the effect of changing it will stay inside the declaration. For an `internal` member, you have to assume that the effect can reach anywhere in the module.

## Summary

- There are six access levels: `private`, `fileprivate`, `internal`, `package`, `public`, `open`; the default is `internal`.
- `private(set)` keeps reading wide and writing narrow; assignment from outside does not compile.
- Access to a `private` member from another file does not compile.
- An access level does not change the moment of evaluation; the kind of the property determines the moment.
- This lesson's moment: **on first use** (two outside accesses to a `private lazy` cache).

## Next Step

The type system topic ends here. The next topic moves on to memory management: when an object is released and when `deinit` runs.
