본문 바로가기

학습 노트/Swift (2021)

128 ~ 134. Initializer and Deinitializer (생성자와 소멸자)

Initializers

열거형, 구조체, 클래스는 모두 설계도의 역할을 한다.
이들을 이용해 생성한 것을 인스턴스라고 한다.
새로운 인스턴스를 생성하는 것을 초기화라고 한다.
이 초기화를 담당하는 것이 initializer이다.
initializer는 모든 속성을 기본값으로 초기화해서 인스턴스를 기본 상태로 만드는 역할을 한다.
인스턴스가 초기화 되었다라는 것은 initializer가 동작을 완료했을 때 인스턴스의 모든 속성이 기본값을 가졌다는 의미이다.
만약 기본값을 가지지 않은 속성이 있다면 초기화에 실해파고 인스턴스가 생성되지 않는다.

초기화를 하는 방법은 두가지이다.

  • 속성을 생성함과 동시에 초기화 하기
class Position {
	var x = 0.0
	var y = 0.0
}

이 경우 initializer를 사용해 다시 초기화할 필요는 없다.

  • initializer를 사용해 초기화 하기.
class Position {
	var x: Double
	var y: Double
	
	init() {
		x = 0.0
		y = 0.0
	}
}

속성이 항상 같은 값으로 초기화된다면 첫 번째 방법을,
속성이 파라미터를 통해 초기화돼야 한다면 두 번째 방법을 주로 사용한다.

class Position {
	var x: Double?
	var y: Double?
}

속성을 optional로 초기화하면 initializer를 사용하지 않아도 오류가 발생하지 않는다.
optional은 기본값을 저장하지 않아도 자동으로 nil로 초기화되기 때문이다.

만약 위와 같이 모든 속성이 기본값을 가지게 된다면 자동으로 default initializer가 생성된다.
이를 기본 생성자라고 부른다.
default initializer는 사용자가 별도의 initializer를 구현하면 더 이상 제공되지 않는다.

Syntax

init(parameters) {
    initialization
}

TypeName(parameters)

초기화는 가능한 한 빨리 완료되어야 하므로 초기화를 제외한 다른 코드를 작성하는 것은 지양해야 한다.
기본적으로 메서드 생성, 호출 문법과 동일하다.

class Size {
	var width: Double
	var height: Double
	
	init(width: Double, height: Double) {
		self.width = width
		self.height = height
	}
}

var s = Size(width: 7, height: 8)
s.width
s.height
결과

7
8

너비와 높이를 파라미터로 받는 initializer를 생성 후 사용했다.
파라미터의 이름과 속성의 이름이 동일해 'self.'를 생략하면 이 둘을 구별할 수 없다는 것에 주의하자.

경우에 따라 파라미터의 수를 달리해서 초기화를 해야 하는 경우가 있다.
이번엔 파라미터를 하나만 받아서 두 값을 모두 초기화하는 initializer를 구현해 본다.

class Size {
	var width: Double
	var height: Double
	
	init(width: Double, height: Double) {
		self.width = width
		self.height = height
	}
	
	init(value: Double) {
		width = value
		height = value
	}
}

이렇게 새 initializer를 생성해도 문제는 없다.
하지만 중복을 없애기 위해 기존에 만든 생성자를 재사용해 보도록 하자.

class Size {
	var width: Double
	var height: Double
	
	init(width: Double, height: Double) {
		self.width = width
		self.height = height
	}
	
	init(value: Double) {
		self.init(width: value, height: value)
	}
}
결과

//error

'self.'를 통해 기존에 생성한 initializer를 사용하려 시도했지만 에러가 발생한다.

class Size {
	var width: Double
	var height: Double
	
	init(width: Double, height: Double) {
		self.width = width
		self.height = height
	}
	
	convenience init(value: Double) {
		self.init(width: value, height: value)
	}
}

이 때는 convenience initializer로 선언해야 한다.
또한 지금처럼 생성자에서 다른 생성자를 불러오는 것은 initializer deligation이라고 하는데,
둘 다 이후에 다시 언급하도록 한다.

initializer들은 종류에 따라 호출 문법이 달라지지 않는다.
따라서 파라미터를 통해 각자가 구별되어야 한다는 것을 명심하자.

 

Memberwise Initializer

struct Sizeval {
	var width = 0.0
	var height = 0.0
}

var sv = Sizeval()
sv.width
결과

0

모든 속성이 생성과 동시에 기본값을 가지므로 기본 생성자가 제공된다.
하지만 구조체라면 기본생성자 외에도 memberwise initializer를 함께 제공한다.

struct Sizeval {
	var width = 0.0
	var height = 0.0
}

var sv = Sizeval(width: 3.4, height: 5.6)
sv.width

파라미터를 통해 속성 전체를 초기화하기 때문에 memberwise initializer라고 부른다.
파라미터의 수가 구조체의 속성의 수와 동일하고, 파라미터와 아규먼트 이름이 속성의 이름과 동일하다.
기본 생성자와 마찬가지로 별도의 생성자를 선언하게 되면 사용할 수 없다.

struct First {
	let a: Int
	let b: Int
	let c: Int
}

let f = First(a: 1, b: 2, c: 3)
f.a
결과

1

속성이 초기화되어있지 않지만 memberwisw initializer는 제공된다.

struct Second {
	let a: Int = 0
	let b: Int = 1
	let c: Int
}

let s = Second(c: 2)

struct Third {
	var a: Int = 0
	var b: Int = 1
	var c: Int
}

let t = Third(a: 0, b: 1, c: 2)

만약 부분적으로 기본값을 가지게 된다면 기본값을 가진 속성을 제외한 initializer를 제공한다.
Second의 속성은 상수로 선언되어 있어 속성 선언 때 한 번 초기화된 값을 다시 변경하는 모습이 된다.
따라서 변수로 선언한 Third의 속성과는 다르게 생성자에서 다시 초기화를 할 수가 없다.

파라미터의 수는 속성의 수와 동일하고
아규먼트 이름과 파라미터 이름은 속성의 이름과 같다.
속성이 상수로 선어 되어 있고, 기본값을 가지고 있다면 생성자의 파라미터에서는 제외된다.
속성이 바로 선언되어 있으면 해당 속성이 포함되어 있는 생성자와 포함되지 않는 생성자를 모두 제공한다.

struct Second {
	let a: Int = 0
	let b: Int = 1
	let c: Int
}

let s = Second(c: 2)

struct Third {
	var a: Int = 0
	var b: Int = 1
	var c: Int

	init(value: Int) {
		a = value
		b = value
		c = value
	}
}

let t = Third(a: 0, b: 1, c: 2)
결과

//error

memberwise initializer는 default initializer와 마찬가지로 생성자를 별도로 생성하면 사용할 수 없게 된다.
하지만 둘 다 사용해야 할 경우가 잦은데, 이 경우엔 매번 새로 생성자를 정의해 주어야 하지만 매우 번거로운 것이 사실이다.

struct Third {
	var a: Int = 0
	var b: Int = 1
	var c: Int
}

extension Third {
	init(value: Int) {
		a = value
		b = value
		c = value
	}
}

let t = Third(a: 0, b: 1, c: 2)

이 때는 extension을 사용해 구조체 자체에는 생성자를 제외해 memberwise initializer를 제공할 수 있도록 하고,
필요한 생성자를 따로 구현해 둘 다 사용할 수 있다.

 

Class Initializers

클래스에서 사용하는 initializer는 designated initializer와 concenience initializer 두 가지로 구분된다.

Designated Initializer (지정 생성자)

Designated initializer는 클래스의 main initializer이다.
따라서 클래스가 가진 모든 속성을 초기화한다는 뜻이다.
이전에 언급한 일반 initializer가 이에 해당한다.

Syntax

init(parameters) {
    initialization
}

 

일반 initializer와 차이가 없지만 기능상의 차이가 있다.
클래스에서 지정 생성자가 작업을 마치기 전에 슈퍼클래스의 지정 생성자를 호출해야 한다.
이것을 initializer delegation이라고 하며 이후에 따로 언급한다.

클래스에서 구현할 수 있는 designated initializer의 수에는 제한이 없지만 보통 하나만 작성한다.
기본 생성자를 사용하거나 designated initializer를 상속받았다면 따로 구현할 필요는 없다.
하지만 이외의 경우엔 최소한 하나의 designated initializer가 필요하다.

class Position {
	var x: Double
	var y: Double
	
	init(x: Double, y: Double) {
		self.x = x
		self.y = y
	}
}

신경 써야 하는 건 클래스의 모든 속성을 초기화해야 한다는 것이다.

Convenience Initializer (간편 생성자)

convenience initializer자체는 모든 속성을 초기화할 필요는 없다.
필요한 속성만 초기화 한 뒤, 다른 initializer를 호출해 나머지 속성을 초기화한다.
이때, 슈퍼클래스의 initializer를 호출할 수 없기 때문에 본인이 속한 클래스 내의 initializer를 호출해야만 한다.
이에 대해서도 후에 언급한다.

class Position {
	var x: Double
	var y: Double
	
	init(x: Double, y: Double) {
		self.x = x
		self.y = y
	}

	convenience init(x: Double) {
		self.init(x: x, y: 80)
	}
}

y 하나만 필요한 값으로 초기화하는 convenience initialer를 작성했다.
이때 'self.'를 사용해서 초기화를 시도하면 designated initializer와 중족 되기 때문에 이를 호출하여 값을 전달한다.

두 initializer의 호출도 차이 없이 사용할 수 있다.

class Position {
	var x: Double
	var y: Double
	
	init(x: Double, y: Double) {
		self.x = x
	self.y = y
	}
	
	convenience init(x: Double) {
		self.init(x: x, y: 80)
	}
}

var p = Position(x: 100, y: 10)
p.x
p.y

p = Position(x: 900)
p.x
p.y
결과

100
10
900
80

Initializer Inheritance (생성자 상속)

생성자도 다른 속성들과 마찬가지로 서브클래스에 상속 가능하다.
단, 일반 속성들 보다는 조건이 조금 까다롭다는 차이점이 있다.

기본적으로 슈퍼클래스에서 구현한 initializer는 서브클래스로 상속되지 않는다.

class Figure {
	var name: String
	
	init(name: String) {
		self.name = name
	}
	
	func draw() {
		print("draw \(name)")
	}
}

class Rectangle: Figure {
	var width: Double
	var height: Double
}
결과

//error

Rectangle 클래스는 figure 클래스를 상속받고 있다.
이때 생성자를 함께 상속받았다고 가정하고 생성자를 호출하게 되면
Rectangle에서 선언한 다른 속성들을 초기화할 수 없고, 이는 생성자의 규칙을 어기게 된다.

따라서 두 가지 조건이 필요하다.

  • 서브클래스의 모든 속성이 기본값으로 초기화되어있고, designated initializer를 직접 구현하지 않았다면 슈퍼클래스의 모든 designated initializer가 상속된다.
class Figure {
	var name: String
	
	init(name: String) {
		self.name = name
	}
	
	func draw() {
		print("draw \(name)")
	}
}

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
}
  • 서브클래스가 모든 designated initializer를 상속받았거나 오버 라이딩했다면 모든 convenience initializer가 상속된다.
class Figure {
	var name: String
	
	init(name: String) {
		self.name = name
	}
	
	func draw() {
		print("draw \(name)")
	}
}

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
	
	init(name: String, width: Double, height: Double){
		self.name = name
		self.width = width
		self.height = height
	}
}
결과

//error

만약 모든 속성이 Rectangle에서 선언된 속성이었다면 올바른 코드이지만 name은 슈퍼클래스에서 생성된 속성이므로 에러가 발생한다.
또한, 슈퍼클래스의 designated initializer를 호출하고 있지 않다.
따라서 다음과 같이 수정한다.

class Figure {
	var name: String
	
	init(name: String) {
		self.name = name
	}
	
	func draw() {
		print("draw \(name)")
	}
}

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
	
	init(name: String, width: Double, height: Double){
		self.width = width
		self.height = height
		super.init(name: name)
	}
}

위와 같이 본인이 선언한 속성을 초기화하고, 슈퍼클래스의 생성자를 호출해 상속받은 속성을 초기화한다.

class Figure {
	var name: String
	
	init(name: String) {
		self.name = name
	}
	
	func draw() {
		print("draw \(name)")
	}
}

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
	
	init(name: String, width: Double, height: Double){
		self.width = width
		self.height = height
		super.init(name: name)
	}

	override init(name: String) {
		width = 0.0
		height = 0.0
		super.init(name: name)
	}
}

오버 라이딩하는 경우에도 자신이 생성한 속성을 초기화하고, 슈퍼클래스의 desginated initializer를 호출한다.
단, convenience initializer의 경우 오버 라이딩이 적용되지 않는다.

 

Required Initializer (회수 생성자)

class Figure {
	var name: String
	
	init(name: String) {
		self.name = name
	}
	
	func draw() {
		print("draw \(name)")
	}
}

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
}

Figure 클래스를 상속받는 Rectangle 클래스의 모든 속성이 기본값을 가진다.
또한 designated initializer를 구현하고 있지 않기 때문에 슈퍼클래스의 initializer를 상속받게 된다.
하지만 name 속성을 초기화하는 initializer가 필요해졌다면 이때 required initializer를 사용한다.

Syntax

required init(parameters) {
    initialization
}

 

required 키워드를 init 키워드 앞에 추가하는 것으로 쉽게 구현할 수 있다.

class Figure {
	var name: String
	
	required init(name: String) {
		self.name = name
	}
	
	func draw() {
		print("draw \(name)")
	}
}

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
}

이렇게 Figure의 생성자를 required initializer로 선언하게 되면,
상속받은 서브클래스도 동일한 생성자를 선언하도록 강제한다.

하지만 위의 코드는 오류가 나지 않는데, 이 required initializer도 규칙이 존재한다.

  • 모든 속성이 기본값을 가지고, 별도의 initializer를 선언하지 않았다면 required initializer는 상속된다.
class Figure {
	var name: String
	
	required init(name: String) {
		self.name = name
	}
	
	func draw() {
		print("draw \(name)")
	}
}

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
	
	init() {
		width = 0.0
		height = 0.0
		super.init(name: "nil")
	}
}
결과

//error

따라서 이렇게 initializer를 작성하면 규칙이 깨져 required initializer를 구현해야 하는 상황이 됐다.

class Figure {
	var name: String
	
	required init(name: String) {
		self.name = name
	}
	
	func draw() {
		print("draw \(name)")
	}
}

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
	
	init() {
		width = 0.0
		height = 0.0
		super.init(name: "nil")
	}
	
	required init(name: String) {
		width = 0.0
		height = 0.0
		super.init(name: name)
	}
}

이렇게 동일한 형태의 required initializer를 추가하는 것으로 오류가 사라졌다.
슈퍼클래스에 존재하는 initializer를 동일하게 서브클래스에서 구현하는 것은 오버 라이딩이다.
오버 라이딩 시에는 init 앞에 override 키워드를 추가했었다.

 

Initializer Delegation

initializer delegation은 초기화 코드에서 중복을 최소화하고, 효율을 위해 사용한다.
initializer delegation은 값 형식과 참조 형식에서 다른 방식으로 구현해야 한다.

Value Type Initializer Delegation

struct Size {
	var width: Double
	var height: Double
	
	init(w: Double, h: Double) {
		width = w
		height = h
	}
	
	init(value: Double) {
		width = value
		height = value
	}
}

모든 속성이 초기화되므로 지금은 잘 작동하는 코드이지만,
초기화 규칙이 바뀐다면 모든 initializer를 수정해야 하는 번거로움이 생긴다.
따라서 모든 속성을 초기화하는 initializer를 구현하고, 다른 initializer가 이를 호출하도록 구현하는 것이 좋다.

struct Size {
	var width: Double
	var height: Double
	
	init(w: Double, h: Double) {
		width = w
		height = h
	}
	
	init(value: Double) {
		self.init(w: value, h: value)
	}
}

이렇게 다른 initializer를 호출하여 속성을 초기화 하는 것을 initializer delegation이라고 한다.

Class Initializer Delegation

클래스는 상속을 지원하고, 기본적으로 제공하는 생성자가 두 개이기 때문에 조금 번거로울 수 있다.
상속계층을 따라 올라가면서 모든 속성을 초기화할 수 있도록 신경 쓰도록 하다.

  • Delegate Up
    designated initializer는 반드시 슈퍼클래스의 designated initializer를 호출해야 한다.
    동일한 클래스에 있는 designated initializer를 호출하는 것은 불가능하다.
  • Delegate Across
    convenience initializer는 동일한 클래스에 있는 다른 initializer를 호출해야 한다.
    슈퍼클래스의 initializer를 호출하는 것은 불가능하다.
  • convenience initializer를 호출했을 때 최종적으로 동일한 클래스에 있는 designated initializer가 호출되어야 한다.
class Figure {
	let name: String
	
	init(name: String) {
		self.name = name
	}
	
	convenience init() {
		self.init(name: "unKnown")
	}
}

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
	
	init(name: String, width: Double, height: Double) {
		self.width = width
		self.height = height
		super.init(name: name)
	}
	
	convenience init(value: Double) {
		self.init(name: "Rectangle", width: value, height: value)
	}
}

 

Figure 클래스에는 name 속성과
name을 파라미터로 초기화하는 designated initializer,
name을 "unknown"으로 초기화 하는 convenience initializer로 구성되어 있다.

Rectangle 클래스에는 width와 height 속성을 가지며, Figure 클래스를 상속받고,
상속받은 name과 width, height를 각자의 파라미터로 초기화하는 designated initializer,
상속받은 name을 "Rectangle"로 초기화하고, 파라미터로 width와 height로 초기화하는 convenience initializer로 구성되어있다.

Rectangle의 Designated initializer는 슈퍼클래스인 Figure의 designated initializer를 호출하고 있다. 이는 첫 번째 규칙인 delegate up을 만족시킨다.
Rectangle의 Convenience initializer는 동일한 클래스의 initializer이자 designated initializer를 호출하고 있다. 이는 두 번째 규칙인 delegate across를 만족시킨다.

코드를 더 확장해 보자.

class Figure {
	let name: String
	
	init(name: String) {
		self.name = name
	}
	
	convenience init() {
		self.init(name: "unKnown")
	}
}

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
	
	init(name: String, width: Double, height: Double) {
		self.width = width
		self.height = height
		super.init(name: name)
	}
	
	convenience init(value: Double) {
		self.init(name: "Rectangle", width: value, height: value)
	}
}

class Square:  Rectangle {
	convenience init(value: Double) {
		self.init(name: "Square", width: value, height: value)
	}
	
	convenience init() {
		self.init(value: 0.0)
	}
}

이번엔 Rectangle을 상속받는 Square 클래스가 추가됐다.

Square 클래스는 어떠한 속성도 가지지 않으며, designated initializer도 가지지 않는다. 그렇기 때문에 상속 관계인 Square의 designated initializer를 상속받는다.
첫 번째 convenience initializer에서 호출하는 designated initializer는 상속받았지만 자기 자신의 initializer 취급을 한다.
따라서 슈퍼클래스를 가리키는 'super.' 키워드가 아닌 자기자신을 가르키는 'self.'가 되어야 하며, delegate across이다.
두 번째 convenience initializer는 자신의 첫 번째 convenience initializer를 호출하고 있다.
이로써 모든 convenience initializer는 designated initializer를 호출하도록 구성되어 있다. 이는 세 번째 규칙을 만족시킨다.

클래스 초기화

클래스 초기화는 두 가지 단계로 이루어진다.

  • 선언돼있는 모든 속성이 초기화된다.
    초기화는 서브클래스에서 슈퍼클래스 방향으로 상속계층을 타고 순서대로 진행된다.
    만약 초기화할 수 없는 속성이 있다면 초기화는 실패하고 더 이상 진행되지 않는다.
    만약 모든 속성이 초기화되면 첫 번째 단계가 완료되고, 인스턴스의 유효성이 확보된다.
  • 첫번째 단계에서 할 수 없었던 작업을 진행한다.
    슈퍼클래스에서 서브클래스 순으로 진행한다.
    두 번째 단계에선 인스턴스 속성에 접근하거나 메서드를 호출하는 것이 가능하다.
    부가적인 단계이기 때문에 작업 없이 완료되는 경우도 있다.

Square 클래스의 initializer 중에 파라미터가 없는 convenience initializer를 호출한다.
initializer가 호출되면 인스턴스를 저장할 메모리 공간이 생성된다. 단, 메모리가 초기화된 것은 아니다.
이어서 자신의 다른 convenience initializer를 호출하고, 다시 이어서 Rectangle에서 상속받은 designated initializer를 호출한다.
여기까지가 delegate across이다.
호출된 designated initializer는 delegate up이다. 따라서 Rectangle의 designated initializer가 호출된다.
여기서 속성을 초기화하고 다시 Figure의 designated initializer를 호출한다.
여기까지 왔다면 호출할 initializer가 없으므로 상속계층을 초기화하면서 첫 번째 단계가 완료된다.

두 번째 단계는 다시 내려가면서 부가적인 작업을 진행한다.
첫 번째로 호출되었던 Square의 파라미터가 없는 initializer까지 내려오면 초기화 과정이 종료된다.

모든 initializer에는 delegation 코드가 존재한다.
따라서 'super.'나 'self.'를 사용해서 다른 initializer를 호출하고 있다.
각 단계에서 실행되는 코드는 delegation 코드를 기준으로 결정된다.

init(name: String, width: Double, height: Double) {
	self.width = width
	self.height = height
	super.init(name: name)
}

여기서 속성을 초기화하는 코드는 delegation 앞에 존재한다.
이런 경우라면 첫 번째 과정에서 실행된다.
반대로 delegation 뒤에 존재하면 두 번째 과정에서 실행된다.

 

Failable initializer

지금까지 정리한 initializer들은 실패를 가정하지 않아, 실패하는 경우 오류를 발생시킨다.
컴파일 단계에서 실패하지 않는다는 것이 보장되어야 한다.
따라서 Nonfailable initializer라고 분류한다.

Failable initializer는 실패를 허용하고, 에러 대신 결과로 nil을 반환한다.
Optional과 비슷한 특성을 가진다.

 

Syntax

초기화에 성공하면 인스턴스를 반환하고, 실패하면 nil을 반환.
init?(parameters) {
    initialization
}

초기화에 성공하면 인스턴스를 반환하고 강제 추출의 형식이기 때문에 Nonoptional 형식이며, 실패하면 crash가 발생한다.
init!(parameters) {
    initialization
}

 

struct Position {
	let x: Double
	let y: Double
	
	init?(x: Double, y: Double) {
		guard x >= 0.0, y >= 0.0 else { return nil }
		
		self.x = x
		self.y = y
	}
	
	init!(value: Double) {
		guard value >= 0.0 else {
			return nil
		}
		self.x = value
		self.y = value
	}
}

var a = Position(x: 12, y: 34)
type(of: a)
a = Position(x: -12, y: 34)
type(of: a)
결과

Position
Optional<__lldb_expr_17.Position>.Type
nil
Optional<__lldb_expr_17.Position>.Type

initi? 결과의 자료형은 Optional Position이다.

struct Position {
	let x: Double
	let y: Double
	
	init?(x: Double, y: Double) {
		guard x >= 0.0, y >= 0.0 else { return nil }
		
		self.x = x
		self.y = y
	}
	
	init!(value: Double) {
		guard value >= 0.0 else {
			return nil
		}
		self.x = value
		self.y = value
	}
}

var b = Position(value: 12)
type(of: b)
b = Position(value: -12)
type(of: b)
결과

Position
Optional<__lldb_expr_17.Position>.Type
nil
Optional<__lldb_expr_17.Position>.Type

하지만 init! 의 경우에도 Optional Position과 nil이 반환된다.
이는 반환형인 IUO의 구조 변경으로 optional과 같이 취급해 실패 시 nil을 반환하도록 변경되었기 때문이다.
다음과 같이 확인해 보자.

struct Position {
	let x: Double
	let y: Double
	
	init?(x: Double, y: Double) {
		guard x >= 0.0, y >= 0.0 else { return nil }
		
		self.x = x
		self.y = y
	}
	
	init!(value: Double) {
		guard value >= 0.0 else {
			return nil
		}
		self.x = value
		self.y = value
	}
}

var b: Position = Position(value: 12)
type(of: b)
b = Position(value: -12)
type(of: b)
결과

//error

이번엔 자료형을 Position으로 강제해 Optional로 바꾸지 못하게 했다.
이번엔 위에서 파악한 대로 크래쉬가 발생한다.

init! 은 IUO를 반환하기 때문에 IUO의 처리방식을 미리 알고 있어야 대응할 수 있다.
또한, 초기화에 실패한 경우 크래쉬가 발생한다.

위의 두 가지 단점 때문에 주로 init? 이 사용된다.

failable initializer에도 몇 가지 규칙이 있다.

  • failable initializer는 오버 로딩이 가능하다.
struct Position {
	let x: Double
	let y: Double
	
	init?(x: Double, y: Double) {
		guard x >= 0.0, y >= 0.0 else { return nil }
		
		self.x = x
		self.y = y
	}
	
	init!(value: Double) {
		guard value >= 0.0 else {
			return nil
		}
		self.x = value
		self.y = value
	}

	init(value: Double) {
		self.x = value
		self.y = value
	}
}
결과

//error

따라서 위와 같이 파라미터의 이름과 자료형이 같으면 에러가 발생한다.

struct Position {
	let x: Double
	let y: Double
	
	init?(x: Double, y: Double) {
		guard x >= 0.0, y >= 0.0 else { return nil }
		
		self.x = x
		self.y = y
	}

	init!(value: Double) {
		guard value >= 0.0 else {
			return nil
		}
		self.x = value
		self.y = value
	}
	
	init(value: Int) {
		self.x = value
		self.y = value
	}
}

그 말은 위와 같이 파라미터의 자료형을 다르게 하거나 파라미터의 이름을 바꾸면 다른 initialzer로 인식한다는 뜻이다.

또한 failable과 nonfailable을 구분하지 않기 때문에 파라미터의 중복에 유의해야 한다.

  • Nonfailable initializer에서는 init! 만 호출할 수 있다.
  • Failable initializer는 다른 failable initializer를 호출할 수도, nonfailable initializer를 호출해도 괜찮다.
  • 슈퍼클래스의 deignated intializer가 failable인지 nonfailable인지 구분하지 않는다.
  • 수퍼클래스의 failable initializer를 서브클래스에서 failable 형태로 오버 라이딩하거나, nonfailable 형태로 오버 라이딩해도 문제없다.
    단, nonfailable 형태로 오버 라이딩했다면 상위 구현을 호출할 때 강제 추출을 진행해야 한다.

 

Deinitializer (소멸자)

initializer와 반대로 인스턴스가 메모리에서 제거되기 전에 정리 작업을 구현하기 위해 사용한다.

Syntax

deinit {
    Deinitializeation
}

 

deinitializer는 클래스 전용이다.
또한, 클래스 내에 하나로 제한되며, 인스턴스가 메모리에서 제거되기 직전 자동으로 호출되기 때문에 호출 문도 필요 없다.

class Size {
	var width = 0.0
	var height = 0.0
}

class Position {
	var x = 0.0
	var y = 0.0
}

class Rect {
	var origin = Position()
	var size = Size()
}

이렇게 클래스 내에 deinitializer가 정의되어 있지 않다면 자동으로 제공된다.
스위프트에서 클래스가 사용했던 대부분의 메모리는 자동으로 제거된다.

class Size {
	var width = 0.0
	var height = 0.0
}

class Position {
	var x = 0.0
	var y = 0.0
}

class Rect {
	var origin = Position()
	var size = Size()
}

//-----

var r : Rect? = Rect()
r = nil

위와 같이 nil을 전달하면 메모리가 자동으로 제거된다.
또한 Rect내에서 생성하는 인스턴스 또한 자동으로 제거된다.

따라서 deinitializer를 직접 구현하는 경우는 매우 드물다.

file이나 network연결처럼 자동으로 정리되지 않는 리소스를 대상으로 사용하거나,
디버깅 용도로 활용한다.

디버깅 용도로 사용하기 위해 로그를 호출해 보자.

class Size {
	var width = 0.0
	var height = 0.0
}

class Position {
	var x = 0.0
	var y = 0.0
}

class Rect {
	var origin = Position()
	var size = Size()
	
	deinit {
		print("deinit \(self)")
	}
}

var r: Rect? = Rect()
결과

Rect

위와 같이 인스턴스 생성까지만 실행하면 로그를 확인할 수 없다.
하지만 위에서 언급했듯 nil을 저장하는 과정을 거치게 되면

class Size {
	var width = 0.0
	var height = 0.0
}

class Position {
	var x = 0.0
	var y = 0.0
}

class Rect {
	var origin = Position()
	var size = Size()

	deinit {
		print("deinit \(self)")
	}
}

var r: Rect? = Rect()
r = nil
결과

Rect
deinit __lldb_expr_37.Rect

 


Log

2021.09.12.
블로그 이전으로 인한 글 옮김 및 수정