Swift Interview Questions
Question: What is the difference between a class
and a struct
in Swift?
Answer:
In Swift, both classes and structures (struct
) are used to create custom data types, but they differ in several key aspects, primarily in how they manage memory, behavior, and mutability. Understanding these differences is crucial for choosing the right type for your application.
1. Value Type vs. Reference Type
-
Class: A class is a reference type. This means when you assign an instance of a class to a new variable or pass it to a function, it is referenced. Both variables will point to the same instance of the object, and any changes made to one will affect the other.
-
Struct: A struct is a value type. When you assign an instance of a struct to a new variable or pass it to a function, a copy of the struct is made. Changes to one instance do not affect the other.
Example:
class Car {
var model: String
init(model: String) {
self.model = model
}
}
var car1 = Car(model: "Tesla")
var car2 = car1 // car2 is a reference to the same object as car1
car2.model = "BMW"
print(car1.model) // "BMW"
print(car2.model) // "BMW"
Here, car1
and car2
reference the same instance of Car
. Changing car2
’s model affects car1
because both refer to the same object.
struct House {
var address: String
}
var house1 = House(address: "123 Main St")
var house2 = house1 // house2 gets a copy of house1
house2.address = "456 Oak St"
print(house1.address) // "123 Main St"
print(house2.address) // "456 Oak St"
Here, house1
and house2
are independent copies, so changing house2
’s address does not affect house1
.
2. Inheritance
-
Class: Classes support inheritance, meaning a class can inherit properties, methods, and behavior from another class. This allows for code reuse and the creation of class hierarchies.
class Animal { func makeSound() { print("Animal sound") } } class Dog: Animal { override func makeSound() { print("Bark") } } let dog = Dog() dog.makeSound() // Output: "Bark"
-
Struct: Structs do not support inheritance. They cannot inherit properties or methods from other structs. Each struct is a completely independent type.
3. Deinitializers
-
Class: Classes can have deinitializers (
deinit
), which are called when an instance of the class is deallocated. This is useful for cleaning up resources before an object is destroyed.class Car { var model: String init(model: String) { self.model = model } deinit { print("\(model) is being deinitialized") } }
-
Struct: Structs do not have deinitializers. Since structs are value types and copied when passed around, they are automatically deallocated when they go out of scope.
4. Mutability
-
Class: The properties of a class instance can be changed even if the class instance is declared as a constant (using
let
), as long as the properties themselves are declared asvar
.class Car { var model: String init(model: String) { self.model = model } } let myCar = Car(model: "Tesla") myCar.model = "BMW" // Allowed because 'model' is a variable property
-
Struct: The properties of a struct can only be changed if the struct instance is declared as a variable (using
var
). If the struct is declared as a constant (let
), its properties cannot be modified, even if those properties are declared asvar
in the struct definition.struct House { var address: String } let myHouse = House(address: "123 Main St") myHouse.address = "456 Oak St" // Error: Cannot assign to property: 'myHouse' is a 'let' constant
5. Memory Management
-
Class: Classes are reference types, and Swift uses Automatic Reference Counting (ARC) to manage memory. This means the class instance is deallocated when there are no more references to it.
-
Struct: Structs are value types and are copied when passed around, so each copy has its own separate memory. There is no need for ARC because each copy of the struct is independent.
6. Use Cases
-
Class: Use a class when:
- You need to manage shared data or resources.
- You need inheritance, polymorphism, or dynamic dispatch.
- You want to use reference semantics (i.e., shared state across different parts of your app).
- You need deinitializers to clean up resources.
-
Struct: Use a struct when:
- You want value semantics (i.e., copies of the data, not shared references).
- You want to make your types lightweight and fast.
- You do not need inheritance, but want to bundle data together (such as with data models).
- You want safer code, especially when working with multithreading or avoiding shared mutable state.
Summary of Differences:
Feature | Class | Struct |
---|---|---|
Type | Reference type | Value type |
Memory | Passed by reference (shared) | Passed by value (copied) |
Inheritance | Supports inheritance | Does not support inheritance |
Deinitializers | Supports deinit | Does not support deinit |
Mutability (constant vs var) | Properties can change in constants if declared as var | Properties cannot change in constants |
Memory Management | Managed by ARC | Managed automatically (no ARC) |
Use Cases | Shared state, complex data, inheritance | Independent data, lightweight objects, thread safety |
Conclusion:
- Use classes when you need reference semantics, inheritance, or shared mutable state.
- Use structs when you need value semantics, immutable objects, or lightweight data containers.
Swift encourages the use of structs for many scenarios (like working with small data models) because of their safety and efficiency, but classes are necessary when shared state, inheritance, or complex behavior is required.
Question: How do you handle memory management in Swift? Explain ARC (Automatic Reference Counting).
Answer:
In Swift, memory management is handled primarily through Automatic Reference Counting (ARC). ARC is a memory management feature that automatically keeps track of the number of references to each object and deallocates objects when they are no longer needed. This is critical in managing memory in an efficient way to avoid memory leaks, but it also requires an understanding of how references and ownership of objects work.
ARC works in the background to automatically manage the memory used by class instances (which are reference types). It is not used for value types (like structs or enums) since they are copied when passed around.
Key Concepts in ARC
-
Reference Counting
- Every class instance has an associated reference count, which is a counter that tracks the number of strong references to that object.
- When the reference count drops to zero, meaning no other parts of your program are using the object, ARC will automatically deallocate the object and free its memory.
-
Strong References
- By default, when you assign an instance of a class to a variable, that variable holds a strong reference to the object.
- As long as there is at least one strong reference to an object, ARC will keep it in memory.
class Car { var model: String init(model: String) { self.model = model } } var car1: Car? = Car(model: "Tesla") var car2 = car1 // Both car1 and car2 hold strong references to the same object.
In this example, both
car1
andcar2
hold strong references to the sameCar
instance, so it remains in memory. -
Weak and Unowned References
-
Weak references: Used to prevent retain cycles where two objects hold strong references to each other, causing them to never be deallocated. A weak reference does not increase the reference count of the object it refers to. If the object it refers to is deallocated, the weak reference automatically becomes
nil
.- Weak references are typically used for delegates or when you don’t want to keep an object alive but still want to refer to it.
class Employee { var name: String var manager: Manager? init(name: String) { self.name = name } } class Manager { var name: String var employees: [Employee] = [] init(name: String) { self.name = name } } var manager: Manager? = Manager(name: "Alice") var employee: Employee? = Employee(name: "John") employee?.manager = manager manager?.employees.append(employee!) // No retain cycle because employee is weakly referenced
-
Unowned references: Similar to weak references, but the key difference is that an unowned reference assumes the object it refers to will never be deallocated while the reference exists. It is used when you know the reference will never become
nil
(typically in cases where one object outlives the other, like when one object owns another object).class Customer { var name: String var card: CreditCard? init(name: String) { self.name = name } } class CreditCard { var number: String var customer: Customer init(number: String, customer: Customer) { self.number = number self.customer = customer } } var customer = Customer(name: "John") var card = CreditCard(number: "1234", customer: customer) customer.card = card // No retain cycle, card refers to customer with unowned reference
-
In the case of unowned references, if the referenced object is deallocated while the reference is still in use, it leads to a runtime crash. Therefore, unowned references are generally used for objects that are logically owned by one another (like an object and its delegate).
-
-
Retain Cycles (Strong Reference Cycles)
- A retain cycle occurs when two objects hold strong references to each other, preventing either from being deallocated because their reference counts never reach zero.
- Retain cycles are common in closures and object relationships, and can lead to memory leaks if not handled correctly.
Example of a Retain Cycle:
class Person { var name: String var friend: Person? init(name: String) { self.name = name } } var person1: Person? = Person(name: "John") var person2: Person? = Person(name: "Alice") person1?.friend = person2 person2?.friend = person1 // Retain cycle here, both instances reference each other
In this example, both
person1
andperson2
have strong references to each other, so they will never be deallocated, resulting in a memory leak. To fix this, you can use weak or unowned references.Correcting the Retain Cycle with Weak References:
class Person { var name: String var friend: Person? init(name: String) { self.name = name } } var person1: Person? = Person(name: "John") var person2: Person? = Person(name: "Alice") person1?.friend = person2 person2?.friend = person1 // No retain cycle if 'friend' is a weak reference
-
ARC and Closures
- Closures can capture and store references to variables and constants from the surrounding context, which can create retain cycles if closures capture self in an object. To avoid memory leaks, you must explicitly declare whether a reference is weak or unowned within closures.
Example of a Retain Cycle in a Closure:
class ViewController { var title = "My View" func setupClosure() { let closure = { print(self.title) // Captures 'self' strongly, leading to a retain cycle } } }
Solution: Use Weak or Unowned References:
class ViewController { var title = "My View" func setupClosure() { let closure: () -> Void = { [weak self] in print(self?.title ?? "No title") // Avoid retain cycle by capturing self weakly } } }
ARC Summary:
- ARC automatically manages memory by keeping track of strong references to class instances.
- When there are no strong references left to an object, ARC deallocates it.
- Weak and unowned references help avoid retain cycles by not increasing reference counts, allowing for proper deallocation of objects.
- Retain cycles can occur when two objects hold strong references to each other, preventing either from being deallocated.
- Memory leaks due to retain cycles can be avoided by using weak or unowned references where appropriate.
ARC makes memory management easier in Swift compared to manual memory management in languages like C or Objective-C, but understanding how references work and being mindful of retain cycles is crucial for writing efficient, memory-safe Swift code.
Read More
If you can’t get enough from this article, Aihirely has plenty more related information, such as Swift interview questions, Swift interview experiences, and details about various Swift job positions. Click here to check it out.
Tags
- Swift
- Swift programming
- Optionals
- Memory management
- Automatic Reference Counting
- ARC
- Closures
- Value types
- Reference types
- Structs
- Classes
- Generics
- Protocols
- Error handling
- Functional programming
- Map
- Filter
- Reduce
- Guard
- If let
- Singleton
- IBOutlet
- IBAction
- Super keyword
- Multithreading
- GCD
- Concurrency
- Async
- Await
- Type inference
- Performance optimization
- Swift interview questions
- IOS development
- Swift best practices
- Swift development tips
- Swift programming interview