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

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

<하위 클래스에서 담당하는 세부사항만 고치면 된다>
- 단지 잘 설계하는 것이 힘들 뿐이다.
1.2. 사용
- 문법적으로 보도록 해보자. 아이템 쪽을 예시로 갈 것이다.
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#에서는 다중 상속을 받으려고 하면 다른 클래스를 상속받지 못하도록 막는다. 하나 초과로 상속받을 수 없다는 이야기.
- 빈 상태로 두고 한 번 사용해보도록 할 것이다.
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
{
}
- 하지만 에러가 발생.
- 초기화, 즉 객체 생성이 안되었기 때문이다.
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 내용을 초기화해줘야 한다.
- 생성자를 써볼 수 있다.
public class Potion : Item
{
public Potion(string name, string description)
{
Name = name;
Description = description;
}
}
- 에러가 발생한다.
- 아이템에서 보면 get은 public이라 문제가 안되지만 set은 아니다.
- 변경, 초기화는 아이템 내부 클래스만 이용 가능하다.
- 여기서 두가지 방법이 있는데 접근 제한자를 protected로 바꾼다.
- protected는 클래스를 상속받았을 때 사용이 가능하다.

<상속을 받아야지만 할당된 재산을 쓸 수 있다와 비슷한 이치이다>
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는 상속 관계에서만 사용할 수 있다.
- 다른 방법이 존재하는데 아이템도 생성자를 보유할 수 있다.
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;
}
}
- 포션의 생성자가 아닌 아이템의 생성자를 만들어 사용할 수 있다.
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;
}
}
- 생성할 수 있어야 하는 것이 맞는 것인데 안된다.
- 포션은 아이템의 존재를 상속받았으니 알고 있다.
- 반대로 아이템에서는 포션이라는 것을 알고 있지 않다.
- 상속 관계에서는 상위 객체는 하위 객체가 누구인지 모른다.

<상위 객체 입장에서는 딱히 알 필요가 없기 때문이다>
- 이것을 첫번째 전제조건으로 둬야 한다.
- 포션이란 변수에서 아이템에 담는 것이 어불성설인데 포션은 아이템이다. 아이템은 포션이 아니다. 상속 관계는 이렇게 생각하는 것이 좋다.
- 안 쪽에 포션을 담아두겠다고 포션을 상속했는데 아이템을 담아두는 것이 말이 안되는 것.
- 이게 반대로 이루어졌어야 한다. 앞에 아이템이 적히고 뒤에 포션이 되어야 한다.
public class Potion : Item
{
public Potion(string name, string description) : base(name, description)
{
}
}
- new 할당은 새로운 객체를 만들기 때문에 탈락.
- 매개변수 뒤 쪽에다가 base()라는 키워드를 호출한다.
- 다르면 다른대로 입력을 하고 같으면 그대로 매개변수를 입력해주면 된다.
- 이제 바로 잡는다면
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, 아이템 생성자를 호출해서 사용하는 것이 있다.
- 실수할 여지가 존재하는데 포션 블록 범위인지 아이템의 블록 범위인지 알 필요가 존재.
- 외운다기 보다는
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을 통해서 확인해볼 수 있다.
- 부모가 먼저 생성되고 자식이 나중 생성된다.
- 이 상황에서 아이템을 상속받아 포션을 만들었고 다른 아이템을 아이템을 상속받아 만들어보도록 하자.
public class Sword : Item
{
public Sword(string name, string description) : base(name, description)
{
}
}
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으로 다뤄보도록 하자.
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에 없지만 각 아이템에 개별적 기능이 있다고 가정하자.
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으로 생성되었지만 각 객체에 대한 것의 기능을 사용하고 싶을 수 있다.
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();
}
}
- 이것은 불가하지만
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();
}
}
- 이것은 된다.
- 하지만 다른 자료형을 변환시키려고 하면 터진다.
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("검", "초보자용 검이다.");' 과정을 풀어서 생각해보자.
Item sword = new Sword("검", "초보자용 검이다."); //은
Sword sword =new Sword("검", "초보자용 검이다.");
Item swordItem = (Item)sword;
- 이 과정이 생략된 것이다.
- Sword로 선언된 것을 아이템으로 선언되게끔 하기 위해 캐스팅되는 과정을 생략한 것이다.
- 이렇게 Item이 상위 객체이고 하위 객체인 Sword가 아래에서 위로 캐스팅이 이루어졌으므로 업 캐스팅이라고도 한다.

<Sword: 썩씨딩 유, Item>
- 그렇다면 다운 캐스팅은 다음과 같다.
Sword s = (Sword)sword;
s.Upgrade();
- 여기 까지 오게 되면 생각해봐야 할 것은 업 캐스팅 차원에서는 암시적으로 형변환이 일어났다.
- 명확한 문제는 Sword와 Potion은 아이템이지만 Item은 Sword일 것이라고 가정할 수 없다.
- 객체가 어떤 상황일까를 이해해보록 하자. Item은 암시적으로 Sword라는 것을 알 수 없다.
- 따라서 적어줘야 하고 논리적으로 변환이 될 수 있어야 되는지 검증하고 검증이 완료되었을 때 바꿀 수 있어야 한다.
- 이 역시 2가지 방법이 존재한다.
- 첫번째 방법은
if (sword is Sword)
- sword에 담겨있는 것이 Sword 클래스라면 이라는 것이다.
Sword s;
// 다운 캐스팅
if (sword is Sword)
{
}
s = (Sword)sword;
s.Upgrade();
- 상당히 직관적이다.
- 다음은 두번째 방법.
Sword s;
// 다운 캐스팅
if (sword is Sword)
{
s = (Sword)sword;
}
s = sword as Sword;
- 변환이 불가하다면 쓰레기 값으로 둔다.
- null 이라는 키워드가 나온다.
- 그러니까 변환이 가능하다면 변환이 가능한 객체가 아니라면 null로 처리.
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 객체가 결과야 같겠지만 별도의 방법으로 호출하는 것이다.
- 상황을 한 번 보도록 하자. 검과 포션으로 묶어 관리하는데 배열에도 묶어 관리 가능하지 않을지?
using System;
class Program
{
static void Main(string[] args)
{
Item[] items = new Item[]
{
new Sword("검", "초보자용 검이다."),
new Potion("빨간 포션", "체력을 50 회복해주는 포션이다.")
};
}
}
- 그런데 포션 아이템을 사용하면 플레이어 스탯에 영향이 일어나고 검은 장비가 되어야 한다.
- 현 상황에서는
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();
}
}
}
- 사용했다고 발생한다.
- 그런데 이런 식으로 사용되길 바란 것은 아니고 즉, 다른 행동이 일어났어야 했다는 이야기이다.
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이다. 이것은 아이템에서 해보도록 하자.
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를 해줘야 한다.
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 등의 코드가 추가될 수 있고 사용이 될 수 있다.
- 실수가 벌어질 수 있다는 이야기이다.
- 실수 예시는 다음과 같다.
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를 사용. 외부에서는 불가하도록 하는.
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 방법도 있고 또 다르게는 추상 클래스가 존재한다.
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 클래스로만 사용하도록 제약을 검.
- 필드 변수들 말고도 함수도 물려주는 과정에서 나를 상속받은 클래스에서 새로 초기화하도록 강제하는 것이
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 함수의 경우, 추상 메서드라고 부른다.
- 그런데 추상 메서드에 에러가 발생하는데 자식 객체에서 다시 재정의 해야 한다는 것이다.

<뭔 짓을 해도 재정의를 해야만 한다>
- 강제적으로 재정의를 해야 하므로 세미콜론으로 끝내야 한다.
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()을 재정의 해줘야 한다. 강제 재구현이 필요하므로 클래스 자체에서 에러가 발생한다.
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("포션 버림");
}
}
- 이렇게 재정의 강제를 적용시켜 반드시 하게끔 만든다.
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을 호출함으로써 자식 클래스에서 세부 구현된 내용을 사용할 수 있게끔 한다.
- 그런데 다음을 보자.
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거나 한다.
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
using System;
class Program
{
static void Main(string[] args)
{
}
}
public sealed class A
{
}
- 그냥 생성해서 쓰라고 만들어 놓은 것이다.
- 상속이 불가하다.
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
{
}
- 이런 느낌으로 사용된다.
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 클래스
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에서는 나눠서 선언하나 하나의 클래스로 작동하도록 한다.
- 이것도 유니티에서도 잘 사용은 하지 않는다.
- 다음은 클래스 내부에서 함수를 선언하고 매개변수를 더해서 반환해주는 것을 만들고 싶다.
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)
{
}
}
- 오버로딩이 있을 수 있고 배열 생성이 있을 수 있겠다.
- 하지만 일일이 이렇게 하긴 힘드므로 이를 쉽게 해줄 키워드가 존재한다.
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를 추가하면 직접적으로 할 수 있다.
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;
}
}
- 플레이어의 환경에서도 사용하도록 할 수 있도록 해볼 것이다.
- 아이템 추상 클래스 만들기
// 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
{
}
- 플레이어 쪽에서 인벤토리 기능을 수행한다고 가정해보자.
// Player.cs
public class Player
{
private Item[] _inventory = new Item[5];
// 체력
// 공격력
// 인벤토리
// 아이템 추가
// 사용
}
- 이정도 선에서 추가를 해보도록 하자.
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)
{
}
}
- 어떤 아이템을 추가하는 것인지 알아야 하는데 매개변수로 아이템을 받도록 할 것.
public class Player
{
~
// 아이템 추가
public void AddItem(Item item)
{
// 인벤토리가 꽉 찼을 경우
if (_inventoryConunt == _inventory.Length)
{
Console.WriteLine("인벤토리가 꽉 찼습니다.");
return;
}
// 아니면 정상 동작
_inventory[_inventoryConunt] = item;
_inventoryConunt++;
}
~
}
- 이제 아이템 사용하는 쪽을 생각해야 한다.
- 인벤토리 5칸에서
// 사용 : 어디 자리에 어떻게 사용할지
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--;
}
}
// 사용 : 어디 자리에 어떻게 사용할지
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];
}
}
}
- 끝 처리.
// 사용 : 어디 자리에 어떻게 사용할지
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--;
}
- 그런데 아무것도 없는 상황이란 예외 사항 처리가 필요할 것이다.
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를 호출할 때 자신을 넣어줘서 호출하도록 해볼 것.
// 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에서 사용
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("포션 사용");
}
}
- 상속 구조를 통해 구성이 이렇게 될 수 있다.
- 그런데 인벤토리를 별개 클래스로 두면 플레이어에서 배열을 선언할 것이 아니라 직관적으로 인벤토리 객체의 기능을 사용하게끔 만들 수도 있다.




