본문 바로가기

학습 노트/Swift (2021)

122 ~ 127. Inheritance and Polymorphism (상속과 다형성)

Inheritance (상속)

코드의 중복 문제와 유지보수의 편의성을 위해 공통적인 속성을 공유하는 방식이다.

예외도 있지만 대부분의 상속관계에 있는 클래스들은 Class Hierarchy(클래스 계층)를 구성한다.
클래스 계층에서 가장 상위에 있는 클래스를 Base Class 혹은 Root Class라고 한다.
그 다음 클래스들은 이 베이스 클래스를 상속 받고, 이러한 관계에서 상위에 존재하는 클래스는 Super Class 혹은 Parent Class , 하위에 존재하는 클래스를 Subclass 혹은 Child Class 라고 부른다.
따라서 정리하면 베이스 클래스 아래로는 하나 이상의 서브클래스가 존재하지만 위로는 수퍼클래스가 존재하지 않는다.
여러개의 서브클래스가 하나의 수퍼클래스를 상속 받는 것은 문제가 없다. 하지만, 하나의 서브클래스가 여러개의 수퍼클래스를 상속 받는 것은 불가능하다.
이는 다중상속의 개념이지만 swift에선 불가능하다. 비슷한 기능은 protocol(프로토콜)을 통해 구현할 수 있다.

다른 클래스를 상속 받는 것을 Subclassing이라고 한다.
수퍼클래스를 상속 받는 서브클래스는 수퍼클래스의 멤버를 자신이 선언한 것 마냥 사용하는 것은 물론, 자신에 맞게 수정도 가능하다.
이것을 Overriding(재정의)라고 한다.

Syntax

class ClassNameSuperClassName {

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

class Circle: Figure {
	var radius = 0.0
}

let c = Circle(name: "Circle")
c.draw()
c.radius
결과

draw Circle
0

Circle 클래스는 생성자를 선언한 적이 없다.
하지만 Figure 클래스를 상속 받았으므로 자신이 선언한 것 처럼 사용할 수 있다.
draw 메소드도 마찬가지이다.

서브클래스는 수퍼클래스로부터 멤버를 상속 받는다.

final class

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

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

}

class Square: Rectangle {

}

Square 클래스는 Rectangle 클래스를, Rectangle 클래스는 Figure 클래스를 상속 받는다.
이와 같이 모든 클래스는 상속의 대상이 될 수 있지만 경우에 따라 금지해야 하는 때가 있을 수 있다.

 

Syntax

final class ClassNameSuperClassName {

}

 

class Figure {
	var name = "Uknown"

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

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

}

class Square: Rectangle {

}
결과

//error

Square 클래스가 상속 받으려는 클래스가 파이널 클래스라는 오류가 발생한다.
final 키워드를 class 키워드 앞에 작성하는 것 만으로 쉽게 구현할 수 있다.
파이널 클래스는 다른 클래스를 상속 받을 수 있지만, 다른 클래스가 파이널 클래스를 상속 받는 것은 불가능해 진다.

 

Overriding (재정의)

상속은 수퍼클래스의 멤버를 서브클래스에서 자유롭게 사용 할 수 있도록 한다.
이 때 멤버가 서브클래스에 적합하다면 그냥 사용하면 되지만, 적합하지 않다면 직접 수정하는 것도 가능하다. 이것이 Overriding이다.

오버라이딩이 가능한 멤버는 메소드, 속성, 서브스크립트, 생성자로 제한된다.
오버라이딩 구현에는 두가지의 방법이 있다.

  1. 수퍼클래스를 기반으로 새로운 코드를 추가하기.
  2. 수퍼클래스의 구현을 무시하고 완전히 재정의 하기.

메소드 오버라이딩

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

class Circle: Figure {
	var radius = 0.0
}

let c = Circle(name: "Circle")
c.draw()
결과

draw Circle

상속 파트에서 사용했던 코드이다.
Circle 클래스는 Figure 클래스를 상속 받는다.
이 상태라면 Figure 클래스에 정의 된 draw 메소드를 호출하게 된다.

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

class Circle: Figure {
	var radius = 0.0
	
	override func draw() {
		print("figure draw \(name)")
	}
}

let c = Circle(name: "Circle")
c.draw()
결과

figure draw Circle

오버라이딩은 재정의 할 멤버의 앞에 override 키워드를 추가 하는 것으로 간단히 구현 된다.
인스턴스의 출력 부분에서도 별도의 조치 없이 자동으로 재정의 된 draw 메소드를 호출 하는 것을 볼 수 있다.

다만 이렇게 재정의 한다고 원래의 상속 받은 멤버가 사라지는 것은 아니다.

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

class Circle: Figure {
	var radius = 0.0
}

let c = Circle(name: "Circle")
c.draw()
결과

draw Circle

상속 파트에서 사용했던 코드이다.
Circle 클래스는 Figure 클래스를 상속 받는다.
이 상태라면 Figure 클래스에 정의 된 draw 메소드를 호출하게 된다.

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

class Circle: Figure {
	var radius = 0.0
	
	override func draw() {
		super.draw()
		print("figure draw \(name)")
	}
}

let c = Circle(name: "Circle")
c.draw()
결과

draw Circle figure
draw Circle

'super.'로 접근하면 상속받은 원본 멤버에도 접근이 가능하다.

속성 오버라이딩

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

class Circle: Figure {
	var radius = 0.0
	
	override func draw() {
		super.draw()
		print("figure draw \(name)")
	}
}

//-----

class Oval: Circle {
	override var radius: Double = 12.34
}
결과

//error

속성을 오버라이딩 하는 경우 메소드와는 다르게 override 키워드 만으로는 구현이 불가능 하다.
계산 속성을 구현하거나, property observer를 사용한다.

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

class Circle: Figure {
	var radius = 0.0
	
	override func draw() {
		super.draw()
		print("figure draw \(name)")
	}
}

//-----

class Oval: Circle {
	override var radius: Double {
		return super.radius
	}
}
결과

//error

Circle.radius는 읽기 전용이 아니다.
읽기와 쓰기가 모두 가능한 속성을 읽기 전용 속성처럼 다루는 것은 불가능하다.

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

class Circle: Figure {
	var radius = 0.0
	
	override func draw() {
		super.draw()
		print("figure draw \(name)")
	}
}

//-----

class Oval: Circle {
	override var radius: Double {
		get {
			return super.radius
		}
		set {
			super.radius = newValue
		}
	}
}

위와 같이 getter와 setter 모두 구현해서 읽기와 쓰기를 모두 가능하게끔 구형해야 에러가 사라진다.

class Figure {
	var name = "Uknown"
	
	init(name: String) {
		self.name = name
	}

	func draw() {
		print("draw \(name)")
	}
}

class Circle: Figure {
	var radius = 0.0
	
	var diameter: Double {
		return radius * 2
	}
	
	override func draw() {
		super.draw()
		print("figure draw \(name)")
	}
}

class Oval: Circle {
	override var diameter: Double {
		get {
			return super.diameter
		}
		set {
			super.radius = newValue / 2
		}
	}
	
	override var radius: Double {
		get {
			return super.radius
		}
		set {
			super.radius = newValue
		}
	}
}

이번엔 Circle 클래스에서 읽기 전용 계산 속성 diameter를 생성하고,
Circle 클래스를 상속 받는 Oval 클래스에서 읽기와 쓰기를 모두 구현했다.
위에서 작성했던 radius 속성과는 반대이다.

Circle.diameter 속성은 읽기 전용 계산속성이기 때문에
Oval.diameter를 통해 값을 저장 할 수는 있지만 변수 저장속성으로 바뀌지는 않는다.
따라서 읽기 전용인 것에는 변함이 없다.
따라서 Oval.radiusdml setter는 다른 값을 변경하도록 구현된다.

다시말해 읽기 전용 속성을 읽기와 쓰기가 가능한 속성으로 오버라이딩 하는 것은 허용된다.
하지만, 읽기와 쓰기가 가능한 속성을 읽기 전용 속성으로 오버라이딩 하는 것은 허용되지 않는다.

getter와 setter에서 불러 온 속성은 'super.'를 통해 속성에 접근하고 있다.
'super.'대신 'self.'를 쓰는 것은 오버라이딩을 사용 할 때 흔히 하는 실수로, 찾아내기 어렵기 때문에 사용에 각별한 주의가 필요하다.
오버라이딩한 멤버에서 self에 접근 할 때는 재귀호출이 일어나지 않도록 유의하도록 하자.

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

class Circle: Figure {
	var radius = 0.0
	
	var diameter: Double {
		return radius * 2
	}
	
	override func draw() {
		super.draw()
		print("figure draw \(name)")
	}
}

class Oval: Circle {
	override var diameter: Double {
		get {
			return super.diameter
		}
		set {
			super.radius = newValue / 2
		}
	}
	
	override var radius: Double {
		willSet {
			print(newValue)
		}
		didSet {
			print(oldValue)
		}
	}
}

이번엔 property observer를 추가한다.
Circle.radius가 변수 저장속성이기 때문에 속성을 오버라이딩하고 property observer를 추가 할 수 있다.
하지만 Circle.diameter는 읽기 저장 속성이기 때문에 property observer를 추가 할 수 없다.

오버라이딩 금지하기

상속을 final class로 금지 시키듯이 오버라이딩도 오버라이딩을 금지시킬 수 있는 방법을 제공한다.

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

class Circle: Figure {
	var radius = 0.0
	
	var diameter: Double {
		return radius * 2
	}
	
	override func draw() {
		super.draw()
		print("figure draw \(name)")
	}
}

class Oval: Circle {
	override var diameter: Double {
		get {
			return super.diameter
		}
		set {
			super.radius = newValue / 2
		}
	}
	
	override var radius: Double {
		get {
			return super.radius
		}
		set {
			super.radius = newValue
		}
	}
}
결과

//error

Figure.draw 메소드의 선언 앞에 final 키워드를 붙여 오버라이딩을 금지 시켰다.
따라서 오버라이딩 중이었던 Circle.draw 메소드 부분에서 오류가 발생한다.
이는 저장속성에서도 마찬가지로 Circle.radius에 final 키워드를 추가 할 시 Oval.radius에서 에러가 발생한다.
하지만 이것은 오버라이딩이 금지됐을 뿐이지, 상속이 되지 않는 다는 것을 의미하진 않는다.

 

Upcasting and Downcasting

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

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
	
	override func draw() {
		super.draw()
		print("⬛️ \(width) x \(height)")
	}
}

class Square: Rectangle {

}

let f = Figure(name: "Unknown")
f.name
결과

UnKnown

Figure 클래스에는 name 속성과 생성자, draw 메소드가 선언되어 있다.
Rctangle 클래스는 width와 height 속성을 가지며, draw 메소드를 오버라이딩 한다.
Square 클래스는 Rectangle 클래스를 상속 받는다.

새로운 Figure 인스턴스인 f를 생성하고 해당 인스턴스의 name 속성에 접근할 수 있는 것은 당연하다.
하지만 Rectangle 클래스의 속성에 접근하는 것은 불가능하다.

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

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
	
	override func draw() {
		super.draw()
		print("⬛️ \(width) x \(height)")
	}
}

class Square: Rectangle {

}

let r = Rectangle(name: "Rect")
r.height
r.width
r.name
결과

0
0

Rectangle의 인스턴스 r에서 자신의 속성인 height와 width에 접근 할 수 있는 것은 당연하다.
또한 Figure로부터 상속 받은 name 속성에도 접근 할 수 있다.

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

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
	
	override func draw() {
		super.draw()
		print("⬛️ \(width) x \(height)")
	}
}

class Square: Rectangle {

}

let s = Square(name: "Square")
s.width
s.name
결과

0
Square

Square 클래스에는 어떤 속성도 선언되어 있지 않지만 상속 받은 Figure와 Square 클래스의 속성을 사용할 수 있다.

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

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
	
	override func draw() {
		super.draw()
		print("⬛️ \(width) x \(height)")
	}
}

class Square: Rectangle {

}

let f = Figure(name: "Unknown")
f.name

let r = Rectangle(name: "Rect")
r.height
r.width
r.name

let s = Square(name: "Square")
s.width
s.name

인스턴스 f의 자료형은 figure 클래스이다.
인스턴스 r의 자료형은 Rectangle 클래스이다.
인스턴스 s의 자료형은 Square 클래스이다.
단, Square 클래스가 Rectangle 클래스를 상속받고 있기 때문에 s의 자료형은 Rectangle 클래스이기도 하다.
또한 Rectangle 클래스가 Figure 클래스를 상속 받고 있으므로 s의 자료형은 Figure 클래스이기도하다.

Figure 클래스는
name 속성, name parameter를 받는 생성자, draw 메소드로 구성 된 클래스이다.
Rectangle 클래스는
Figure 클래스를 상속받고, width 속성, height 속성으로 구성 된 클래스이다.
Square 클래스는
Rectangle 클래스를 상속받는 클래스이다.

이러한 관계는 다음과 같은 결과를 나타낸다.

Upcasting (업캐스팅)

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

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
	
	override func draw() {
		super.draw()
		print("⬛️ \(width) x \(height)")
	}
}

class Square: Rectangle {

}

let f = Figure(name: "Unknown")
f.name

let r = Rectangle(name: "Rect")
r.height
r.width
r.name

let s: Figure = Square(name: "Square")
s.width
s.name

이렇게 서브클래스 인스턴스를 수퍼클래스 형식으로 저장 하는 것을 Upcasting이라고 한다.

Square
name width height

square 인스턴스를 생성하면 각각의 속성에 해당하는 저장공간이 생성된다.
이 인스턴스를 Figure 클래스로 업캐스팅 하면 Figure 클래스에 선언된 멤버로 접근 범위가 제한된다.

Square
name width height

그러므로 width, height 속성이 저장되긴 하지만 name을 제외한 속성들에는 접근 할 수 없다.
따라서 위의 코드에선 's.width' 라인에서 에러가 발생한다.

동일한 클래스 계층에서 진행되는 업캐스팅은 안전하고 항상 성공한다.
이 때 '안전하다.'의 의미는 에러가 발생하지 않는 다는 것을 의미한다.
그렇기 때문에 업캐스팅은 별도의 문법이 존재하지 않는다.

DownCasting (다운캐스팅)

다운캐스팅은 업캐스팅된 인스턴스를 원래대로 처리하기 위해 필요하다.
항상 성공하지도 않고, 안전하지도 않다.
또한, 다운캐스팅 시에는 타입캐스팅 연산자를 사용한다.

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

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
	
	override func draw() {
		super.draw()
		print("⬛️ \(width) x \(height)")
	}
}

class Square: Rectangle {

}

let f = Figure(name: "Unknown")
f.name

let r = Rectangle(name: "Rect")
r.height
r.width
r.name

let s: Figure = Square(name: "Square")

let downcastS = s as! Square
downcastS.width
결과

0

다운캐스팅에 성공해서 업캐스팅 시에는 접근할 수 없었던 width에 접근 가능해진 것을 확인 할 수 있다.

위와 같이 원래의 자료형으로 다운캐스팅 하는 것은 항상 성공한다.
또한 업캐스팅된 클래스의 서브클래스 이면서, 원본 클래스의 수퍼 클래스로 다운캐스팅 하는 것도 가능하다.

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

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
	
	override func draw() {
		super.draw()
		print("⬛️ \(width) x \(height)")
	}
}

class Square: Rectangle {

}

class Sample: Square {
	var angle = 45.0
}

let s: Figure = Square(name: "Square")
let sam = s as! Sample
결과

//error

이번엔 Square 클래스를 상속 받는 Sample 클래스를 생성하고, s 인스턴스를 Sample 클래스로 다운캐스팅 했다.
동일한 상속계층에 존재하지만 원래의 형식인 Square보다 아래에 위피한다.
이 경우엔 다운캐스팅에 실패한다.

Square
name width height

현재는 세개의 속성이 저장되어 있다.
이 속성들 중에는 수퍼클래스에서 상속 받은 속성도 저장되어 있다.
하지만 Sample 클래스에는 angle 속성이 선언되어 있다.

Square 클래스를 Sample 클래스로 다운캐스팅 하면 angle 속성에 접근할 수 있어야 하는데,
메모리에는 angle 속성이 존재하지 않는다. 따라서 원본보다 하위에 존재하는 서브클래스로의 다운캐스팅은 성공할 수 없다.

이는 angle 속성의 존재 유무와는 상관 없이 원본보다 하위 서브클래스로의 다운캐스팅은 허용되지 않는다.

 

Type Casting

타입 캐스팅은 인스턴스 형식을 확인하거나 다른 형식으로 인스턴스를 처리할 때 사용한다.

Type Check Operator

Syntax

expression is Type

 

좌변엔 표현식이 오는데 주로 형식을 확인 할 대상을 작성한다.
우변엔 항상 형식을 작성한다.
좌변과 우변의 형식이 동일하면 true,
좌변과 우변이 동일한 상속 계층에 있고, 우변이 수퍼클래스라면 true,
이외엔 false를 반환한다.

let num = 123
num is Int
결과

true

num 상수의 자료형은 Int가 맞으므로 true가 반환된다.

let num = 123
num is Double
num is String
결과

false
false

String과 Double은 Int와 호환되지 않기 때문에 False가 반환된다.

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

class Triangle: Figure {
	override func draw() {
		super.draw()
		print("🔺")
	}
}

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
	
	override func draw() {
		super.draw()
		print("⬛️ \(width) x \(height)")
	}
}

class Square: Rectangle {

}

class Circle: Figure {
	var radius = 0.0
	
	override func draw() {
		super.draw()
		print("🔴")
	}
}

let t = Triangle(name: "Triangle")
let r = Rectangle(name: "Rect")
let s = Square(name: "Square")
let c = Circle(name: "Circle")

r is Rectangle
r is Figure
r is Square
결과

true
true
false

코드에 선언 된 클래스의 상속 관계는 다음과 같다.

Figure
Triangle Rectangle Circle
  Square  

인스턴스 r의 형식은 Rectangle 클래스이므로 true
인스턴스 r의 형식인 Rectangle 클래스의 수퍼클래스는 Figure 클래스이므로 true
인스턴스 r의 형식인 Rectangle 클래스는 Square 클래스의 수퍼 클래스이므로 다운캐스팅이 불가하며, false
가 반환된다.

Type Casting Operator

Compile Time Castexpression as Type
Runtime Cast expression as? Type conditional Cast
expression as! Type Forced Cast

타입캐스팅 연산자는 좌변이 우변의 형식과 호환된다면 우변으로 변환 된 인스턴스를 반환한다.
이 때, 새로운 인스턴스를 반환 하는 것이 아닌 우변에 존재하는 멤버만 접근할 수 있는 임시 신스턴스를 반환한다.

서로 호환되는 형식으로 캐스팅 하는 것을 Bridging(브릿징)이라고 하는데 compile time cast는 이 때 주로 사용한다.

let nsstr = "str" as NSString
결과

str

String은 NSString과 호환된다.
따라서 위와 같이 compile time cast를 구현할 수 있다.
캐스팅에 실패하면 에러를 표시한다.

runtime cast는 실행해야 결과를 알 수 있다.
optional 때와 마찬가지로 forced cast를 사용하면 실패 시 크래쉬가 발생한다.
따라서 forced crash 보다는 conditional cast를 사용하는 것이 좋다.

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

class Triangle: Figure {
	override func draw() {
		super.draw()
		print("🔺")
	}
}

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
	
	override func draw() {
		super.draw()
		print("⬛️ \(width) x \(height)")
	}
}

class Square: Rectangle {

}

class Circle: Figure {
	var radius = 0.0
	
	override func draw() {
		super.draw()
		print("🔴")
	}
}

let t = Triangle(name: "Triangle")
let r = Rectangle(name: "Rect")
let s = Square(name: "Square")
let c = Circle(name: "Circle")

//-----

t as? Triangle
t as! Triangle
결과

Triangle
Triangle

원래의 클래스로 타입캐스팅 하는 것은 당연히 성공한다.
타입 캐스팅은 다운캐스팅에 사용하거나 값형식을 다른 형식으로 캐스팅 할 때 사용한다.

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

class Triangle: Figure {
	override func draw() {
		super.draw()
		print("🔺")
	}
}

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
	
	override func draw() {
		super.draw()
		print("⬛️ \(width) x \(height)")
	}
}

class Square: Rectangle {

}

class Circle: Figure {
	var radius = 0.0
	
	override func draw() {
		super.draw()
		print("🔴")
	}
}

let t = Triangle(name: "Triangle")
let r = Rectangle(name: "Rect")
let s = Square(name: "Square")
let c = Circle(name: "Circle")

//-----

var upcast: Figure = s
upcast = s as Figure
결과

Square
Square

이전에 작성했던 업캐스트 방법 외에도 타입 캐스팅 연산자를 사용해 업캐스트를 할 수 있다.

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

class Triangle: Figure {
	override func draw() {
		super.draw()
		print("🔺")
	}
}

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
	
	override func draw() {
		super.draw()
		print("⬛️ \(width) x \(height)")
	}
}

class Square: Rectangle {

}

class Circle: Figure {
	var radius = 0.0
	
	override func draw() {
		super.draw()
		print("🔴")
	}
}

let t = Triangle(name: "Triangle")
let r = Rectangle(name: "Rect")
let s = Square(name: "Square")
let c = Circle(name: "Circle")

//-----
var upcast: Figure = s

upcast as? Square
upcast as! Square
upcast as? Rectangle
upcast as! Rectangle
upcast as? Triangle
//upcast as! Triangle
결과

Square
Square
Square
Square
nil
----------
//error

원래의 자신의 클래스로 돌아오거나 수퍼클래스로의 타입캐스팅은 forced, conditional의 구분 없이 성공한다.
하지만 직접적인 상속관계가 없는 Circle이나 Triangle로의 타입캐스팅은
conditional의 경우 nil을, forced의 경우 오류를 반환한다.

실제로 다운캐스팅을 구현 할 때는

if let c = upcast as? Circle {
    
}

conditional cast와 optional binding을 함께 사용해서 구현하는 것이 좋다.

Polymorphism (다형성)

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

class Triangle: Figure {
	override func draw() {
		super.draw()
		print("🔺")
	}
}

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
	
	override func draw() {
		super.draw()
		print("⬛️ \(width) x \(height)")
	}
}

class Square: Rectangle {

}

class Circle: Figure {
	var radius = 0.0
	
	override func draw() {
		super.draw()
		print("🔴")
	}
}

let t = Triangle(name: "Triangle")
let r = Rectangle(name: "Rect")
let s = Square(name: "Square")
let c = Circle(name: "Circle")

//-----

let list = [t, r, s, c]
결과

[{{name "Triangle"}}, {{name "Rect"}, width 0, height 0}, {{{name "Square"}, width 0, height 0}}, {{name "Circle"}, radius 0}]

배열은 같은 자료형의 데이터만 담을 수 있지만 지금은 여러가지 클래스로 만든 여러가지 인스턴스를 저장하고 있다.
이게 가능한 이유는 저장할 때 가장 인접한 수퍼캐스트로 업캐스팅 되어서 저장되기 때문으로 베이스 클래스인 Figure 클래스로 저장된다.

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

class Triangle: Figure {
	override func draw() {
		super.draw()
		print("🔺")
	}
}

class Rectangle: Figure {
	var width = 0.0
	var height = 0.0
	
	override func draw() {
		super.draw()
		print("⬛️ \(width) x \(height)")
	}
}

class Square: Rectangle {

}

class Circle: Figure {
	var radius = 0.0
	
	override func draw() {
		super.draw()
		print("🔴")
	}
}

let t = Triangle(name: "Triangle")
let r = Rectangle(name: "Rect")
let s = Square(name: "Square")
let c = Circle(name: "Circle")

let list = [t, r, s, c]

//-----

for item in list {
item.draw()
}
결과

draw Triangle
🔺
draw Rect
⬛️ 0.0 x 0.0
draw Square
⬛️ 0.0 x 0.0
draw Circle
🔴

모두 같은 Figure 클래스로 업캐스팅 되어 저장 됐지만,
반복문을 통해 draw 메소드를 호출하면 오버라이딩 한 메소드가 호출된다.
이러한 특징이 다형성이다.

업캐스팅한 인스턴스를 통해 메소드를 호출하더라도 실제 형식에서 오버라이딩 한 메소드가 호출 된다.
이는 속성에는 적용되지 않으며, 속성에 접근하고 싶다면 다운캐스팅을 거쳐야 한다.

for item in list {
	item.draw()
	
	if let c = item as? Circle {
		c.radius
	}
}

위와 같이 타입캐스팅에 성공하는 경우 접근할 수 있도록 설계해야 한다.

 

Any and AnyObject (범용 자료형)

범용 자료형은 코드의 범용성을 높혀준다는 장점이 있지만 코드의 가독성을 해치고, 유지보수를 어렵게 한다.
프레임워크 사용시에 제한적으로 사용되기 때문에 해당 부분을 위주로 살펴본다.

var data = 1
data = 2.3
결과

//error

일반적으로 변수를 생성하면 해당 자료형의 데이터 밖에 저장하지 못한다.

var data: Any = 1
data = 2.3
data = "str"
data = [1, 2, 3]

하지만 자료형을 Any로 바꾸면 다른 형식에 맞게 저장 할 수 있게 된다.

anyobject는 참조형식만 저장할 수 있다.

var obj: AnyObject = NSString()
obj = 1
결과

//error

오브젝트를 저장하고 다시 값형식을 저장하면 에러가 발생한다.

외에도 Any가 접두어로 붙는 여러 형식이 있는데 이를 Type-Erading Wrapper라고 부른다.
말 그대로 이들은 형식에 대한 정보를 가지고 있지 않아 사용하기 위해서는 타입캐스팅을 반드시 필요로 한다.

var data: Any = NSString()

data.count
결과

//error

따라서 위와 같은 접근 보다는

var data: Any = NSString()

if let str = data as? String {
	print(str.count)
}
결과

0

위와 같이 타입캐스팅을 거쳐서 접근해야 한다.

만약 배열이 저장되어 있는 경우라면

var data: Any = NSString()

if let str = data as? String {
	print(str.count)
} else if let list = data as? [Int] {

}

위와 같이 타입 캐스팅을 하나 더 추가한다.

Type Casting Pattern

위의 배열에 저장된 경우 타입캐스팅을 작성하는 경우
타입캐스팅 패턴을 사용하면 코드의 가독성이 높아진다.

타입 캐스팅 패턴은 타입캐스팅과 스위치문을 함께 사용한다.

switch data {
case let str as String:
	print(str.count)
case let list as [Int]:
	print(list.count)
case is Double:
	print("Double")
default:
	break
}

스트링으로 캐스팅 되는 값을 매칭시킨다.
값이 매칭 된다면 str에 바인딩한다.
정수 배열로 매칭된 값을 list 상수로 바인딩한다.
주로 as를 사용하지만 is를 사용해서 구현할 수도 있다.
이외의 경우 종료한다.

타입캐스팅 패턴은 범용 자료형으로 되어있거나 업캐스팅 된 인스턴스를 다루는 경우 자주 사용한다.

 

Overloading (오버로딩)

하나의 형식에서 동일한 이름을 가진 다수의 멤버를 구현 할 때 사용한다.

func process() {
    
}

func process() {
    
}
결과

//error

위와 같이 동일한 스코프에 동일한 이름을 가진 함수가 존재하는 경우 호츌시의 모호함 때문에 에러가 발생한다

func process(value: Int) {
	print("Int")
}

func process(value: String) {
	print("String")
}
결과

Int
String

하지만 다른 형식의 parameter를 하나씩만 추가해도 에러는 사라진다.
이것이 오버로딩이다. swift는 오버로딩을 지원하는 언어이다.

오버로딩을 지원하지 않는 언어의 경우 함수의 이름 만으로만 함수를 구별하기 때문에 파라미터가 달라도 함수의 이름이 같다면 오류가 발생한다.
따라서 파라미터에 따라 이름을 달리 생성할 수 밖에 없다.
이 경우 같은 기능을 제공하더라도 파라미터의 종류 마다 하나씩 구현해야 하고, 함수의 이름이 많아진다.

swift는 함수, 메소드, 서브스크립트, 생성자에 대해 오버로딩을 지원한다.

오버로딩 규칙

  • 함수 이름이 동일하다면 파라미터의 수로 구별한다.
func process(v1: Int) {
	print("Int")
}
func process(v1: Int, v2: Int) {
	print("Int2")
}
  • 함수의 이름과 파라미터의 수가 동일하다면 파라미터의 자료형으로 구별한다.
    두 함수는 이름도 같고 파라미터의 수도 같지만 파라미터의 형식이 다르기 때문에 둘을 구별 할 수 있다.
func process(value: Int) {
	print("Int")
}

func process(value: String) {
	print("String")
}
  • 험수의 이름과 파라미터 수와 파라미터 형식이 같다면 아규먼트 이름으로 구별한다.
    두 함수는 이름과 파라미터 수, 파라미터 이름이 같지만 아규먼트 이름이 달라 둘을 구별 할 수 있다.
func process(value: String) {
	print("String")
}
func process(thing value: String) {
	print("thing")
}
  • 함수의 이름과 파라미터의 수, 파라미터 형식, 아규먼트 이름이 동일하다면 반환형으로 구별한다.
func process(value: String) -> String{
	return "return"
}
func process(value: String) -> Int{
	return 3
}

process(value: "top")
결과

//error

하지만 에러가 발생한다.

이번 경우엔 반환형으로 함수를 구별하기 때문에 변수나 상수에 저장하는 과정을 거쳐야 한다.

func process(value: String) -> String{
	return "return"
}
func process(value: String) -> Int{
	return 3
}

var intvalue: Int = process(value: "top")
var stringvalue: String = process(value: "top")
결과

3
return

저장할 변수나 상수의 자료형을 명시적으로 선언해 주면 반환값과 일치하는 함수로 구별해서 적용한다.
명시적 선언이 마음에 안 든다면

func process(value: String) -> String{
	return "return"
}
func process(value: String) -> Int{
	return 3
}

var intvalue = process(value: "top") as Int
var stringvalue = process(value: "top") as String
결과

3
return

타입캐스팅을 사용하는 방법도 있다.

규칙이 4가지나 존재하지만 가급적이면 반환형에서 구분해야 하는 경우는 피하는 것이 좋다.
함수를 기준으로 정리했지만 메소드, 생성자, 서브스크립트 모두 동일하게 적용된다.

struct Rectangle {
	func area() -> Double {
		return 0.0
	}
	static func area() ->Double {
		return 0.0
	}
}

let r = Rectangle()

r.area()
Rectangle.area()
결과

0
0

인스턴스 메소드와 형식 메소드는 각각 인스턴스로 호출하고, 형식 이름으로 호출하는 호출문의 차이가 있다.
따라서 완전히 동일한 메소드라도 인스턴스 메소드와 형식 메소드로 동시에 구현해도 문제가 되지 않는다.

 


Log

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