본문 바로가기

Unity 개발 공부

[내배캠 사전캠프] 5일차 C# 상속,인터페이스,가상메서드, 추상클래스 (25.03.27)

상속 ( 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 기능), 몇몇 메서드는 반드시 구현해야 할 때
  • 인터페이스보다 코드 재사용성↑