[Swift] Structures and Classes

Ben·2024년 5월 27일
0

iOS

목록 보기
11/23

The Swift Programming Language - Structures and Classes 섹션에서 구조체와 클래스를
'Structures and classes are general-purpose, flexible constructs that become the building blocks of your program’s code.' 라고 정의 내리고 있다.

직역: 구조체 (Structure)와 클래스 (Class)는 프로그램 코드의 구성요소가 되는 범용적이고 유연한 구성입니다.

즉, 프로그램 코드의 구성요소가 되는 '사용자 정의 타입' 이라고 볼 수 있다.


Comparing Structures and Classes

먼저 둘의 공통점은 아래와 같다.

  • 값을 저장하는 프로퍼티 정의
  • 기능 제공을 위한 메서드 정의
  • Subscript 구문을 사용하여 값에 접근을 제공하는 Subscript 정의
  • 초기화 상태를 설정하기 위한 초기화 정의 (initializers) -> via init()
  • extension을 통한 기본 구현을 넘어 기능 확장
  • Protocol을 채택하여 특정 종류의 표준 기능을 제공

클래스는 구조체에 없는 추가적인 기능이 있다.

  • 상속 (inheritance)을 사용하면 한 클래스 (subclass)가 다른 클래스 (superclass)의 특성을 사용할 수 있다.
  • 타입 캐스팅을 사용하면 런타임 시점에 클래스 인스턴스의 타입을 확인하고 (check) 해석 (interpret)할 수 있다.
  • 초기화 해제 구문 (deinitializers)을 사용하면 클래스의 인스턴스가 할당된 리소스를 해제할 수 있다. -> via deinit()
  • 참조 카운팅은 하나 이상의 클래스 인스턴스 참조를 허락한다.

그러나 클래스 사용에 있어 단점이 존재한다.

"The additional capabilities that classes support come at the cost of increased complexity. As a general guideline, prefer structures because they’re easier to reason about, and use classes when they’re appropriate or necessary. In practice, this means most of the custom types you define will be structures and enumerations."

"클래스가 지원하는 추가 기능 (위의 4가지)은 복잡성을 증가시킨다.
일반적으로 구조체를 사용하는 것이 이해하기 쉽기 때문에 선호하며, 클래스는 적절한 경우 또는 필요에 의해서만 사용한다" 라고 한다.

Swift는 '타입 안정성 (Type-Safety)'과 '타입 추론 (Type-Inference)'을 추구하는 언어이기에,
일반적으로 컴파일 시점에 타입과 관련된 에러를 최소화하고 안정성을 높이기 위한 명목 하에 타입 추론을 이용하여 추론하기 쉬운 구조체 사용을 더 권장하고 있다.

그렇다면, 구조체와 클래스 이 둘을 어느 상황에 맞게 사용해야 하는지는 다음과 같다.

  • 기본적으로 구조체를 사용한다.
  • Objective-C와 상호 운용이 필요하면 클래스를 사용한다.
  • 모델링하는 데이터의 ID를 제어해야 하는 경우 클래스를 사용
    • ID는 '식별 가능한', '고유성', '독립성', '개체 무결성' 으로 해석이 가능할것 같다 :)
  • Protocol을 struct에 채택하여 동작을 구현하고 공유한다.
    • struct의 경우 struct간의 상속이 불가능 하기 때문에, 프로토콜을 채택하여 상속과 비슷한 효과를 낼 수 있다.

참조: Choosing Between Structures and Classes


Definition Syntax

// 픽셀 기반의 화면 해상도를 나타내는 구조체
struct Resolution {
    // structure definition goes here
    var width: Int = 0
    var height: Int = 0
}

// 비디오 화면을 위한 특정 비디오 모드를 설명하는 클래스
class VideoMode {
    // class definition goes here
    var resolution: Resolution = Resolution()
    var interlaced: Bool = false
    var frameRate: Double = 0.0
    var name: String?	// nil
}

위의 사용자 정의 타입인 구조체와 클래스에서는 프로퍼티 기본값을 이용하여 각각의 프로퍼티를 선언 및 초기화하였다.

Structure and Class Instances

프로퍼티를 초기화 하는 방법에는 초기화 메서드를 이용하는 방법도 존재하는데 아래와 같다.

struct Resolution {
    var width: Int
    var height: Int
    
    init() {
        self.width = 0
        self.height = 0
    }
    
    init(width: Int, height: Int) {
        self.width = width
        self.height = height
    }
}

class VideoMode {
    var resolution: Resolution
    var interlaced: Bool
    var frameRate: Double
    var name: String?
    
    init() {
        self.resolution = Resolution()
        self.interlaced = false
        self.frameRate = 0.0
        self.name = ""
    }
    
    init(resolution: Resolution, interlaced: Bool, frameRate: Double, name: String?) {
        self.resolution = resolution
        self.interlaced = interlaced
        self.frameRate = frameRate
        self.name = name
    }
}

각각의 Resolution과 VideoMode에 두개의 초기화 메서드를 생성해주었다.
init() 메서드를 사용하면 '기본 생성자 (default constructor 또는 default initializer)'로써, 프로퍼티 기본값을 사용한 것과 동일하다.

반면에 init(args) 메서드를 사용하면 '인수 생성자 (arguments constructor 또는 argument initializer)'로써, 구조체/클래스의 인스턴스를 생성하는 시점에 값을 주입하여 인스턴스의 프로퍼티를 초기화한다.

let resolution1: Resolution = Resolution()
let resolution2: Resolution = Resolution(width: 10, height: 10)

let videoMode1: VideoMode = VideoMode()
let videoMode2: VideoMode = VideoMode(resolution: Resolution(width: 10, height: 10), 
									  interlaced: true, 
                                      frameRate: 10.0, 
                                      name: "VideoMode2")

💡 VideoMode 클래스의 기본생성자와 인수생성자에서 Resolution의 인스턴스가 생성 또는 주입됨에 따라 이를 의존성 주입 (dependency injection) 이라고 한다.


Structures and Enumerations Are Value Types

Swift에서 모든 구조체와 열거형은 값 타입 (value type)이다.

let hd: Resolution = Resolution(width: 1920, height: 1080)
var cinema: Resolution = hd

cinema.width = 2048
print("cinema is now \(cinema.width) pixels wide")
// Prints "cinema is now 2048 pixels wide"
print("hd is still \(hd.width) pixels wide")
// Prints "hd is still 1920 pixels wide"

Resolution struct 타입의 인스턴스 hd를 생성하고 cinema라는 새로운 변수에 hd를 대입해주었다.
cinema의 width를 변경하여도 hd의 width에는 영향을 주지 않는데,
이는 cinemahd의 값이 복사 되었기 때문이다.

💡 값 타입의 복사를 '깊은 복사 (deep copy)' 라고 한다.

값 타입은 메모리의 스택 (stack) 영역에 저장되어진다.

따라서, 스택 영역에는 hdcinema에 대한 독립적인 주소를 가지고 있으므로, 둘은 서로 완전히 분리된 인스턴스이다.

func address(of object: UnsafeRawPointer) -> String {

    let address = Int(bitPattern: object)
    return String(format: "%p", address)
}

var hd: Resolution = Resolution(width: 1920, height: 1080)
var cinema: Resolution = hd

print("Address of `hd`: \(address(of: &hd))")
print("Address of `cinema`: \(address(of: &cinema))")

💡 스택 영역에 저장된 hdcinema가 서로 다른 메모리 주소값을 가지고 있음을 알 수 있다.

그러나 컬렉션 타입의 구조체 (Array, Set, Dictionary 등)String 타입에는 COW (Copy-on-Write) 라는 기법이 내장 구현되어 있어 값 복사가 일어나도 동일한 메모리 주소를 갖고 있다.

Classes Are Reference Types

struct가 값 타입이었다면, 반대로 class는 참조 타입 (reference types)이다.

let tenEighty: VideoMode = VideoMode()

tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0
let alsoTenEighty: VideoMode = tenEighty
alsoTenEighty.frameRate = 30.0

print("The frameRate property of tenEighty is now \(tenEighty.frameRate)")
// Prints "The frameRate property of tenEighty is now 30.0"

VideoMode class 타입의 인스턴스인 tenEighty를 생성하고 각 프로퍼티들을 초기화 한 뒤, alsoTenEighty라는 상수를 만들어 tenEighty를 대입해주었다.
alsoTenEighty의 frameRate를 변경하면, tenEighty의 frameRate도 같이 변경된다.

let alsoTenEighty: VideoMode = tenEighty

이 부분에서 복사가 일어나긴 했지만, class의 복사는 값 타입처럼 값 자체를 복사하여 넘겨주는 것이 아니라 class의 인스턴스를 참조한다.

💡 참조 타입의 복사를 '얕은 복사 (shallow copy)' 라고 한다.

참조 타입은 힙 (heap) 영역에 저장되어진다.

class의 인스턴스는 runtime 시점에 생성이 이루어지는데, 힙 영역에서 인스턴스가 생성되면 tenEightalsoTenEighty가 VideoMode 인스턴스의 메모리 주소를 참조하게 된다.

func address(of object: UnsafeRawPointer) -> String {
    let address = Int(bitPattern: object)
    return String(format: "%p", address)
}

print("Address of `VideoMode Instance`: \(Unmanaged.passUnretained(tenEighty).toOpaque())")
print("Address of `VideoMode Instance`: \(Unmanaged.passUnretained(alsoTenEighty).toOpaque()) \n")

print("Address of `tenEighty`: \(address(of: &tenEighty))")
print("Address of `tenEighty`: \(address(of: &alsoTenEighty))")

💡 tenEightyalsoTenEighty는 동일한 VideoMode 인스턴스의 주소를 참조하고 있고, 인스턴스를 참조하는 두 변수의 메모리 주소는 스택 영역에서 따로 존재한다.

profile
 iOS Developer

0개의 댓글