Swift is designed to be safe by default. Values are immutable unless explicitly marked mutable, and function parameters are constants by default. This prevents accidental side effects and makes code easier to reason about.
But real-world code isn’t always purely functional. Sometimes, you need to modify a value directly.
That’s where inout parameters come in.
This article explores inout in depth - what it is, how it works, when to use it, and (just as importantly) when not to.
inout Parameter?An inout parameter allows a function to modify the value of a variable passed into it.
By default, function parameters are immutable::
func increment(_ value: Int) {
value += 1 // ❌ Error: 'value' is a constant
}To allow mutation, you mark the parameter as inout:
func increment(_ value: inout Int) {
value += 1
}And when calling the function, you pass the variable using &:
var number = 10
increment(&number)
print(number) // 11That & is not just syntax - it’s a signal:
“This value will be mutated.”
inout Works Under the HoodAt first glance, inout looks like pass-by-reference. But Swift doesn’t expose raw references like that.
Instead, it uses a model called copy-in copy-out:
This gives you the benefits of mutation while preserving Swift’s safety guarantees.
In practice, thanks to compiler optimizations, it often behaves like a reference but you should think of it as controlled mutation, not direct memory access.
inout Actually MattersMost Swift developers either:
inout completely, orBoth approaches miss the point.
inout exists for one reason:
To make mutation explicit, controlled, and intentional.
Compare these two functions:
func updateBalance(_ balance: Int) -> Intfunc updateBalance(_ balance: inout Int)The first one looks pure. The second one makes a stronger promise:
“I will mutate what you give me.”
That’s not just implementation - it’s API design.
balance = updateBalance(balance)vs
updateBalance(&balance)When mutation is the goal, inout is more direct and expressive.
A common misconception is that inout is primarily for performance.
It’s not.
Swift already has:
The real value of
inoutis clarity of intent, not micro-optimization.
func double(_ value: inout Int) {
value *= 2
}
var x = 5
double(&x)
print(x) // x = 10This is straightforward: the function directly mutates the input.
func swapValues(_ a: inout Int, _ b: inout Int) {
let temp = a
a = b
b = temp
}
var x = 10
var y = 20
swapValues(&x, &y)
print(x) // x = 20
print(y) // y = 10This is a classic use case where returning values would be awkward.
func removeNegatives(from array: inout [Int]) {
array.removeAll { $0 < 0 }
}
var numbers = [3, -1, 5, -2, 7]
removeNegatives(from: &numbers)
print(numbers) // [3, 5, 7]This keeps mutation localized and avoids unnecessary intermediate arrays.
inout vs Return Values: This Is a Design DecisionThis isn’t just syntax - it’s philosophy.
func normalized(_ value: Double) -> DoubleThis is pure. Input → Output. No surprises.
This should be your default.
inout for Mutationfunc normalize(_ value: inout Double)This says:
“This function changes existing state.”
That’s a stronger contract.
inoutinoutWhen in doubt, return a value.
inoutUse inout when mutation is the purpose - not a side effect.
Good use cases:
Example:
func applyDiscount(_ price: inout Double, percentage: Double) {
price -= price * percentage / 100
}inoutAvoid it when:
func squared(_ value: Int) -> IntMaking this inout would hurt clarity.
value = normalize(value)This supports chaining. inout does not.
func process(_ data: inout Data)What does "process" do?
If mutation isn’t obvious, don’t use inout.
&increment(&number) // ✅
increment(number) // ❌let value = 10
increment(&value) // ❌var x = 10
func modify(_ a: inout Int, _ b: inout Int) {
a += b
}
modify(&x, &x) // ❌Swift prevents unsafe memory access.
func update(_ value: inout Int) {
DispatchQueue.global().async {
value = 5 // ❌ Not allowed
}
}inout only lives within the function’s execution.
inoutinout looks simple but it’s easy to misuse if you treat it as a shortcut instead of a design tool.
inout When a Return Value Is Betterfunc increment(_ value: inout Int)Better:
func increment(_ value: Int) -> IntIf it’s a transformation, don’t force mutation.
update(&value)vs
value = update(value)This is not an improvement.
Clarity > fewer lines.
func process(_ data: inout [Int])If it clears the array, the name should say so.
combine(&value, &value) // ❌Swift blocks this for safety.
It’s not raw reference - it’s controlled mutation via copy-in copy-out.
inout does not work across async boundaries.
inoutToo much inout leads to:
If everything is mutable, you’re fighting Swift.
inout only when mutation is the core intentinout just to “save a line”If your API becomes harder to reason about,
inoutis the wrong choice.
inout is deceptively simple but it carries real design weight.
It forces you to decide:
Swift gives you both tools. Your job is to choose wisely.
Used well, inout makes your code:
Used poorly, it introduces:
So don’t avoid it but don’t default to it either.
Use
inoutwhen it earns its place.
If you have suggestions, feel free to connect with me on X and send me a DM. If this article helped you, Buy me a coffee.