상속 ( Inheritance)
부모 (base) 클래스가 가진 기능(필드, 매서드)를 자식 (derived) 클래스가 물려받는것.
여러 클래스가 "공통 기능"을 공유해야할때 쓴다.
예) 동물(Animal) -> 개(Dog), 고양이(Cat)
//부모클래스
Class Animal
{
public string Name; // 속성(property) 없이는 잘안쓰지만 일단은 해당처럼한다, private일때는 변수는 소문자,,
public void Eat() => Console.WriteLine($"{Name} is eating."); // => 는 축약 표현식으로 { } 스코프 블록을 생략할때 쓴다.
}
//자식 클래스
Class Dog : Animal
{
public void Bark() => Console.WriteLine ($"{Name} says : Woof !");
}
static void Main()
{
Dog myDog = new Dog();
myDog.Name = "Buddy";
myDog.Eat(); // 부모 메서드 사용
myDog.Bark(); // 자식 고유 메서드
}
처럼 할 수 있다.
인터페이스(Interface)란?
약속서 같은것.
계약(Contract) 라고도 한다.
무엇을 할것인가 만 정의하며 구체적 구현은 없다.
예) "자동차" 인터페이스가 start() 와 stop() 만 정의하면, 모든 자동차 클래스(Tesla, Bmw 등)은
이 메서드를 반드시 구현 해야한다.
메서드 이름과 시그니처만 정의 -> 실제 동작(내용)은 클래스가 구현
다중 상속을 허용 -> 여러 계약(인터페이스)를 한 클래스가 동시에 이행 가능하다.
interface IPlayable
{
void Play(); // 이 기능을 반드시 구현하세요 약속한것
}
class Video : IPlayable
{
public void Play() => Console.WriteLine("영상 재생!");
}
가상메서드(Virtual Method) + 오버라이드 (Override)
virtual : 부모 클래스가 "이 메서드는 자식이 자유롭게 바꿀 수 있어요" 라고 표시하는 키워드
override: 자식 클래스에서 부모의 가상 메서드(부모의 기능)를 자기 방식대로 재정의(덮어쓰기)
다형성(polymorphism) , 부모타입으로 묶어두고, 실제 호출시에는 자식 고유 동작을 실행해야할때 쓴다.
좀더 언제쓰는지 알아보자면
예를들어)
-공통 인터페이스 유지 + 세부구현 다르게할때 : 코드 일관성 유지하면서 다양한 동작 제공
-여러 클래스에 공통 로직+ 다른 세부동작필요 : 코드 중복 줄이고 확장성 높인다.
-컬렉션(List<Animal>)에 여러자식 객체 담아서 일괄 처리 : 루프 하나로 다양한 객체 처리 가능
예) Animal 타입 리스트 -> 각기 다른 Sound 출력해야할때.
class Animal
{
public virtual void Speak() => Console.WriteLine("소리냄");
}
class Dog : Animal
{
public override void Speak() => Console.WriteLine("멍멍!");
}
class Cat : Animal
{
public override void Speak() => Console.WriteLine("Meow!");
}
Static void Main()
{
List<Animal> pets = new() { new Dog(), new Cat()};
foreach ( var pet in pets ) pet.Speak();
}
하면 출력
멍멍!
Meow!
여기서 추가로 배워둘것(추가 문법)
여러개 객체를 담는 상자 List<T>
List<T>는 T타입의 값을 여러 개 모아두는 컬렉션
List<타입> 변수이름 = new List <타입>();
//문자열 (string) 여러개를 담는 리스트
List<string> names = new List<string>();
// 숫자(int) 여러개를 담는 리스트
List<int> numbers = new List<int> ();
따라서 위에 코드 예제를 풀어서 설명하자면
List<Animal> pets = new() { new Dog(), new Cat()};
우선
= 오른쪽에 new()는 new List<Animal>() 에서 List<Animal>이 생략된 것 이다.
List<T >에서 T가 Animal 이라는 class 이름이 될 수 있는 것은, T에 기본타입인 자료형 int, string, bool 도 들어가고,, 사용자의 정의 타입인 class Animal, class Dog 들도 가능하기 때문이다.
따라서 List<Animal>은 Animal 객체를 저장하는 컬렉션이 되는것이다.
마지막으로 new()뒤에 붙는 중괄호 { ...} 부분은 컬렉션 초기화 문법(collection initializer) 이라고한다
List<Animal> pets = new() { new Dog(), new Cat() };
이 한줄 코드가
List<Animal> pets = new List<Animal>(); // 1. 리스트 생성
pets.Add( new Dog() ); // 2. 첫번째요소 추가
pets.Add( new Cat() ); // 3. 두번째 요소 추가
와 동일하다.
new( ) { x, y}
는 아래와 동일하다.
new List<T>();
list.Add(x);
list.Add(y);
여기서 Add 는 List<T> 전용 메서드로 C# 표준 라이브러리에서 제공해준다.
직접 구현할 필요없으며, Add도 이미 있는것이고 { } 방식도 이미 있는방식이니
짧게 간추려서
List<T>변수 = new() { new 클래스객체(), new 클래스객체2() }; 이런식으로 쓰는것도 편리하다.
그렇다면 Dog(), Cat() 앞의 new는 무엇인가?
해당
List pets = new() { new Dog(), new Cat() }; 의 역할은 Animal이라는 리스트안에 Dog, Cat의 객체(인스턴스)를 만들어주는 역할을 한다.
Dog myDog = new Dog();
Cat myCat = new Cat();
를 매번 해주는것보다 저렇게 리스트화 하여
foreach 문과 함께 쓰는것이다.
다음은 foreach ( var pet in pets ) pet.Speak(); 이다.
먼저 foreach ( 변수타입 반복변수 in 배열) 은
조건식을 쓰지않고 배열을 처음부터 끝까지 반복할때 쓰인다.
예를들면
string games [ ] = new string [3] { " League of Legends", "메이플스토리", "디아블로" };
foreach ( string game in games)
{
Console.WriteLine ( game) ;
}
실행결과
League of Legends
메이플스토리
디아블로
이렇게 뿜어낸다.
그런데,, foreach는 배열뿐아니라 C#에서 IEnumerable 인터페이스를 구현하는 객체, 모든 컬렉션(collection)과 함께 사용할 수 있다. 예제에서는 List<T>와 사용중이다.
여기서 컬렉션(Collection) 이란.
여러개의 데이터를 한곳에 모아둔 것을 말한다.
C#에서 대표 컬렉션 : 배열 ( string[]), List<T>, Dictionary<K,V>, Queue<T>, Stack<T> 등이 있다.
여러 아이템을 순서대로 꺼내 볼 수 있다.
여기서 IEnumerable 인터페이스란,
"열거할 수 있다" 라는 계약, 반복(iteration) 가능 계약이라고도 한다.
Ienumerable<T>를 구현하면 foreach 문으로 순회 가능.
컬렉션 대부분(List,Array, Dictionary 등)이 이 인터페이스를 구현하므로 foreach문을 쓸 수 있다.
foreach 문은 내부에서 IEnumerable.GetEnumerator() 호출을 하며 이를 통해 "다음항목"으로 이동하며 반복한다.
Ienumerable<T>는 C#에서 기본으로 제공하는 " 이 객체 안에 T 타입 요소들이 순서대로 들어있고, 하나씩 꺼내볼 수 있다" 는 것을 보장하는 인터페이스(interface) 이다.
프레임워크(BCL-Base Class Library) 안에 이미 정의되어있어 직접 사용자가 만들 필요는 없다.
using System.Collections.Generic;
이 네임스페이스에 들어있다.
// 네임스페이스는 C#에서 클래스,인터페이스,열거형 등을 논리적으로 묶어주는 폴더의 역할이다. 서로 다른 라이브러리나 내가 만든 클래스가 같은 이름을 쓰더라도 충돌을 피하게 해준다.
// 제너릭(Generic) 은 클래스,메서드를 만들때 타입을 미리 정하지않고(T placeholder) 사용할때 구체적 타입(string, int, Animal 등)으로 채워 넣겠다라는 방식이다. 같은 기능 코드를 여러 타입마다 재작성 없이 재사용 가능하게 하는 것이다.
List<T>에서 T는 즉 어떤 타입이든 올 수 있다라는 뜻
Ienumerable<T>의 계약내용
GetEnumerator() :반복할 수 있는 객체(Enumerator)를 반환
IEnumerator<T>.MoveNext() : 다음 요소로 이동, 요소가 있으면 true
IEnumerator<T>.Current : 현재 요소를 가져옴
IEnumerator<T>.Reset() : 반복을 처음으로 되돌림(잘안쓴다)
열거자(Enumerator)란?
열거자 = IEnumerator<T> 인터페이스를 구현한 객체
역할: 컬렉션의 요소를 하나씩 순서대로 꺼내주는 역할.
핵심 메서드/프로퍼티:
MoveNext(), Current, Reset()
정리하자면,
IEnumerable<T> ( "컬렉션 약속")
"나는 여러 아이템을 담을수 있고, 하나씩 꺼내볼 수 있다." 라는 약속. foreach 문을 쓸수있다.
IEnumerator<T> ("꺼내는도구")
실제로 "다음 아이템을 꺼내는 방법"을 알려주는 도구(객체)
MoveNext() -> 다음 아이템으로 이동
Current -> 현재 아이템 가져오기
yield return : C#이 자동으로 IEnumerator<T> 구현체(열거자)를 만들어주는 키워드.
개발자가 복잡한 상태 관리 코드 없이 한줄 씩 값을 반환하도록 간단히 작성한것.
object : C#에선 모든것(object)의 부모
숫자든 문자열이든, 클래스든 다 object 타입으로 간주될 수 있다.
예시) 비제네릭 IEnumerator는 반환 타입이 object라서 어떤 타입이든 담을 수 있게 만든 옛날 방식
책장에서 책 꺼내 읽기 (비유)
- 책장(IEnumberable): 여러 책을 담은 컬렉션
- 책갈피(IEnumerator): 현재 몇 번째 책을 읽고 있는지 표시
- MoveNext(): 다음 책으로 책갈피 이동
- Current: 책갈피가 가리키는 책 꺼내기
- Reset(): 책갈피를 첫 페이지 전으로 되돌리기
이제,, 직접 열거자 구현해보자면.
using System;
using System.Collections;
using System.Collections.Generic;
// 1. 열거자(Enumerator) 직접 구현
class MyNumbersEnumerator : IEnumerator<int>
{
// 데이터를 저장하는 배열
private int[] data = { 1, 2, 3 };
// 현재 요소의 위치를 나타내는 인덱스. 시작할 때는 -1 (아직 아무것도 가리키지 않음)
private int index = -1;
// 현재 위치의 값을 반환 (예: index가 0이면 data[0]인 1을 반환)
public int Current
{
get { return data[index]; }
}
// 비제네릭 인터페이스의 Current도 구현 (타입 변환)
object IEnumerator.Current
{
get { return Current; }
}
// 다음 요소로 이동. index를 1 증가시키고 배열의 범위 내이면 true를 반환
public bool MoveNext()
{
index++; // index를 증가시킴
return index < data.Length; // 만약 index가 0,1,2이면 true. 3이면 false → 반복 종료
}
// 열거자를 초기 상태로 되돌림 (다시 처음부터 시작)
public void Reset()
{
index = -1;
}
// 리소스 정리 (여기서는 특별히 정리할 게 없음)
public void Dispose()
{
}
}
// 2. IEnumerable 인터페이스를 구현하여 열거자를 제공
class MyNumbers : IEnumerable<int>
{
// GetEnumerator 메서드에서 새 열거자(MyNumbersEnumerator)를 생성하여 반환
public IEnumerator<int> GetEnumerator()
{
return new MyNumbersEnumerator();
}
// 비제네릭 IEnumerable의 GetEnumerator도 구현 (위의 GetEnumerator 호출)
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
yield return 사용해서 간략 구현해보자면
using System;
using System.Collections;
using System.Collections.Generic;
class MyNumbers : IEnumerable<int>
{
// 데이터를 저장하는 배열
int[] data = { 1, 2, 3 };
// GetEnumerator 메서드: 반복 가능한 열거자 제공
public IEnumerator<int> GetEnumerator()
{
// 배열 data의 각 요소를 순서대로 꺼내기 위해 foreach 사용
foreach (int n in data)
{
// 각 요소 n을 "반환"합니다.
// 이 한 줄이, 이전의 MoveNext(), Current, Reset() 등을
// 컴파일러가 자동으로 생성하도록 대신해 줍니다.
yield return n;
}
}
// 비제네릭 IEnumerable의 GetEnumerator 구현
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
var 키워드는 무엇인가?
var names = new List<string>();
var는 컴파일러에게 타입을 스스로 추론하게 하는 키워드
그러면, names 라는 변수는 List<String> 타입이 된다.
List<string>names = new List<string>(); 과 같은 말이다.
주의: var는 모든곳에 쓰는게아니라 오른쪽에 타입이 명확할때만 사용이 가능하다!
따라서
List<string> fruits = new() { "Apple", "Banana", "Cherry" };
foreach ( string fruit in fruits)
{
Console.WriteLine(fruit);
}
이거를 var을 써보면,,
List<string> fruits = new() { "Apple", "Banana", "Cherry" };
foreach ( var fruit in fruits)
{
Console.WriteLine(fruit);
}
fruit 타입이 자동으로 string으로 결정된것이다.
foreach가 실제로 하는 일(컴파일된 코드)
foreach (var item in collection)
Console.writeLine(item);
이렇게 하면 컴파일러는 내부적으로 아래와 같이 코드 변환
var enumerator = collection.GetEnumerator();
while(enumerator.MoveNExt())
{
var item = enumerator.Current;
Console.WriteLine(item);
}
enumerator.Dispose();
추가적으로 C#에서 타입(type)에는
기본 타입: int, string, bool 등
클래스 타입: Dog, Cat, MyNumbers 등
인터페이스 타입: IEnumerable<int>, IEnumerator<int> 등
구조체(struct) 타입, 열거형(enum) 타입 등 온갖게 타입이 될 수 있다...
구조체(struct)
메모리에 "값자체" 저장, 가벼워서 작고 단순한 데이터에 적합, 상속은 불가하다.
x,y 좌표처럼 작고 고정된 필드에 묶일때라던가
클래스보다 메모리, 성능 부담을 적게하기위해 쓰인다.
struct Point
{
public int x;
public int y;
}
Point P = new() { x=3, y=5};
Console.WriteLine($"({p.x}, {p.y}");
하면 (3,5) 가 된다.
이런 코드도 가능하다, x, y값을 사용자가 입력하면 송출한다.
struct Point
{
public int x;
public int y;
public void Speak()
{
Console.Write("X 값을 입력하세요: ");
string inputX = Console.ReadLine(); // ① 문자열 입력 받기
x = int.Parse(inputX); // ② 문자열 → 정수 변환
Console.Write("Y 값을 입력하세요: ");
string inputY = Console.ReadLine(); // ③ 문자열 입력 받기
y = int.Parse(inputY); // ④ 문자열 → 정수 변환
Console.WriteLine($"Point 좌표: ({x}, {y})");
}
}
class Program
{
static void Main()
{
Point p = new Point(); // 객체 생성
p.Speak(); // 사용자 입력 받아서 출력
}
}
다음은 열거형이다.
열거형(enum)
상태(state)나 옵션(choice)를 정해진 목록으로 표현할때.
정수기반 값 목록, 관련된 상수 값을 이름으로 관리..
읽기 쉽다.
상속이 불가하다.
하나의 이름 목록만 가진다. 필드가 여러개가아니다.
예)
enum DayofWeek { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday}
DayofWeek today = Dayofweek.Wednesday;
ConsoleWriteLine(today);
//Wednesday
여기서 .(닷)으로 wednesday를 꺼내쓸 수 있는데 이유는 { } 안에 있는 애들은 열거형 멤버 이기 때문이고 내부적으로는 0,1,2,3... 의 정수값을 가진다.
.(닷)은 타입 안에 멤버를 꺼내쓰는 문법이기때문이다.
따라서 Dayofweek라는 타입에서 Wednesday를 꺼내 쓰겠단 말이다.여기선 상수 3에 해당한다..
DayofWeek today; // DayofWeek 타입의 변수 today를 선언하고. 클래스와 같이 인스턴스 생성이 필요없이, '상수모음' 이기때문에 변수를 선언해준것
today = DayofWeek.Wednesday; 인것이다.
마지막으로
추상메서드란?
인스턴스(new) 불가능,
직접 만들 수는 없지만, 공통기능은 미리 제공하고, 꼭 구현해야할 메서드는 자식이 채우도록 약속
일부 메서드 구현 가능 + 일부는 강제(abstract)
언제쓰냐면, 여러 클래스가 공통 로직을 공유하되, 세부동작만 다르게 하고싶을때 사용한다.
예) 동물은 공통으로 Eat(), Sleep()이 가능하지만, Speak()는 다르다.
abstract class Animal
{
public void Eat() => Console.WriteLine("냠냠"); // 공통구현
public abstract void Speak(); // 반드시 자식이 구현
}
abstract 메서드: 선언만 있고 본문은 없으며 자식 클래스에서 반드시 override 해야한다.
class Dog : Animal
{
public override void Speak() => Console.WriteLine("멍멍!");
}
class Cat : Animal
{
public override void Speak() => Console.WriteLine("야옹!");
}
static void Main()
{
// Animal a = new Anima(); 는 불가능하다.
Animal dog = new Dog(); // 여기서 추상클래스의 인스턴스(객체)는 생성이 안되므로, 다형성을 이용해서 이와 같이 Animal 클래스의 변수 dog에 Dog() 의 인스턴스를 생성할 수 있다. 이렇게되면 dog는 Animal의 메서드, Dog의 메서드 모두를 사용할 수 있다.
Animal cat = new Cat();
dog.Eat(); dog.Speak(); // 냠냠/멍멍!
cat.Eat(); cat.Speak(); // 냠냠/야옹!
}
장단점
장점: 공통코드한번작성후 재사용 가능, 강제구현 오류 방지.
단점: 추상클래스는 단일 상속만 가능, 인터페이스보다 유연성 떨어짐.

궁금한점:
그렇다면 Animal이 추상클래스로 쓰였으니, 객체생성이안되는데, 공통으로 쓰는 메서드만 호출하고싶다.
하지만 상속되는 클래스들이 dog, cat만 있는게 아니라 엄청많은경우 이렇게 일일이 객체를 써줘야만 공통 메서드를 쓸 수 있는 것인가? 하면 방법이 있다.
List를 이용하면된다.
먼저,
abstract class Animal
{
public void Eat() => Console.WriteLine("냠냠");
}
class Dog : Animal { }
class Cat : Animal { }
class Cow : Animal { }
이라고 쳐보자..
기존데로라면
Animal dog = new Dog();
Animal cat = new Cat();
Animal cow = new Cow();
dog.Eat();
cat.Eat();
cow.Eat();
이런식으로 수동이다.
지금은 세가지 하위 클래스가 있지만 10개가 넘어간다고 쳐보자.
List<Animal> pets = new()
{
new Dog(),
new Cat(),
new Cow()
};
foreach (Animal pet in pets)
{
pet.Eat();
}
할경우 출력으로
냠냠
냠냠
냠냠
이 될것이다.
궁금한점2
어차피 상속받는상태니까,
객체를 생성할때
Animal dog = new Dog();
가 아니라
Dog dog = new Dog(); 해도
부모의 메서드도 쓸 수 있는 것아닌가?
맞다. 쓸수있다. 하지만 코드 유연성을 위해 위와 같이 쓴다. Animal dog = new Dog();
Animal 변수로 선언하면
1. 모든 Animal 자식을 하나의 변수로 다룰 수 있고,
Animal animal = new Dog();
animal.Eat(); // 냠냠
animal.MakeSound(); // Bark
animal = new Cat();
animal.Eat(); // 냠냠
animal.MakeSound(); // Meow!
2. 컬렉션으로 한꺼번에 처리도 가능하다.
List<Animal> pets = new() { new Dog(), new Cat(), new Bird() };
foreach (Animal pet in pets)
pet.Eat(); // 모든 동물 “냠냠” 호출
3. 의존성 분리되어있어서
구현 클래스(Dog)에 의존하지않고
추상(Animal/Interface)에 의존하므로 테스트 및 유지보수도 쉽다.

정리하자면,

왜 인터페이스가 1위인가?
- 경량 계약만 정의 → 구현 자유
- 다중 상속 지원 → 여러 역할 결합 가능
- LINQ, DI(의존성 주입), ASP.NET Core 등 C# 생태계 전반에서 표준
✅ 언제 virtual/override?
- 기본 동작을 제공하면서, 필요 시 자식에서 재정의
- 예: GUI 컨트롤, 게임 오브젝트 행동 등
✅ 언제 abstract class?
- 공통 코드가 많고(base 기능), 몇몇 메서드는 반드시 구현해야 할 때
- 인터페이스보다 코드 재사용성↑
'Unity 개발 공부' 카테고리의 다른 글
| [내배캠 사전캠프] 7일차 C# 기초 응용 수학문제풀이(25.03.31) (0) | 2025.03.31 |
|---|---|
| [내배캠 사전캠프] 6일차 C# 난수생성,문자열처리,out,ref,is,as (25.03.28) (0) | 2025.03.27 |
| [내일배움캠프 사전캠프] 4일차 C# 기초문법 복습 및, 활용해보기 (25.03.26) (0) | 2025.03.26 |
| [내일배움캠프 사전캠프] 4일차 C# 캐스팅, 제어문, 반복문 프로그래밍 연습. (25.03.26) (1) | 2025.03.26 |
| [내일배움캠프 사전캠프] 3일차 C# 문법기초 클래스, 함수, 변수 파헤치기 (25.03.24) (0) | 2025.03.24 |