Swift 5 : 프로퍼티(Property)

버들비·2020년 7월 15일
0

Swift

목록 보기
7/10

프로퍼티는 클래스, 구조체, 열거형과 관련된 값이다. 프로퍼티에는 저장 프로퍼티와 계산 프로퍼티가 있다. 저장 프로퍼티는 클래스와 구조체에서만 사용 가능하고, 계산 프로퍼티는 클래스와 구조체, 열거형 모두에서 사용 가능하다. 저장 프로퍼티는 프로퍼티 옵저버를 이용해 값이 변할때마다 모니터링할 수 있다.

저장 프로퍼티(Stored Property)

단순히 값을 저장하는 프로퍼티. let 또는 var 를 이용한다.

public private(set) var

get에 대해서는 public 하지만, set에 대해서는 private 한 프로퍼티를 설정할 수 있다.

class Human {
    public private(set) var name: String
    
    init(name: String) {
        self.name = name
    }
}

var john = Human(name: "John")

print(john.name) // John 이 출력
john.name = "paul" // Error!

john이라는 인스턴스 내부의 name은 외부에서 접근이 가능하되, 외부에서 변경이 불가능하다. 변경은 클래스 내부에서만 가능하다.

구조체를 상수(let)로 선언할 때 저장 프로퍼티의 불변성

let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// 범위 값은 0, 1, 2, 3 입니다.
rangeOfFourItems.firstValue = 6
// 에러 발생!

구조체 FixedLengthRange는 상수(let)로 선언되었기 때문에 저장 프로퍼티를 변경할 수 없다.
하지만 클래스는 let 으로 선언해도 프로퍼티 변경이 가능하다. 클래스는 참조 타입이기 때문.

프로퍼티의 지연저장(Lazy stored property)

프로퍼티의 값이 사용되기 전에 미리 계산되는걸 원치 않을경우, 프로퍼티 선언 앞에 lazy 키워드를 붙이면 된다.
지연 프로퍼티는 반드시 변수(var)로 선언해야 된다. 지연 프로퍼티는 처음 사용되기 전에는 값을 갖지 않는 프로퍼티이기 때문이다.

지연 프로퍼티는 프로퍼티가 특정 요소에 의존적이어서 적절한 값을 바로 알지 못하는 경우에 유용하다. 또 복잡한 계산이 필요한 작업을 지연 프로퍼티로 선언하면 인스턴스 초기화 시점에서 복잡한 계산을 피할 수 있다.

class DataImporter {
    /*
        DataImporter는 외부 파일에서 데이터를 가져오는 클래스입니다.
         이 클래스는 초기화 하는데 매우 많은 시간이 소요된다고 가정하겠습니다.
     */
    var filename = "data.txt"
    // 데이터를 가져오는 기능의 구현이 이 부분에 구현돼 있다고 가정
}

class DataManager {
    lazy var importer = DataImporter()
    var data = [String]()
    // 데이터를 관리하는 기능이 이 부분에 구현돼 있다고 가정
}

let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// DataImporter 인스턴스는 이 시점에 생성돼 있지 않습니다.

print(manager.importer.filename)
// the DataImporter 인스턴스가 생성되었습니다.
// "data.txt" 파일을 출력합니다.

DataManager라는 클래스를 선언하고 이 클래스는 데이터를 가져오는 DataImporter클래스를 갖고 있습니다. 그리고 이 DataImporter는 실제 디스크 파일에서 데이터를 가져오기 때문에 초기화시 많은 시간이 소요됩니다. 그래서 이 클래스를 지연 프로퍼티(lazy var importer = DataImporter()) 로 선언합니다. 이 프로퍼티는 코드에서 볼 수 있듯 DataManager 인스턴스 manager를 생성하고 거기에 data 를 넣어도 그 시점에 DataImporter인스턴스는 생성돼 있지 않습니다. 다시 말하면 지연 프로퍼티로 선언해 놓았기 때문에 실제 그 프로퍼티를 사용하기 전에는 복잡하고 시간일 오래 소요되는 연산을 할 필요가 없다는 것입니다.
manager.importer.filename가 실행돼 실제 importer 프로퍼티에 처음 접근할 때 비로소 importer인스턴스는 생성됩니다.

계산 프로퍼티(Computed Property)

계산 프로퍼티는 실제 값을 저장하는게 아니라, getter 와 setter를 이용해 값을 탐색하고 다른 프로퍼티 값을 설정한다.
다음은 get 과 set 을 이용한 예제 코드이다.

class TestGetAndSet {
    var testX : Int = 1
    var x: Int {
        get {
            return testX
        }
        set(newValue) {
            testX = newValue * 2
        }
    }
}
var test1: TestGetAndSet = TestGetAndSet()
test1.x = 10
print(test1.x)	// 20

class TestGetOnly {
    var testX : Int = 1
    var x: Int {
        get {
            return testX
        }
    }
}
var test2: TestGetOnly = TestGetOnly()
print(test2.x)	// 1

클래스 내에 var x: Int 부분이 계산 프로퍼티이다. get 과 set 둘다 사용할 경우, test1.x=10 으로 프로퍼티에 접근이 가능하다.
아래쪽 TestGetOnly 의 경우 test2.x=10 을 치면 에러가 뜬다. set 이 없는 경우 get-only 프로퍼티이기에 값을 할당할 수 없다.
연산 프로퍼티는 값이 고정돼 있지 않기 때문에 var 로 선언되어야 한다.
set 에서 (newValue) 부분의 이름은 다른것으로 바꿔도 무관하다.

set(itIsOkay) {
            testX = itIsOkay * 2
        }

setter 의 파라미터명을 생략해도 된다. 이 경우 set 내부에서 newValue 키워드를 사용해야 한다.

set {
    testX = newValue * 2
    }

계산 프로퍼티에 대한 조금 더 복잡한 예제는 다음과 같다.

struct Point {
    var x = 0.0, y = 0.0
}
struct Size {
    var width = 0.0, height = 0.0
}
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)
        }
    }
}
var square = Rect(origin: Point(x: 0.0, y: 0.0),
                  size: Size(width: 10.0, height: 10.0))
let initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0) 
// .center 라는 연산 프로퍼티 setter에 Point(x: 15.0, y: 15.0) 이 전달된다.
// set 은 origin.x 와 origin.y 에 각각 10.0이라는 값을 할당한다.
// get 에서 새로운 centerX와 centerY 가 연산된다. 그리고 연산된 값을 기반으로 한 새로운 좌표값 구조체 Point(x: centerX, y: centerY)를 반환한다.
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// "square.origin is now at (10.0, 10.0)" 출력

프로퍼티 옵저버

프로퍼티에서 새 값이 설정(set)될 때마다 이벤트를 감지할 수 있는 기눙. 새 값이 이전값과 같은 값이라도 프로퍼티 옵저버는 항상 호출된다.
프로퍼티 옵저버는 지연 저장 프로퍼티에서는 사용될 수 없다.
계산 프로퍼티는 setter 에서 값의 변화를 감지 할 수 있기 때문에 프로퍼티 옵저버를 정의할 필요가 없다.
프로퍼티에서는 다음 두가지 옵저버를 제공한다.

  • willSet : 값이 저장되기 바로 직전에 호출 됨
  • didSet : 새 값이 저장되고 난 직후에 호출 됨
    willSet에서는 새 값의 파라미터명을 지정할 수 있는데, 지정하지 않으면 기본 값으로 newValue를 사용한다.
    didSet에서는 바뀌기 전의 값의 파라미터명을 지정할 수 있는데, 지정하지 않으면 기본 값으로 oldValue를 사용한다.

다음은 프로퍼티 옵저버 예제이다.

class StepCounter {
    var totalSteps: Int = 0 {
        willSet(newTotalSteps) {
            print("About to set totalSteps : \(totalSteps) to \(newTotalSteps)")
        }
        didSet {
            if totalSteps > oldValue  {
                print("Added \(totalSteps - oldValue) steps")
            }
        }
    }
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// About to set totalSteps : 0 to 200
// Added 200 steps
stepCounter.totalSteps = 360
// About to set totalSteps to 360
// Added 160 steps
stepCounter.totalSteps = 896
// About to set totalSteps to 896
// Added 536 steps

totalSteps 라는 변수가 바뀌기 직전 willSet 에 해당하는 코드가 실행되고, 변수가 바뀐 뒤에는 didSet 에 해당되는 코드가 실행됐다.
willSet 은 newValue 의 파라미터명을 newTotalSteps로 정의했고, didSet은 기본값인 oldValue 를 사용했다.

타입 프로퍼티

어떤 클래스 내의 프로퍼티를 사용하고 싶은데, 클래스 전체를 메모리에 할당해서 사용할 필요는 없을때 사용하는 방법.
타입 프로퍼티는 특정 타입에 속한 프로퍼티로, 그 타입에 해당하는 단 하나의 프로퍼티만 생성된다.
인스턴스 프로퍼티와 다르게 타입 프로퍼티는 항상 초기값을 지정해야한다. 타입프로퍼티에는 이니셜라이저가 없어서 초기화 되지 않기 때문이다.

타입 프로퍼티는 static 또는 class 키워드를 사용한다. static 으로 선언된 타입 프로퍼티는 오버라이딩이 불가능 하고, class 로 선언된 타입 프로퍼티는 오버라이딩이 가능하다.

class SomeClass {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 27
    }
    class var overrideableComputedTypeProperty: Int {
        return 107
    }
}
print(SomeClass.computedTypeProperty)
// Prints "27"

변수에 클래스를 할당해 인스턴스를 형성하지 않고도, SomeClass.computedTypeProperty 를 통해 프로퍼티를 사용할 수 있다.

타입 프로퍼티 뿐만 아니라 타입 메소드도 존재한다. 타입 프로퍼티처럼 쓸 수 있다.

0개의 댓글