What semantics in swift
In Swift, "semantics" refers to how types behave when they are assigned to variables, passed to functions, or modified in memory. Swift relies heavily on three core data-handling behaviors: Value Semantics, Reference Semantics, and Move Semantics.
1. Value Semantics (The Default)
Value semantics mean that data is copied when it is assigned to a new variable or passed to a function. Each variable holds its own independent instance of the data, ensuring that changes to one variable never accidentally impact another.
Types: Structs (struct), enumerations (enum), and tuples (tuple).
Standard Library: Most core Swift types—like Int, String, Array, and Dictionary—are structs and exhibit value semantics.
Benefits: Provides excellent thread safety and highly predictable code behavior
💡 Optimization: Copy-on-Write (COW)
### 2. Reference Semantics
Reference semantics mean that variables do not hold the actual data directly; instead, they hold a pointer or reference to the shared instance stored elsewhere in memory (on the heap).
*** Types:** Classes (class), actors (actor), and closures.
*** Behavior:** If you assign a class instance to a new variable, both variables point to the exact same object. Mutating one will affect the other.
*** Benefits:** Supports inheritance, object identity, and sharing live application state.
class vs actor in swift
The primary difference is that actors provide built-in compile-time thread safety by serializing access to their mutable state, while classes do not protect against data races when accessed concurrently across multiple threads. Both are reference types allocated on the heap, but actors achieve this protection through a feature called actor isolation.
Key Differences Explained
1. Concurrency and Thread Safety
Classes: Multiple threads can modify a class instance at the same time. This causes data races, memory corruption, or crashes if you do not use manual synchronization like DispatchQueue or locks
Actors: Swift guarantees that only one task can execute inside an actor at any given time. It automatically serializes incoming calls to prevent simultaneous mutations.
2. Accessing State (await)
Classes: You can read and write to class properties instantly from anywhere in your code.
Actors: If you are outside the actor, you must use the await keyword to access its properties or methods. This suspends your code if the actor is busy executing another task.
3. Inheritance
Classes: Can inherit properties, methods, and initializers from a superclass.
Actors: Do not support subclassing or inheritance. They can only conform to protocols to share behavior.
Code Example: Non-Thread-Safe Class
class ClassCounter {
var value = 0
func increment() { value += 1 }
}
let classCounter = ClassCounter()
// Calling this from multiple threads simultaneously causes a data race
classCounter.increment()
Code Example: Thread-Safe Actor
class ClassCounter {
var value = 0
func increment() { value += 1 }
}
let classCounter = ClassCounter()
// Calling this from multiple threads simultaneously causes a data race
classCounter.increment()
GCD
In Swift, the term GCD can refer to two entirely different concepts: Grand Central Dispatch (for multi-threading) or the Greatest Common Divisor (the mathematical algorithm).
1. Grand Central Dispatch (Multi-Threading)
**
**Grand Central Dispatch (GCD) is Apple’s low-level framework used to handle asynchronous tasks and concurrent code execution using DispatchQueue.
Main Queue vs. Background Queue
The most common use case is sending a heavy task to a background thread to prevent the UI from freezing, then updating the user interface on the main thread.
import Foundation
// 1. Move heavy work to a background queue
DispatchQueue.global(qos: .userInitiated).async {
// Example: Fetch data from a network or process an image
let processedData = "Some heavy data results"
// 2. Bounce back to the main queue to update the UI
DispatchQueue.main.async {
print("Update UI with: \(processedData)")
}
}
Creating Custom Queues
You can create your own queues depending on how you want your tasks to execute:
Serial Queue: Executes tasks one at a time, in order.
Concurrent Queue: Executes multiple tasks at the same time.
// Serial Queue (default)
let serialQueue = DispatchQueue(label: "com.app.serialQueue")
serialQueue.async { print("Task 1") }
serialQueue.async { print("Task 2") } // Waits for Task 1 to finish
// Concurrent Queue
let concurrentQueue = DispatchQueue(label: "com.app.concurrentQueue", attributes: .concurrent)
concurrentQueue.async { print("Task A") }
concurrentQueue.async { print("Task B") } // Runs at the same time as Task A
Serial queue vs actor in swift
In Swift/iOS, Serial DispatchQueue and Actor both help protect shared mutable data, but they solve the problem at different levels.
Does serial Queue is thread safe
Yes, a serial queue is thread-safe for the tasks it executes because it guarantees that only one task runs at a time, preventing race conditions.
An actor is Swift's modern way of protecting mutable state from concurrent access.
1. Serial Queue
A serial queue executes one task at a time.
final class Counter {
private var value = 0
private let queue = DispatchQueue(label: "counter.queue")
func increment() {
queue.async {
self.value += 1
}
}
func getValue(completion: @escaping (Int) -> Void) {
queue.async {
completion(self.value)
}
}
}
Because the queue is serial:
Task 1 → finish
Task 2 → finish
Task 3 → finish
Only one task accesses value at a time.
When to use Serial Queue
Use it when:
- Working with existing GCD-based code
- You need precise queue control
- You have legacy UIKit/iOS code
- You need sync, async, .barrier, QoS, etc.
- You are protecting a small resource
2. Actor
An actor is Swift's modern way of protecting mutable state from concurrent access.
actor Counter {
private var value = 0
func increment() {
value += 1
}
func getValue() -> Int {
return value
}
}
Usage:
let counter = Counter()
Task {
await counter.increment()
let value = await counter.getValue()
print(value)
}
Notice:
await counter.increment()
3. Main difference with example
Imagine you have a shared cache.
Serial Queue
final class ImageCache {
private var cache: [String: UIImage] = [:]
private let queue = DispatchQueue(
label: "image.cache.queue"
)
func set(_ image: UIImage, for key: String) {
queue.async {
self.cache[key] = image
}
}
func get(
_ key: String,
completion: @escaping (UIImage?) -> Void
) {
queue.async {
completion(self.cache[key])
}
}
}
You have to manually decide:
"Every access to cache must happen through queue."
Actor
actor ImageCache {
private var cache: [String: UIImage] = [:]
func set(_ image: UIImage, for key: String) {
cache[key] = image
}
func get(_ key: String) -> UIImage? {
cache[key]
}
}
Usage:
let cache = ImageCache()
Task {
await cache.set(image, for: "profile")
let image = await cache.get("profile")
}
Swift understands that cache is actor-isolated.



Top comments (0)