<C#에서 다중 상속을 사용하지 않는 이유>

  • 다이아몬드 문제

다중 상속을 사용하면 한 개의 클래스가 두 개 이상의 부모 클래스로부터 동일한 맴버를 상속받는다.

A라는 클래스를 B와 C가 상속받는다고 해보자.

D라는 클래스가 B와 C를 상속받게 된다면 결국엔 A를 상속받은 것과 같게 된다. 이것을 다이아몬드 모양으로 상속받는다고 한다. 다이아몬드로 B가 상속받은 A 클래스와 C가 상속받은 A가 동일함으로써 어떤 부모 클래스의 맴버를 사용해야할지 모호해지는 문제가 생겨난다.

  • 설계의 복잡성 증가

다중 상속을 하게 되면 어떤 클래스로부터 어떤 맴버를 상속을 받아야할지 상속 관계를 파악하기 어려워지며, 클래스 간의 관계가 복잡해진다.

  • 이름 충돌과 충돌 해결의 어려움

마찬가지로 같은 변수명이 있을 수 있고 그로 인해 중첩이 되어 사용할 수 없는 경우가 나타날 수 있다.

  • 일관성과 단순성 유지

그로 인해 C#은 일관성과 단순성을 유지하고 있다.

 

<인터페이스를 사용하는 이유>

  • 코드의 재사용성

우리는 변수를 쓰다보니 조금 더 함축해서 다양하게 쓰고 싶어 배열을 사용하였고, 코드들에 대한 일련 묶음들을 재사용 하고 싶기 때문에 메서드를 만들었고, 메서드나 필드를 한 곳에 뭉쳐 사용하고 싶기 때문에 Struct나 Class를 만들었다. 이처럼 동일한 코드들을 계속 재구현하지 않고 재사용하기 위해 인터페이스를 사용한다.

  • 다중 상속 제공
  • 유연한 설계

인터페이스는 구현을 제시할 뿐 실제 구현은 클래스가 하기 때문에 구현부와 제시부가 연관되지 않아도 된다.

 

 

인터페이스(Interface)

클래스가 구현해야하는 맴버들을 정의하는 것. 클래스에 대한 제약 조건을 건다.

클래스가 인터페이스를 구현할 경우, 모든 인터페이스 맴버들을 구현해야하며, 다중 상속을 지원한다.

 

인터페이스 구현

인터페이스를 구현할 때는 interface라는 키보드를 사용한다. 대표적인 규칙으로 이름이 I(대문자i)로 시작한다.

// 아이템을 사용할 수 있는 인터페이스 정의
public interface IUsable
{
    void Use(); // 사용 메서드 선언
}
// 인터페이스를 구현하는 클래스 생성
// 아이템 클래스
public class Item : IUsable
{
    public string Name { get; set; } // 자동 프로퍼티와 필드 역할을 같이 한다

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

// 플레이어 클래스
public class Player
{
    public void UseItem(IUsable item)
    {
        item.Use();
    }
}
// 게임 실행
static void Main()
{
    Player player = new Player();
    Item item = new Item { Name = "Health Potion" };
    player.UseItem(item);
}

 

다중상속

// 인터페이스 1
public interface IItemPickable
{
    void PickUp();
}

// 인터페이스 2
public interface IDroppable
{
    void Drop();
}

// 다중상속
// 아이템 클래스
public class Item : IItemPickable, IDroppable
{
    public string Name { get; set; }

    public void PickUp()
    {
        Console.WriteLine("아이템 {0}을 주웠습니다.", Name);
    }

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

// 플레이어 클래스
public class Player
{
    public void InteractWithItem(IItemPickable item)
    {
        item.PickUp();
    }

    public void DropItem(IDroppable item)
    {
        item.Drop();
    }
}

// 게임 실행
static void Main()
{
    Player player = new Player();
    Item item = new Item { Name = "Sword" };

    // 아이템 주울 수 있음
    player.InteractWithItem(item);

    // 아이템 버릴 수 있음
    player.DropItem(item);
}

 

<인터페이스와 추상클래스>

인터페이스

- 추상적인 동작만 정의하고 구현을 아예 갖지 않는다.

- 클래스가 아니며, 다중 상속이 가능하다.

- 클래스 간의 결합도를 낮추고, 유연한 상호작용이 가능하다.

- 코드의 재사용성과 확장성을 향상시킨다.

- 인터페이스를 구현하는 클래스가 모든 동작을 구현해야하기 때문에 작업량이 많다.

 

 

추상클래스

- 일부 동작의 구현을 가지고 구현을 할 수 있다.

- 단일상속만 가능하다.

- 공통된 동작을 추상화하여 코드의 중복을 방지하고 확장성을 제공한다.

- 구현된 동작을 가지고 있어 하위 클래스에서 재정의하지 않아도 될 때 유용하다.

- 다중 상속이 불가능하고, 상속으로 밀접하게 결합된 클래스로 유연성이 제한된다.

 

 

열거형 (Enums)

서로 연관된 상수들의 집합. 열거형의 상수 값 은 정수 값이다.

- 가독성
- 자기 문서화
- switch문과 호완성

 

열거형 구현

// 열거형 정의
public enum Month
{
    January = 1,
    February,
    March,
    April,
    May,
    June,
    July,
    August,
    September,
    October,
    November,
    December
}

internal class Program
{
    static void Main(string[] args)
    {
        // 처리하는 함수
        static void ProcessMonth(int month)
        {
            if (month >= (int)Month.January && month <= (int)Month.December)
            {
                Month selectedMonth = (Month)month;
                Console.WriteLine("선택한 월은 {0}입니다.", selectedMonth);
                // 월에 따른 처리 로직 추가
            }
            else
            {
                Console.WriteLine("올바른 월을 입력해주세요.");
            }
        }

        // 실행
        int userInput = 7; // 사용자 입력 예시
        ProcessMonth(userInput);
    }
}

 

 

제너릭

클래스나 메서드를 일반화시켜서 다양한 자료형에 대응할 수 있는 기능.

코드를 하나만 짜서 다양한 자료형들을 사용할 수 있게 하기 때문에 재사용성이 높아진다. <T> 키워드를 사용하며(클래스나 메서드를 사용할 때는 구체적인 자료형을 넣어준다.) 선언하는 시점이 아닌 어떤 자료형들에 대해 사용하는지에 따라 결정된다.

// 제너럴 클래스
class Stack<T>
{
    private T[] elements;
    private int top;

    public Stack()
    {
        elements = new T[100];
        top = 0;
    }

    public void Push(T item)
    {
        elements[top++] = item;
    }

    public T Pop()
    {
        return elements[--top];
    }
}

// 제너릭을 두 개 이상 사용
class Pair<T1, T2>
{
    public T1 First { get; set; }
    public T2 Second { get; set; }

    public Pair(T1 first, T2 second)
    {
        First = first;
        Second = second;
    }

    public void Display()
    {
        Console.WriteLine($"First: {First}, Second: {Second}");
    }
}

static void Main(string[] args)
{
    // 제너릭 클래스 사용 예시
    Stack<int> intStack = new Stack<int>();
    intStack.Push(1);
    intStack.Push(2);
    intStack.Push(3);
    Console.WriteLine(intStack.Pop()); // 출력 결과: 3

    // 제너릭을 두 개 이상 사용 예시
    Pair<int, string> pair1 = new Pair<int, string>(1, "One");
    pair1.Display(); // 출력 결과: First: 1, Second: One

    Pair<double, bool> pair2 = new Pair<double, bool>(3.14, true);
    pair2.Display(); // 출력 결과: First: 3.14, Second: True
}

 

 

out & ref 키워드

매개변수로 전달하면 반환 값을 매개변수로 직접적으로 받아올 수 있다.

// out & ref
// out 키워드 사용 예시
static void Divide(int a, int b, out int quotient, out int remainder) // out은 항상 값을 채워준다
{
    quotient = a / b;
    remainder = a % b;
}

// ref 키워드 사용 예시
static void Swap(ref int a, ref int b) // ref는 사용할지 안할지 모른다
{
    int temp = a;
    a = b;
    b = temp;
}

static void Main(string[] args)
{
    // out
    int quotient, remainder;
    Divide(7, 3, out quotient, out remainder);
    Console.WriteLine($"{quotient}, {remainder}"); // 출력 결과: 2, 1

    // ref
    int x = 1, y = 2;
    Swap(ref x, ref y);
    Console.WriteLine($"{x}, {y}"); // 출력 결과: 2, 1
}

 

  • ref 매개변수를 사용하면 메서드 내의 해당 변수 값을 직접 변경할 수 있기 때문에 주의 해야한다.
  • ref 매개변수는 값에 대한 복사가 없기 때문에 훨씬 빠르다. 그렇기 때문에 성능적으로는 좋지만 ref가 많아지면 코드의 가독성이 떨어지고 유지보수가 어려워지기 때문에 적절히 사용해야한다.
  • out 매개변수는 매소드 내에서 무조건 바꿔야하기 때문에 out이라는 인자로 넘어간 변수가 이전 값과 현재 값을 비교해야 한다면 이전 값을 유지하기 위해 복사하거나 다른 변수에 저장 해놓는 등의 방식을 취해야한다.

 

 

상속

부모 클래스라 불리는 기존의 클래스를 확장하거나 재사용할 수 있는 자식 클래스라 불리는 새로운 클래스를 생성하는 것.

자식 클래스는 부모 클래스의 맴버를 상속받을 수 있으며, 부모 클래스의 기능을 확장하거나 새로운 클래스로 재정의하는 것이 가능하다. 코드를 재사용할 수 있고 계층 구조로 표현할 수 있으며 유지 보수가 증가한다.

  • 단일 상속: 부모 클래스 하나, 자식 클래스 하나
  • 다중 상속: 부모 클래스가 여러 개 (C#에서는 지원X)
  • 인터페이스 상속: 인터페이스를 상속할 때만 다중 상속 가능. 하나의 클래스와 여러 개의 인터페이스를 상속한다.
    // 부모 클래스
    public class Animal
    {
        public string Name { get; set; }
        public int Age { get; set; }

        public void Eat()
        {
            Console.WriteLine("Animal is eating.");
        }

        public void Sleep()
        {
            Console.WriteLine("Animal is sleeping.");
        }
    }

    // 자식 클래스
    public class Dog : Animal
    {
        public void Bark()
        {
            Console.WriteLine("Dog is bark.");
        }
    }

    public class Cat : Animal
    {
        public void Meow()
        {
            Console.WriteLine("Cat is meow.");
        }

        public void Sleep() // 부모 클래스인 Animal의 Sleep을 숨긴다
        {
            Console.WriteLine("Cat Sleep!");
        }
    }

    static void Main(string[] args)
    {
        Dog dog = new Dog();
        dog.Name = "Bobby";
        dog.Age = 3;

        dog.Eat();
        dog.Sleep();
        dog.Bark();

        Cat cat = new Cat();
        cat.Name = "Kkami";
        cat.Age = 10;

        cat.Eat();
        cat.Sleep();
        cat.Meow();
    }
}

 

 

다형성

같은 타입이지만 다양한 동작을 수행할 수 있는 능력

 

가상 메서드

부모 클래스에서 정의되고 자식 클래스에서 재정의된다.

virtual 키워드를 사용하여 선언되며, 자식 클래스에서 필요에 따라 재정의 될 수 있다. 이를 통해 자식 클래스에서 부모 클래스의 메서드를 변경하거나 확장 가능하다.

// 가상 메서드
public class Unit
{
    public virtual void Move() // virtual -> 자신을 상속한 자식들이 재구현 할 수 있음을 선언. 실형태가 다를 수 있음을 확인해라
    {
        Console.WriteLine("두발로 걷기");
    }

    public void Attack()
    {
        Console.WriteLine("Unit 공격");
    }
}

public class Marine : Unit
{

}

public class Zergling : Unit
{
    public override void Move() // override로 재정의
    {
        Console.WriteLine("네발로 걷기");
    }
}

// 가상 메서드 호출
// #1 참조형태와 실형태가 같을때
Marine marine = new Marine();
marine.Move();
marine.Attack();

Zergling zergling = new Zergling();
zergling.Move();
zergling.Attack();

// Unit 클래스로 관리
// #2 참조형태와 실형태가 다를때
List<Unit> list = new List<Unit>();
list.Add(new Marine());
list.Add(new Zergling());

foreach (Unit unit in list)
{
    unit.Move();
}

 

추상 클래스와 메서드

직접적으로 인스턴스를 생성할 수 없다. 주로 이걸 기점으로 만든다는 느낌으로 베이스로 사용된다.

abstract 키워드를 사용하여 선언되며, 추상 메서드는 추상 메서드를 포함할 수 있다. 선언만 하고 구현하지 않기 때문에 자식 클래스에서 반드시 구현되어야 한다.

// 추상클래스
abstract class Shape
{
    public abstract void Draw();
}

class Circle : Shape
{
    public override void Draw() // 추상 메서드를 상속 받았기 때문에 반드시 구현
    {
        Console.WriteLine("Drawing a circle");
    }
}

class Square : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a square");
    }
}

class Triangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a triangle");
    }
}

// 추상 메서드 호출
// Shape shape = new Shape(); // 인스턴스를 만들 수 없음
List<Shape> list = new List<Shape>();
list.Add(new Circle());
list.Add(new Square());
list.Add(new Triangle());

foreach (Shape shape in list)
{
    shape.Draw();
}

 

 

오버라이딩과 오버로딩

 

오버라이딩

부모 클래스에 이미 정의되어 있는 메서드를 자식이 재정의 하는 것.(덮어쓰기) 이때 메서드의 이름과 매개변수의 반환 타입이 똑같아야 한다.

부모의 클래스에 정의되어 있는 동작이 아니라 새로운 동작을 재구현 할 수 있다.

 

오버로딩

매개변수의 갯수나 타입, 순서가 다른 동일한 이름의 메서드들이 여러 개 있는 것.

함수를 읽어올 때 골라 읽어올 수 있다.

 

 

객체지향 프로그래밍
  • 캡슐화 - 관련된 데이터와 기능을 하나의 단위로 묶는 것. 정보를 은닉하고 외부에서 직접적인 접근을 제한하여 안정성과 유지보수성을 높힌다.
  • 상속 - 부모 클래스의 특성과 동작을 자식 클래스가 상속받아 재사용한다. 코드의 중복을 줄이고 코드의 구조화와 유지보수를 용이하게 한다.
  • 다형성 - 오버로딩, 오버라이딩을 통해 하나의 메서드 이름으로 다양한 동작을 할 수 있다. 코드의 가독성과 재사용성을 높힌다.
  • 추상화 - 복잡한 기능을 단순화하여 필요한 기능에 집중한다.(세부 구현을 감추고 개념을 구현)
  • 객체 - 데이터와 메서드를 가지고 있으면서 실제로 상호작용하며 프로그램이 동작하는 것. 클래스로 구현되어 모듈화나 재사용성이 높다.

 

 

클래스

객체를 생성하기 위한 템플릿 또는 설계도. 데이터와 메서드를 하나로 묶은 사용자 정의 타입. 속성이나 동작에 대한 정의가 되어 있다. 클래스를 통해 인스턴스(객체)를 만들 수 있다.

- 구성요소: 필드(맴버변수), 메서드(맴버함수), 생성자(객체가 형성될 때 자동으로 호출되는 메서드), 소멸자(메모리에서 소멸될 때 자동으로 생성되는 메서드)

 

객체

클래스로 생성된 인스턴스. 클래스의 실체화된 형태. 독립적인 상태로 고유의 데이터를 가지고 있다.

 

// 클래스 선언
class Person
{
    public string Name;
    public int Age;

    public void PrintInfo()
    {
        Console.WriteLine("Name: " + Name);
        Console.WriteLine("Age: " + Age);
    }
}

Person p = new Person(); // 레퍼런스 타입
p.Name = "John";
p.Age = 30;
p.PrintInfo(); // 출력: Name: John, Age: 30

 

구조체와 클래스

  • 구조체와 클래스 모두 사용자 정의 형식. 우리가 원하는 자료형과 기능들을 뭉쳐 놓은 상태이다.
  • 구조체는 값 형식이며, 스택이라는 메모리에 각자 자동으로 할당된다.
  • 클래스는 참조 형식이며, 힙 영역에 동적 할당된다.
  • 구조체는 상속을 받을 수 없지만 클래스는 단일 상속과 다중상속이 가능하다.
  • 구조체는 작은 크기의 데이터나 단순한 구조를 작성할 때 적합하고 복잡한 경우에 클래스가 훨씬 더 사용하기 편하다.

 

접근 제한자

  • public: 외부에서 자유롭게 접근 가능
  • private: 같은 클래스 내부의 메서드나 필드에서 접근 가능
  • protected: 같은 클래스 내부와 상속 받은 자식까지만 접근 가능

 

필드

클래스 내부에 선언된 변수(맴버 변수). 데이터를 저장한다.

객체의 특징이나 속성들을 표현하기 위해 사용하며, 일반적으로 외부에서 접근하지 못하도록  private로 접근 제한자를 사용한다.

class Player
{
    // 필드 선언
    private string name;
    private int level;
}

 

메서드

클래스 내부에 선언된 함수(맴버 함수). 클래스의 동작을 정의한다.

클래스나 구조체에 대한 기능들을 정리하며, 일반적으로 외부에서 호출할 수 있도록 public으로 만들어준다.

class Player
{
    // 필드
    private string name;
    private int level;

    // 메서드
    public void Attack()
    {
        // 공격 동작 구현
    }
}

Player player = new Player();  // Player 클래스의 인스턴스 생성
player.Attack();  // Attack 메서드 호출

 

생성자

객체를 생성할 때 호출되는 메서드. 객체를 초기화하고 필요한 초기값을 설정하는 역할을 한다.

  • 클래스와 동일한 이름을 가지고 있으며, 반환 타입이 없다.
  • 여러 개를 정의 할 수 있고 매개변수의 개수와 타입에 따라 다른 생성자를 호출 할 수 있다.(오버로딩)
  • 기본적으로 매개변수가 없는 디폴트 생성자가 자동으로 생성되지만, 최소 하나라도 생성자를 만들었다면 디폴트 생성자는 더 이상 만들어지지 않는다.
class Person
{
    private string name;
    private int age;

    public void PrintInfo()
    {
        Console.WriteLine($"Name: {name}, Age: {age}");
    }
}



Person person1 = new Person();

 

소멸자

객체가 소멸이 될 때 호출되는 메서드.

  • 클래스와 동일한 이름을 가지고 있으며, 이름 앞에 ~ 기호를 붙인다.
  • 가비지 컬렉터(Garbage Collector)가 클래스가 아무데도 사용되지 않아서 메모리를 해제할 때 소멸자가 자동으로 호출된다.
  • 파일 핸들, 네트워트 연결, 데이터베이스 연결 등 외부적인 파일들에 대한 자원을 해제할 수 있다.
  • 배열이나 다른 클래스들의 선언을 인위적으로 해제할 수 있다.
  • 소멸이 언제 되는지에 대한 로깅과 디버깅을 할 수 있다.
  • 오버로딩 할 수 없다.
class Person
{
    private string name;

    public Person(string newName)
    {
        name = newName;
        Console.WriteLine("Person 객체 생성");
    }

    ~Person()
    {
        Console.WriteLine("Person 객체 소멸");
    }
}

 

프로퍼티(Property)

은닉화한 필드값(private 등)을 외부에서도 접근할 수 있는 중간 매개 역할을 만들어주는 것.

프로퍼티를 사용하여 데이터 유효성이나 제한을 거는 등 필드 접근을 제한하면 코드의 안정성과 가독성을 높힐 수 있다.

class Person
{
    private string name;
    private int age;

    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    public int Age
    {
        get { return age; }
        set { age = value; }
    }
}

Person person = new Person();
person.Name = "John";   // Name 프로퍼티에 값 설정
person.Age = 25;        // Age 프로퍼티에 값 설정

Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");  // Name과 Age 프로

보통 변수와 동일한 이름을 사용한다.(완전히 동일하면 안되기 때문에 대문자로 변환)

 

프로퍼티 접근 제한자 적용과 유효성 검사 예제

class Person
{
    private string name;
    private int age;

    public string Name
    {
        get { return name; }
        private set { name = value; } // 프로퍼티 자체는 외부에서 접근 가능하지만 세팅은 클래스 내부에서 하겠다는 제한
    }

    public int Age
    {
        get { return age; }
        set
        {
            if (value >= 0) // 대입하려는 값이 유효한가(나이에 음수가 들어오지 못하도록 제한)
                age = value;
        }
    }
}

Person person = new Person();
person.Name = "John";     // 컴파일 오류: Name 프로퍼티의 set 접근자는 private입니다.
person.Age = -10;         // 유효성 검사에 의해 나이 값이 설정되지 않습니다.

Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");  // Name과 Age 프로퍼티에 접근하여 값을 출력합니다.

 

자동 프로퍼티

프로퍼티를 간단히 정의하고 사용할 수 있도록 한다. 필드의 선언과 접근자 메서드의 구현을 컴파일러가 자동으로 처리한다.

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Person person = new Person();
person.Name = "John";     // 값을 설정
person.Age = 25;          // 값을 설정

Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");  // 값을 읽어 출력

 

 

+ Recent posts