Working with Classes
1. Defining Classes
| Feature | Description | Example |
|---|---|---|
| Class Declaration | Defines a new class using the class keyword. |
|
| Inheritance | Classes can inherit from a single parent class using :. |
|
| Properties | Variables and constants stored in a class. | |
| Methods | Functions defined within a class. | |
| Initializers | Code that runs when an instance is created. | |
| Deinitializers | Code that runs when an instance is deallocated. | |
2. Creating Class Instances
| Method | Description | Example |
|---|---|---|
| init() | Default initializer for a class | |
| init(parameters) | Custom initializer with parameters | |
| convenience init() | Convenience initializer that calls another initializer | |
Example: Creating Instances with Different Initializers
class Student {
var name: String
var gradeLevel: Int
init(name: String, gradeLevel: Int) {
self.name = name
self.gradeLevel = gradeLevel
}
convenience init(name: String) {
self.init(name: name, gradeLevel: 1)
}
}
let student1 = Student(name: "Alice", gradeLevel: 10)
let student2 = Student(name: "Bob")
3. Understanding Reference Semantics
| Concept | Description | Notes |
|---|---|---|
| Reference Type | Classes are reference types in Swift. | Multiple variables can point to the same instance. |
| Value Type | Structs and enums are value types in Swift. | Each variable has its own copy of the data. |
| Memory Allocation | Reference types are allocated on the heap. | Value types are typically allocated on the stack. |
| Identity vs Equality | Reference types can be checked for identity using ===. | Value types are compared by value, not identity. |
Example: Reference Type Behavior
class Person {
var name: String
init(name: String) {
self.name = name
}
}
var person1 = Person(name: "Alice")
var person2 = person1
person2.name = "Bob"
print(person1.name) // Output: Bob
print(person1 === person2) // Output: true
Example: Value Type Behavior
struct Person {
var name: String
}
var person1 = Person(name: "Alice")
var person2 = person1
person2.name = "Bob"
print(person1.name) // Output: Alice
print(person1 == person2) // Output: true
Note: Understanding reference semantics is crucial for managing memory and avoiding unexpected behavior in Swift applications.
4. Using Inheritance
| Feature | Description | Example |
|---|---|---|
| class inheritance | Defines a new class that inherits from a superclass | |
| overriding methods | Replaces a superclass method with a new implementation | |
| super keyword | Accesses the superclass's implementation of a method or property | |
Example: Inheritance Hierarchy
class Shape {
var color: String = "blue"
func area() -> Double {
return 0.0
}
}
class Rectangle: Shape {
var width: Double = 0.0
var height: Double = 0.0
override func area() -> Double {
return width * height
}
}
class Square: Rectangle {
var side: Double = 0.0
override var width: Double {
get {
return side
}
set {
side = newValue
}
}
override var height: Double {
get {
return side
}
set {
side = newValue
}
}
}
Note: Swift supports single inheritance only. A class can inherit from one superclass, but can conform to multiple protocols.
5. Overriding Methods
| Method | Description | Notes |
|---|---|---|
| override | Keyword to indicate method overriding | Must be used when overriding a superclass method |
Example: Overriding a Method
class Animal {
func makeSound() {
print("Animal makes a sound")
}
}
class Dog: Animal {
override func makeSound() {
print("Dog barks")
}
}
let dog = Dog()
dog.makeSound() // Output: "Dog barks"
Note: The
override keyword is required when overriding a method from a superclass.6. Using Super to Access Parent
| Feature | Description | Usage |
|---|---|---|
| super | Keyword to access parent class methods and properties | super.methodName() or super.propertyName |
Example: Calling Parent Method
class Vehicle {
func startEngine() {
print("Vehicle engine started")
}
}
class Car: Vehicle {
override func startEngine() {
super.startEngine()
print("Car engine started")
}
}
let myCar = Car()
myCar.startEngine()
// Output:
// Vehicle engine started
// Car engine started
Example: Accessing Parent Property
class Animal {
var name: String = "Generic Animal"
}
class Dog: Animal {
override var name: String {
get {
return super.name + " (Dog)"
}
set {
super.name = newValue
}
}
}
let myDog = Dog()
print(myDog.name)
// Output: Generic Animal (Dog)
7. Preventing Overrides
| Feature | Description | Usage |
|---|---|---|
| final | Prevents a class from being subclassed | final class MyClass { } |
| final (method) | Prevents a method from being overridden | final func myMethod() { } |
Example: Using final to prevent subclassing
final class BaseClass {
func doSomething() {
print("Base implementation")
}
}
// This will cause a compile-time error
// class DerivedClass: BaseClass { } // ❌ Error: 'BaseClass' is final
Example: Using final to prevent method override
class Parent {
final func protectedMethod() {
print("This cannot be overridden")
}
}
class Child: Parent {
// This will cause a compile-time error
// override func protectedMethod() { } // ❌ Error: cannot override 'protectedMethod'
}
8. Using Type Properties and Methods
| Property/Method | Description | Example |
|---|---|---|
| type property | Shared by all instances of a type, declared with static or class |
|
| type method | Callable on the type itself, not instances; declared with static or class |
|
Example: Type Properties and Methods in a Class
class Vehicle {
static var totalVehicles = 0
let model: String
init(model: String) {
self.model = model
Vehicle.totalVehicles += 1
}
static func getVehicleCount() -> Int {
return totalVehicles
}
}
let car1 = Vehicle(model: "Tesla Model S")
let car2 = Vehicle(model: "Ford Mustang")
print(Vehicle.getVehicleCount()) // 2
Note:
static properties and methods are not overridden in subclasses, whereas class ones can be.9. Implementing Deinitializers
Deinitializers are used to perform cleanup tasks before an instance of a class is deallocated. They are defined using the deinit keyword and are called automatically when an instance is about to be deallocated.
| Feature | Description | Notes |
|---|---|---|
| deinit | deinit keyword |
Used to define a deinitializer |
| Execution | Called before deallocation | Automatically invoked by the runtime |
| Limitation | Cannot be called explicitly | No parameters allowed |
Example: Basic Deinitializer
class MyClass {
var name: String
init(name: String) {
self.name = name
print("Initializing \(name)")
}
deinit {
print("Deinitializing \(name)")
}
}
let obj = MyClass(name: "Test")
// Output:
// Initializing Test
// When obj goes out of scope, deinit is called
// Output:
// Deinitializing Test
Note: Deinitializers are not called if the instance is still referenced by a strong reference.
Warning: Avoid retaining strong references in deinitializers to prevent retain cycles.
10. Understanding Identity Operators (===, !==)
| Operator | Description | Usage | Notes |
|---|---|---|---|
| === | Identity operator checks if two constants or variables refer to the same instance | if object1 === object2 { ... } |
Unlike ==, which compares values, === compares object identity |
| !== | Identity operator checks if two constants or variables refer to different instances | if object1 !== object2 { ... } |
Useful for checking if two references point to different objects |
Example: Using Identity Operators
class Person {
var name: String
init(name: String) {
self.name = name
}
}
let person1 = Person(name: "Alice")
let person2 = Person(name: "Alice")
let person3 = person1
print(person1 === person2) // false - different instances
print(person1 === person3) // true - same instance
Note:
=== and !== are only available for class instances, not value types like structs or enums.