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)는 프로그램 코드의 구성요소가 되는 범용적이고 유연한 구성입니다.
즉, 프로그램 코드의 구성요소가 되는 '사용자 정의 타입' 이라고 볼 수 있다.
먼저 둘의 공통점은 아래와 같다.
클래스는 구조체에 없는 추가적인 기능이 있다.
그러나 클래스 사용에 있어 단점이 존재한다.
"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)'을 추구하는 언어이기에,
일반적으로 컴파일 시점에 타입과 관련된 에러를 최소화하고 안정성을 높이기 위한 명목 하에 타입 추론을 이용하여 추론하기 쉬운 구조체 사용을 더 권장
하고 있다.
그렇다면, 구조체와 클래스 이 둘을 어느 상황에 맞게 사용해야 하는지는 다음과 같다.
// 픽셀 기반의 화면 해상도를 나타내는 구조체
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
}
위의 사용자 정의 타입인 구조체와 클래스에서는 프로퍼티 기본값
을 이용하여 각각의 프로퍼티를 선언 및 초기화하였다.
프로퍼티를 초기화 하는 방법에는 초기화 메서드를 이용하는 방법도 존재하는데 아래와 같다.
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) 이라고 한다.
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에는 영향을 주지 않는데,
이는 cinema
에 hd
의 값이 복사 되었기 때문이다.
💡 값 타입의 복사를 '깊은 복사 (deep copy)' 라고 한다.
값 타입은 메모리의 스택 (stack) 영역에 저장되어진다.
따라서, 스택 영역에는 hd
와 cinema
에 대한 독립적인 주소를 가지고 있으므로, 둘은 서로 완전히 분리된 인스턴스이다.
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))")
💡 스택 영역에 저장된
hd
와cinema
가 서로 다른 메모리 주소값을 가지고 있음을 알 수 있다.
그러나 컬렉션 타입의 구조체 (Array, Set, Dictionary 등)
과 String 타입
에는 COW (Copy-on-Write) 라는 기법이 내장 구현되어 있어 값 복사가 일어나도 동일한 메모리 주소를 갖고 있다.
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 시점에 생성이 이루어지는데, 힙 영역에서 인스턴스가 생성되면 tenEight
와 alsoTenEighty
가 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))")
💡
tenEighty
와alsoTenEighty
는 동일한 VideoMode 인스턴스의 주소를 참조하고 있고, 인스턴스를 참조하는 두 변수의 메모리 주소는 스택 영역에서 따로 존재한다.