프로그래밍 언어 - C#(7) 상속

이형눈이이상해
이형눈이이상해
∙∙조회 0

1. 상속

1.1. 개요

  • 상속이란 키워드는 구조 설계 쪽이다.
  • 기본 골자가 같을 때, 함수를 코드 덩어리로 묶어서 했듯이 클래스 단위로 골격을 재활용하고 싶을 때 상속을 쓸 수 있을 것이다.
  • 공통된 것은 묶고 상세 사항은 클래스별로 하는 것이 상속이다.
  • 프로그램 언어는 공통적으로 몬스터로 분류되는 것을 모두 분류해서 부르게끔 해준다.

<공통 분류 항목이면 불려진다>

  • 범위 안 정의된 것들을 스플래시 데미지를 준다거나 그럴 수 있다는 이야기이다.
  • 상위 클래스가 있고 몬스터를 상속받은 하위 클래스가 존재.
  • 구조 자체가 명확해서 상위 클래스가 있고 하위 클래스가 존재.
  • 빈 화살표(흰 화살표)로 하위 클래스에서 상위 클래스를 가리키게 되어 있음.
  • 하위 클래스는 세부만 담당하도록 하고 코드 양을 줄일 수 있다.

<하위 클래스에서 담당하는 세부사항만 고치면 된다>

  • 단지 잘 설계하는 것이 힘들 뿐이다.

1.2. 사용

  • 문법적으로 보도록 해보자. 아이템 쪽을 예시로 갈 것이다.
csharp
using System;

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

// 기반 클래스(부모)
public class Item
{
    public string Name { get; private set; }
    public string Description {get; private set;}

    public void Use()
    {
        
    }

    public void Drop()
    {
        
    }
}

// 하위 클래스(자식)
public class Potion
{
    
}

  • 문법적으로 포션이 아이템을 상속받도록 할 것이다.
  • 상속에서 주의해야 할 것은 Potion은 Item을 상속받고 있는데 C#에서는 다중 상속을 받으려고 하면 다른 클래스를 상속받지 못하도록 막는다. 하나 초과로 상속받을 수 없다는 이야기.
  • 빈 상태로 두고 한 번 사용해보도록 할 것이다.
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        Potion potion;
        potion.Use();
        potion.Drop();
    }
}

// 기반 클래스(부모)
public class Item
{
    public string Name { get; private set; }
    public string Description {get; private set;}

    public void Use()
    {
        Console.WriteLine($"{Name} 아이템을 사용했다.");
    }

    public void Drop()
    {
        Console.WriteLine($"{Name} 아이템을 버렸다.");
    }
}

// 하위 클래스(자식)
public class Potion : Item
{
    
}

  • 하지만 에러가 발생.
  • 초기화, 즉 객체 생성이 안되었기 때문이다.
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        Potion potion = new Potion();
        potion.Use();
        potion.Drop();
    }
}

// 기반 클래스(부모)
public class Item
{
    public string Name { get; private set; }
    public string Description {get; private set;}

    public void Use()
    {
        Console.WriteLine($"{Name} 아이템을 사용했다.");
    }

    public void Drop()
    {
        Console.WriteLine($"{Name} 아이템을 버렸다.");
    }
}

// 하위 클래스(자식)
public class Potion : Item
{
    
}

  • 이제 아이템에 있는 함수를 사용할 수 있다.
  • 포션에서 뭔가 초기 데이터, Name과 Description 내용을 초기화해줘야 한다.
  • 생성자를 써볼 수 있다.
csharp
public class Potion : Item
{
    public Potion(string name, string description)
    {
        Name = name;
        Description = description;
    }
}

  • 에러가 발생한다.
  • 아이템에서 보면 get은 public이라 문제가 안되지만 set은 아니다.
  • 변경, 초기화는 아이템 내부 클래스만 이용 가능하다.
  • 여기서 두가지 방법이 있는데 접근 제한자를 protected로 바꾼다.
  • protected는 클래스를 상속받았을 때 사용이 가능하다.

<상속을 받아야지만 할당된 재산을 쓸 수 있다와 비슷한 이치이다>

csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        Potion potion = new Potion();
        potion.Use();
        potion.Drop();
    }
}

// 기반 클래스(부모)
public class Item
{
    public string Name { get; protected set; }
    public string Description {get; private set;}

    protected void Use()
    {
        Console.WriteLine($"{Name} 아이템을 사용했다.");
    }

    public void Drop()
    {
        Console.WriteLine($"{Name} 아이템을 버렸다.");
    }
}

// 하위 클래스(자식)
public class Potion : Item
{
    public Potion(string name, string description)
    {
        Name = name;
        Description = description;
    }
}

  • main에서는 호출을 못하지만 아이템 본인이거나 아이템을 상속받은 곳에서만 사용할 수 있다.
  • protected는 상속 관계에서만 사용할 수 있다.
  • 다른 방법이 존재하는데 아이템도 생성자를 보유할 수 있다.
csharp
public class Item
{
    public string Name { get; protected set; }
    public string Description {get; private set;}

    public Item(string name, string description)
    {
        Name = name;
        Description = description;
    }
    public void Use()
    {
        Console.WriteLine($"{Name} 아이템을 사용했다.");
    }

    public void Drop()
    {
        Console.WriteLine($"{Name} 아이템을 버렸다.");
    }
}

// 하위 클래스(자식)
public class Potion : Item
{
    public Potion(string name, string description)
    {
        Name = name;
        Description = description;
    }
}

  • 포션의 생성자가 아닌 아이템의 생성자를 만들어 사용할 수 있다.
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        Potion potion = new Item("빨간 포션", "체력을 50 회복한다.");
        potion.Use();
        potion.Drop();
    }
}

// 기반 클래스(부모)
public class Item
{
    public string Name { get; protected set; }
    public string Description {get; private set;}

    public Item(string name, string description)
    {
        Name = name;
        Description = description;
    }
    public void Use()
    {
        Console.WriteLine($"{Name} 아이템을 사용했다.");
    }

    public void Drop()
    {
        Console.WriteLine($"{Name} 아이템을 버렸다.");
    }
}

// 하위 클래스(자식)
public class Potion : Item
{
    public Potion(string name, string description)
    {
        Name = name;
        Description = description;
    }
}

  • 생성할 수 있어야 하는 것이 맞는 것인데 안된다.
  • 포션은 아이템의 존재를 상속받았으니 알고 있다.
  • 반대로 아이템에서는 포션이라는 것을 알고 있지 않다.
  • 상속 관계에서는 상위 객체는 하위 객체가 누구인지 모른다.

<상위 객체 입장에서는 딱히 알 필요가 없기 때문이다>

  • 이것을 첫번째 전제조건으로 둬야 한다.
  • 포션이란 변수에서 아이템에 담는 것이 어불성설인데 포션은 아이템이다. 아이템은 포션이 아니다. 상속 관계는 이렇게 생각하는 것이 좋다.
  • 안 쪽에 포션을 담아두겠다고 포션을 상속했는데 아이템을 담아두는 것이 말이 안되는 것.
  • 이게 반대로 이루어졌어야 한다. 앞에 아이템이 적히고 뒤에 포션이 되어야 한다.
csharp
public class Potion : Item
{
    public Potion(string name, string description)  : base(name, description)
    {
    }
}

  • new 할당은 새로운 객체를 만들기 때문에 탈락.
  • 매개변수 뒤 쪽에다가 base()라는 키워드를 호출한다.
  • 다르면 다른대로 입력을 하고 같으면 그대로 매개변수를 입력해주면 된다.
  • 이제 바로 잡는다면
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        Potion potion = new Potion("빨간 포션", "체력을 50 회복한다.");
        potion.Use();
        potion.Drop();
    }
}

// 기반 클래스(부모)
public class Item
{
    public string Name { get; protected set; }
    public string Description {get; private set;}

    public Item(string name, string description)
    {
        Name = name;
        Description = description;
    }
    public void Use()
    {
        Console.WriteLine($"{Name} 아이템을 사용했다.");
    }

    public void Drop()
    {
        Console.WriteLine($"{Name} 아이템을 버렸다.");
    }
}

// 하위 클래스(자식)
public class Potion : Item
{
    public Potion(string name, string description)  : base(name, description)
    {
    }
}

  • Potion의 생성자로 들어오고 부모 생성자로 건네주었을 때, 결과적으로 데이터가 초기화가 생성자에서 되었으므로 Use라는 것을 했을 때 아이템 이름까지 발생해야 한다.
  • 실제로 실행했을 때 정상적으로 '빨간 포션 아이템을 사용했다.'와 '빨간 포션 아이템을 버렸다.'가 출력된다.
  • 지금 상황에서 포션의 생성자가 호출되는데 생성자에서는 base, 아이템 생성자를 호출해서 사용하는 것이 있다.
  • 실수할 여지가 존재하는데 포션 블록 범위인지 아이템의 블록 범위인지 알 필요가 존재.
  • 외운다기 보다는
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        Potion potion = new Potion("빨간 포션", "체력을 50 회복한다.");
        potion.Use();
        potion.Drop();
    }
}

// 기반 클래스(부모)
public class Item
{
    public string Name { get; protected set; }
    public string Description {get; private set;}

    public Item(string name, string description)
    {
        Name = name;
        Description = description;
        Console.WriteLine("아이템 생성자");
    }
    public void Use()
    {
        Console.WriteLine($"{Name} 아이템을 사용했다.");
    }

    public void Drop()
    {
        Console.WriteLine($"{Name} 아이템을 버렸다.");
    }
}

// 하위 클래스(자식)
public class Potion : Item
{
    public Potion(string name, string description)  : base(name, description)
    {
        Console.WriteLine("포션 생성자");
    }
}

  • 이렇게 Console.WriteLine을 통해서 확인해볼 수 있다.
  • 부모가 먼저 생성되고 자식이 나중 생성된다.
  • 이 상황에서 아이템을 상속받아 포션을 만들었고 다른 아이템을 아이템을 상속받아 만들어보도록 하자.
csharp
public class Sword : Item
{
    public Sword(string name, string description) : base(name, description)
    {
        
    }
}

csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        Potion potion = new Potion("빨간 포션", "체력을 50 회복한다.");
        Sword sword = new Sword("검", "초보자용 검이다.");
        potion.Use();
        potion.Drop();
        sword.Use();
        sword.Drop();
    }
}

// 기반 클래스(부모)
public class Item
{
    public string Name { get; protected set; }
    public string Description {get; private set;}

    public Item(string name, string description)
    {
        Name = name;
        Description = description;
    }
    public void Use()
    {
        Console.WriteLine($"{Name} 아이템을 사용했다.");
    }

    public void Drop()
    {
        Console.WriteLine($"{Name} 아이템을 버렸다.");
    }
}

// 하위 클래스(자식)
public class Potion : Item
{
    public Potion(string name, string description)  : base(name, description)
    {
    }
}

public class Sword : Item
{
    public Sword(string name, string description) : base(name, description)
    {
        
    }
}

  • 이제 두 종류의 아이템이 생겼다. 모두 잡아서 부르는 것을 해보도록 할 것이다.
  • 반대로 포션과 검 형으로 변수를 생성했으면 담겨져야 할 데이터는 포션의 데이터여야 하고 검은 검의 데이터여야 한다. 지금은 각각 생성해서 넣어준 것이다. 이제 앞 쪽 변수를 Item으로 다뤄보도록 하자.
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        Item potion = new Potion("빨간 포션", "체력을 50 회복한다.");
        Item sword = new Sword("검", "초보자용 검이다.");
        potion.Use();
        potion.Drop();
        sword.Use();
        sword.Drop();
    }
}

  • 포션, 검에 대한 함수를 쓸 수 있는 것은 알겠지만 Item에 없지만 각 아이템에 개별적 기능이 있다고 가정하자.
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        Item potion = new Potion("빨간 포션", "체력을 50 회복한다.");
        Item sword = new Sword("검", "초보자용 검이다.");
        potion.Use();
        potion.Drop();
        potion.UseTo(); // 불가
        sword.Use();
        sword.Drop();
        sword.Upgrade(); // 불가
    }
}

// 기반 클래스(부모)
public class Item
{
    public string Name { get; protected set; }
    public string Description {get; private set;}

    public Item(string name, string description)
    {
        Name = name;
        Description = description;
    }
    public void Use()
    {
        Console.WriteLine($"{Name} 아이템을 사용했다.");
    }

    public void Drop()
    {
        Console.WriteLine($"{Name} 아이템을 버렸다.");
    }
}

// 하위 클래스(자식)
public class Potion : Item
{
    public Potion(string name, string description)  : base(name, description)
    {
    }

    public void UseTo()
    {
        Console.WriteLine("다른 대상에게 포션을 사용합니다");
    }
}

public class Sword : Item
{
    public Sword(string name, string description) : base(name, description)
    {
        
    }

    public void Upgrade()
    {
        Console.WriteLine("검 강화");
    }
}

  • 이런 식으로 했을 때 개별 아이템의 것에서 main에서 호출이 불가하다.
  • Item이라는 자료형 안에 UseTo(), Upgrade()는 없다.
  • 자식 클래스의 인스턴스가 담겨져 있어도 만일 변수의 자료형을 상위 객체로 다루고 있어도 상위 객체가 가진 기능만을 다룰 수 있다.
  • Sword, Potion은 Item 클래스를 알고 Item은 모른다. Item 입장에서는 아무 상관없다.
  • 자료형을 상위 객체로 잡는 순간 하위 객체에 대한 것을 인식조차 못한다.
  • 그런데 애초에 Item으로 생성되었지만 각 객체에 대한 것의 기능을 사용하고 싶을 수 있다.
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        Item potion = new Potion("빨간 포션", "체력을 50 회복한다.");
        Item sword = new Sword("검", "초보자용 검이다.");
        potion.Use();
        potion.Drop();
        
        sword.Use();
        sword.Drop();

        Sword s = sword();
        s.Upgrade();
    }
}

  • 이것은 불가하지만
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        Item potion = new Potion("빨간 포션", "체력을 50 회복한다.");
        Item sword = new Sword("검", "초보자용 검이다.");
        potion.Use();
        potion.Drop();
        
        sword.Use();
        sword.Drop();

        Sword s = (Sword)sword;
        s.Upgrade();
    }
}

  • 이것은 된다.
  • 하지만 다른 자료형을 변환시키려고 하면 터진다.
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        Item potion = new Potion("빨간 포션", "체력을 50 회복한다.");
        Item sword = new Sword("검", "초보자용 검이다.");
        potion.Use();
        potion.Drop();
        
        sword.Use();
        sword.Drop();

        Sword s = (Sword)potion;
        s.Upgrade();
    }
}

  • 이것을 안전하게 해주는 방법이 있다. 그것을 다루기 전에 'Item sword = new Sword("검", "초보자용 검이다.");' 과정을 풀어서 생각해보자.
csharp
Item sword = new Sword("검", "초보자용 검이다."); //은
Sword sword =new Sword("검", "초보자용 검이다.");
Item swordItem = (Item)sword;

  • 이 과정이 생략된 것이다.
  • Sword로 선언된 것을 아이템으로 선언되게끔 하기 위해 캐스팅되는 과정을 생략한 것이다.
  • 이렇게 Item이 상위 객체이고 하위 객체인 Sword가 아래에서 위로 캐스팅이 이루어졌으므로 업 캐스팅이라고도 한다.

<Sword: 썩씨딩 유, Item>

  • 그렇다면 다운 캐스팅은 다음과 같다.
csharp
Sword s = (Sword)sword;
s.Upgrade();

  • 여기 까지 오게 되면 생각해봐야 할 것은 업 캐스팅 차원에서는 암시적으로 형변환이 일어났다.
  • 명확한 문제는 Sword와 Potion은 아이템이지만 Item은 Sword일 것이라고 가정할 수 없다.
  • 객체가 어떤 상황일까를 이해해보록 하자. Item은 암시적으로 Sword라는 것을 알 수 없다.
  • 따라서 적어줘야 하고 논리적으로 변환이 될 수 있어야 되는지 검증하고 검증이 완료되었을 때 바꿀 수 있어야 한다.
  • 이 역시 2가지 방법이 존재한다.
  • 첫번째 방법은
csharp
if (sword is Sword)

  • sword에 담겨있는 것이 Sword 클래스라면 이라는 것이다.
csharp
Sword s;
// 다운 캐스팅
if (sword is Sword) 
{
}
s = (Sword)sword;
s.Upgrade();

  • 상당히 직관적이다.
  • 다음은 두번째 방법.
csharp
Sword s;
        // 다운 캐스팅
        if (sword is Sword) 
        {
            s = (Sword)sword;
        }
        s = sword as Sword;

  • 변환이 불가하다면 쓰레기 값으로 둔다.
  • null 이라는 키워드가 나온다.
  • 그러니까 변환이 가능하다면 변환이 가능한 객체가 아니라면 null로 처리.
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        Item potion = new Potion("빨간 포션", "체력을 50 회복한다.");
        Item sword = new Sword("검", "초보자용 검이다.");
        potion.Use();
        potion.Drop();
        
        sword.Use();
        sword.Drop();

        Sword s;
        // 다운 캐스팅
        if (sword is Sword) 
        {
            s = (Sword)sword;
            s.Upgrade();
        }
        (sword as Sword).Upgrade();
    }
}

  • 예상은 2번 업그레이드가 되어야 한다.
  • 지금 s의 객체와 sword 객체가 결과야 같겠지만 별도의 방법으로 호출하는 것이다.
  • 상황을 한 번 보도록 하자. 검과 포션으로 묶어 관리하는데 배열에도 묶어 관리 가능하지 않을지?
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        Item[] items = new Item[]
        {
            new Sword("검", "초보자용 검이다."),
            new Potion("빨간 포션", "체력을 50 회복해주는 포션이다.")
        };
    }
}

  • 그런데 포션 아이템을 사용하면 플레이어 스탯에 영향이 일어나고 검은 장비가 되어야 한다.
  • 현 상황에서는
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        Item[] items = new Item[]
        {
            new Sword("검", "초보자용 검이다."),
            new Potion("빨간 포션", "체력을 50 회복해주는 포션이다.")
        };

        foreach (var item in items)
        {
            item.Use(); 
        }
    }
}

  • 사용했다고 발생한다.
  • 그런데 이런 식으로 사용되길 바란 것은 아니고 즉, 다른 행동이 일어났어야 했다는 이야기이다.
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        Item[] items = new Item[]
        {
            new Sword("검", "초보자용 검이다."),
            new Potion("빨간 포션", "체력을 50 회복해주는 포션이다.")
        };

        foreach (var item in items)
        {
            item.Use(); 
        }
    }
}

// 기반 클래스(부모)
public class Item
{
    public string Name { get; protected set; }
    public string Description {get; private set;}

    public Item(string name, string description)
    {
        Name = name;
        Description = description;
    }
    public void Use()
    {
        Console.WriteLine($"{Name} 아이템을 사용했다.");
    }

    public void Drop()
    {
        Console.WriteLine($"{Name} 아이템을 버렸다.");
    }
}

// 하위 클래스(자식)
public class Potion : Item
{
    public Potion(string name, string description)  : base(name, description)
    {
    }
    
    public void Use()
    {
        Console.WriteLine("다른 대상에게 포션을 사용합니다");
    }
}

public class Sword : Item
{
    public Sword(string name, string description) : base(name, description)
    {
        
    }

    public void Upgrade()
    {
        Console.WriteLine("검 강화");
    }

    public void Use()
    {
        Console.WriteLine("검 장비");
    }
}

  • 이렇게 사용하면 아이템은 둘의 존재를 모른다.
  • 아이템에서 포션과 검을 알던가 아이템 쪽에서 사용한다는 것을 위임한다거나 해야 한다.
  • 이 함수 기능 자체를 맞게끔 쓰라고 권한 위임을 해줄 때의 키워드를 다룰 것이다.
  • 필요 키워드는 2개인데 하나는 virtual이다. 이것은 아이템에서 해보도록 하자.
csharp
public class Item
{
    public string Name { get; protected set; }
    public string Description {get; private set;}

    public Item(string name, string description)
    {
        Name = name;
        Description = description;
    }
    public virtual void Use()
    {
        Console.WriteLine($"{Name} 아이템을 사용했다.");
    }

    public void Drop()
    {
        Console.WriteLine($"{Name} 아이템을 버렸다.");
    }
}

  • 가상 함수로 동작해서 하위 클래스에서 재정의가 가능해진다.
  • 다음으로는 포션에서 상속되었고 Use도 상속을 받았다. 같은 함수명으로 해주면서 override를 해줘야 한다.
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        Item[] items = new Item[]
        {
            new Sword("검", "초보자용 검이다."),
            new Potion("빨간 포션", "체력을 50 회복해주는 포션이다.")
        };

        foreach (var item in items)
        {
            item.Use(); 
        }
    }
}

// 기반 클래스(부모)
public class Item
{
    public string Name { get; protected set; }
    public string Description {get; private set;}

    public Item(string name, string description)
    {
        Name = name;
        Description = description;
    }
    public virtual void Use()
    {
        Console.WriteLine($"{Name} 아이템을 사용했다.");
    }

    public void Drop()
    {
        Console.WriteLine($"{Name} 아이템을 버렸다.");
    }
}

// 하위 클래스(자식)
public class Potion : Item
{
    public Potion(string name, string description)  : base(name, description)
    {
    }
    
    public override void Use()
    {
        Console.WriteLine("다른 대상에게 포션을 사용합니다");
    }
}

public class Sword : Item
{
    public Sword(string name, string description) : base(name, description)
    {
        
    }

    public void Upgrade()
    {
        Console.WriteLine("검 강화");
    }

    public override void Use()
    {
        Console.WriteLine("검 장비");
    }
}

  • 이런 형식이다. virtual, override.
  • 지금은 순회이지만 인벤토리의 격자 형태가 있을 때, 몇번째 아이템을 선택하면 몇번째 칸에 접근해야 하고 use를 하게 되는 것이다.
  • 자신의 기능을 수행하도록 별개로 가능했다.
  • 이번엔 클래스 아이템은 자식 객체가 상속받아 사용할 수 있는데 협업 환경에서는 이 코드로 팀원이 추가적인 new Armor 등의 코드가 추가될 수 있고 사용이 될 수 있다.
  • 실수가 벌어질 수 있다는 이야기이다.
  • 실수 예시는 다음과 같다.
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        Item[] items = new Item[]
        {
            new Sword("검", "초보자용 검이다."),
            new Potion("빨간 포션", "체력을 50 회복해주는 포션이다."),
            new Item("아이템", "아?")
        };

        foreach (var item in items)
        {
            item.Use(); 
        }
    }
}

  • 아이템이 사용할 때마다 다른 종류의 일들이 벌어져야 하는데 가상 함수로 된 그 이상도 그 이하도 아니다.
  • 아이템 객체를 만든다는 것은 설계 목적에 부합되지 않은 형태이다.
  • 이 Item 객체를 직접 생성하지 말라고 강요하는 방법은 무엇이 있을까?
  • protected를 사용. 외부에서는 불가하도록 하는.
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        Item[] items = new Item[]
        {
            new Sword("검", "초보자용 검이다."),
            new Potion("빨간 포션", "체력을 50 회복해주는 포션이다."),
            new Item("아이템", "아?")
        };

        foreach (var item in items)
        {
            item.Use(); 
        }
    }
}

// 기반 클래스(부모)
public class Item
{
    public string Name { get; protected set; }
    public string Description {get; private set;}

    protected Item(string name, string description)
    {
        Name = name;
        Description = description;
    }
    public virtual void Use()
    {
        Console.WriteLine($"{Name} 아이템을 사용했다.");
    }

    public void Drop()
    {
        Console.WriteLine($"{Name} 아이템을 버렸다.");
    }
}

  • 생성자를 막을 수 있다.

2. 추상 클래스

2.1. 부모 클래스로에서의 선언

  • 1.2. 후반부에서 사용했던 protected 방법도 있고 또 다르게는 추상 클래스가 존재한다.
csharp
public abstract class Item
{
    public string Name { get; protected set; }
    public string Description {get; private set;}

    public Item(string name, string description)
    {
        Name = name;
        Description = description;
    }
    public virtual void Use()
    {
        Console.WriteLine($"{Name} 아이템을 사용했다.");
    }

    public void Drop()
    {
        Console.WriteLine($"{Name} 아이템을 버렸다.");
    }
}

  • 이것은 인스턴스를 만들 수 없다.
  • 그 자체로는 만들 수 없고 물려받는 용도로만 사용이 가능하도록 제시되는 클래스이다.

<그러니까 바보짓은 하지 말자>

  • 그런데 abstract가 이 정도 기능만 있는 것이 아니라 추가적으로 가능해지는 것이 존재한다. 오로지 base 클래스로만 사용하도록 제약을 검.
  • 필드 변수들 말고도 함수도 물려주는 과정에서 나를 상속받은 클래스에서 새로 초기화하도록 강제하는 것이
csharp
public abstract class Item
{
    public string Name { get; protected set; }
    public string Description {get; private set;}

    public Item(string name, string description)
    {
        Name = name;
        Description = description;
    }
    public virtual void Use()
    {
        Console.WriteLine($"{Name} 아이템을 사용했다.");
    }

    public abstract void Drop()
    {
        Console.WriteLine($"{Name} 아이템을 버렸다.");
    }
}

2.2. 추상 메서드

  • abstract가 붙은 Drop 함수의 경우, 추상 메서드라고 부른다.
  • 그런데 추상 메서드에 에러가 발생하는데 자식 객체에서 다시 재정의 해야 한다는 것이다.

<뭔 짓을 해도 재정의를 해야만 한다>

  • 강제적으로 재정의를 해야 하므로 세미콜론으로 끝내야 한다.
csharp
public abstract class Item
{
    public string Name { get; protected set; }
    public string Description {get; private set;}

    public Item(string name, string description)
    {
        Name = name;
        Description = description;
    }
    public virtual void Use()
    {
        Console.WriteLine($"{Name} 아이템을 사용했다.");
    }

    public abstract void Drop();
}

  • 이 클래스를 상속받을 때, 자식에선 Drop()을 재정의 해줘야 한다. 강제 재구현이 필요하므로 클래스 자체에서 에러가 발생한다.
csharp
public class Potion : Item
{
    public string Name;
    public Potion(string name, string description)  : base(name, description)
    {
        this.Name = name; // 이 클래스 자체적.
        base.Name = name; // 상위 클래스.
    }
    
    public override void Use()
    {
        Console.WriteLine("다른 대상에게 포션을 사용합니다");
    }

    public override void Drop()
    {
        Console.WriteLine("포션 버림");
    }
}

  • 이렇게 재정의 강제를 적용시켜 반드시 하게끔 만든다.
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        Item[] items = new Item[]
        {
            new Sword("검", "초보자용 검이다."),
            new Potion("빨간 포션", "체력을 50 회복해주는 포션이다.")
        }; 

        foreach (var item in items)
        {
            item.Drop(); 
        }
    }
}

  • 아이템 클래스 잘못 사용하는 것을 막고 Drop의 기능은 알고는 있음
  • 단, 재정의 사항은 모를 뿐.
  • 키워드 : 가상 함수 테이블
  • 이렇게 Drop을 호출함으로써 자식 클래스에서 세부 구현된 내용을 사용할 수 있게끔 한다.
  • 그런데 다음을 보자.
csharp
public abstract class Item
{
    public string Name { get; protected set; }
    public string Description {get; private set;}
    public int id{ get; private set; }

    public Item(string name, string description)
    {
        Name = name;
        Description = description;
    }
    public virtual void Use()
    {
        Console.WriteLine($"{Name} 아이템을 사용했다.");
    }

    public abstract void Drop();
}

  • 프로퍼티는 정확히는 함수다.
  • 내부적으로 임시 변수를 만들어 연결해주는 것.
  • 추상 메서드는 abstract가 붙는데 프로퍼티에 붙일 수 있다. 따라서 추상 메서드에도 붙일 수 있다.
  • 딱히 사용은 잘 안하긴 하지만.
  • private 전제조건이 이럴 때는 잘 맞지 않으므로 없거나 protected거나 한다.
csharp
public abstract class Item
{
    public string Name { get; protected set; }
    public string Description {get; private set;}
    public abstract int Id{ get; protected set; } 

    public Item(string name, string description)
    {
        Name = name;
        Description = description;
    }
    public virtual void Use()
    {
        Console.WriteLine($"{Name} 아이템을 사용했다.");
    }

    public abstract void Drop();
}

// 하위 클래스(자식)
public class Potion : Item
{
    public override int Id { get; protected set; }
    public string Name;
    public Potion(string name, string description)  : base(name, description)
    {
        this.Name = name; // 이 클래스 자체적.
        base.Name = name; // 상위 클래스.
    }
    
    public override void Use()
    {
        Console.WriteLine("다른 대상에게 포션을 사용합니다");
    }

    public override void Drop()
    {
        Console.WriteLine("포션 버림");
    }
}

3. 정리

3.1. 정리

  • 메서드를 재정의를 할 때는 override
  • 추상 클래스는 abstract
  • 오버로딩과 햇갈릴 수 있는 것이 오버라이딩이긴 한데 이 곳에서 파생된다.
  • 여러 케이스와 상호작용 적용을 많이 봐둬야 한다.
  • 유니티에서 더 잘 알 수 있다는 것. 느낌만을 보도록 한다는 것.

3.2. 실사용

  • 키워드 관련 코드 Sealed
csharp
using System;

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

public sealed class A
{
    
}

  • 그냥 생성해서 쓰라고 만들어 놓은 것이다.
  • 상속이 불가하다.
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        B b = new();
        b.Foo();
    }
}

public sealed class A
{
    public void Foo()
    {
        Console.WriteLine("Foo");
    }
}

public class B : A
{
    
}

  • 이런 느낌으로 사용된다.
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        B b = new();
        b.Foo();
    }
}

public class A
{
    public virtual void Foo()
    {
        Console.WriteLine("Foo");
    }
}

public class B : A
{
    public sealed override void Foo()
    {
        Console.WriteLine("Foo B");
    }
}

public class C : B
{
    public override void Foo()
    {
        Console.WriteLine("Foo C");
    }
}

  • 특히나 유니티에서는 잘 사용은 하지 않는다.
  • 이번엔 partial 클래스
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        A a = new();
        a.Foo1();
        a.Foo2();
    }
}

// A1.class
using System;

public partial class A
{
    public void Foo1()
    {
        Console.WriteLine("Foo1");
    }
}

// A2.class
using System;

public partial class A
{
    public void Foo2()
    {
        Console.WriteLine("Foo2");
    }
}

  • 유틸 관련해서 사용하는 경우가 있음.
  • 같은 이름의 클래스가 존재할 때 에러가 발생.
  • 그런데 partial을 사용하면 일단 에러가 발생하진 않는다.
  • 그러나 별개의 공간에서 선언했다고 하더라도 partial에서는 나눠서 선언하나 하나의 클래스로 작동하도록 한다.
  • 이것도 유니티에서도 잘 사용은 하지 않는다.
  • 다음은 클래스 내부에서 함수를 선언하고 매개변수를 더해서 반환해주는 것을 만들고 싶다.
csharp
using System;

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

public class A
{
    // 매개변수가 2개에서 3개로 늘어난다고 한다면?
    public int sum(int a, int b)
    {
        
    }
    // 1. 
    public int sum(int a, int b, int c)
    {
        
    }
    // 2. 
    public int sum(int[] arr)
    {
        
    }
}

  • 오버로딩이 있을 수 있고 배열 생성이 있을 수 있겠다.
  • 하지만 일일이 이렇게 하긴 힘드므로 이를 쉽게 해줄 키워드가 존재한다.
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        A a = new A();
        int[] arr = new int[] { 1, 23, 4, 5, 56, 6 };
        
        int sum =a.Sum(arr);
    }
}

public class A
{
    public int Sum(int[] arr)
    {
        int result = 0;

        foreach (int i in arr)
        {
            result += i;
        }
        
        return result;
    }
}

  • 여기서 params를 추가하면 직접적으로 할 수 있다.
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        A a = new A();
        int[] arr = new int[] { 1, 23, 4, 5, 56, 6 };
        
        int sum =a.Sum(2,3,4,5,5,6,6,67);
    }
}

public class A
{
    public int Sum(params int[] arr)
    {
        int result = 0;

        foreach (int i in arr)
        {
            result += i;
        }
        
        return result;
    }
}

  • 플레이어의 환경에서도 사용하도록 할 수 있도록 해볼 것이다.
  • 아이템 추상 클래스 만들기
csharp
// Item.cs


public abstract class Item
{
    public int Id { get; private set; }
    public string Name { get; private set; }

    public Item(int id, string name)
    {
        Id = id;
        Name = name;
    }
    public abstract void Use();
}
// Potion.cs


public class Potion : Item
{
    public Potion() : base(1, "포션")
    {}
    
    public override void Use()
    {}
}
// Sword.cs


public class Sword : Item
{
    public Sword() : base(2, "검")
    {}
    
    public override void Use()
    {}
}
// Player.cs


public class Player
{
    
}

  • 플레이어 쪽에서 인벤토리 기능을 수행한다고 가정해보자.
csharp
// Player.cs

public class Player
{
    private Item[] _inventory = new Item[5];
    
    // 체력
    // 공격력
    // 인벤토리
    
    // 아이템 추가
    
    // 사용
}

  • 이정도 선에서 추가를 해보도록 하자.
csharp
public class Player
{
    // 인벤토리
    private Item[] _inventory = new Item[5];
    // 인벤토리 : 실제 아이템이 몇개 들었는지. 여기에 = 0;을 해도 되긴 하다.
    private int _inventoryConunt;
    
    // 체력
    private int _health;
    // 공격력
    private int _attack;
    // 생성자에서의 초기화
    public Player()
    {
        _health = 100;
        _attack = 10;
        _inventoryConunt = 0;
    }
    
    // 아이템 추가
    public void AddItem()
    {
        
    }

    // 사용 : 어디 자리에 어떻게 사용할지
    public void UseItem(int index)
    {
        
    }
}

  • 어떤 아이템을 추가하는 것인지 알아야 하는데 매개변수로 아이템을 받도록 할 것.
csharp


public class Player
{
    ~

    // 아이템 추가
    public void AddItem(Item item)
    {
        // 인벤토리가 꽉 찼을 경우
        if (_inventoryConunt == _inventory.Length)
        {
            Console.WriteLine("인벤토리가 꽉 찼습니다.");
            return;
        }
        // 아니면 정상 동작
        _inventory[_inventoryConunt] = item;
        _inventoryConunt++;
    }

    ~
}

  • 이제 아이템 사용하는 쪽을 생각해야 한다.
  • 인벤토리 5칸에서
csharp
// 사용 : 어디 자리에 어떻게 사용할지
    public void UseItem(int index)
    {
        // [P] [P] [S] [P] [S]
        //                  ^
        // 맨 뒤 아이템을 사용하면 아이템이 사라진다. ... [ ]
        // 가리키던 곳에서 하나 줄어야 할 것. ... [P] 
        //                                    ^
        // 그런데 중간에 있는 것을 사용한다면? 
        // count가 -1 되어야 하는데 한 칸 씩 당겨와줘야 하는 것이 있어야 한다.
        // 그리고 추가해주는 형태여야 할 것이다. 
        if (index == _inventoryConunt - 1) // index가 맨 끝 가리킴.
        {
            // 맨 마지막 아이템을 사용
            _inventory[index].Use();  // Item이 가지고 있는 Use를 사용할 수 있다.
            
            // 해당 칸을 비우고 (해당 칸은 의미 없는 쓰레기칸)
            _inventory[index] = null;
            
            // 아이템이 사라졌으니 카운트를 줄여야 한다.
            _inventoryConunt--;
        }
    }

csharp
// 사용 : 어디 자리에 어떻게 사용할지
    public void UseItem(int index)
    {
        // [P] [P] [S] [P] [S]
        //                  ^
        // 맨 뒤 아이템을 사용하면 아이템이 사라진다. ... [ ]
        // 가리키던 곳에서 하나 줄어야 할 것. ... [P] 
        //                                    ^
        // 그런데 중간에 있는 것을 사용한다면? 
        // count가 -1 되어야 하는데 한 칸 씩 당겨와줘야 하는 것이 있어야 한다.
        // 그리고 추가해주는 형태여야 할 것이다. 
        if (index == _inventoryConunt - 1) // index가 맨 끝 가리킴.
        {
            // 맨 마지막 아이템을 사용
            _inventory[index].Use();  // Item이 가지고 있는 Use를 사용할 수 있다.
            
            // 해당 칸을 비우고 (해당 칸은 의미 없는 쓰레기칸)
            _inventory[index] = null;
            
            // 아이템이 사라졌으니 카운트를 줄여야 한다.
            _inventoryConunt--;
        }
        else // 그게(맨 끝을 가리키는 것) 아닌 경우
        {
            // [x] [] [y] [z] []
            //     ^
            // 한 칸씩 끌어와야 한다.
            _inventory[index].Use();
            _inventory[index] = null; // 비워주기까지는 동일.
            
            // 배열의 총 길이만큼 당겨줘야 한다. 
            for (int i = index; i < _inventoryConunt; i++) // 꽉차지 않아도 알 수 있음.
            //for (int i = index + 1; i < _inventoryConunt; i++) // index + 1부터
            {
                
                // 배열 바깥 범위에 대한 처리가 필요하거나 
                if (i == _inventoryConunt - 1 /*배열 끝자리인가?*/)
                {
                    _inventory[i] = null; // 빈 자리를 null로.
                }
                // index = 1. 아니면 한 칸을 끌어와줘야 한다. 
                else
                {
                    _inventory[i] = _inventory[i + 1];
                }
                // index + 1부터 돌게 하거나
                //_inventory[i - 1] = _inventory[i];
            }
        }
    }

  • 끝 처리.
csharp
// 사용 : 어디 자리에 어떻게 사용할지
    public void UseItem(int index)
    {
        // [P] [P] [S] [P] [S]
        //                  ^
        // 맨 뒤 아이템을 사용하면 아이템이 사라진다. ... [ ]
        // 가리키던 곳에서 하나 줄어야 할 것. ... [P] 
        //                                    ^
        // 그런데 중간에 있는 것을 사용한다면? 
        // count가 -1 되어야 하는데 한 칸 씩 당겨와줘야 하는 것이 있어야 한다.
        // 그리고 추가해주는 형태여야 할 것이다. 
        if (index == _inventoryConunt - 1) // index가 맨 끝 가리킴.
        {
            // 맨 마지막 아이템을 사용
            _inventory[index].Use();  // Item이 가지고 있는 Use를 사용할 수 있다.
            
            // 해당 칸을 비우고 (해당 칸은 의미 없는 쓰레기칸)
            _inventory[index] = null;
            
            // 아이템이 사라졌으니 카운트를 줄여야 한다. 이 사항은 공통되므로 바깥에서 처리해도 된다.
            //_inventoryConunt--;
        }
        else // 그게(맨 끝을 가리키는 것) 아닌 경우
        {
            // [x] [] [y] [z] []
            //     ^
            // 한 칸씩 끌어와야 한다.
            _inventory[index].Use();
            _inventory[index] = null; // 비워주기까지는 동일.
            
            // 배열의 총 길이만큼 당겨줘야 한다. 
            for (int i = index; i < _inventoryConunt; i++) // 꽉차지 않아도 알 수 있음.
            //for (int i = index + 1; i < _inventoryConunt; i++) // index + 1부터
            {
                
                // 배열 바깥 범위에 대한 처리가 필요하거나 
                if (i == _inventoryConunt - 1 /*배열 끝자리인가?*/)
                {
                    _inventory[i] = null; // 빈 자리를 null로.
                }
                // index = 1. 아니면 한 칸을 끌어와줘야 한다. 
                else
                {
                    _inventory[i] = _inventory[i + 1];
                }
                // index + 1부터 돌게 하거나
                //_inventory[i - 1] = _inventory[i];
            }
        }
        _inventoryConunt--;
    }

  • 그런데 아무것도 없는 상황이란 예외 사항 처리가 필요할 것이다.
csharp
public void UseItem(int index)
    {
        // 1. 아이템이 아무것도 없는데 사용하려 할 경우 (_inventoryCount == 0) 
        //    혹은 -1을 입력받으려고 할 경우
        // 2. 빈 자리의 아이템을 사용하려고 할 경우
        if (_inventoryConunt == 0 || index < 0 || _inventoryConunt <= index) 
        {
            return;
        }
        
        if (index == _inventoryConunt - 1) 
        {
            _inventory[index].Use();  
            _inventory[index] = null;
        }
        else 
        {
            _inventory[index].Use();
            _inventory[index] = null; .
            
            for (int i = index; i < _inventoryConunt; i++)
            {
                if (i == _inventoryConunt - 1)
                {
                    _inventory[i] = null; 
                } 
                else
                {
                    _inventory[i] = _inventory[i + 1];
                }
            }
        }
        _inventoryConunt--;
    }

  • 이제 사용을 해야 한다.
  • 포션과 검은 Player에게 영향을 준다.
  • 즉, 플레이어에게 접근해야 한다.
  • 플레이어가 Use를 해야 하므로 사용해야겠다고 명령을 해야 한다.
  • 사용하는 조건에서 Item에 플레이어가 이 Use를 호출할 때 자신을 넣어줘서 호출하도록 해볼 것.
csharp
// Item.cs


public abstract class Item
{
    public int Id { get; private set; }
    public string Name { get; private set; }

    public Item(int id, string name)
    {
        Id = id;
        Name = name;
    }
    public abstract void Use(Player owner);
}

// Player.cs
public Player()
    {
        _health = 100;
        _attack = 10;
        _inventoryConunt = 0;
    }

    public void Heal(int amount)
    {
        _health += amount;
    }

    public void PowerUp(int value)
    {
        _attack += value;
    }

public void UseItem(int index)
    {
        
        if (_inventoryConunt == 0 || index < 0 || _inventoryConunt <= index) 
        {
            return;
        }
        
        if (index == _inventoryConunt - 1) 
        {
            _inventory[index].Use(this);  
            _inventory[index] = null;
        }
        else 
        {
            _inventory[index].Use(this);
            _inventory[index] = null;
            
            for (int i = index; i < _inventoryConunt; i++)
            {
                if (i == _inventoryConunt - 1)
                {
                    _inventory[i] = null; 
                } 
                else
                {
                    _inventory[i] = _inventory[i + 1];
                }
            }
        }
        _inventoryConunt--;
    }

// 하위 클래스.cs

public class Potion : Item
{
    public Potion() : base(1, "포션")
    {}
    
    public override void Use(Player owner)
    {
        owner.Heal(50);
    }
}

// 

  • Main에서 사용
csharp
using System;

class  Program
{
    static void Main(string[] args)
    {
        // 체력 100, 공격력 10
        Player player = new();
        player.PrintInfo("플레이어 생성");
        
        // 인벤토리에 아이템 추가 : [P] [P] [S] [P] [P]
        player.AddItem(new Potion());
        player.AddItem(new Potion());
        player.AddItem(new Sword());
        player.AddItem(new Potion());
        player.AddItem(new Potion());
        
        // 인벤토리에 아이템 사용 : [P] [P] [S] [P] [ ]
        player.UseItem(4);
        player.PrintInfo("포션 사용");
        
        // 인벤토리에 아이템 사용 : [P] [P] [ ] [P] [ ]
        player.UseItem(2);
        player.PrintInfo("검 사용");
        
        
        // 인벤토리에 아이템 사용 : [P] [P] [ ] [ ] [ ]
        player.UseItem(2);
        player.PrintInfo("포션 사용");
    }
}

  • 상속 구조를 통해 구성이 이렇게 될 수 있다.
  • 그런데 인벤토리를 별개 클래스로 두면 플레이어에서 배열을 선언할 것이 아니라 직관적으로 인벤토리 객체의 기능을 사용하게끔 만들 수도 있다.