본문 바로가기
Swift/Language Guide

Properties - #1/1

by diosmio 2021. 9. 12.

Stored Properties (저장 프로퍼티)

  • 특정 Class / Struct의 일부가 되는 상.변수
  • Default value를 부여할 수 있다
  1. 구조체를 상수로 선언하면 프로퍼티가 변수더라도 변경할 수 없다
    struct FixedLengthRange {
        var firstValue: Int
        let length: Int
    }
    let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
    rangeOfFourItems.firstValue = 6​
    firstValue는 변수지만 rangeOfFourItems 인스턴스 자체가 상수이므로 변경불가. 값 타입의 특징
  2. Lazy Stored Properties
    • 개념
      • iOS는 APP의 메모리 사용량이 너무 높아지면 APP을 죽이기 때문에, Swift에서 메모리는 굉장히 예민한 주제이며 보다 효율적으로 관리할 수 있는 방향으로 이어져 왔다
      • 개발자는 실제로 필요한 경우가 아니라면, 비싼 코스트의 작업을 하는 것을 지양해야 한다
      • Swift에서 비싼 코스트의 작업을 사용시점에 계산하게 할 수 있는 lazy variable이라는 메커니즘을 제공한다
      • lazy variable은 변수가 처음 요청되었을 때만 사용자가 지정한 함수를 사용하여 생성된다
      • 개념 예시→ 이처럼 필요하지만 사용하기도 전에 불러오기 부담스러운 것들을 lazy로 선언해준다
      • 인스타그램에선 usage없이 메모리를 사용하지 않기 위해, APP을 실행할때 모든 영상들을 불러오는게 아닌 User가 보고자하는 스토리를 클릭할때서야 해당 영상을 로딩한다
    • 고려사항
      1. lazy 저장 프로퍼티는 반드시 var와 사용되어야 한다.
      2. 상수 프로퍼티는 초기화가 끝나기 전까지 반드시 값을 가져야 한다는 rule이 있기 때문
      3. Class와 Struct에서만 사용할 수 있다
      4. Computed Properties (계산 프로퍼티)와는 사용될 수 없다.
      5. lazy는 처음 사용될 때 초기화되고 그 값을 계속 유지하므로 매번 값을 새로 연산하여 도출하는 계산 프로퍼티와는 사용될 수 없다
      6. lazy variable을 closure로 정의할 때 self를 사용하더라도 메모리 누수 걱정이 없다(Class 내부 함수에서 self를 사용하면 순환참조 risk가 있다는데 다음 시간에 알아보자)
      7. 초기화가 완료되면 그 즉시 결과를 돌려주고 끝나버리기 때문
  3. Stored Properties and Instance Variables
    • Objective-C와 비교하고 있는데 전혀 이해되지 않으므로 영문으로 재확인 필요

 

 

 

 

Computed Properties (계산 프로퍼티)

  1. 개념
    • 계산 프로퍼티는 값을 실질적으로 저장하지 않고 요청이 올때마다 계산하게 된다
    계산 프로퍼티는 매번 값이 바뀔 수 있으므로 변수(var)로 지정해야 한다
    • getter : 해당 프로퍼티를 사용할때 호출되며 계산된 값을 return한다
    • 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 {
              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))
      //getter 호출
      let initialSquareCenter = square.center
      //setter 호출
      square.center = Point(x: 15.0, y: 15.0)​
  2. 축약표현
    • getter의 body가 단일 표현식이라면 return을 생략해도 된다
    • setter는 디폴트로 newValue를 통해 전달된 값을 읽을 수 있다
      struct CompactRect {
          var origin = Point()
          var size = Size()
          var center: Point {
              get {
                  Point(x: origin.x + (size.width / 2),
                        y: origin.y + (size.height / 2))
              }
              set {
                  origin.x = newValue.x - (size.width / 2)
                  origin.y = newValue.y - (size.height / 2)
              }
          }
      }​
  3. 읽기전용 계산 프로퍼티
    • setter는 생략이 가능하며 이 경우 읽기전용 프로퍼티가 된다
    • 읽기전용일 경우 get( )은 아래와 같이 생략될 수 있다
      struct Cuboid {
          var width = 0.0, height = 0.0, depth = 0.0
          var volume: Double {
              return width * height * depth
          }
      }​

 

 

 

Property Observers (프로퍼티 감시자)

    1. 개념
      • 프로퍼티의 값이 변경되면, 자동으로 특정 코드 block을 실행시킬 수 있다
      • 사실 change보다는 set을 감시한다고 보는게 맞다. 현재 값과 새로운 값이 같더라도 호출되기 때문
      • willset : 새로운 값이 전달된다. 따로 지정하주지 않으면 newValue라는 이름으로 읽을 수 있다. didset보다 먼저 호출된다
      • didset : 기존 값이 전달된다. 따로 지정해주지 않으면 oldValue라는 이름으로 읽을 수 있다
      • 예제
        class StepCounter {
            var totalSteps: Int = 0 {
                willSet(newTotalSteps) {
                    print("About to set totalSteps to \(newTotalSteps)")
                }
                didSet {
                    if totalSteps > oldValue  {
                        print("Added \(totalSteps - oldValue) steps")
                    }
                }
            }
        }
        let stepCounter = StepCounter()
        stepCounter.totalSteps = 200
        // About to set totalSteps 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​
    2. 특이사항
      • inout 파라미터로 함수에 전달하면 willset과 didset이 호출된다
        이는 inout의 실제 동작이 copy를 베이스로 이루어지기 때문.
        함수 파라미터로 inout을 전달하면 실제론 참조가 전달되는게 아니다.
        일반적인 경우와 동일하게 값을 copy해서 넘겨주고 (함수 내에서 값 변경 후) 함수가 종료될 때 원본에 다시 copy를 하는 "copy-in-copy-out"방식이므로
        함수가 종료될 때 값 변경이 발생하는 메커니즘으로 observer가 호출된다

      • 하지만, 최적화 옵션을 주면 call by reference로 작동하는 듯하다;;

 

 

 

 

Property Wrapper

  1. 개념
    • 목적 : "프로퍼티를 정의하는 코드"와 "프로퍼티가 저장되는 방식(default설정, getter, setter rule) 코드"를 분리하여 동일한 저장방식을 사용하는 프로퍼티에 대해 재사용성을 높힌다
    • wrappedValue라는 계산 프로퍼티가 필수적으로 정의되어야 하고
      아래 예제처럼 default설정 / 읽을때 / 저장될때 등 custom rule을 적용할 수 있다
    • 예제
      //프로퍼티 저장 방식
      @propertyWrapper
      struct TwelveOrLess {
      		//필수는 아니지만 wrapper 용도이므로 
      		//외부에서 접근할 수 없도록 private으로 지정하는 것이 깔끔하다
          private var number = 0
          var wrappedValue: Int {
              get { return number }
              set { number = min(newValue, 12) }
          }
      }
      
      //프로퍼티 정의
      struct SmallRectangle {
          @TwelveOrLess var height: Int
          @TwelveOrLess var width: Int
      }
      
      var rectangle = SmallRectangle()
      print(rectangle.height)
      // Prints "0"
      
      rectangle.height = 10
      print(rectangle.height)
      // Prints "10"
      
      rectangle.height = 24
      print(rectangle.height)
      // Prints "12"​
    • 변형예제
      struct SmallRectangle {
          private var _height = TwelveOrLess()
          private var _width = TwelveOrLess()
          var height: Int {
              get { return _height.wrappedValue }
              set { _height.wrappedValue = newValue }
          }
          var width: Int {
              get { return _width.wrappedValue }
              set { _width.wrappedValue = newValue }
          }
      }​
      height와 width를 계산 프로퍼티로 바꾸고 TwelveOrLess의 계산 프로퍼티 rule을 따르도록 연결한 형태
  2. init( ) 지정
    • Property Wrapper에도 init( ) 정의가 가능하다
    • parameter를 받는 init( )을 정의할 수도 있는데 코드 표현은 아래 예제 참고
    • 파라미터명을 wrappedValue로 지정하면 기존 default 설정하는 방식 "var height: Int = 1" 으로도 parameter가 전달된다
    • 예제
      @propertyWrapper
      struct SmallNumber {
          private var maximum: Int
          private var number: Int
      
          var wrappedValue: Int {
              get { return number }
              set { number = min(newValue, maximum) }
          }
      
          init() {
              maximum = 12
              number = 0
          }
          init(wrappedValue: Int) {
              maximum = 12
              number = min(wrappedValue, maximum)
          }
          init(wrappedValue: Int, maximum: Int) {
              self.maximum = maximum
              number = min(wrappedValue, maximum)
          }
      }​
    • struct MixedRectangle {
      		//init( )
          @SmallNumber var temp: Int
      		//init(wrappedValue: Int)
      		@SmallNumber var height: Int = 1
      		//init(wrappedValue: Int, maximum: Int)
          @SmallNumber(maximum: 9) var width: Int = 2
      		//init(wrappedValue: Int, maximum: Int)
      		@SmallNumber(wrappedValue: 3, maximum: 4) var temp2: Int
      }
  3. Projecting a Value
    • 프로퍼티를 읽고 쓰는 방식은 Property Wrapper가 있든 없든 차이가 없어서 프로퍼티를 call하는 site에는 어떤 영향도 주지 않는다
    • 하지만 이런 저장방식 복붙 외에 각 프로퍼티에 상.변수를 추가하고 싶은 경우가 있을 수 있다.
      (예로, height와 width는 정수를 하나씩 저장하는 프로퍼티인데 홀수인지 짝수인지 저장하는 값도 부차적으로 갖고 싶다. )
    • Wrapper에 projectedValue라는 이름으로 만들어주면 한 개정도는 이 기능을 지원할 수 있다
    • 사용하는 방법은 $를 붙혀주면 projectedValue가 읽히게 된다
    • 예제
      @propertyWrapper
      struct SmallNumber {
          private var number = 0
      		//이 wrapper를 사용한 프로퍼티는 Bool 변수를 부차적으로 갖게 된다
          var projectedValue = false
          var wrappedValue: Int {
              get { return number }
              set {
                  if newValue > 12 {
                      number = 12
                      projectedValue = true
                  } else {
                      number = newValue
                      projectedValue = false
                  }
              }
          }
      }
      struct SomeStructure {
          @SmallNumber var someNumber: Int
      }
      var someStructure = SomeStructure()
      
      someStructure.someNumber = 4
      //$를 붙혀주면 projectedValue가 읽힌다
      print(someStructure.$someNumber)
      // Prints "false"​

 

 

 

Global and Local Values

  • 지금 다루고 있는 프로퍼티 감시자 설정, Wrapper로 저장방식 규칙 설정은 Class나 Struct의 프로퍼티가 아니더라도 전역, 지역 변수에도 설정할 수 있다

  • Global value는 lazy를 붙힐 수 없다
    전역 변수는 Swift에서 초기화가 지연되는 3가지 타입 중 하나로 이미 지연 초기화가 적용되어 있다

    [ 지연 초기화 3가지 ]
    1. Global value
    2. 타입 프로퍼티 (static)
    3. 인스턴스 프로퍼티 (=lazy)

      * 지연 초기화 : 실제 코드가 해당 값에 접근하기 전까지는 초기값을 계산, 할당하지 않는다
  • 예제 - 일반 함수의 지역 변수에 사용
    func someFunction() {
        @SmallNumber var myNumber: Int = 0
    
        myNumber = 10
        // now myNumber is 10
    
        myNumber = 24
        // now myNumber is 12
    }​

 

 

 

Type Properties (타입 프로퍼티)

  1. 개념
    • 인스턴스에 속하는 "인스턴스 프로퍼티"와 대응되는 개념으로 인스턴스가 아닌 Type 자체가 보유한 프로퍼티이다
    • 모든 인스턴스에 보편적인 정보를 정의하는데 유용하다
    (저장 인스턴스 프로퍼티와 달리) 저장 타입 프로퍼티는 Default를 반드시 부여해야 한다
    Default를 부여하는 것 외에 초기화해줄 방법이 없기 때문,,
    init( ) 함수는 인스턴스를 생성해야 실행되므로 타입 프로퍼티의 초기화 역할을 해줄 수 없다
    • 예제
      struct AudioChannel {
          static let thresholdLevel = 10
          static var maxInputLevelForAllChannels = 0
          var currentLevel: Int = 0 {
              didSet {
                  if currentLevel > AudioChannel.thresholdLevel {
                      // cap the new audio level to the threshold level
                      currentLevel = AudioChannel.thresholdLevel
                  }
                  if currentLevel > AudioChannel.maxInputLevelForAllChannels {
                      // store this as the new overall maximum input level
                      AudioChannel.maxInputLevelForAllChannels = currentLevel
                  }
              }
          }
      }
      
      var leftChannel = AudioChannel()
      var rightChannel = AudioChannel()
      
      leftChannel.currentLevel = 7
      print(leftChannel.currentLevel)
      // Prints "7"
      print(AudioChannel.maxInputLevelForAllChannels)
      // Prints "7"
      
      rightChannel.currentLevel = 11
      print(rightChannel.currentLevel)
      // Prints "10"
      print(AudioChannel.maxInputLevelForAllChannels)
      // Prints "10"​

'Swift > Language Guide' 카테고리의 다른 글

Initialization - #1/2  (0) 2021.09.13
Methods - #1/1  (0) 2021.09.12
Structures and Classes - #1/1  (0) 2021.09.12
Control Flow - #2/2  (0) 2021.09.12
Control Flow - #1/2  (0) 2021.09.12

댓글