다형성

다형성은 객체가 여러 형태를 가질 수 있다는 것을 의미합니다.

virtaul은 자식 클래스에서 재정의(override)를 할 수 있게 하는 키워드이며,

override는 부모 클래스에 virtual이나 abstract로 선언된 프로퍼티나 메소드를 재정의하는 키워드 입니다.

virtual 키워드를 사용한 함수를 가상 함수라 합니다.

 

코드를 보고 설명을 보겠습니다!


[그림 1] 부모 클래스

Weapon을 부모 클래스로 private한 정수 _damage와 Damage 프로퍼티,

virtual 키워드를 사용해서 자식 클래스에서 재정의가 가능한 Attack 메소드와

재정의가 불가능한 MulNum 메소드가 있습니다.

 

[그림 2] 자식 클래스

Knife는 Weapon 클래스를 상속하고 있고, Attack을 override(재정의) 했습니다.

LongSword는 Knife 클래스를 상속하고 있고, Attack을 override 했습니다.

Gun 클래스는 Weapon 클래스를 상속하고 있고, Attack을 override 했습니다.

 

new

[그림 3] new

[그림 1]을 보면 MulNum 메소드는 virtual이나 abstract 키워드를 사용하지 않았기 때문에 override를 할 수 없습니다. 자식 클래스에서 override 키워드를 사용하지 않고 MulNum 메소드를 정의했더니 new 키워드를 사용해 Weapon.MulNum() 멤버를 숨기라는 경고가 나옵니다. [그림 2]에 50번째 줄에는 new를 사용했더니 경고가 사라진 것을 볼 수 있습니다.

 

[그림 2]에서 Knife의 MulNum 멤버는 new를 사용하지 않았고, Gun의 MulNum 멤버는 new를 사용했습니다.

new를 사용하는 지에 따라 어떤 영향이 있는지 확인하겠습니다.

 

[그림 4] new 사용 여부 테스트

똑같이 인스턴스를 만들어서 같은 값을 넣어주고, 각 객체의 MulNum 함수를 호출한 후, 출력했습니다.

 

[그림 5] 결과

각각 MulNum 함수를 잘 호출한 것을 볼 수 있습니다.

일단 이 결과로 보면 new 키워드를 사용하지 않아도 영향이 없는 것으로 보입니다.

 

자식 클래스 타입의 인스턴스를 만들고 MulNum 함수를 호출해서 사용했을 때,

부모 클래스 Weapon의 MulNum 함수를 호출하지 않고,

자신의 타입에 맞는 자식 클래스의 MulNum 함수를 호출합니다.

 

여기서 보면 의문점이 생길 수 있습니다.

그러면 가상 함수를 왜 만들고 자식 클래스에서 왜 가상 함수를 재정의할까요?

그리고 이것이 왜 다형성일까요?

 

이제 한번 알아보겠습니다.


[그림 6]

메소드들의 매개 변수 타입이 Weapon이며, 각각 weapon의 Attack 멤버와 MulNum 함수를 호출합니다. 

 

[그림 7] 테스트

knife와 gun의 Damage를 10으로 다시 설정해주고, [그림 6]의 MultiplyNum 메소드에 인수로 넘겨줍니다.

 

[그림 8] 결과

[그림 5]처럼 각 타입에 맞는 MulNum 함수가 호출되는 것이 아닌 Weapon의 MulNum 함수가 호출됩니다.

 

[그림 6]의 MultiplyNum 메소드의 매개 변수 타입이 Weapon이기 때문에 Weapon의 MulNum 함수가 호출됩니다. 이렇게 된다면 제가 원하지 않는 코딩 결과가 나옵니다.

 

이때, virtual과 override 된 멤버를 사용해서 코딩하면 원하는 결과를 얻을 수 있습니다.

 

다형성

코딩으로 먼저 보겠습니다.

 

[그림 9]

Knife의 자식 클래스인 LongSword 클래스 타입인 인스턴스를 생성해줍니다.

[그림 6]의 Attack에 인수로 knife와 longSword를 넘겨줍니다.

 

[그림 6]의 Attack의 매개 변수 타입은 Weapon 입니다.

Knife는 Weapon을 상속하고,

LongSword는 Knife를 상속합니다. 즉, Weapon도 상속합니다.

그래서 knife와 longSword는 Weapon 타입도 될 수가 있어서 인수로 넘겨줄 수 있습니다.

 

[그림 6]의 Attack의 인자인 weapon은 들어오는 인수에 따라 Knife가 될 수도 있고, Gun이 될 수도 있으며, LongSword가 될 수도 있습니다. 또, 들어오는 타입마다 실행하는 코드나 결과값이 달라져야 할 수도 있습니다. 이것이 다형성입니다.

Weapon 타입의 weapon이 여러 형태를 가질 수 있기 때문에 다형성의 특징을 가졌다고 할 수 있습니다.

 

[그림 2]에서 Knife 클래스와 LongSword 클래스에서 Attack 멤버는 override 되어 있고,

[그림 1]에서 Weapon 클래스의 Attack 멤버는 virtual를 사용했습니다.

 

[그림 10] 결과

[그림 8]의 결과와 달리 원하는 결과가 나왔습니다.

 

가상 함수를 사용해서 재정의하면 인스턴스의 타입을 런타임할 때, 체크해서 맞는 버전의 함수를 호출합니다.

knife가 Knife 클래스 타입이기 때문에 Knife의 Attack 함수를 호출하고,

longSword가 LongSword 클래스 타입이기 때문에 LongSword의 Attack 함수를 호출합니다.

 

만약, override 하지 않았다면 부모 클래스의 Attack 함수를 호출합니다.

 

[그림 11]

Knife클래스에서 재정의한 Attack 멤버를 주석 처리하고 [그림 9]를 다시 실행했습니다.

 

[그림 12] 결과

Knife 클래스에서 override 하지 않았기 때문에 부모 클래스인 Weapon의 Attack 함수를 호출했습니다.

 

sealed

자식 클래스에서 더이상 해당 멤버를 재정의 할 수 없도록 할 수도 있습니다.

 

[그림 13]

Knife 클래스에 있는 Attack 멤버에 sealed 키워드를 추가했습니다.

Knife 클래스를 상속하고 있는 LongSword 클래스에서 Attack 멤버를 override 하려 하니까 오류가 나는 것을 볼 수 있습니다. 즉, Knife의 자식 클래스들은 더이상 Attack 멤버를 재정의 할 수 없습니다.

 

base

부모 클래스의 필드나 프로퍼티, 메소드를 사용하고 싶으면 base 키워드를 사용하면 됩니다. 단, 접근 지정자가 private이면 안됩니다. this가 자기 자신을 가르키는 것처럼 base는 부모 클래스를 가르킵니다.

 

base.Damage를 하면 부모 클래스의 프로퍼티인 Damage를 가져올 수 있고,

base.MulNum()을 하면 부모 클래스의 멤버인 MulNum을 가져올 수 있습니다.

자식 클래스의 생성자에 부모 클래스의 다른 생성자를 호출하고 싶으면 base() 키워드를 사용하면 됩니다.

 

[그림 14]

테스트를 위해 Weapon 클래스에 기본 생성자를 만들어주고,

 

[그림 15]

Knife 클래스에 기본 생성자를 만들어주고, 이름 옆에 base()를 사용합니다.

public Knife() : base()는 객체가 생성되면,

부모 클래스의 생성자를 먼저 호출하고, Knife 생성자를 호출합니다.

 

[그림 16] 결과

결과를 보면 Weapon 클래스의 생성자가 먼저 호출되고, Knife 클래스의 생성자가 호출됐습니다.

 

매개 변수가 있는 생성자를 호출하고 싶으면 base(변수)를 하면 됩니다.

즉, public Knife() : base(10)을 하면,

Weapon 클래스에 있는 생성자 중에 매개 변수에 10이 들어갈 수 있는 생성자를 호출합니다.


제가 공부한 부분을 정리한 내용이기 때문에 틀린 부분 있을 수 있습니다!!

혹시 틀린 부분이 있으면 알려주세요!!

+ Recent posts