타입캐스팅, 초기화, 옵셔널 체인😱

DEVJUN·2022년 6월 18일
0

Swift 문법

목록 보기
12/14
post-thumbnail

1. 타입 캐스팅

  부모 클래스로부터 상속된 자식 클래스는 자기 자신의 타입이기도 하면서, 동시에 부모클래스의 타입이기도 하다.

	class Vehicle {
    	var currentSpeed = 0.0
        
        func accelerate() {
        	self.currentSpeed +=1
        }
    }
    
    class Car: Vehicle {
    	var gear: Int {
        	return Int(self.currentSpeed / 20) + 1
        }
    
    	func wiper() {
        	
        } 
    }
    
    let trans: Vehicle = Car()

  마지막 구문에서 상수로 선언된 trans는 Car 클래스의 인스턴스를 할당받지만 Vehicle 타입으로 선언되었다. Car 클래스는 Vehicle 클래스를 상속받은 자식 클래스이며, 따라서 Vehicle 클래스에 정의된 모든 프로퍼티와 메소드를 물려받는다. Vehicle 클래스 타입에 구현되어 있어야 하는 프로퍼티와 메소드가 상속을 통해 모두 구현되어 있으므로 Car 클래스는 형식상 Vehicle 타입으로 간주할 수 있다. 하지만 반대로 Vehicle 인스턴스를 Car 클래스 타입 변수나 상수에 할당할 수는 없다. Car 타입이 되기 위해 가져야 할 요소들 중에서 gear 프로퍼티나 wiper() 메소드가 Vehicle 클래스에는 정의되어 있지 않기 때문❗️이다.

  따라서 상속 관계에 있는 클래스일 경우 부모 클래스 또는 상위 클래스 타입의 변수나 상수에 인스턴스를 할당하는 것은 허용되지만, 그 반대는 일반적으로 성립되지 않는다.

	let car: Car = Vehicle() // Error!

  단순히 바로 위의 부모 클래스 타입으로만 선언될 수 있는 것은 아니다. 부모 클래스의 부모 클래스, 즉 상위 클래스에 해당하는 타입은 모두 사용할 수 있다.

	class SUV: Car {
    	var fourWheel = false
    }
    
    let jeep: Vehicle = SUV()

  이처럼 상위 클래스 타입으로 선언하면 할수록 사용할 수 있는 메소드와 프로퍼티, 초기화 구문의 범위는 점점 줄어들겠지만, 이 변수에 할당할 수 있는 객체의 종류는 늘어날 것이다.

1.
	func move(param: SUV) {
    	param.accelerate()
    }

2.
    func move(param: Vehicle) {
    	param.accelerate()
    }    

  함수나 메소드의 인자값을 정의할 때 하위 클래스 타입으로 선언하는 것보다 상위 클래스 타입으로 선언하면 인자값으로 사용할 수 있는 객체의 범위가 훨씬 넓어진다.

  위의 코드 1번에서 SUV 타입의 인스턴스를 인자값으로 입력받아 메소드를 실행한다. SUV 타입은 구체화된 클래스 형태이므로 SUV 클래스이거나 적어도 이 클래스를 상속받은 하위 클래스의 인스턴스만 인자값으로 사용할 수 있지만 2번에서 Vehicle 타입의 인스턴스를 인자값으로 받아 메소드를 실행하면 훨씬 넓은 범위의 객체를 인자값으로 받을 수 있게 된다.

1 - 1 타입 비교 연산

  스위프트는 타입 비교 연산자 is를 지원한다. 변수나 상수 또는 인스턴스에 이 연산자를 사용하면 할당된 값을 비교하는 것이 아니라 타입이 일치하는지 여부를 비교하고 그 결과를 Bool형태로 반환해준다.

	인스턴스(또는 변수, 상수) is 비교대상 타입
    
    SUV() is SUV // true
    SUV() is Car // true
    SUV() is Vehicle //true
    
    Car() is Vehicle // true
    Car() is SUV // false

  인스턴스를 할당받을 수 있는 타입이라면 결과로 true를, 할당 받을 수 없는 타입이라면 false를 반환한다.

1.
	let myCar: Vehicle = SUV()
    
    if myCar is SUV {
    	print("myCar는 SUV 타입임")
    } else {
    	print("myCar는 SUV 타입아님")
    }
    
    // myCar는 SUV 타입임

2.  
	let newCar: Vehicle = Car()
    
    if newCar is SUV {
    	print("newCar는 SUV 타입임")
 	} else {
    	print("newCar는 SUV 타입아님")
    }
    
    // newCar는 SUV 타입아님

1 - 2 타입 캐스팅 연산

	let someCar: Vehicle = SUV()

  위 코드에서 someCar 상수는 실제로는 SUV 클래스의 인스턴스가 할당되어 있지만, 컴파일러는 이 상수를 Vehicle 타입으로 간주한다. 타입 어노테이션이 실제 대입된 값의 타입보다 우선하기 때문이다. 그래서 Vehicle 클래스에 선언되지 않은 프로퍼티나 메소드를 사용할수는 없다. someCar 상수를 이용하여 SUV() 클래스에 선언된 프로퍼티를 사용하고 싶거나, SUV 타입을 인자값으로 받는 함수에 사용하려면 어떻게 해야할까?

  스위프트에서는 이같은 경우를 위해 제한적으로 타입을 변환할 수 있는 기능을 제공한다. 이를 타입 캐스팅이라고 한다. 허용되는 범위 내에서만 타입을 변환할 수 있다.

  타입 캐스팅은 업 캐스팅(Up Casting)다운 캐스팅(Down Casting)으로 나누어진다.

  • 업 캐스팅(Up Casting)

    • 하위 클래스 타입을 상위 클래스 타입으로 변환할 때
    • 캐스팅하기 전 타입이 하위 클래스, 캐스팅한 후 타입이 상위 클래스일 때
    • 캐스팅한 결과, 캐스팅하기 전 타입보다 추상화될 때
    • 일반적으로 캐스팅 과정에서 오류가 발생할 가능성이 없음

  • 다운 캐스팅(Down Casting)

    • 상위 클래스 타입을 하위 클래스 타입으로 캐스팅할 때
    • 캐스팅하기 전 타입이 상위 클래스, 캐스팅한 후 타입이 하위 클래스
    • 캐스팅한 결과, 캐스팅하기 전 타입보다 구체화될 때
    • 캐스팅 과정에서 오류가 발생할 가능성이 있음
    • 오류에 대한 처리 방식에 따라 옵셔널 캐스팅과 강제 캐스팅으로 나누어짐



  타입 캐스팅을 위한 연산자는 as이다. 업 캐스팅을 할 때는 단순히 as 연산자만 사용하면 되지만, 다운 캐스팅을 할 때는 as? 또는 as!를 상황에 맞게 사용해야 한다.

    let anyCar: Car = SUV()
    let anyVehicle = anyCar as Vehicle
    
    let anySUV = anyCar as? SUV
    if anySUV != nil {
    	print("\(anySUV!) 캐스팅 성공!")
    }
    // SUV 캐스팅 성공

  앞에서 Car 타입으로 선언된 상수 anyCar를 SUV 타입으로 캐스팅하였다. anyCar 상수가 본래 선언되었던 Car 클래스보다 캐스팅하고자 하는 SUV 클래스가 하위이므로 다운 캐스팅에 해당한다. 따라서 오류 가능성이 있으므로 옵셔널 캐스팅을 위한 as? 연산자를 사용하였다. anyCar에 할당된 본래 값이 SUV 인스턴스이므로 예시 구문에서의 다운 캐스팅은 성공적으로 실행되며 그 결과로 옵셔널 형태인 SUV?타입이 반환된다.

  위의 코드를 다음과 같이 축약해서 사용할 수도 있다.

	if let anySUV = anyCar as? SUV {
    	print("\(anySUV) 캐스팅 성공!")
    }

  만약 다운 캐스팅이 반드시 성공할 것이라는 확신이 있다면 다음과 같이 강제 캐스팅 구문을 사용해도 된다.

	let anySUV = anyCar as! SUV
    print("\(anySUV) 캐스팅 성공!")



1 - 3   Any, AnyObject 🤔

  타입 캐스팅을 수행할 때 일반적으로 상속 관계에 있는 클래스들끼리만 캐스팅할 수 있다. 상속관계에 있지 않은 클래스 간에는 타입 캐스팅 할 수 없으므로 억지로 캐스팅하려 하면 오류가 발생한다. 하지만 상속 관계에 있지 않아도 타입 캐스팅할 수 있는 예외가 있는데, 바로 Any와 AnyObject 타입을 사용할 때이다.

  Any와 AnyObject는 무엇이든 다 받아들일 수 있는 일종의 범용 타입이다.

	var allCar: AnyObeject = Car()
    allCar = SUV()

  또한 모든 클래스의 인스턴스는 AnyObject 타입으로 선언된 함수나 메소드의 인자값으로 사용될 수 있으며, AnyObject 타입을 반환하는 함수나 메소드는 모든 종류의 클래스를 반환할 수 있다는 의미로 해석되기도 한다. 고정된 하나의 타입만을 저장할 수 있는 배열이나 딕셔너리, 지합에서도 AnyObject 타입을 사용할 수 있는데, 이는 모든 클래스를 저장할 수 있다는 뜻이다.

	func move(_ param: AnyObject) -> AnyObject {
    	return param
    }
	
    move(Car())
    move(Vehicle())
    
    ===============================================

	var list = [AnyObject]()
    list.append(Vehicle())
    listappend(Car())
    

  결론적으로 AnyObject라는 것은 클래스이기만 하면 된다라는 의미로 해석할 수 있고, Any는 클래스에 제한되지 않고, 스위프트에서 제공하는 모든 타입을 허용한다고 해석할 수 있다.

	var value: Any = "Sample String"
    value = 3
    value = false
    value = [1, 3, 4 , 5]
    value = {
    	print("함수")
    }
    

  value에 정수, 문자열, 배열 익명함수까지 전부 대입된다.이는 함수의 매개변수로 넘길 때 배열이나 딕셔너리, 집합 등에서도 마찬가지로 작용한다.

  Any가 정말 매력적이고 좋아보일수 있지만 Any 타입의 남용은 스위프트에서 사용되는 정적인 타입들을 모두 동적인 타입으로 바꾸어 버리는 결과를 가져온다. 즉 Any 타입을 사용하면 실제로 값이 할당되는 시점에 타입이 정해진다. 동적인 타입은 코드를 작성하기에 편리하다는 장점이 있지만, 실행해보기 전에는 값의 타입을 알기 어려우므로 컴파일러가 오류를 잡아내지 못한다. 모든 오류가 런타임 오류로 발생하는 결과를 낳게 된다. 따라서 앱 개발의 생산성을 저하하는 결과를 낳게 될 수 있다.


2. 초기화 구문

  구조체나 클래스는 모두 정의된 내용을 그대로 사용할 수 없다. 항상 인스턴스를 생성해서 메모리 공간을 할당 받은 다음에 사용해야 한다. 이를 초기화라고 한다.

  초기화 메소드는 형태나 문법은 다를지라도 다른 객체 지향 언어에서도 찾아볼 수 있는 개념으로, 인스턴스가 생성될 때의 형식과 할 일을 정의한다고 하여 생성자(Constructor)라고 부르기도 한다.

2 - 1   init 초기화 메소드

	init(<매개변수> : <타입>, <매개변수> : <타입>, ...) {
    	1. 매개변수의 초기화
        2. 인스턴스 생성 시 기타 처리할 내용
    }

  init 메소드는 다음과 같은 특징을 갖는다

  • 초기화 메소드의 이름은 init으로 통일된다.
  • 매개변수의 개수, 이름, 타입은 임의로 정의할 수 있다.
  • 매개변수의 이름과 개수, 타입이 서로 다른 여러 개의 초기화 메소드를 정의할 수 있다.
  • 정의된 초기화 메소드는 직접 호출되기도 하지만, 대부분 인스턴스 생성 시 간접적으로 호출된다.

struct Resolution {
	var width = 0
    var height = 0
    
    // 초기화 메소드: Width를 인자값으로 받음
    init(width: Int) {
    	self.width = width
    }
}

class VideoMode {
	var resolution = Resolution(width: 2048)
    var interlaced = false
    var frameRate = 0.0
    var name: String?
    
	// 초기화 메소드: interlaced, frameRate 두 개의 인자값을 받음
    init(interlaced: BOool, frameRate: Double) {
    	self.interlaced = interlaced
        self.frameRate = frameRate
    }
}

// Resolution 구조체에 대한 인스턴스를 생성
// 호출시 init 메소드 생략 가능
let resolution = Resolution(width: 4096)

// videoMode 클래스에 대한 인스턴스를 생성
let videoMode = VideoMode(interlaced: true, frameRate: 40.0)



class VideoMode {
	var resolution = Resolution(width: 2048)
    var interlaced = false
    var frameRate = 0.0
    var name: String?
    
    
    // 초기화 될 때 name 인자값만 받는 init 구문
    init(name: String) {
    	self.name = name
    }
    
    /// 초기화 될 때 interlaced 인자값만 받는 init 구문
    init(interlaced: String) {
    	self.interlaced = interlaced
    }
    
	// 초기화 메소드: interlaced, frameRate 두 개의 인자값을 받음
    init(interlaced: BOool, frameRate: Double) {
    	self.interlaced = interlaced
        self.frameRate = frameRate
    }
}	

  위 코드는 여러가지 초기화 메소드를 보여주고 있다. 초기화 메소드의 오버로딩 예이다.

class VideoMode {
	var resolution = Resolution(width: 2048)
    var interlaced = false
    var frameRate = 0.0
    var name: String?
    
    
    // 초기화 될 때 name 인자값만 받는 init 구문
    init(name: String) {
    	self.name = name
    }
}	

let defaultVideoMode = VideoMode() // Error!
let nameVideoMode = VideoMode(name: "홍길동") // ( 0 )

  init 메소드를 추가한 후, 기본 초기화 구문을 이용해서 객체를 생성하려고 하면 오류가 발생한다. 이러한 오류를 해결하기 위해선
인자값이 있는 init 메소드에서 매개변수에 기본값을 지정하면 해결 할 수 있다.

	// 초기화 될 때 name 인자값만 받는 init 구문
    init(name: String = "") {
    	self.name = name
    }
    
    let defaultVideoMode = VideoMode() // ( 0 )
    let nameVideoMode = VideoMode(name: "홍길동") // ( 0 )

2 - 2 초기화 구문의 오버라이딩

  클래스에서는 초기화 구문도 일종의 메소드이므로, 자식 클래스에서 오버라이딩할 수 있다.

class Base {
}

class ExBase: Base {
	override init() {
    
    }
}

  위 예제에서 클래스 ExBase는 기본 클래스로 선언된 Base 클래스를 상속하도록 작성되었다. 초기화 구문을 오버라이딩 하면 더 이상 부모 클래스에서 정의한 초기화 구문이 실행되지 않는다. 만약 부모 클래스의 기본 초기화 구문에서 프로퍼티를 초기화했다면, 자식 클래스에서 기본 초기화 구문을 오버라이딩함으로써 부모 클래스 프로퍼티가 누락된다. 프로퍼티가 초기화되지 못하는 상황은 오류를 발생시키므로 오버라이딩된 초기화 구문 내부에 super.init 구문을 작성하면 된다.

class Base {
	var baseValue: Doubel
    init(inputValue: Double) {
    	self.baseValue = inputValue
    }

}

class ExBase: Base {
	override init(inputValue: Double) {
    	super.init(inputValue: 10.5)
    }
}

  위 코드에서 초기화 과정에서 입력받은 인자값을 baseValue 프로퍼티에 할당하는 구문이다. 자식 클래스인 ExBase에서 초기화 구문을 오버라이딩하면 해당 초기화 구문은 새롭게 작성되므로 ExBase 클래스를 초기화할 때 baseValue에 대한 값 할당이 이루어지지 않는다.

  baseValue 프로퍼티는 옵셔널 타입이 아닌 일반 타입이므로 반드시 초기값이 있어야 하지만 초기화 구문의 오버라이딩으로 인해 초기값 할당 구문이 누락되면 오류가 발생한다. 이러한 오류를 방지하기 위해 오버라이딩된 초기화 구문에서 부모 클래스의 초기화 구문을 직접 호출해 주게 된다.



3.  옵셔널 체인⛓

  옵셔널 타입은 앞 챕터에서도 본것 같이 nil이 할당될 수 있는 값을 말한다. 따라서 항상 nil 여부를 검사하여 정상적인 값이 저장된 것을 확인한 후에 사용되는 것이 안전하므로 if 구문을 통한 번거로움이 동반된다.

  옵셔널 타입이 중첩되어 있을 때 매번 if 구문을 중첩해서 작성하는 것은 코드를 작성해야 하는 입장에서 상당한 부담이 된다. 이러한 옵셔널의 치명적인 단점을 극복하고 복잡한 코드를 간결하게 줄여주는 방법으로 도입된 것이 ⛓옵셔널 체인이다.

  옵셔널 체인(Optional Chain)은 옵셔널 타입으로 정의된 값이 하위 프로퍼티나 메소드를 가지고 있을 때, 이 요소들을 if 구문을 쓰지 않고도 간결하게 사용할 수 있는 코드를 작성하기 위해 도입되었다.

struct Company {
	var ceo: Human?
    var companyName: String?
}
var startup: Company? = Company(ceo: Human(name: "대표", man: false), companyName: "KaKao")

if let company = startup {
	if let ceo = company.ceo{
    	if let name = ceo.name {
        	print("대표이사의 이름은 \(name)") // 대표이사의 이름은 대표
        }
    }
}

  위 코드는 옵셔널 체인을 적용하지 않은 if로 불편하게 참조하는 코드이다.

  이를 옵셔널 체인 형태로 하여 name 프로퍼티를 참조하려면

	if let name = startup?.ceo?.name {
    	print("대표이사의 이름은 \(name)")
    }
	
    // 옵셔널 체인을 이용하여 값을 할당할 경우
    startup?.ceo?.name = "LEE"

  옵셔널 체인을 이용하여 값을 할당할 경우 startup변수나 ceo 프로퍼티가 빈 값이라면 아무런 값도 할당되지 않은 채로 구문은 종료되지만 오류는 결코 발생하지 않으므로 안전하게 값을 할당할 수 있다.

  옵셔널 체인은 옵셔널 타입을 여러번 중첩하는데 이를 반환할 때는 여러개의 옵셔널 타입으로 감싸져 반환되지 않고 오직 하나의 옵셔널 객체로 감싸져 반환된다.👍🏻

profile
🧑🏻‍💻iOS

0개의 댓글