[C#] 상속 관계에 있는 클래스의 형변환

is 연산자와 as 연산자




C++의 다운 캐스팅과 연관된 연산자인 듯 하다.

예전 dynamic_cast<>를 공부할 때 비슷한 내용을

다뤘던 것 같다. C#에서는 좀 더 세련된(?) 형변환을

위해 멋진 연산자 두 개를 제공한다. 바로 is와 as




class Mammal{

public void Nurse() { ... }

}


class Dog : Mammal {

public void Bark() { ... }

}


class Cat : Mammal {

public void Meow() { ... }

}


익히 알고 있듯이 포유류는 포유류 이고

개도 고양이도 모두 포유류이다.


Mamal m = new Mammla();

m.Nurse();


m = new Dog();

m.Nurse();


Dog dog = (Dog)m;     // 다운 캐스팅이다.

dog.Nurse();                // 하지만 m이 가리키고 있는 것이

dog.Bark();                  // Dog이기 때문에 가능하다.


m = new Cat();

m.Nurse();


Cat cat = (Cat)m;        // 역시 다운 캐스팅.

cat.Nurse();                // 하지만 m이 가리키고 있는 것이

cat.Meow();                // Cat이기 때문에 가능하다.




위의 코드를 is 와 as 연산자를 사용해서 바꾸어보자.

먼저 is와 as를 간단하게 설명하자면


is

객체가 해당 형식에 해당하는지 검사하여

그 결과를 bool 값으로 반환한다.


as

형식 변환 연산자와 같은 역할을 한다.

다만 형변환 연산자가 변환에 실패하는 경우에

예외를 던지는 반면, as 연산자는 객체 참조를

null로 만들어 버린다. (dynamic_cast와 같은 성질)




Mammal m = new Dog();

Dog dog;


if( m is Dog ){                    // 말그대로  m 이 Dog를 가리키고 있니?

dog = (Dog)m;

dog.Bark();

}


Mammal m2 = new Cat();

Cat cat = m2 as Cat;         // m2를 Cat으로 형변환하라.

if( cat != null ){                 // 다운 캐스팅을 하라는 말인데

cat.Meow();             // 이 경우 m2는 Cat를 가리키고 있기 때문에 가능하다.

}                                      // 만약 m2 = new Mammal() 이었다면 불가능.

 // 파생 클래스는 기본 클래스보다 더 많은 정보를 담고 있는데

 // 기본 클래스에 없는 부분을 호출하려 한다면 문제가 발생.

// 때문에 이 경우 cat는 null이 된다.




C++의 시점에서 설명이 된 듯한데

확실히 C++의 dynamic_cast<> 보다는

훨씬 사용법이 간편한 것 같다.




일반적으로 형식 변환 연산자 대신 as 연산자를

사용하는 쪽이 더 권장 되는데, 형식 변환에 실패하더라도

예외가 일어나 갑자기 코드의 실행이 갑자기 점프하는 일이

없으므로 코드 관리가 더 수월해지기 때문이란다.


주의해야할 점은 as 연산자는 참조 형식에 대해서만

사용이 가능하므로 값 형식의 객체는 기존의 형식

변환 연산자를 사용해야 한다.





[C++] 인터페이스 상속과 구현 상속(3)


인터페이스와 더불어 그 함수의

필수적인 구현을 물려받게 하는 비가상 함수




1. 멤버 함수의 인터페이스 만을 상속받고 싶은 경우

2. 함수의 인터페이스 및 구현을 모두 상속받고 그 상속받은

구현이 오버라이드가 가능하게 만들었으면 할 경우

3. 또한 인터페이스와 구현을 상속받되 어떤 것도

오버라이드할 수 없도록 막고 싶은 경우




class Shape{

public:

virtual void draw const = 0;                ----- 1번

virtual void error (const string& msg);----- 2번

int objectId() const;                            ----- 3번


}


인터페이스와 그 함수의 필수적인 구현을 물려받게 하는 것

즉, 위의 예시에서 3번에 해당하는 경우가 되겠다.




int objectId() const; ----- 3번

객체의 식별자를 내어주는 함수인데

객체 식별자를 계산하는 방법은 항상 똑같다는

전재하에 선언된 함수라 할 수 있다.


실제 계산 방법은 Shape::objectID의 정의에서

결정되고, 파생 클래스에서는 이를 바꿀 수 없다.




매우 간단하기 때문에 더 이상 설명이 필요없지만,

인터페이스 상속과 구현 상속을 하는데 있어

자주 일어나는 실수에 대해 알아보도록 하자.




1. 모든 멤버 함수를 비가상 함수로 선언하지 말자.

이러한 경우 파생 클래스를 만들어도 자신만의 특별한

동작을 만들 여지가 없어진다. 기본 클래스로 사용할

용도로 디자인 한 것이라면 대부분 가상 함수를 갖고 있게 된다.




2. 모든 멤버 함수를 가상 함수로 선언하지 말자.

이러한 부분이 맞는 경우도 있겠지만, 분명히 파생 클래스에서

재정의가 안 되어야 하는 함수도 분명히 있을 것이다.

이러한 경우는 비가상 함수로 만들어 입장을 확실히 하자.




POINT!!

비가상 함수는 인터페이스 상속과 더불어 필수 구현의 상속을 뜻한다.

[C++] 인터페이스 상속과 구현 상속(2)


인터페이스만을 물려주는 상속에 이어

인터페이스뿐만 아니라 그 함수의 기본 구현도

물려받는 상속에 대해 알아보도록 하자.


1. 멤버 함수의 인터페이스 만을 상속받고 싶은 경우

2. 함수의 인터페이스 및 구현을 모두 상속받고 그 상속받은

구현이 오버라이드가 가능하게 만들었으면 할 경우

3. 또한 인터페이스와 구현을 상속받되 어떤 것도

오버라이드할 수 없도록 막고 싶은 경우




class Shape{

public:

virtual void draw const = 0;                ----- 1번

virtual void error (const string& msg);----- 2번

int objectId() const;                            ----- 3번


}


인터페이스뿐만 아니라 그 함수의 기본 구현을 상속 받는것

즉, 위의 예시에서 2번에 해당하는 경우가 되겠다.


단순 가상 함수로 선이 되어 있는데, 순수 가상 함수와

비교했을 때 몇가지 다른 면을 가지고 있다. 파생 클래스가

인터페이스를 상속받는 것은 똑같지만, 파생 클래스 쪽에서

오버라이드 할 수 있는 함수 구현부도 제공한다는 점이다.




virtual void error (const string& msg); ----- 2번

이 함수가 뜻하는 것은 실행 중에 에러가 났을 때

이 함수를 제공하는 것은 모든 파생 클래스가 해야 하는

 일이지만, 그렇다고 각 파생 클래스마다 똑같은 방법으로

에러를 처리할 필요는 없다는 것이다. (재정의 가능)


또한 Shape 클래스에서 제공하는 기본 에러 처리

(기본 구현)를 사용해도 된다는 의미도 있다. ( 구현 상속 )




하지만 단순 가상 함수에서 함수 인터페이스와 기본 구현을

한꺼번에 지정하도록 내버려 두는 것은 위험할 수도 있다.

아래의 예제를 보도록 하자.




모든 비행기는 fly함수를 상속 받아

각각의 비행 방식을 설정할 수 있으며

기본 동작으로 움직일 수도 있다.


위의 두 비행기는 비행 동작이 같기 때문에

코드 중복을 피하기 위해 기본 비행 원리를

Airplane::fly 함수의 본문으로 제공했다.


여기까지는 객체 지향 기술의 장점을

모두 빼다박은 훌륭한 코드이다.




하지만 여기서 비행 방식이 완전히 다른

ModelC라는 비행기가 들어왔다.

때문에 fly 함수를 재정의 해야 하지만

만약 이부분을 깜빡했다면 큰 문제로 이어질 수 있다.


여기서 문제는 Airplane::fly 함수가 기본 동작을

구현해서 가지고 있다는 점이 아니라, ModelC가

기본 동작을 물려받는 데 아무런 제약이 없다는 것이다.

이 문제를 해결하기 위해서는 크게 두가지 방법이 있다.




01. 

가상 함수의 인터페이스와 그 가상 함수의

기본 구현을 잇는 연결관계를 끊어 버리는 방법




이제 fly함수가 순수 가상 함수로 선언되어 있기 때문에

파생 클래스에서는 무조건 이 함수를 재정의 해야한다.

ModelA , ModelB는 비가상 함수인 ( 구현만을 물려 받는 )

deflultFly를 fly함수 내에서 호출하면 된다.


ModelC는 fly를 재정의하지 않으면 안되기 때문에

앞서 보았던 재정의를 깜빡하는 일은 이제 할 수 없다.




02.

순수 가상 함수의 구현을 통해

함수의 선언부, 정의부를 나누는 방법




순수 가상 함수가 들어왔다는 것 말고는

이전 설계와 원리 모두 똑같다. 순수 가상 함수가

구체 파생 클래스에서 재선언되어야 한다는

사실을 활용하되, 자체적으로 순수 가상

함수의 구현을 구비해 두는 것이다.




하지만 이 방법은 01 방법과 다르게

함수의 각기 다른 보호 수준을 부여할 수 있는

융통성이 날아가 버린다. protected 영역에 있던

defaultfly가 순수 가상 함수의 영역으로 들어왔기 때문


저번 포스팅에서 순수 가상 함수의 구현을 주는 일은

단순 가상 함수에 대한 기본 구현을 보다 안전하게

제공하는 메커니즘으로 활용할 수 있다고 했는데

바로 이 부분이라 할 수 있겠다.




얘기가 길어졌지만, 이번 포스팅의 주제는

단순 가상 함수를 선언하는 목적은 파생 클래스로 하여금

함수의 인터페이스뿐만 아니라 그 함수의 기본 구현도

물려받게 하자는 것이다.



다음 포스팅에서는 인터페이스와 더불어 그 함수의

필수 구현을 물려받게하는 경우, 3번에 해당하는

내용을 다루도록 하겠다.


Point!!

단순 가상 함수는 인터페이스 상속과 더불어

기본 구현의 상속도 가능하도록 지정합니다.






+ Recent posts