Porperty
프로퍼티란 클래스, 구조체, 열거형에서 소속된 변수 및 속성(Attributes)등을 불리우는 개념입니다. 프로퍼티의 종류는 크게 아래와 같이 분류할 수 있습니다.
- Stored Property : 저장 프로퍼티
- Computed Property : 연산 프로퍼티
- Type Property : 타입 프로퍼티
1. Stored Property
1.1 Stored Property(저장 프로퍼티)
- 저장 프로퍼티는 변수(var)나 상수(let)를 클래스나 구조체의 인스턴스의 한 부분으로 저장 하는 것을 말합니다.
- 객체가 생성이 되면 자동적으로 초기화됩니다.
- 클래스, 구조체에서는 지원되지만, 열거형(Enum)에는 지원되지 않습니다.
- var로 선언하면 “변수”를 저장합니다
- let으로 선언하면 “상수”를 저장하고, 선언 이후 변경 불가합니다.
- Stored Property는 사용 시점에 따라서 Lazy Stored Property라는 것도 있습니다.
클래스로 선언된 저장프로퍼티
import UIKit
class PersonInfoClass {
let name: String
var age: Int
}
let Person = PersonInfoClass(name: "John", age: 18)
Person.age = 19
print(Person)
Person이란 인스턴스를 let, 즉 “상수”로 선언 했는데, age란 저장 프로퍼티가 var라 해도 변경할 수 있는 이유
클래스는 참조타입이기 때문에 위의 그림과 같이 지역 상수 Person은 스택에 할당 되고 실제 PersonInfoClass
인스턴스는 힙에 할당 됩니다.
스택에 있는 John(Person)은 힙 영역에 있는 인스턴스를 참조하고 있는 형태입니다. 따라서 Person 안에는 힙에 할당된 인스턴스의 주소값이 저장되어있습니다.
즉, Person이란 클래스 인스턴스를 let으로 생성한다는 것은 실제 힙 영역에 저장된 저장 프로퍼티 name, age와는 상관 없이 스택 영역에 저장된 Person 안의 주소값이 상수로 설정된다는 것을 의미합니다.
구조체로 선언된 저장프로퍼티
import UIKit
struct PersonInfoStruct {
let name: String
var age: Int
}
var John = PersonInfoStruct(name: "John", age: 18)
John.age = 19
print(John)
Person이라는 구조체 인스턴스를 let으로 할당할 경우, 구조체는 값타입이므로 구조체의 저장프로퍼티들도 모두 스택 영역에 할당됩니다. 따라서 변경 할 수 없습니다.
1.2 Lazy Stored Property(지연 저장 프로퍼티)
- 프로퍼티가 호출되기 전까지는 선언만 될 뿐 초기화되지 않고 있다가, 프로퍼티가 호출되는 순간에 초기화 되는 저장 프로퍼티
- 즉, 값이 사용되기 전까지는 연산되지 않는다.(사용되는 시점에 메모리에 값을 올립니다.)
- lazy라는 키워드를 사용하여 선언
- 최초 값을 인스턴스 초기화가 끝날때까지 갖을 수 없기때문에 지연 저장 프로퍼티는 항상 변수(var)를 사용해 줘야 합니다. 상수(let) 프로퍼티들은 초기화가 끝나기 전에 값이 있어야하기 때문에 지연(lazy)로 선언 될 수 없습니다.
class DataImporter {
//DataImporter for getting an external file
var filename = "data.txt"
init() { print("DataImporter init") }
}
class DataManager {
lazy var importer = DataImporter()
var data = [String]()
}
let manager = DataManager()
manager.data.append("Some Data") //DataImporter 초기화되지않음
manager.data.append("Some more data") //DataImporter 초기화되지않음
manager.importer.filename = "test.txt" //DataImporter 초기화 -> print("DataImporter init")
지연 프로퍼티를 사용하면 인스턴스의 초기화 시 불필요한 모든 저장프로퍼티를 초기화 하지 않아도 됩니다. 그러므로, 불필요한 메모리 낭비를 방지할 수 있습니다.
2. Computed Property
- 클래스, 구조체, 열거형에서 모두 사용 가능합니다
- 저장 공간을 갖지 않고, 다른 “저장 프로퍼티”의 값을 읽어 연산을 실행하거나, 프로퍼티로 전달받은 값을 다른 프로퍼티에 저장합니다.
- 항상 var로 선언되어야 합니다.
- 반드시 연산 프로퍼티를 위한 저장 프로터티가 하나 있어야 합니다.
- 실제 값을 가지고 있는 것이 아니라, getter, setter등을 통해서 값을 설정하고 전달해줍니다.
- getter: 어떤 저장 프로퍼티의 값을 연산해서 return할 것인지, return 구문이 항상 존재해야 합니다.
- setter: 파라미터로 받은 값을 어떤 저장 프로퍼티에 어떻게 설정할 것인지를 구현합니다.
- 연산 프로퍼티는 어떠한 값을 저장하는 것이 아니기 때문에, 타입 추론을 통해 형식을 알 수 없어서, 반드시 선언할 때 타입 어노테이션을 통해 자료형을 명시해야합니다.
- get, set 동시에 구현 가능하며, get만 구현하는 것도 가능하지만 set만 구현하는것은 불가능합니다.
- set에서 파라미터를 생략할 수 있으며, newValue 키워드를 사용합니다.
- 기존 언어의 Getter, Setter보다 코드의 분산을 줄일수있으며 직관적입니다.
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 { //var로 선언, 타입 명시
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set(newCenter) { //파라미터 생략가능(newValue 사용)
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)
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
//square.origin is now at (10.0, 10.0)
get-only
class sayHi {
var name: String = ""
var plusHi: String {
get { //get 생략 가능
return self.name + " Hi!"
}
}
//값을 리턴하지만 저장하지는 않음
}
3. Type Property
- 객체 자체에 관련된 값을 다뤄야할때, 인스턴스를 생성하지 않고 클래스나 구조체 자체에 값을 저장하는 것을 타입 프로퍼티라고 합니다.
- 타입프로퍼티는 인스턴스에 속한 값이 아닌 클래스나 구조체 자체에 속한 값이므로 인스턴스를 생성하지 않고, 클래스나 구조체 자체에 저장하며, 저장된 값은 모든 인스턴스가 공동으로 사용할 수 있습니다.(전역변수와 같은 느낌)
- 이 값은 복사된 값이 아니라 실제로 하나의 값이므로 하나의 인스턴스에서 타입프로퍼티의 값을 변경하면 나머지 인스턴스들이 일괄적으로 변경된 값을 적용받습니다.
- 타입 프로퍼티는 저장 타입 프로퍼티와 연산 타입 프로퍼티가 있습니다.
- static 키워드를 사용하여 정의합니다.
- 모든 타입이 공통적인 값을 정의하는 데 유용하게 사용됩니다(ex 싱글톤..)
3.1 Stored Type Property(저장 타입 프로퍼티)
- 변수와 상수 모두 사용가능하지만, 반드시 초깃값을 설정해야합니다.
- 다중스레드 환경에서도 무조건 한 번만 초기화된다는 보장을 받기 때문이다.
3.2 Computed Type Property(연산 타입 프로퍼티)
- 저장 타입 프로퍼티처럼 인스턴스에 접근하지 않고 타입 이름만으로 사용이 가능합니다.
- 연산 프로퍼티와 마찬가지로 값을 저장하는게 아니므로 var로만 정의가 가능합니다.
- 연산 타입 프로퍼티는 Subclass에서 오버라이딩이 가능합니다.(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 SomeClass {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 27
}
class var overrideableComputedTypeProperty: Int {
return 107
}
}
class ChildSomeClass : SomeClass{
// 슈퍼클래스의 특정 타입 프로터티를 재정의 가능
override static var overrideableComputedTypeProperty: Int{
return 2222
}
}
// 별도의 인스턴스 생성없이 바로 ‘.’을 통해서 프로퍼티 접근 가능
SomeStructure.storedTypeProperty = "Another value."
print(SomeStructure.storedTypeProperty) // Prints "Some value."
print(SomeStructure.storedTypeProperty) // Prints "Another value."
print(SomeEnumeration.computedTypeProperty) // Prints "6"
print(SomeClass.computedTypeProperty) // Prints "27"
4. Property Observer(프로퍼티 옵저버 란?)
- 새값이 설정될 때 해당 이벤트를 감지할 수 있는 옵저버를 제공
- 프로퍼티 옵저버는 새 값이 이전 값과 같더라도 항상 호출
- 지연 저장 프로퍼티에서는 사용할 수 없음
- willSet: 값이 저장되기 바로 직전에 호출됨
- didSet: 새 값이 저장되고 난 직후에 호출됨
저장프로퍼티에 추가된 프로퍼티 옵저버
var name: String = "Unknown" {
willSet(newName) { //파라미터 생략 가능(newValue)
print("현재 이름 = \(name), 바뀔 이름 = \(newName)")
}
didSet(oldName) { //파라미터 생략 가능(oldValue)
print("현재 이름 = \(name), 이전 이름 = \(oldName)")
}
}
name = "James"
연산 프로퍼티에 추가된 프로퍼티 옵저버
- 부모 클래스의 연산 프로퍼티를 오버라이딩 할 경우 프로퍼티 옵저버를 추가할 수 있습니다.
class Human {
var name = "Unknown"
var alias: String {
get {
return name + "(현재이름)"
}
set {
name = newValue + "(별명)"
}
// willSet { } // error! 'willSet' cannot be provided together with a getter
// didSet { } // error! 'didSet' cannot be provided together with a getter
// setter를 통해 값 변경을 감지할 수 있으므로 프로퍼티 옵저버를 만들수 없습니다.
}
}
//alias란 부모 클래스의 연산 프로퍼티를 "오버라이딩"하면, 연산 프로퍼티에 프로퍼티 옵저버를 추가해서 사용할 수 있습니다.
class Man: Human {
override var alias: String {
willSet {
print("현재 alias = \(alias), 바뀔 alias = \(newValue)")
}
didSet {
print("현재 alias = \(alias), 바뀌기 전 alias = \(oldValue)")
}
}
}
let man: Man = .init()
man.alias = "James Dean"