본문 바로가기
전공/C# 프로그래밍

7강. 객체지향 프로그래밍과 클래스 (4)다형성

by 임 낭 만 2023. 4. 18.

다형성

Introduction to 다형성 (Polymorphism)

  • 부모 클래스 객체가 자식 클래스 메소드를 사용하려면, 자식 클래스로 형식 변환 필요
    • 조건문 통해 자식 클래스 타입을 구별하고 해당 타입의 메소드 호출
  • 다형성은 형식변환 수행 없이 자식 클래스 메소드 호출 지원
using System;

namespace IntroPolymor
{
    class Animal
    {
        public int Age { get; set; }
        public Animal() { this.Age = 0; }

        public void Eat() { Console.WriteLine("냠냠 먹습니다."); }
        public void Sleep() { Console.WriteLine("쿨쿨 잠을 잡니다."); }
    }
    class Dog : Animal
    {
        public string Color { get; set; }
        public void Bark() { Console.WriteLine("멍멍 짓습니다."); }
    }
    class Cat : Animal
    {
        public void Meow() { Console.WriteLine("냥냥 웁니다."); }
    }
    class MainApp
    {
        static void Main(string[] args)
        {
            Animal[] Animals = new Animal[]{ new Dog(), new Cat(), new Dog(), new Cat()};
            foreach (var item in Animals)
            {
                item.Eat();
                if(item is Dog)                
                    ((Dog)item).Bark();                
                else
                    ((Cat)item).Meow();
            }
        }
    }
}

다형성 (Polymorphism)

  • 클래스의 객체가 여러 형태를 가질 수 있음을 의미  (하위 형식 다형석 Subtype Polymorphism)
    • 자신을 상속받아 만들어진 자식 클래스를 통해 다형성 실현
  • 다형성 지원을 위한 핵심 기능은 메소드 오버라이드(override)
    • 자식 클래스에서 부모 클래스의 메소드를 재정의
    • 부모 클래스 메소드에 virtual 키워드, 자식 클래스 메소드에 override 키워드 사용
    • 용도가 비슷해 보일 수 있는 메소드 하이딩 (hiding) 또는 오버로딩 (overloading) 과의 구분 필요
  • 메소드 오버라이드와 오버로드 사이의 차이
    • 오버라이드의 경우 부모/자식 클래스 메소드의 이름, 반환타입, 매개변수의 수, 개별 매개변수 타입이 모두 같음
    • 오버로드는 매소드 이름은 같지만, 매개변수의 수 또는 개별 매개변수 타입이 다름

변수 하이딩 (hiding)

  • 부모와 자식 클래스에 이름이 같은 프로퍼티 또는 필드 변수를 선언
    • new 키워드를 사용함
    • 자식 클래스의 객체는 자식 클래스의 변수, 부모 클래스의 객체는 부모 클래스의 변수 사용
using System;

namespace HidingVar
{
    class Parent
    {
        public int Variable { get; set; } = 273;
    }
    class Child : Parent
    {
        public new string Variable { get; set; } = "hiding";
    }
    class MainApp
    {
        static void Main(string[] args)
        {
            Child child = new Child();
            Console.WriteLine(child.Variable);
            Console.WriteLine(((Parent)child).Variable);
        }
    }
}

메소드 하이딩 (hiding)

기반 클래스의 메소드를 감추고 파생 클래스 구현만 표시

파생 클래스 버전의 메소드를 new 한정자로 수식

오버라이드와 다름 → 완전한 다형성 표현의 한계

  • 부모와 자식 클래스에 반환 값, 이름, 매개변수 모두 같은 메소드를 선언
    • New 키워드를 사용함
    • 자식 클래스 객체는 자식 클래스의 메소드, 부모 클래스 객체는 부모 클래스의 메소드 사용
using System;

namespace HidingMethod
{
    class Parent
    {
        public void Method()
        {
            Console.WriteLine("부모의 메소드");
        }
    }
    class Child : Parent
    {
        public new void Method()
        {
            Console.WriteLine("자식의 메소드");
        }
    }
    class MainApp
    {
        static void Main(string[] args)
        {
            Child child = new Child();
            child.Method();
            ((Parent)child).Method();
        }
    }
}

메소드 오버라이딩 (overriding)

  • 자식 클래스에서 부모 클래스의 메소드를 재정의
  • 부모와 자식 클래스에 반환 값, 이름, 매개변수 모두 같은 메소드를 선언
    • 부모 클래스 메소드에 virtual 키워드, 자식 클래스 메소드에 override 키워드 사용
    • 부모/자식 클래스의 객체 모두 자식 클래스에서 재정의한 메소드 사용
  • base 키워드 이용해 부모 클래스 메소드 호출 가능
    • 자식 클래스 메소드 안에서 부모 클래스 메소드 호출
    • 부모 클래스의 메소드 안에 코드를 자식 클래스 안에서 중복해서 작성할 필요 없음

오버라이딩

  • 조건 : 대상 메소드를 virtual 키워드로 선언
  • private로 선언한 메소드는 오버라이딩 불가
  • 재정의를 위해 Override 키워드 사용
using System;

namespace Overriding
{
    class Animal
    {
        public virtual void Cry() { Console.WriteLine("소리 내어 웁니다."); }
    }
    class Dog : Animal
    {
        public override void Cry()
        {
            Console.WriteLine("멍멍 짓습니다.");
        }        
    }
    class Cat : Animal
    {
        public override void Cry()
        {
            base.Cry();
            Console.WriteLine("냥냥 웁니다.");
        }        
    }

    class MainApp
    {
        static void Main(string[] args)
        {
            Animal[] Animals = new Animal[] { new Dog(), new Cat(), new Dog(), new Cat() };

            foreach (var item in Animals)
            {                
                item.Cry();                
            }
        }
    }
}

메소드 하이딩과 출력 결과 비교

using System;

namespace Methodhiding
{
    class Animal
    {
        public virtual void Cry() { Console.WriteLine("소리 내어 웁니다."); }
    }
    class Dog : Animal
    {
        public new void Cry() // 메소드 하이딩
        {
            Console.WriteLine("멍멍 짓습니다.");
        }        
    }
    class Cat : Animal
    {
        public new void Cry() // 메소드 하이딩
        {
            base.Cry();
            Console.WriteLine("냥냥 웁니다.");
        }        
    }

    class MainApp
    {
        static void Main(string[] args)
        {
            Animal[] Animals = new Animal[] { new Dog(), new Cat(), new Dog(), new Cat() };

            foreach (var item in Animals)
            {                
                item.Cry();                
            }
        }
    }
}

오버라이딩 제한 : sealed 키워드

  • sealed 키워드를 메소드 앞에 붙이면 더 이상 오버라이딩하지 말라는 의미
    • 원래 virtual 키워드가 붙어 오버라이딩할 수 있는 메서드를 어느 지점 이후로 못하게 함

메소드의 오버라이딩 봉인

대상 : virtual 가상 메소드를 오버라이딩한 메소드

오작동 위험이 있거나 잘못 오버라이딩함으로써 문제가 예상되는 경우

using System;

namespace OverridingSealed
{
    class Base {
        public virtual void SealMe() { }
    }
    class Derived : Base {
        public sealed override void SealMe() { }
    }
    class WantToOverride : Derived {    // 컴파일 에러
        public override void SealMe() { }
    }

    class MainApp
    {
        static void Main(string[] args)
        {            
        }
    }
}

object 기본 메소드 확장

  • 일반적으로 ToString 의 경우 클래스의 인스턴스 값을 적절하게 표현하도록 재정의
using System;

namespace ObjectToString
{
    public class Point
    {
        int x, y;
        public Point(int x, int y)
        {
            this.x = x;
            this.y = y;
        }
        public override string ToString()
        {
            return "X: " + x + ", Y: " + y;
        }
    }

    class MainApp
    {
        static void Main(string[] args)
        {
            Point pt = new Point(5, 10);
            Console.WriteLine(pt.ToString());
        }
    }
}

  • 동일한 책인지 확인하기 위해 ISBN 필드 정보를 비교하도록 Equals() 메소드 재정의
using System;

namespace ObjectEquals
{
    class Book
    {
        decimal isbn13;
        string title;
        string contents;
        public Book (decimal isbn13, string title, string contents)
        {
            this.isbn13 = isbn13;
            this.title = title;
            this.contents = contents;
        }
        public override bool Equals(object obj)
        {
            if(obj == null)
            {
                return false;
            }
            Book book = obj as Book;
            if (book == null)
            {
                return false;
            }
            return this.isbn13 == book.isbn13;
        }
    }

    class MainApp
    {
        static void Main(string[] args)
        {
            Book book1 = new Book(1234, "book1", "...");
            Book book2 = new Book(1234, "book1", "...");
            Book book3 = new Book(5678, "book3", "...");

            Console.WriteLine("book1 == book2: " + book1.Equals(book2));
            Console.WriteLine("book1 == book3: " + book1.Equals(book3));
        }
    }
}

메소드 오버로드

  • 메소드의 이름은 같지만, 매개변수의 수 또는 매개변수의 타입이 다른 다수의 메소드 정의
    • 메소드의 반환 타입은 고려하지 않음
using System;

namespace MethodOverload
{
    class Mathematics
    {
        public int Abs (int value)
        {
            return (value >= 0) ? value : -value;
        }
        public double Abs(double value)
        {
            return (value >= 0) ? value : -value;
        }
        public decimal Abs(decimal value)
        {
            return (value >= 0) ? value : -value;
        }
    }

    class MainApp
    {
        static void Main(string[] args)
        {
            Mathematics math = new Mathematics();
            Console.WriteLine(math.Abs(-5));
            Console.WriteLine(math.Abs(-10.052));
            Console.WriteLine(math.Abs(20.01m));
        }
    }
}

연산자 오버로드

  • 연산자를 타입 별로 재정의 하여 사용
    • 예를 들어, + (더하기) 연산자를 보면 정수형 타입과 문자열 타입에 연산자 역할이 다름
    • 정수형 타입에서는 정수의 덧셈 역할
    • 문자열 타입에서는 문자열을 이어 붙이는 역할

사용자 정의 타입 : 연산자 오버로드 없는 경우

  • 연산자 오버로드 없이 더하기 연산은 일반적인 메소드 이용해 각 기능 구현
    • 예를 들어, 무게의 단위를 나타내는 Kilogram 을 클래스로 정의
using System;

namespace OperatorOverload
{
    public class Kilogram
    {
        double mass;
        public Kilogram (double value)
        {
            this.mass = value;                
        }
        public Kilogram Add(Kilogram target)
        {
            return new Kilogram(this.mass + target.mass);
        }
        public override string ToString()
        {
            return mass + "kg";
        }
    }

    class MainApp
    {
        static void Main(string[] args)
        {
            Kilogram kg1 = new Kilogram(5);
            Kilogram kg2 = new Kilogram(10);

            Kilogram kg3 = kg1.Add(kg2);
            Console.WriteLine(kg3); // 출력 결과: 15 kg 
        }
    }
}

사용자 정의 타입 : 연산자 오버로드 사용

  • 연산자 오버로드를 이용하여 + 연산자에 의미 부여

using System;

namespace OperatorOverload
{
    public class Kilogram
    {
        double mass;
        public Kilogram (double value)
        {
            this.mass = value;                
        }
        public static Kilogram operator +(Kilogram op1, Kilogram op2)
        {
            return new Kilogram(op1.mass + op2.mass);
        }
        public override string ToString()
        {
            return mass + "kg";
        }
    }
    class MainApp
    {
        static void Main(string[] args)
        {
            Kilogram kg1 = new Kilogram(5);
            Kilogram kg2 = new Kilogram(10);

            Kilogram kg3 = kg1 + kg2;
            Console.WriteLine(kg3); // 출력 결과: 15 kg 
        }
    }
}

댓글