본 글은 야곰의 스위프트 프로그래밍: Swift5 교재를 토대로 공부한 내용과 찾아본 내용을 요약한 것입니다.
프로퍼티는 크게 저장 프로퍼티(Stored Properties) 와 연산 프로퍼티(Computed Properties), 타입 프로퍼티(Type Properties)로 나뉩니다. 기존 프로그래밍 언어에서 사용되던 인스턴스 변수는 저장 프로퍼티로, 클래스 변수는 타입 프로퍼티로 구분지을 수 있습니다. 또한, 프로퍼티의 값이 변하는 것을 감시하는 프로퍼티 감시자(Property Observers)도 있습니다.
- 저장 프로퍼티는 클래스와 구조체에서 사용됩니다.
- 특정 클래스나 구조체의 인스턴스와 연관된 값을 저장합니다.
- 상수(let) 또는 변수(var)로 선언할 수 있습니다.
- 초기값을 설정할 수 있으며, 초기화 중에 값을 설정할 수도 있습니다.
struct FixedLengthRange {
var firstValue: Int
let length: Int
}
Swift에서 지연 저장 프로퍼티(Lazy Stored Properties)는 처음 접근될 때까지 초기값이 계산되지 않는 프로퍼티입니다. 즉, 인스턴스 초기화가 완료된 후에도 해당 프로퍼티의 초기값이 즉시 계산되지 않고, 실제로 해당 프로퍼티에 처음으로 접근하는 순간에 초기값이 계산되고 저장됩니다. 이는 초기화 과정에서 복잡하거나 계산 비용이 많이 드는 작업을 지연시키고자 할 때 유용합니다.
지연 저장 프로퍼티는 lazy 키워드를 사용하여 선언합니다. 그리고 중요한 점은 지연 저장 프로퍼티가 반드시 변수(var)로 선언되어야 한다는 것입니다. 왜냐하면 상수(let) 프로퍼티는 인스턴스 초기화가 완료되는 시점에 이미 값을 가지고 있어야 하기 때문입니다.
class DataLoader {
// 이 데이터 로딩 작업은 시간이 많이 소요될 수 있습니다.
func loadData() -> [String] {
// 복잡한 데이터 로딩 로직을 여기에 구현...
return ["One", "Two", "Three"]
}
}
class DataManager {
lazy var dataLoader = DataLoader()
func processData() {
// dataLoader 프로퍼티에 처음 접근하는 순간 loadData가 호출됩니다.
let data = dataLoader.loadData()
print(data)
}
}
let manager = DataManager()
manager.processData() // 이 시점에서 dataLoader의 loadData 메서드가 호출됩니다.
- 연산 프로퍼티는 클래스, 구조체, 열거형에서 사용됩니다.
- 인스턴스 내/외부의 값을 연산하여 적절한 값을 돌려주는 접근자(getter) 역할이나 은닉화된 내부의 프로퍼티 값을 간접적으로 설정하는 설정자(setter)의 역할을 할 수도 있습니다.
- 값을 저장하지 않고, 대신 get과 set 블록을 통해 다른 프로퍼티와 값들에 의존하여 값을 계산합니다.
- set에서는 새로운 값을 받아 필요한 연산을 수행할 수 있으며, get에서는 요청된 값을 계산하여 반환합니다.
- 읽기 전용 연산 프로퍼티는 get 블록만을 가지며, 이 경우 get 키워드와 괄호를 생략할 수 있습니다.
struct Point {
var x: Int, y: Int
}
struct Size {
var width: Int, height: Int
}
struct Rect {
var origin: Point
var size: Size
var center: Point { // 연산 프로퍼티 예시
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set(newCenter) {
origin.x = newCenter.x - (size.width / 2)
origin.y = newCenter.y - (size.height / 2)
}
}
}
프로퍼티 감시자를 사용하면 프로퍼티의 값이 변경됨에 따라 적절한 작업을 취할 수 있습니다. 프로퍼티 감시자는 프로퍼티의 값이 새로 할당될 때마다 호출합니다. 프로퍼티 감시자는 지연 저장 프로퍼티에 사용할 수 없으며 오로지 일반 저장 프로퍼티에만 적용할 수 있습니다. 또한, 프로퍼티 재정의해 상속받은 저장 프로퍼티 또는 연산 프로퍼티에도 적용할 수 있습니다. 물론 상속받지 않은 연산 프로퍼티에는 프로퍼티 감시자를 사용할 필요가 없으며 할 수도 없습니다. 연산 프로퍼티의 접근자와 설정자를 통해 프로퍼티 감시자를 구현할 수 있기 때문입니다.
willSet
: 프로퍼티의 값이 변경되기 직전에 호출
: 전달되는 전달인자는 프로퍼티가 변경될 값(newValue)
didSet
: 프로퍼티의 값이 변경된 직후에 호출
: 전달되는 전달인자는 프로퍼티가 변경되기 전의 값(oldValue)
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("About to set totalSteps to \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue {
print("Added \(totalSteps - oldValue) steps")
}
}
}
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// 출력: About to set totalSteps to 200
// 출력: Added 200 steps
stepCounter.totalSteps = 360
// 출력: About to set totalSteps to 360
// 출력: Added 160 steps
연산 프로퍼티와 프로퍼티 감시자는 전역변수와 지역변수 모두에 사용할 수 있습니다. 함수나 메서드, 클로저, 클래스, 구조체, 열거형 등의 범위 안에 포함되지 않았던 변수나 상수, 즉 우리가 프로퍼티를 다루기 전에 계속해서 사용했던 변수와 상수는 모두 전역변수 또는 전역상수에 해당됩니다.
- 타입 프로퍼티는 특정 타입 인스턴스가 아닌 타입 자체에 속하는 프로퍼티입니다.
- 모든 인스턴스가 공유하는 값을 정의할 때 사용합니다.
- static 키워드를 사용해 선언하며, 클래스에서 오버라이드를 허용하려면 class 키워드를 사용합니다.
- 저장 타입 프로퍼티는 항상 기본값을 가져야 하며, 지연 초기화됩니다.
struct SomeStructure {
// 저장 타입 프로퍼티
static var storedTypeProperty = "Some value."
// 연산 타입 프로퍼티
static var computedTypeProperty: Int {
return 1
}
}
enum SomeEnumeration {
// 저장 타입 프로퍼티
static var storedTypeProperty = "Some value."
// 연산 타입 프로퍼티
static var computedTypeProperty: Int {
return 6
}
}
// class로 선언된 타입 프로퍼티는 클래스에서만 사용할 수 있고, 이를 통해 선언된 계산 타입 프로퍼티는 서브클래스에서 오버라이드할 수 있습니다.
//오직 계산 타입 프로퍼티에만 사용될 수 있으며, 저장 타입 프로퍼티에는 사용할 수 없습니다.
class ParentClass {
class var overrideableComputedTypeProperty: Int {
return 107
}
}
class SubClass: ParentClass {
override class var overrideableComputedTypeProperty: Int {
return 108
}
}
Swift에서 키 경로(KeyPath)는 객체의 특정 프로퍼티에 대한 참조를 나타내는 타입입니다. 키 경로를 사용하면 프로퍼티의 값에 간접적으로 접근하고, 프로퍼티의 값을 검색하거나 수정하는 등의 작업을 수행할 수 있습니다. 이는 프로그래밍에서 데이터에 대한 유연한 작업을 가능하게 하며, 특히 컬렉션의 요소를 다루거나, 모델 객체의 특정 필드에 대한 작업을 일관되고 타입 안전한 방법으로 수행할 때 유용합니다.
Swift의 키 경로 기능은 Swift 4에서 도입되었으며, \ 연산자를 사용하여 표현합니다. 기본적으로 Swift에서는 두 가지 타입의 키 경로를 제공합니다.
KeyPath
: 읽기 전용 접근을 위한 키 경로입니다.
특정 타입의 인스턴스에서 특정 타입의 값을 가진 프로퍼티에 대한 읽기 접근을 나타냅니다.
프로퍼티의 값을 읽을 수는 있지만, 수정할 수는 없습니다.
WritableKeyPath
: 쓰기 가능한 접근을 위한 키 경로입니다.
특정 타입의 인스턴스에서 특정 타입의 값을 가진 프로퍼티에 대한 읽기 및 쓰기 접근을 나타냅니다.
프로퍼티의 값을 읽고 수정할 수 있습니다.
struct Person {
var name: String
var age: Int
}
let nameKeyPath = \Person.name
let ageKeyPath = \Person.age
let person = Person(name: "Alice", age: 30)
// KeyPath를 사용하여 값을 읽습니다.
let name = person[keyPath: nameKeyPath]
let age = person[keyPath: ageKeyPath]
print(name) // 출력: Alice
print(age) // 출력: 30
// WritableKeyPath를 사용하여 값을 수정합니다.
var mutablePerson = person
mutablePerson[keyPath: nameKeyPath] = "Bob"
print(mutablePerson.name) // 출력: Bob
메서드는 특정 타입에 관련된 함수를 뜻합니다. 클래스, 구조체, 열거형 등은 실행하는 기능을 캡슐화한 인스턴스 메서드를 정의할 수 있습니다. 메서드는 크게 인스턴스 메서드와 타입 메서드로 구분됩니다.
인스턴스 메서드는 특정 타입의 인스턴스에 속한 함수입니다. 이 메서드들은 해당 타입의 개별 인스턴스에 호출되어 인스턴스 내의 데이터를 액세스하고 수정할 수 있습니다. 인스턴스 메서드는 해당 타입의 개별 인스턴스에 속한 프로퍼티나 다른 메서드에 접근할 때 사용됩니다.
class Counter {
var count = 0
func increment() {
count += 1
}
func increment(by amount: Int) {
count += amount
}
func reset() {
count = 0
}
}
let counter = Counter()
counter.increment()
print(counter.count) // 1
counter.increment(by: 5)
print(counter.count) // 6
counter.reset()
print(counter.count) // 0
// 인스턴스 프로퍼티나 메서드에 접근
struct Point {
var x = 0.0, y = 0.0
func isToTheRightOf(x: Double) -> Bool {
return self.x > x
}
}
// 이니셜라이저에 사용
struct Size {
var width: Double
var height: Double
init(width: Double, height: Double) {
self.width = width
self.height = height
}
}
타입 메서드는 타입 자체에 호출되는 메서드입니다. 즉, 이 메서드들은 인스턴스가 아닌 타입에 속해 있으며, 타입 레벨에서 작업을 수행합니다. 타입 메서드는 static 키워드를 사용하여 정의되거나, 클래스의 경우 오버라이드가 가능한 타입 메서드를 만들기 위해 class 키워드를 사용하여 정의됩니다. 타입 메서드 내부에서는 self는 타입 자체를 가리킵니다.
class AClass {
static func staticTypeMethod() {
print("AClass staticTypeMethod")
}
class func classTypeMethod() {
print("AClass classTypeMethod")
}
}
class BClass: AClass {
/*
// 오류. 재정의 불가
override static func staticTypeMethod() {
}
*/
override class func classTypeMethod() }
print("BClass classTypeMethod")
}
}
타입 메서드는 인스턴스 메서드와 달리 self 프로퍼티가 타입 그 자체를 가리킨다는 점이 다릅니다. 인스턴스 메서드에서 self가 인스턴스를 가리킨다면 타입 메서드의 self는 타입을 가리킵니다. 그래서 타입 메서드에서 self 프로퍼티를 사용하면 타입 프로퍼티 및 타입 메서드를 호출할 수 있습니다.
// 시스템 음량은 한 기기에서 유일한 값
struct SystemVolume {
// 타입 프로퍼티를 사용하면 언제나 유일한 값이 된다.
static var volume: Int = 5
// 타입 프로퍼티를 제어하기 위해 타입 메서드를 사용
static func mute() {
self.volume = 0 // SystemVolume.volume = 0과 같은 표현
// Self.volume = 0과 같은 표현
}
}