Class vs Struct vs Actor & MainActor
In Swift, actor
, struct
, and class
are three distinct constructs used for defining data structures and managing concurrency. Each has its own purpose and characteristics. Let's go through their differences:
Struct (struct
):
- A
struct
is a value type in Swift, which means it is copied when assigned to a new variable or passed as a function argument. It's suitable for representing lightweight data structures. - Structs don’t support inheritance. They are final by default, which means you cannot subclass them.
- Structs are immutable by default. If you want to modify their properties, you need to mark the properties as
var
. - They are usually used for small data models, like coordinates, simple configurations, or other value-like entities.
Example of a struct:
struct Point {
var x: Int
var y: Int
}
var p1 = Point(x: 1, y: 2)
var p2 = p1 // Copying the struct
p2.x = 3 print(p1.x) // Output: 1 (p1 remains unchanged)
print(p2.x) // Output: 3
Class (class
):
- A
class
is a reference type in Swift, which means when you assign a class instance to another variable or pass it as an argument, you are passing a reference to the same instance in memory. - Classes support inheritance, making them suitable for building complex object hierarchies.
- Classes can be marked as
final
to prevent inheritance oropen
to allow subclassing (by default, they areopen
). - They support the concept of deinitializers (
deinit
) to clean up resources when the instance is deallocated. - Classes are generally used when you need to manage more complex objects with shared references.
Example of a class:
class Person {
var name: String
init(name: String) {
self.name = name
}
}
var person1 = Person(name: "Alice")
var person2 = person1 // Both variables now reference the same instance
person2.name = "Bob"
print(person1.name) // Output: "Bob" (changes are reflected in both variables)
print(person2.name) // Output: "Bob"
Actor (actor
):
- An
actor
is a special type introduced in Swift to manage concurrency safely. It is used for encapsulating mutable state and ensuring that access to that state is serialized and protected from data races. - Actors are reference types, similar to classes, but they have built-in support for isolation and synchronization of their state, making them safe to use in concurrent scenarios.
- To access the properties and methods of an actor, you need to use the
await
keyword, which ensures that the access is serialized and coordinated. - Actors are ideal for handling concurrent operations and shared resources without worrying about explicit locking.
Example of an actor:
actor BankAccount {
private var balance: Double = 0.0
func deposit(amount: Double) {
balance += amount
}
func withdraw(amount: Double) -> Double {
let newBalance = balance - amount
if newBalance >= 0 {
balance = newBalance
return balance
} else {
return 0
}
}
}
let account = BankAccount()
Task {
await account.deposit(amount: 100) // balance 100
let withdrawnAmount = await account.withdraw(amount: 50) // balance 50
print(withdrawnAmount) // Output: 50
await account.deposit(amount: 25) // balance 75
let withdrawnAmount2 = await account.withdraw(amount: 50) // balance 25
print(withdrawnAmount2) // Output: 25
}
To summarize, you choose between struct
, class
, or actor
based on the specific requirements of your application. Use struct
for simple data modeling, class
for managing complex object hierarchies, and actor
for safe concurrent access to mutable state. Check below for more easy way.
We another Keyword MainActor
MainActor (@MainActor
): == DispatchQueue.main.async()
@MainActor
is an attribute to mark parts of your code that should be executed on the main (UI) thread. It is particularly useful in concurrent programming to ensure that certain operations that involve UI updates or other main-thread-only tasks are performed on the main thread. This helps avoid race conditions and synchronization issues.@MainActor
is a powerful tool for making your code more concurrent-friendly and ensuring that main-thread-only operations are performed safely.- Can be used on Class level, properties and methods. When you call a method or access a property marked with the
@MainActor
attribute from a different thread, Swift will automatically dispatch that work onto the main thread, ensuring that the code is executed safely - To access the properties and methods of an
@MainActor
, you need to use theawait
keyword, which ensures that the access is serialized and coordinated. @MainActor
is a global actor that uses the main queue for executing its work
class ViewModel {
@MainActor
func updateLabel() {
// Update UI elements here
self.titleLabel.text = "Updated on the main thread"
}
func someConcurrentFunction() async {
// ...
await updateLabel() // This will be dispatched to the main thread
// ...
}
}
Value Types and Reference Types
As discussed, in Swift there are two types of instances:
First Value Types, where each instance keeps a unique copy of its data. Int, String, Array, Dictionary, Float, Double, struct, enum, & Tuple all are value types.
The second, Reference Types, where instances share a single copy of the data, and the type is usually defined as a Class, NSObject & UIKIT elements.
Use a value type when:
- Comparing instance data with == makes sense
- You want copies to have independent state
- The data will be used in code across multiple threads
Use a reference type (e.g. use a class) when:
- Comparing instance identity with === makes sense
- You want to create shared, mutable state
Follow me Lakshminaidu C for more content.