프로퍼티는 크게 저장, 연산, 타입 프로퍼티로 나눌 수 있다.
클래스
와 구조체
에서만 사용할 수 있음클래스
, 구조체
, 열거형
에서 쓸 수 있음클래스 또는 구조체의 인스턴스와 연관된 값을 저장하는 가장 단순한 개념의 프로퍼티
struct CoordinatePoint {
var x: Int
var y: Int
}
// 구조체에는 기본적으로 저장 프로퍼티를 매개변수로 갖는 이니셜라이저가 있음
let tomaPoint: CoordinatePoint = CoordinatePoint(x: 10, y: 5)
class Position {
var point: CoordinatePoint
let name: String
// 프로퍼티 기본값을 지정하지 않는다면 이니셜라이저를 따로 정의해야함
init(name: String, currentPoint: CoordinatePoint) {
self.name = name
self.point = currentPoint
}
}
// 사용자 정의 이니셜라이저를 호출해야만함
// 그렇지 않으면 프로퍼티 초깃값을 할당할 수 없기 때문에 인스턴스 생성이 불가능함
let tomaPosition: Position = Position(name: "toma", currentPoint: tomaPoint)
구조체는 프로퍼티에 맞는 이니셜라이저를 자동으로 제공하지만, 클래스는 그렇지 않아서 클래스 인스턴스의 저장 프로퍼티를 사용하는 일은 더 번거로움.
하지만 클래스의 저장 프로퍼티에 초깃값을 지정해주면 따로 사용자 정의 이니셜라이저를 구현할 필요가 없음.
struct CoordinatePoint {
var x: Int = 0
var y: Int = 0
}
// 프로퍼티에 초깃값을 할당했다면 굳이 전달인자로 초깃값을 넘길 필요가 없음
let tomaPoint: CoordinatePoint = CoordinatePoint()
// 물론 초깃값을 할당할 수 있는 이니셔라이저도 사용 가능
let matoPoint: CoordinatePoint = CoordinatePoint(x: 10, y: 5)
class Position {
var point: CoordinatePoint = CoordinatePoint() // 저장 프로퍼티
let name: String = "Unknown" // 저장 프로퍼티
}
// 초기값을 지정했다면 사용자 정의 이니셜라이저를 사용하지 않아도 됨
let tomaPositon: Position = Position()
tomaPosition.point = tomaPoint
tomaPosition.name = "toma"
초깃값을 미리 지정하면 인스턴스를 만드는 과정이 훨씬 간편해진다.
그러나 의도와 맞지않게 인스턴스가 사용될 가능성이 있고, 인스턴스를 생성한 후 일일이 원하는 값을 할당해주어야 한다
Positon의 name 프로퍼티는 한번 값을 할당해준 후 변경하지 못하도록 상수로 정의하고 싶었으나, 인스턴스를 생성한 후 값을 할당해주어야 하기 때문에 그렇게 할수 없었다.
저장 프로퍼티에 값이 있어도 그만, 없어도 그만인 옵셔널이라면 굳이 초깃값을 넣어주지 않아도 된다. 즉, 이니셜라이저에서 옵서녈 프로퍼티에 꼭 값을 할당해주지 않아도 된다.
// 좌표
struct CoordinatePoint {
// 위치는 x, y 값이 모두 있어야 하므로 옵셔널이면 안된다.
var x: Int
var y: Int
}
// 사람의 위치 정보
class Position {
// 현재 사람의 위치를 모를 수도 있다. - 옵셔널
var point: CoordinatePoint?
let name: String
init(name: String) {
self.name = name
}
}
// 이름은 필수지만 위치는 모를 수도 있다.
let tomaPosition: Position = Position(name: "toma")
// 위치를 알게되면 그 때 위치 값을 할당해준다.
tomaPosition.point = CoordinatePoint(x: 20, y: 10)
이렇게 옵셔널과 이니셜라이저를 적절히 사용하면 다른 프로그래머가 사용할 때, 의도대로 구조체와 클래스를 사용할 수 있도록 유도할 수 있다.
프로퍼티를 옵셔널로 선언하는 것과는 조금 다은 용도로 필요할 때 값이 할당되는 지연 저장 프로퍼티가 있다.
지연 저장 프로퍼티는 호출이 있어야 값을 초기화하며 이때 lazy
키워드를 사용한다.
상수는 인스턴스가 완전히 생성되기 전에 초기화해야하므로 필요할 때 값을 할당하는 지연 저장 프로퍼티와는 맞지 않는다.
따라서 지연 저장 프로퍼티는 var
키워드를 사용하여 번수로 정의한다.
지연 저장 프로퍼티는 주로 복잡한 클래스나 구조체를 구현할 때 많이 사용된다.
struct CoordinatePoint {
var x: Int = 0
var y: Int = 0
}
class Position {
lazy var point: CoordinatePoint = CoordinatePoint()
let name: String
init(name: String) {
self.name = name
}
}
let tomaPosition: Position = Position(name: "toma")
// 이 코드를 통해 point 프로퍼티로 처음 접근할 때
// point 프로퍼티의 CoordinatePoint가 생성된다.
print(tomaPosition.point) // x: 0, y: 0
다중 스레드와 지연 저장 프로퍼티 다중 스레드 환경에서 지연 저장 프로퍼티에 동시다발적으로 접근할 때는 한 번만 초기화된다는 보장이 없다.
생성되지 않은 지연 저장 프로퍼티에 많은 스레드가 비슷한 시점에 접근한다면, 여러 번 초기화될 수 있다.
struct CoordinatePoint {
var x: Int // 저장 프로퍼티
var y: Int // 저장 프로퍼티
// 대칭점을 구하는 메서드 - 접근자
// Self는 타입 자기 자신을 뜻한다
// Self 대신 CoordinatePoint를 사용해도 된다.
func oppositePoint() -> Self {
return CoordinatePoint(x: -x, y: -y)
}
// 대칭점을 설정하는 메서드 - 설정자
// mutating 키워드에 관한 내용은 10.2.1절 참고
mutating func setOppositePoint(_ opposite: CoordinatePoint) {
x = -opposite.x
y = -opposite.y
}
}
var tomaPosition: CoordinatePoint = CoordinatePoint(x: 10, y: 20)
// 현재 좌표
print(tomaPosition) // 10, 20
// 대칭 좌표
print(tomaPosition.oppositePoint()) // -10, -20
// 대칭 좌표를 (15, 10)으로 설정하면
tomaPosition.setOppositePoint(CoordinatePoint(x: 15, y: 10))
// 현재 좌표는 -15, -10으로 설정된다.
print(yagomPosition) // -15, -10
struct CoordinatePoint {
var x: Int // 저장 프로퍼티
var y: Int // 저장 프로퍼티
// 대칭 좌표
var oppositePoint: CoordinatePoint { // 연산 프로퍼티
// 접근자
get {
return CoordinatePoint(x: -x, y: -y)
}
// 설정자
set(opposite) {
x = -opposite.x
y = -opposite.y
}
}
}
var tomaPosition: CoordinatePoint = CoordinatePoint(x: 10, y: 20)
// 현재 좌표
print(tomaPosition) // 10, 20
// 대칭 좌표
print(tomaPosition.oppositePoint) // -10, -20
// 대칭 좌표를 (15, 10)으로 설정하면
tomaPosition.oppositePoint = CoordinatePoint(x: 15, y: 10)
// 현재 좌표는 -15, -10으로 설정된다.
print(tomaPosition) // -15, -10
이런 식으로 연산 프로퍼티를 사용하면 하나의 프로퍼티에 접근자와 설정자가 모두 모여있고, 해당 프로퍼티가 어떤 역할을 하는지 좀 더 명확하게 표현 가능함
struct CoordinatePoint {
var x: Int // 저장 프로퍼티
var y: Int // 저장 프로퍼티
// 대칭 좌표
var oppositePoint: CoordinatePoint { // 연산 프로퍼티
// 접근자
get {
// 이곳에서 return 키워들르 생략할 수 있다.
CoordinatePoint(x: -x, y: -y)
}
// 설정자
set {
x = -newValue.x
y = -newValue.y
}
}
}
연산 프로퍼티를 읽기 전용으로 구현하려면 get 메서드만 사용하면 된다.
프로퍼티 감시자를 사용하면 프로퍼티의 값이 변경됨에 따라 적절한 작업을 취할 수 있다.
프로퍼티의 값이 새로 할당될 때마다 호출한다.
(변경되는 값이 현재의 값과 같더라도 호출)
저장 프로퍼티뿐만 아니라 프로퍼티를 재정의해 상속받은 저장 프로퍼티 또는 연산 프로퍼티에도 적용할 수 있다.
상속받지 않은 연산 프로퍼티에는 프로퍼티 감시자를 사용할 필요가 없으며 할수도 없다. 연산 프로퍼티의 접근자와 설정자를 통해 프로퍼티 감시자를 구현할 수 있기 때문이다.
→ 연산 프로퍼티는 상속받았을 때만 프로퍼티 재정의를 통해 프로퍼티 감시자를 사용한다.
프로퍼티의 값이 변경되기 직전에 호출하는 willSet
메서드와 프로퍼티의 값이 변경된 직후에 호출하는 didSet
메서드가 있다.
willSet
메서드와 didSet
메서드에는 매개변수가 하나씩 있다.
willSet
메서드에 전달되는 전달인자는 프로퍼티가 변경될 값이고 매개변수의 이름을 따로 지정하지 않으면 newValue
가 자동 지정된다.
didSet
메서드에 전달되는 전달인자는 프로퍼티가 변경되기 전의 값이고 매개변수의 이름을 따로 지정하지 않으면 oldValue
가 자동 지정된다.
class Account {
var credit: Int = 0 {
willSet {
print("잔액이 \(credit)원에서 \(newValue)원으로 변경될 예정이다.")
}
didSet {
print("잔액이 \(oldValue)원에서 \(credit)원으로 변경되었다.")
}
}
}
let myAccount: Account = Account()
// 잔액이 0원에서 1000원으로 변경될 예정이다.
myAccount.credit = 1000
// 잔액이 0원에서 1000원으로 변경되었다.
연산 프로퍼티를 상속받았다면 재정의하여 프로퍼티 감시자를 구현할 수도 있다.
연산 프로퍼티를 재정의해도 기존의 연산 프로퍼티 기능(접근자, 설정자 get, get 메서드)은 동작한다.
class Account {
var credit: Int = 0 {
willSet {
print("잔액이 \(credit)원에서 \(newValue)원으로 변경될 예정입니다.")
}
didSet {
print("잔액이 \(oldValue)원에서 \(credit)원으로 변경되었습니다.")
}
}
var dollarValue: Double { // 연산 프로퍼티
get {
return Double(credit) / 1000.0
}
set {
credit = Int(newValue * 1000)
print("잔액을 \(newValue)달러로 변경 중입니다.")
}
}
}
class ForeignAccount: Account {
override var dollarValue: Double {
willSet {
print("잔액이 \(dollarValue)달러에서 \(newValue)달러로 변경될 예정입니다.")
}
didSet {
print("잔액이 \(oldValue)달러에서 \(dollarValue)달러로 변경되었습니다.")
}
}
}
let myAccount: ForeignAccount = ForeignAccount()
// 잔액이 0원에서 1000원으로 변경될 예정입니다.
myAccount.credit = 1000
// 잔액이 0원에서 1000원으로 변경되었습니다.
// 잔액이 1.0달러에서 2.0달러로 변경될 예정입니다.
// 잔액이 1000원에서 2000원으로 변경될 예정입니다.
// 잔액이 1000원에서 2000원으로 변경되었습니다.
myAccount.dollarValue = 2 // 잔액을 2.0달러로 변경 중입니다.
// 잔액이 1.0달러에서 2.0달러로 변경되었습니다.
만약 프로퍼티 감시자가 있는 프로퍼티를 함수의 입출력 매개변수의 전달인자로 전달한다면 항상 willSet과 didSet 감시자를 호출한다.
함수 내부에서 값이 변경되든 되지 않든 간에 함수가 종료되는 시점에 값을 다시 쓰기 때문이다.
lazy
키워드를 사용해 연산을 늦출 필요가 없다.var wonInPocket: Int = 2000 {
willSet {
print("주머니의 돈이 \(wonInPocket)원에서 \(newValue)원으로 변경될 예정입니다.")
}
didSet {
print("주머니의 돈이 \(oldValue)원에서 \(wonInPocket)원으로 변경되었습니다.")
}
}
var dollarInPocket: Double {
get {
return Double(wonInPocket) / 1000.0
}
set {
wonInPocket = Int(newValue * 1000.0)
print("주머니의 달러를 \(newValue)달러로 변경 중입니다.")
}
}
// 주머니의 돈이 2000원에서 3500원으로 변경될 예정입니다.
// 주머니의 돈이 2000원에서 3500원으로 변경되었습니다.
dollarInPocket = 3.5 // 주머니의 달러를 3.5달러로 변경 중입니다.
이제까지 알아본 프로퍼티 개념은 모두 타입을 정의하고 해당 타입의 인스턴스가 생성되었을 때 사용할 수 있는 인스턴스 프로퍼티
이다.
인스턴스 프로퍼티
는 인스턴스를 새로 생성할 때마다 초깃값에 해당하는 값이 프로퍼티의 값이 되고, 인스턴스마다 다른 값을 지닐 수 있다.
class AClass {
// 저장 타입 프로퍼티
static var typeProperty: Int = 0
// 저장 인스턴스 프로퍼티
var instanceProperty: Int = 0 {
didSet {
// Self.typeProperty는
// AClass.typeProperty와 같은 표현이다.
Self.typeProperty = instanceProperty + 100
}
}
// 연산 타입 프로퍼티
static var typeComputedProperty: Int {
get {
return typeProperty
}
set {
typeProperty = newValue
}
}
}
AClass.typeProperty = 123
let classInstance: AClass = AClass()
classInstance.instanceProperty = 100
print(AClass.typeProperty) // 200
print(AClass.typeComputedProperty) // 200
타입 상수
로 사용할 수도 있다.class Account{
// 타입 상수
static let dollarExchangeRate: Double = 1000.0
var credit: Int = 0
var dollarValue: Double {
get {
return Double(credit) / Self.dollarExchangeRate
// Self.dollarExchangeRate는 Account.dollarExchangeRate와 같은 표현
// Self는 Account 타입을 가리킴.
}
set {
credit = Int(newValue * Account.dollarExchangeRate)
print("잔액을 \(newValue)달러로 변경 중입니다")
}
}
}
함수는 일급시민(일급객체)으로서 상수나 변수에 함수의 참조를 할당할 수 있다.
func someFuction(paramA: Any, paramB: Any) {
print("SomeFunction called...")
}
var functionReference = someFuction(paramA:paramB:)
functionReference("A", "B") // SomeFunction called...
functionReference = anotherFunction(paramA:paramB:)
함수를 참조해두고 나중에 원할 때 호출하거나 다른 함수를 참조하도록 할 수도 있다.
키 경로를 이용해 프로퍼티도 이처럼 값을 바로 꺼내오는 것이 아니라 어떤 프로퍼티의 위치만 참조할 수 있도록 할 수 있다.
키 경로를 사용하여 간접적으로 특정 타입의 어떤 프로퍼티 값을 가리켜야할지 미리 지정해두고 사용할 수 있다.
키경로는 역슬래시/
와 타입
, 마침표.
경로로 구성된다.
class Person {
var name: String
init(name: String) {
self.name = name
}
}
struct Stuff {
var name: String
var owner: Person
}
print(type(of: \Person.name)) // ReferenceWritableKeyPath<Person, String>
print(type(of: \Stuff.name)) // WritableKeyPath<Stuff, String>
키 경로는 타입 외부로 공개된 인스턴스 프로퍼티 혹은 서브스크립트에 한하여 표현할 수 있다.
struct Person {
let name: String
let nickname: String?
let age: Int
var isAdult: Bool {
return age > 18
}
}
let yagom: Person = Person(name: "yagom", nickname: "bear", age: 100)
let hana: Person = Person(name: "hana", nickname: "na", age: 100)
let happy: Person = Person(name: "happy", nickname: nil, age: 3)
let family: [Person] = [yagom, hana, happy]
let names: [String] = family.map(\.name) // ["yagom", "hana", "happy"]
let nicknames: [String] = family.compactMap(\.nickname) // ["bear", "na"]
let adults: [String] = family.filter(\.isAdult).map(\.name) // ["yagom", "hana"]
메서드는 특정 타입에 관련된 함수를 뜻한다.
클래스, 구조체, 열거형 등은 실행하는 기능을 캡슐화한 인스턴스 메서드를 정의할 수 있다.
class LevelClass {
// 현재 레벨을 저장하는 저장 프로퍼티
var level: Int = 0 {
// 프로퍼티 값이 변경되면 호출하는 프로퍼티 감시자
didSet {
print("Level \(level)")
}
}
// 레벨이 올랐을 때 호출할 메서드
func levelUp() {
print("Level Up!")
level += 1
}
// 레벨이 감소했을 때 호출할 메서드
func levelDown() {
print("Level Down")
level -= 1
if level < 0 {
reset()
}
}
// 특정 레벨로 이동할 때 호출할 메서드
func jumpLevel(to: Int) {
print("Jump to \(to)")
level = to
}
// 레벨을 초기화할 때 호출할 메서드
func reset() {
print("Reset!")
level = 0
}
}
var levelClassInstance: LevelClass = LevelClass()
levelClassInstance.levelUp() // Level Up!
//Level 1
levelClassInstance.levelDown() // Level Down
// Level 0
levelClassInstance.levelDown() // Level Down
// Level -1
// Reset!
// Level 0
levelClassInstance.jumpLevel(to: 3) // Jump to 3
// Level3
자신의 프로퍼티 값을 수정할 때 클래스의 인스턴스 메서드는 크게 신경 쓸 필요가 없지만,
구조체나 열거형 등은 값 타입이므로 메서드 앞에 mutationg
키워드를 붙여서 해당 메서드가 인스턴스 내부의 값을 변경한다는 것을 명시해야 함
struct LevelStruct {
// 현재 레벨을 저장하는 저장 프로퍼티
var level: Int = 0 {
// 프로퍼티 값이 변경되면 호출하는 프로퍼티 감시자
didSet {
print("Level \(level)")
}
}
// 레벨이 올랐을 때 호출할 메서드
mutating func levelUp() {
print("Level Up!")
level += 1
}
// 레벨이 감소했을 때 호출할 메서드
mutating func levelDown() {
print("Level Down")
level -= 1
if level < 0 {
reset()
}
}
// 특정 레벨로 이동할 때 호출할 메서드
mutating func jumpLevel(to: Int) {
print("Jump to \(to)")
level = to
}
// 레벨을 초기화할 때 호출할 메서드
mutating func reset() {
print("Reset!")
level = 0
}
}
var levelClassInstance: LevelStruct = LevelStruct()
levelClassInstance.levelUp() // Level Up!
//Level 1
levelClassInstance.levelDown() // Level Down
// Level 0
levelClassInstance.levelDown() // Level Down
// Level -1
// Reset!
// Level 0
levelClassInstance.jumpLevel(to: 3) // Jump to 3
// Level3
class LevelClass {
var level: Int = 0
func jumpLevel(to level: Int) {
print("Jump to \(level)")
self.level = level
}
}
class LevelClass {
var level: Int = 0
func reset() {
// 오류!! self 프로퍼티 참조 변경 불가!
// self = LevelClass()
}
}
struct LevelStruct {
var level: Int = 0
mutating func levelUp() {
print("Level Up!")
level += 1
}
mutating func reset() {
print("Reset!")
self = LevelStruct()
}
}
var levelStructInstance: LevelStruct = LevelStruct()
levelStructInstance.levelUp() // Level Up!
print(levelStructInstance.level) // 1
levelStructInstance.reset() // Reset!
print(levelStructInstance.level) // 0
enum OnOffSwitch {
case on, off
mutating func nextState() {
self = self == .on ? .off : .on
}
}
var toggle: OnOffSwitch = OnOffSwitch.off
toggle.nextState()
print(toggle) // on
사용자 정의 명목 타입의 호출 가능한 값을 구현하기 위해 인스턴스를 함수처럼 호출할 수 있도록 하는 메서드가 있다.
→ 특정 타입의 인스턴스를 문번적으로 함수를 사용하는 것처럼 보이게 할 수 있다.
callAsFunction이라는 이름의 메서드를 구현하면 된다. 이 메서드는 매개변수와 반환 타입만 다르다면 개수에 제한 없이 원하는 만큼 만들 수 있다.
mutating 키워드도 사용할 수 있고, throws와 rethrows도 함께 사용할 수 있다.
struct Puppy {
var name: String = "멍멍이"
func callAsFunction() {
print("멍멍")
}
func callAsFunction(destination: String) {
print("\(destination)(으)로 달려갑니다")
}
func callAsFunction(something: String, times: Int) {
print("\(something)(을)를 \(times)번 반복합니다.")
}
func callAsFunction(color: String) -> String {
return "\(color) 응가"
}
mutating func callAsFunction(name: String) {
self.name = name
}
}
var doggy: Puppy = Puppy()
doggy.callAsFunction() // 멍멍
doggy() // 멍멍
doggy.callAsFunction(destination: "집") // 집(으)로 달려갑니다
doggy(destination: "뒷동산") // 뒷동산(으)로 달려갑니다
doggy(something: "재주넘기", times: 3) // 재주넘기(을)를 3번 반복합니다
print(doggy(color: "무지개색")) // 무지개색 응가
doggy(name: "댕댕이")
print(doggy.name) // 댕댕이
하지만 메서드를 호출하는 것 외에 함수 표현으로는 사용할 수 없다.
let function: (String) -> Void = doggy(destination:) // ❌
let function: (String) -> Void = doggy.callAsFunction(destination:) // ✅
static
키워드를 사용하여 나타냄static
키워드와 class
키워드를 사용할 수 있음static
- 상속 후 메서드 재정의가 불가능class
- 상속 후 메서드 재정의 가능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")
}
}
AClass.staticTypeMethod() // AClass staticTypeMethod
AClass.classTypeMethod() // AClass classTypeMethod
BClass.classTypeMethod() // BClass classTypeMethod
// 시스템 음량은 한 기기에서 유일한 값이어야 한다.
struct SystemVolume {
// 타입 프로퍼티를 사용하면 언제나 유일한 값이 된다.
static var volume: Int = 5
// 타입 프로퍼티를 제어하기 위해 타입 메서드를 사용한다.
static func mute() {
// SystemVolume.volume = 0과 같은 표현이다.
// Self.volume = 0과도 같은 표현이다.
self.volume = 0
}
}
// 내비게이션 역할은 여러 인스턴스가 수행할 수 있다.
class Navigation {
// 내비게이션 인스턴스마다 음량을 따로 설정할 수 있다.
var volume: Int = 5
// 길 안내 음성 재생
func guideWay() {
// 내비게이션 외 다른 재생원 음소거
SystemVolume.mute()
}
// 길 안내 음성 종료
func finishGuideWay() {
// 기존 재생원 음량 복구
SystemVolume.volume = self.volume
}
}
SystemVolume.volume = 10
let myNavi: Navigation = Navigation()
myNavi.guideWay()
print(SystemVolume.volume) // 0
myNavi.finishGuideWay()
print(SystemVolume.volume) // 5