swift-8 프로퍼티

영점·2022년 10월 4일
0

Swift_Study

목록 보기
8/12

스터디를 진행하며 처음부터 다시 Swift를 공부하고 있습니다.
오늘 작성할 파트는 프로퍼티 입니다.

저장 프로퍼티 ( Stored Property )

클래스와 구조체에서만 사용할 수 있고, 값을 저장하기 위해 선언되는 상수/변수

class Fruit {
		let grape : String = "Grape"
		var peach : String = ""
}

struct Fruits {
		let grape : String = "Grape"
		var peach : String = ""
}

→ 여기서 grape와 peach 모두 저장 프로퍼티이다.

저장 프로퍼티 클래스와 구조체 차이점?

클래스 / 구조체의 차이를 잘 알고 있다면 둘의 차이를 알 수 있다.

먼저 클래스의 경우부터 보자면, 옵셔널 상수로 Human 클래스 인스턴스를 선언하고

let dessert : Fruit? = .init()

dessert?.grape = "Shine" // Cannot assign to property: 'grape' is a 'let' constant
dessert?.peach = "Peaches"

grape는 값 변경을 하면 에러가 나지만 peach는 나지 않는걸 볼 수 있는데,
그 이유는 클래스는 참조 타입이기 때문이다. ( 클래스는 힙에 저장됨 / 공유 )

dessert는 스택에, grape와 peach는 에 할당이 되어있으므로
dessert는 에 있는 grape, peach 인스턴스를 참조하고 있는 형태이다.

따라서 현재 dessert는 에 있는 인스턴스들의 주소값을 지니고 있으니
상수로 선언한다는 것은 인스턴스들의 주소값을 상수로 선언한다는 의미이다.

결론적으로 클래스의 경우, 인스턴스 생성시 let이든 var든 프로퍼티에 접근하는 것에 영향을 주지 않고,
위에 선언한 dessert의 상수 안의 값을 변경하는 것에 영향을 준다.

위의 dessert는 상수이므로 nil을 할당할 수 없고, 다른 클래스 인스턴스(의 주소값)도 할당할 수 없다.

하지만 변수로 선언한다면 두개 다 가능하다. ( 하지만 클래스에 선언한 grape는 상수이므로 변경 불가. )

다음으로 구조체의 경우, 모두 에러가 나게 된다.

let food : Fruits? = .init()

food?.grape = "delicious" // Cannot assign to property: 'grape' is a 'let' constant
food?.peach = "sweet" // Cannot assign to property: 'food' is a 'let' constant

먼저 구조체는 값 타입이다. ( 복사! )
때문에 클래스와 다르게 저장 프로퍼티들도 모두 스택에 올라가게 된다.

따라서 상수로 선언을 했으니 구조체의 모든 프로퍼티들은 값을 변경할 수 없게 된 것이다.

값을 변경할 수 없으니 nil을 할당할 수도 없고, 다른 구조체 인스턴스도 받을 수 없다.

마찬가지로 var로 선언하면 둘 다 가능하다. ( 하지만 구조체에 선언한 grape는 상수이므로 변경 불가. )

지연 저장 프로퍼티 ( lazy )

지연 저장된 프로퍼티는 처음 사용될 때까지 초기값은 계산되지 않는 프로퍼티이다.

간단하게, 선언만 될 뿐 초기화되지 않고 있다가 프로퍼티가 호출되는 순간에 초기화 되는
저장 프로퍼티라고 생각하면 된다.

지연 저장 프로퍼티는 이럴때 주로 사용한다.

  1. 인스턴스의 초기화가 완료될 때까지 값을 알 수 없는 외부 요인에 인해 초기값이 달라질 때
  2. 프로퍼티의 초기값으로 필요할 때까지 수행하면 안되는 복잡하거나 계산 비용이 많이 드는 경우
// DataImporter 클래스는 여기서 데이터 가져오기 기능을 제공할 것이다.
class DataImporter {
    var filename = "data.txt"
}

// DataManager 클래스는 여기서 데이터 관리 기능을 제공할 것이다.
class DataManager {
    lazy var importer = DataImporter()
    var data = [String]()
}

let manager = DataManager()
manager.data.append("Some data") // ["Some data"]
manager.data.append("Some more data") // ["Some data", "Some more data"]
// the DataImporter instance for the importer property has not yet been created

print(manager.importer.filename) // Prints "data.txt"

위의 예시에서 DataImporter가 외부 파일에서 데이터를 가져오는 클래스며, 초기화하는데 적지 않은 시간이 걸린다고 가정했을 때, 성능향상과 메모리 낭비를 줄이고자 importer 프로퍼티에 lazy를 사용하였다.

lazy가 붙어 있으므로 importer 프로퍼티의 DataImporter 인스턴스는 
filename 프로퍼티를 조회할 때처럼 importer 프로퍼티에 처음 접근될 때 생성된다.

lazy의 경우 무조건 변수(var)로 선언해야한다.
상수(let)으로 선언하게 되면 필요한 시점에 초기화(init)를 진행할 수 없기 때문.

연산 프로퍼티 ( Computed Property )

연산 프로퍼티 같은 경우, 저장 프로퍼티와 다르게 구조체, 클래스 뿐만아니라 열거형에도 사용이 가능하다.

저장 프로퍼티와 별도의 저장 공간을 갖지 않고, 다른 저장 프로퍼티의 값을 읽어 연산을 실행하거나,
프로퍼티로 전달받은 값을 다른 프로퍼티에 저장한다. ( getter, setter 이용하기 )

getter의 경우 “얻는 것”이므로
어떤 저장 프로퍼티의 값을 연산해서 return할 것인지, return 구문이 항상 존재해야 한다.

setter의 경우 “설정하는 것”이므로
파라미터로 받은 값을 어떤 저장 프로퍼티에 어떻게 설정할 것인지를 구현해야 한다.

연산 프로퍼티 같은 경우 저장 프로퍼티와 다르게 어떠한 값을 저장하지 않기 때문에 타입 어노테이션을 통해 자료형을 명시해야 한다. 그리고 선언된 자료형 뒤에 { }을 붙인다.

class Fruit {
    var grape : String = "Grape"
 
    var getSet : String {
        get {
            return "Shine" + " " + self.grape
        }
        set(grape) {
            self.grape = grape
        }
    }
}

let dessert : Fruit = .init()

//get접근
print(dessert.getSet) //Shine Grape

//set접근
dessert.getSet = "Muscat"
print(dessert.getSet) //Shine Muscat

연산 프로퍼티를 사용하려면 무조건 읽거나 쓸 수 있는 "저장 프로퍼티"가 먼저 존재해야 한다.

위의 예시처럼 저장 프로퍼티에 접근하는 것과 같이
연산 프로퍼티인 getSet을 읽으면 getSet의 getter가 실행되어 get안에 있는 값이 리턴되며,
연산 프로퍼티인 getSet을 쓰면 “Muscat”이란 값이 setter의 파라미터로 넘어가 set 함수가 실행된다.

추가 - set에 대하여!!

set의 파라미터는 단 하나만 존재한다.
만약 변수명을 짓기 어려운 경우, get처럼 파라미터를 날리고 newValue로 접근한다.

class Fruit {
    var grape : String = "Grape"
 
    var getSet : String {
        get {
            return "Shine" + " " + self.grape
        }
        set {
            self.grape = newValue
        }
    }
}

값을 읽기만 한다면 set은 생략해도 된다. get을 적어도되고 그냥 리턴구문만 작성해도 문제없다.

class Fruit {
    var grape : String = "Grape"
 
    var getSet : String {
        return grape
    }
}

하지만 set만 작성하는건 안된다. 무조건 get이 필요하다.

swift.org에는 해당 카테고리에 뭐가 많은데.. 아직 잘 모르겠어서 생략하였다.

프로퍼티 관찰자 didSetwillSet

didSet새로운 값이 저장되자마자 호출되고, willSet값이 저장되기 직전에 호출된다.

didSet은 파라미터 이름과 괄호를 따로 지정하지 않을 경우 oldValue를 사용하고,
willSet은 파라미터 이름과 괄호를 따로 지정하지 않을 경우 newValue를 사용한다.

var fruit : String = "Grape" {
    willSet {
        print("현재 과일명 = \(fruit), 변경된 과일명 = \(newValue)")
    }
}

fruit = "peach"
//현재 과일명 = Grape, 변경된 과일명 = peach
var fruit : String = "Grape" {
    didSet {
        print("변경된 과일명 = \(fruit), 기존 과일명 = \(oldValue)")
    }
}

fruit = "peach"
//변경된 과일명 = peach, 기존 과일명 = Grape

만약 둘 다 같이 선언할 경우 순서는 이렇게 된다.

willSet 실행 → fruit값 변경 → didSet 실행

var fruit : String = "Grape" {
    willSet {
        print("현재 과일명 = \(fruit), 변경된 과일명 = \(newValue)")
    }
    didSet {
        print("변경된 과일명 = \(fruit), 기존 과일명 = \(oldValue)")
    }
}

fruit = "peach"
/*
현재 과일명 = Grape, 변경된 과일명 = peach
변경된 과일명 = peach, 기존 과일명 = Grape
*/

타입 프로퍼티 ( Type Property )

타입 프로퍼티도 연산 프로퍼티처럼 클래스,구조체,열거형에서 사용 가능하다.

저장 타입 프로퍼티연산 타입 프로퍼티가 존재하며,
저장 타입 프로퍼티의 경우 선언할 당시 원하는 값으로 항상 초기화가 되어 있어야 한다.
static을 이용해 선언하며, 자동으로 lazy로 작동한다 ( lazy를 직접 붙일 필요 또한 없다 )

class Fruit {
    static let grape : String = "Grape" //저장 타입 프로퍼티
 
    static var getSet : String { //연산 타입 프로퍼티
        return "Shine" + " " + self.grape 
    }
}

저장 타입 프로퍼티를 static으로 선언할 경우,
initializer가 필수거나 getter/setter를 지정해야 한다.
static으로 선언되는 저장 타입 프로퍼티의 경우 초기화할 때 값을 할당할 initializer가 없기 때문.

타입 프로퍼티는 해당 타입의 인스턴스가 아닌 타입에 대해 조회되고 설정한다.

저장,연산 프로퍼티와 다르게 dessert.(프로퍼티이름)으로 접근이 안되고 ( 그렇게 접근하면 self만 뜸 )
타입 이름을 통해서만 접근이 가능하다.

print(Fruit.grape) //Grape

요약하자면 타입 프로퍼티는 누군가 나를 불러줬을 때 메모리에 올라간다.
( = 저장 프로퍼티의 속성인 lazy )

따라서 위처럼 grape란 프로퍼티를 최초로 호출하면 그때 메모리에 올라가서 초기화가 된다.
또한, 타입 프로퍼티는 모든 타입이 공통적인 값을 정의하는 데 유용하게 사용된다 ( 싱글톤 )

profile
일단 배운내용은 적어두기

0개의 댓글