[Swift] property

·2024년 6월 23일
0

Swift 문법

목록 보기
4/16

프로퍼티

클래스나 구조체 안의 변수나 상수를 흔히 프로퍼티라고 칭함.

  • 저장 프로퍼티
    • 말 그대로 값을 저장
  • 연산 프로퍼티
    • 값을 저장하지 않고 계산
  • 타입 프로퍼티
    • 저장 프로퍼티, 계산 프로퍼티는 특정 타입의 인스턴스와 연결되지만, 타입 자체와 연결되는 타입 프로퍼티도 존재함


1. 저장 프로퍼티

  • 값을 저장하고 있는 프로퍼티
  • 클래스, 구조체에서는 사용이 가능하고 열거형에서는 불가능
struct FixedRadiusCircle {
    var x: Int // 중심의 x 좌표
    var y: Int // 중심의 y 좌표
    let radius: Int // 반지름
}
var tenCircle = FixedRadiusCircle(x: 0, y: 0, radius: 10)
tenCircle.x = 3
print(tenCircle) // FixedRadiusCircle(x: 3, y: 0, radius: 10)

변수로 선언한 구조체는 프로퍼티의 값을 변경할 수 있다.


🚨 에러 
tenCircle.radius = 3 
// Cannot assign to property: 'radius' is a 'let' constant

구조체를 변수로 선언했지만 프로퍼티가 let으로 선언되었으므로 에러


🚨 에러
let threeCircle = FixedRadiusCircle(x: 0, y: 0, radius: 3)
threeCircle.x = 3
// Cannot assign to property: 'threeCircle' is a 'let' constant

구조체를 상수로 선언했다면 구조체 인스턴스의 프로퍼티는 변경할 수 없다.


Q. 다만, 상수로 선언한 클래스 인스턴스의 변수 프로퍼티는 변경이 가능한데, 그 이유는?

  • 클래스 인스턴스가 참조 타입이기 때문 → 상수에는 클래스 인스턴스의 메모리 주소가 저장됨
  • 인스턴스의 내부 프로퍼티를 변경한다 =\= 상수에 저장된 값을 변경한다


1-1. 지연 저장 프로퍼티(Lazy Stored Properties)

이름 그대로 해석해 보자!

  • 값이 처음으로 사용되기 전에는 계산되지 않는 프로퍼티
  • lazy 키워드 붙이기
  • 지연 프로퍼티는 반드시 변수(var) 선언하기
    • 상수는 초기화 되기 전에 값이 있어야 하는 속성을 가지기 때문임

지연 프로퍼티를 사용하면 좋은 점?

  • 처음 접근할 때 계산되므로 성능이 향상되고 메모리 낭비도 줄일 수 있다

ex)

  • test를 저장 프로퍼티로 선언했을 때
<class Test {
    init() {
        print("언제 실행될까요?")
    }
}

class Min {
    var test = Test()
}

let min = Min()

// 언제 실행될까요? 

  • test를 지연 저장 프로퍼티로 선언했을 때
<class Test {
    init() {
        print("언제 실행될까요?")
    }
}

class Min {
    lazy var test = Test()
}

let min = Min()

아무것도 실행되지 않음. 이유는 당연히 지연 저장 프로퍼티(test)에 접근하지 않았으니 아직 초기화가 되지 않았다!


  • test를 지연 저장 프로퍼티로 선언하고, 접근했을 때
class Test {
    init() {
        print("언제 실행될까요?")
    }
}

class Min {
    lazy var test = Test()
}

let min = Min()
min.test // 언제 실행될까요?

Q. 그럼 그냥 모든 프로퍼티를 lazy로 선언하면 안 될까?

lazy 수식어가 표시된 프로퍼티는 여러 쓰레드에서 동시에 접근되고,
프로퍼티가 아직 초기화되지 않은 경우 프로퍼티가 한번만 초기화 된다는 보장이 없습니다.

  • The Swift Programming Language


2. 계산 프로퍼티(Computed Properties)

  • 클래스, 구조체, 열거형에서 선언 가능
  • 저장 프로퍼티와 달리 실제 값을 저장하지 않음
  • gettersetter 로 값을 탐색하고 간접적으로 다른 프로퍼티 값을 설정할 수 있는 방법 제공
  • 값을 저장하지 않으므로 var 로 선언해야 함

    상수는 초기화 되기 전에 값이 있어야 하는 속성을 가짐


  • getter(접근자)
    - 다른 저장 프로퍼티의 값을 이용하여 연산하고 return
    - 다른 프로퍼티들을 적절히 연산해서 구함
  • setter(설정자)
    - 다른 저장 프로퍼티에 값을 설정
    - 파라미터(newValue)가 존재
    - newValue 라는 이름에서도 알 수 있듯, 해당 프로퍼티에 값이 주어졌을 때 이를 newValue로 칭하여 연산에 활용함
    - 파라미터 명을 바꿔 사용할 수 있음
    - 파라미터를 생략할 수 있음, 생략 시에는 newValue 로만 사용 가능

따라서 gettersetter 을 사용하기 위해서는 다른 저장 프로퍼티가 반드시 필요하다!


ex)

struct Student {
    // 저장 프로퍼티
    var name: String
    var koreanAge: Int
    
    // 연산 프로퍼티
    var westernAge: Int {
        get {
            let age = koreanAge - 1
            return age
        }
        
        set(newValue) {
            koreanAge = newValue + 1
        }
    }
}

저장 프로퍼티로 name, koreanAge를, 연산 프로퍼티로 westernAge를 선언한 Student 구조체를 만들었다.

get - koreanAge 프로퍼티를 이용하여 연산한 값을 return
set - koreanAge 프로퍼티에 값을 설정

var student = Student(name: "min", koreanAge: 24)
student.westernAge // 23
student.westernAge = 25
student.koreanAge // 26

name을 min, koreanAge 를 24로 초기화한 student를 선언했다.
이때 westernAge 는 get이 리턴하는 koreanAge - 1 이다.
westernAge 를 25로 변경한 뒤, koreanAge 를 출력하면 26이 된다.


getter의 특성

  • 변수나 상수를 만들지 않고 바로 연산값을 반환할 수 있음
  • return을 생략할 수 있음
    var westernAge: Int {
        get {
            // return koreanAge - 1
						koreanAge - 1
        }
        
        set(newValue) {
            koreanAge = newValue + 1
        }
    }

setter의 특성

  • 파라미터명 변경 가능
// 연산 프로퍼티
    var westernAge: Int {
        get {
            let age = koreanAge - 1
            return age
        }
        
        set(newWesternAge) {
            koreanAge = newWesternAge + 1
        }
    }
  • 파라미터 생략 시 newValue 만 이용 가능
	var westernAge: Int {
        get {
            let age = koreanAge - 1
            return age
        }
        
        set {
            koreanAge = newValue + 1
        }
	}

Q. 연산 프로퍼티를 사용하는 이유는?

연산 프로퍼티를 사용하지 않고, 위와 동일한 작업을 하려면 메서드 2개를 통해 구현해야 함

func setWesternAge() -> Int {
     return koreanAge - 1
}

mutating func setNewKoreanAge(_ westernAge: Int) {
     koreanAge = westernAge + 1
}
  • gettersetter 을 통해 표현하는 것이 더 간결하고 가독성이 좋은 것 같다.

읽기 전용 연산 프로퍼티(Read-Only Computed Properties)

  • getter 만 제공되고, setter 은 제공하지 않는 프로퍼티
  • getter 만 제공된다: 연산을 통한 return 값만 제공
struct Cuboid {
    var width = 0.0, height = 0.0, depth = 0.0
    
    // read-only
    var volume: Double {
        return width * height * depth
    }
}
let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
// the volume of fourByFiveByTwo is 40.0


3. 타입 프로퍼티

  • 클래스, 구조체, 열거형에서 사용 가능
  • 인스턴스가 아닌 특정 타입에 속한 프로퍼티 (타입 자체에 속하는 프로퍼티)
  • 저장 타입 프로퍼티, 연산 타입 프로퍼티 존재
  • 특정 타입의 모든 인스턴스에서 공용으로 혹은 공통으로 사용할 수 있는 변수 정의

인스턴스가 생성됐을 때 타입 프로퍼티가 매번 생성되는 게 아니라, 호출했을 때 한번 메모리에 올라간 뒤로는 해당 프로퍼티를 공유

따라서 인스턴스 생성 시점의 초기화와 관련이 없어, 초기값을 따로 생성할 수 없기에 초기값 지정이 필요함
(호출한다는 것은 지연 저장 프로퍼티처럼 접근했을 때를 의미함.)


선언

  • 저장 프로퍼티와 연산 프로퍼티에 앞에 static 키워드 붙이기
  • class 타입 연산 프로퍼티의 경우 class 키워드를 붙여 서브 클래스에서 오버라이딩할 수 있음
    • 밑의 [**클래스 타입의 연산 프로퍼티]** 참고

타입 프로퍼티에 접근 시에는 인스턴스를 생성하여 접근하는 것이 아닌 타입 자체에 접근해야 함

class TestClass {
    static let storedTypeProperty = "hi"
    static var storedTypeProperty2 = "hi2"
    static var computedTypeProperty: Int {
        return 6
    }
}

print(TestClass.storedTypeProperty) // hi
TestClass.storedTypeProperty2 = "hi2 is gone!"
print(TestClass.storedTypeProperty2) // hi2 is gone!
print(TestClass.computedTypeProperty) // 6

클래스 타입 연산 프로퍼티

  • 서브 클래스에서 재정의 가능
class Vehicle {
    class var name: String {
        return "Vehicle"
    }
}

class Car: Vehicle {
    override class var name: String {
        return "Car"
    }
}

Vehicle.name // "Vehicle"
Car.name // "Car"


5. 프로퍼티 옵저버(Property Observers)

  • observer: 관찰자 → 프로퍼티의 값이 변경되는지 관찰하고 응답

  • 새 값이 설정(set) 될 때마다 이 이벤트를 감지할 수 있는 옵저버 제공

  • 새 값이 이전과 같더라도 호출됨

  • 서브 클래스의 프로퍼티에 옵저버 정의할 수 있음

  • 지연 저장 프로퍼티에서는 사용할 수 없음

    • 원래는 안 됐었는데, Swift 5.3부터는 가능해짐

      class TestClass {
        lazy var test: Int = 0 {
            willSet {
                print("willSet called, newValue:", newValue)
            }
            didSet { 
                print("didSet called!, oldValue:", oldValue)
            }
        }
      }
      
      let testClass = TestClass()
      testClass.test = 3
      /*
       willSet called, newValue: 3
       didSet called!, oldValue: 0
       */
  • 연산 프로퍼티는 setter 에서 값의 변화를 감지하기 때문에 옵저버를 따로 정의할 필요 없음


willSet

  • 값이 저장되기 바로 직전에 호출
  • 새 값을 나타내는 파라미터 newValue 존재
  • 파라미터명 변경 가능
    • 파라미터명 지정하지 않으면 newValue 사용

didSet

  • 새 값이 저장되고 난 직후 호출
  • 바뀌기 전 값 파라미터 oldValue 존재
  • 파라미터명 변경 가능
    • 파라미터명 지정하지 않으면 oldValue 사용


ex)

class WeightArchive {
    var myWeight: Int = 0 {
        willSet {
            print("Today: \(newValue)kg ")
        }
        
        didSet {
            print("YesterDay: \(oldValue)kg")
        }
    }
}

몸무게를 기록하고 이전 몸무게와 현재 몸무게를 확인할 수 있는 class를 구현했다.

let weightArchive = WeightArchive()
weightArchive.myWeight = 55

// Today: 55kg
// YesterDay: 0kg

새 값인 55와 이전 값인 0이 출력되는 것을 알 수 있다.

weightArchive.myWeight = 53
// Today: 53kg
// YesterDay: 55kg

새 값인 53과 이전 값인 55가 출력되는 것을 알 수 있다.

class WeightArchive {

    var myWeight: Int = 0 {
        willSet(todayWeight) {
            print("Today: \(todayWeight)kg ")
        }
        
        didSet(yesterDayWeight) {
            print("YesterDay: \(yesterDayWeight)kg")
        }
    }
}

파라미터 명을 다음과 같이 변경할 수도 있다.


Reference

https://yagom.github.io/swift_basic/contents/13_property/
https://babbab2.tistory.com/120

0개의 댓글

관련 채용 정보