제너릭

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

코드를 하나만 짜서 다양한 자료형들을 사용할 수 있게 하기 때문에 재사용성이 높아진다. <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();
}

 

 

오버라이딩과 오버로딩

 

오버라이딩

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

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

 

오버로딩

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

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

 

 

최대값, 최소값 찾기

사용자로부터 일련의 숫자 5개를 입력받아, 그 중에서 최대값과 최소값을 찾는 프로그램 작성하시오.

 

<나의 풀이 과정>

int[] num = new int[5]; // 사용자에게 입력 받을 배열 초기화

int max = num[0]; // 초기화
int min = num[0]; // 초기화

for (int i = 0; i < 5; i++) // 0~4 인덱스 안의 숫자 비교
{
    Console.Write("숫자를 입력하세요: ");
    num[i] = int.Parse(Console.ReadLine()); // 사용자에게 입력 받기

    if (num[i] > max) // num[i]가 max보다 크다면
    {
        max = num[i];
    }
    if (num[i] < min) // num[i]가 min보다 작다면
    {
        min = num[i];
    }
}
Console.WriteLine("최대값: " + max);
Console.WriteLine("최소값: " + min);

초기화가 min이 초기화 된 상태인 0의 값으로 비교되므로 늘 최소값이 0으로 나오게 된다.

 

int[] num = new int[5];

for (int i = 0; i < 5; i++)
{
    Console.Write("숫자를 입력하세요: ");
    num[i] = int.Parse(Console.ReadLine());
}

int max = num[0];
int min = num[0];

for (int i = 0;i < 5; i++) 
{
    if (num[i] > max)
    {
        max = num[i];
    }
    if (num[i] < min)
    {
        min = num[i];
    }
}
Console.WriteLine("최대값: " + max);
Console.WriteLine("최소값: " + min);

사용자가 입력한 숫자를 for문을 통해 5회 반복하여 배열에 넣어준 뒤, 또다시 for문을 통해 5개의 숫자를 비교한다.

 

 

소수 판별하기

주어진 숫자가 소수인지 판별하는 함수와 코드를 만드시오.

 

<나의 풀이 과정>

// 주어진 숫자가 소수인지 판별하는 함수
static bool IsPrime(int num)
{
    if (num <= 1) // 1보다 작거나 같으면 소수가 아니다
    {
        return false;
    }
    for (int i = 2; i < num; i++) // 2보다 크고 자기 자신과 같은 숫자가 되기 전까지 1씩 증가
    {
        if (num % i == 0) // 나누어 떨어지면 소수가 아니다
        {
            return false;
        }
    }

    return true; // 소수
}

static void Main()
{
    Console.Write("숫자를 입력하세요: "); // 사용자에게 숫자 입력 요청
    int num = int.Parse(Console.ReadLine()); // 사용자가 입력한 값을 정수로 변환하여 저장

    if (IsPrime(num)) // 입력 받은 숫자가 소수라면
    {
        Console.WriteLine(num + "은 소수입니다."); // 소수임을 출력
    }
    else // 소수가 아니라면
    {
        Console.WriteLine(num + "은 소수가 아닙니다."); // 소수가 아님을 출력
    }
}

이렇게 하면 작은 수일 때는 괜찮지만 큰 수를 입력했을 때 메모리를 많이 잡아먹게 된다.

static bool IsPrime(int num)
{
    if (num <= 1)
    {
        return false;
    }
    for (int i = 2; i <= num / 2; i++) // 2부터 자기 자신의 절반까지만 확인
    {
        if (num % i == 0)
        {
            return false;
        }
    }

    return true;
}

약수는 자기 자신을 제외하면 절반까지가 최대이기 때문에 2로 나눠주면 메모리 사용량을 줄일 수 있다. 그러나 49, 81 등 제곱된 수의 약수는 절반까지 도달하지 않아도 소수가 아님을 확인 할 수 있다.

 

static bool IsPrime(int num)
{
    if (num <= 1)
    {
        return false;
    }
    for (int i = 2; i * i <= num; i++) // 2부터 숫자의 제곱근까지만 확인
    {
        if (num % i == 0)
        {
            return false;
        }
    }

    return true;
}

 

 

 

숫자 맞추기

컴퓨터는 1에서 100까지의 숫자를 임의로 선택하고, 사용자는 이 숫자를 맞춘다. 사용자가 입력한 숫자가 컴퓨터의 숫자보다 크면 "너무 큽니다!"라고 알려주고, 작으면 "너무 작습니다!"라고 알려준다. 사용자가 숫자를 맞출 시, 게임 종료.

 

Random random = new Random();
int numberToGuess = random.Next(1, 101);
int userNum = 0;
int numberTry = 0;
bool playAgain = true;

Console.WriteLine("숫자 맞추기 게임을 시작합니다. 1에서 100까지의 숫자 중 하나를 맞춰보세요.");
while (playAgain)
{
    while (numberToGuess != userNum)
    {
        Console.Write("숫자를 입력하세요: ");
        userNum = Convert.ToInt32(Console.ReadLine());

        if (numberToGuess > userNum)
        {
            Console.WriteLine("너무 작습니다!");
        }
        else if (numberToGuess < userNum)
        {
            Console.WriteLine("너무 큽니다!");
        }
        numberTry++;
    }
    Console.WriteLine("축하합니다! {0}번 만에 숫자를 맞추었습니다.", numberTry);
    playAgain = false;
}

 

 

틱택토(콘솔)

 

<나의 풀이 과정>

 

메서드

중복되는 코드를 뭉쳐놓은 하나의 블록. 코드를 재사용하거나 모듈화 할 수 있게 만들어주는 것.

  • 코드의 재사용성
  • 모듈화
  • 가독성과 유지보수성
  • 중복 제거
  • 추상화

 

메서드의 구조

[접근 제한자] [리턴 타입] [메서드 이름]([매개변수])
{
    // 메서드 실행 코드
}
  • 접근 제한자(Access Modifier): 메서드에 접근할 수 있는 범위 지정. (public, private, protected 등)
  • 리턴 타입(Return Type): 메서드가 반환하는 값의 데이터 타입 지정. (반환 값이 없을 경우 void 사용)
  • 메서드 이름(Method Name): 메서드를 호출할 때 사용하는 이름.
  • 매개변수(Parameters): 메서드에 전달되는 입력 값. 0개 이상의 매개변수를 정의할 수 있다.
  • 메서드 실행에 코드(Method Body): 중괄호({}) 안에 메서드가 수행하는 작업을 구현하는 코드 작성.

 

매개변수와 반환값

static void PrintLine()
{
    for (int i = 0; i < 10; i++)
    {
        Console.Write("=");
    }
    Console.WriteLine();
}

static void PrintLine2(int count)
{
    for (int i = 0; i < count; i++)
    {
        Console.Write("=");
    }
    Console.WriteLine();
}

static int Add(int a, int b)
{
    return a + b;
}

static void Main(string[] args)
{
    // 사용 예시
    PrintLine();
    PrintLine2(20);

    int result = Add(10, 20);
    Console.WriteLine(result);
}

 

 

오버로딩

매개변수 목록이 다중 정의된 것. 매개변수의 개수, 타입, 순서가 다르면 다른 메서드로 취급한다. 메서드의 기능이나 작업은 동일하지만 입력값에 따라 다르게 동작해야 할 때 사용된다.

 

// 오버로딩
static int AddNumbers(int a, int b)
{
    return a + b;
}

static float AddNumbers(float a, float b)
{
    return a + b;
}

static int AddNumbers(int a, int b, int c)
{
    return a + b + c;
}


static void Main(string[] args)
{
    // 오버로딩 메서드 호출
    int sum1 = AddNumbers(10, 20);  // 두 개의 정수 매개변수를 가진 메서드 호출
    float sum3 = AddNumbers(10, 20); // 두 개의 실수 매개변수를 가진 메서드 호출
    int sum2 = AddNumbers(10, 20, 30);  // 세 개의 정수 매개변수를 가진 메서드 호출

 

 

재귀 호출

메서드가 자기 자신을 호출하는 것. 호출 스택에 호출된 메서드의 정보를 순차적으로 쌓고, 메서드가 반환되면서 스택에서 순차적으로 제거되는 방식이다. 메모리 사용량이 더 크고 실행 속도가 느릴 수 있으며,  무한 루프에 빠질 수 있기 때문에 주의해야한다.

 

// 재귀 호출
static void CountDown(int n)
{
    if (n <= 0)
    {
        Console.WriteLine("Done");
    }
    else
    {
        Console.WriteLine(n);
        CountDown(n - 1);  // 자기 자신을 호출
    }
}

static void Main(string[] args)
{
    // 재귀 호출
    CountDown(5);
}

 

 

구조체

여러 개의 데이터를 묶어서 하나의 사용자 정의 형식으로 만들기 위한 방법. 값 형식(대입하거나 값을 할당할 때 복사되는 것)으로 분류되며 struct 키워드를 사용하여 선언한다.

*자세한 내용은 class와 비교하며 언급

 

 

+ Recent posts