본문 바로가기

C#

C# 2

728x90

 

생명주기는 스코프 안에 존재할시 살아있다.
괄호 안 = 스코프
스코프는 변수를 나누는 가장 중요한 선이다

 

참조 : 어떤이름으로 불리건 나라는 본질은 변하지 않음

struct : 각 메모리 영역이 생성, 메모리 공간이 다 다름

 

c#에서는 포인터는 없지만 스마트 포인터 같이 작동하는 레퍼런스가 있고 그게 바로 class data =  new class 하는 것과 동일하게 작동한다,

 

힙(heap) 메모리영역 - 동적 메모리 할당

- 클래스에서 생성한 인스턴스가 들어감

- struct를 가진 class는 힙 안에서 자기만의 stack 공간을 점유하여 사용함

 

stack 메모리 영역 - 정적 메모리 할당

- struct data

 

파라미터, 아규먼트

파라미터 (Parameter): 함수 정의 시 명시하는 변수로, 함수가 호출될 때 전달되는 값을 받을 준비

아규먼트 (Argument): 함수가 호출될 때 실제로 전달되는 값입니다. 이는 파라미터에 대응

 

함수

함수는 반복적인 행위를 기능 단위로 묶는 것을 의미합니다. 이를 통해 코드의 재사용성을 높이고 가독성을 향상시킬 수 있다.

 

함수가 출력하는값(return 되는 값)이 정수(int)이면 함수를 void(반환값 없는 함수) 대신 "int 함수명()"으로 선언

 

실습

함수 학습을 위한 min값에서 부터 max값까지 +1씩 증가하는 코드

 

 

배열

- array에 프로그래밍은 0부터 시작

- 4번째인것을 찾아라 했을때 Console.WriteLine(array[3]); 으로 쓰면 된다 (찾을값 -1)

랜덤 엑세스의 특징은 - 시간복잡도에서 이 알고리즘, 수식이 얼마나 빠른가에 대해 / 다이렉트로 엑세스에 접근할 수 있다.

 

Console.WriteLine(array[7]); 이렇게 하면 인덱스가 범위를 벗어났다 라고 뜬다.

 

 

이렇게 작성하면 랜덤 엑세스에대해 보다 쉽게 알아볼 수 있다. 

cmd창에서 아무키나 입력할 시 범위 내 까지 숫자가 나온다.

 

 

index = 10 과같이 할 경우 해당 array배열 범위를 벗어남으로 해당값이 출력되지 않는다.

 

재귀함수를 활용한 간단한 예시

 

* 배열은 요소(element)를 가지고 있고 저 배열은 6개의 요소를 가지고 있다.

* array.length는 요소의 갯수를 알려주는 속성이다.

 

 

  • SumArray 메서드:
    • 메서드는 배열과 배열의 길이를 받아들여 재귀적으로 배열의 합을 계산
    • 함수의 재귀적 호출은 배열의 합이 0일 때까지 진행된다. → 재귀함수는 자기 자신을 호출하는 함수라서 이 함수는 계속 반복되고 그 반복이 오류의 범위로 가거나 필요 이상의 범위로 넘어가기 전에 필요한 부분에서 return 0를 해줘 탈출시켜야 한다.
    • 재귀 호출이 종료되면 합계를 반환한다.
  • Main 메서드:
    • 배열을 정의하고, SumArray 메서드를 호출하여 배열의 합을 계산
    • 배열의 길이를 인수로 전달
    • 계산된 합계를 콘솔에 출력

재귀함수 사용하는 이유

  1. 문제 해결이 자연스럽게 재귀적인 구조를 갖는 경우: 어떤 문제를 해결하는 과정이 자연스럽게 재귀적인 구조를 갖는 경우에 재귀를 사용. 예를 들어, 이진 트리를 순회하거나 분할 정복 알고리즘을 구현할 때 재귀를 사용하는 것이 간편하고 직관적이게 되는 것이다.
  2. 작업이나 계산을 간결하고 이해하기 쉽게 만드는 경우: 때로는 반복문을 사용하여 작업을 수행하는 것보다 재귀 함수를 사용하는 것이 코드를 간결하고 이해하기 쉽게 만들 수 있다. 특히, 문제의 해결 방법이 반복적이지 않고 재귀적인 경우에는 재귀 함수를 사용하는 것이 더 효율적이다.

 

재귀 함수의 호출 스택은 호출의 깊이에 따라 계속해서 쌓이기 때문에, 너무 많은 재귀 호출이 발생하면 스택 오버플로우가 발생하고,  재귀를 사용할 때 메모리 소비나 성능 문제가 발생할 수 있기 때문이다. 재귀를 사용하기 전에 잘 고려하고, 반복문이나 다른 방법을 고려하게된다. 즉, 현업에서는 잘 사용하지 않는다.

 

- 재귀 함수가 에러 발생을 많이 시켜 분기처리를 제대로하지 않으면 무한으로 돌 수도 있고 메모리 초과(stackoverflow) 에러도 발생시킬 수 있다. 가독성 측면에서는 최악인 것이다.

 

재귀함수를 이용해 피보나치 수열 만들기

피보나치수열

 

 

  • static int Fibonacci(int n):  재귀적으로 피보나치 수를 계산
    • n이 1 이하일 때는 그대로 n을 반환
    • 그렇지 않으면 n-1과 n-2의 피보나치 수를 재귀적으로 호출하여 더한 값을 반환
  • static void Main(): 프로그램의 시작점.
    • nterms라는 변수를 정의하여 계산할 피보나치 수열의 항의 개수를 지정
    • 0부터 nterms - 1까지의 값을 반복하여 각 항에 대한 피보나치 값을 계산하고 출력

 

연산자 학습

 

 

 

 

 

& bit  AND결과

- 00000101 + 00000100 = 00000100 곂치는 부분만 1로됨

- 불리언을 대체해서 사용 가능

- 조건문에서 많이 사용

 

bit OR결과

- 00000101 + 00000100 = 00000101 둘중 하나라도 1이면 True

 

bit XOR

- 00000101 + 00000100 = 00000001 둘중 하나만 1이어야 1

 

bit OR /  bit XOR 연산자가 현업에서 제일 많이 사용된다.

            = 할당 연산자
            += 할당과 더하기 연산자
            -= 할당과 빼기 연산자
            *= 할당과 곱하기 연산자
            /= 할당과 나누기 연산자
            %= 할당과 나머지 연산자
            &= 할당과 and 연산자
            |= 할당과 or 연산자
            ^= 할당과 xor 연산자
            <<= 할당과 left shift연산자
            >>= 할당과 right shift연산자

 

클래스 (Class)

  • 참조 타입: 클래스는 참조 타입(reference type)입니다. 클래스 객체는 힙(heap)에 저장되고, 변수는 객체의 참조를 저장한다.
  • 참조 복사: 클래스 객체를 다른 변수에 할당하면, 두 변수는 동일한 객체를 참조한다.

 

dog1과 dog2는 동일한 객체를 참조하므로, 하나의 객체에서 변경된 내용이 다른 객체에도 반영된다.

 

구조체 (Struct)

  • 값 타입: 구조체는 값 타입(value type). 구조체 변수는 스택(stack)에 저장되고, 변수 자체가 데이터를 저장
  • 값 복사: 구조체 변수를 다른 변수에 할당하면, 데이터의 복사본이 생성됩니다. 각 변수는 독립적인 값을 가진다.

cat1과 cat2는 각각 독립적인 구조체 인스턴스를 가집니다. 하나의 인스턴스에서 변경된 내용이 다른 인스턴스에 영향을 주지 않습니다.

 

 

  • 클래스 (Class)
    • 참조 타입(reference type)
    • 객체는 힙(heap)에 저장되며, 변수가 객체를 참조함
    • 변수에 객체를 할당하면 참조가 복사됨 (동일 객체 참조)
    • 객체의 데이터는 여러 변수가 공유함
    • 예제에서 dog1과 dog2는 동일한 객체를 참조
    • 하나의 형태를 정의할 때 사용
  • 구조체 (Struct)
    • 값 타입(value type), 복사 (다른객체로 인식)
    • 변수 자체가 데이터를 저장하며, 스택(stack)에 저장됨
    • 변수에 구조체를 할당하면 값이 복사됨 (독립적인 데이터)
    • 각 구조체 인스턴스는 독립적인 값을 가짐
    • 예제에서 cat1과 cat2는 서로 다른 독립적인 구조체 인스턴스

 

C#에서 상속, 다형성, override, virtual 키워드는 객체 지향 프로그래밍(OOP)의 중요한 개념이다.

상속 (Inheritance)

상속은 한 클래스가 다른 클래스의 특성과 행동을 상속받아 확장하거나 수정할 수 있게 한다.

상속을 통해 코드 재사용성이 높아지고, 클래스 간의 관계를 정의할 수 있다.

 

 

 

  • Dog 클래스는 Animal 클래스를 상속받아 Eat 메서드를 사용할 수 있다
  • Dog 클래스는 추가로 Bark 메서드를 정의하여 자신의 특성을 확장한다

 

 

다형성 (Polymorphism)

다형성은 같은 메서드 호출이 객체의 실제 타입에 따라 다르게 동작하도록 하는 기능입니다. 일반적으로 상속과 인터페이스를 통해 구현됩니다.

 

 

 

  • MakeSound 메서드는 virtual 키워드로 선언되어 자식 클래스에서 재정의될 수 있다
  • Dog와 Cat 클래스는 MakeSound 메서드를 override 키워드를 사용해 재정의한다.
  • Animal 타입의 변수 myDog와 myCat는 각각 Dog와 Cat 객체를 참조하며, 실제 객체 타입에 따라 적절한 MakeSound 메서드가 호출된다.

 

인터페이스 (Interface)

인터페이스는 C#에서 함수들의 정의를 내려주는 개념으로, 특정 클래스가 반드시 구현해야 하는 메서드들을 강제하기 위해 사용된다. 인터페이스를 통해 함수의 통일성을 유지하고, 클래스 간의 일관성을 보장할 수 있다.

인터페이스의 주요 특징:

  1. 함수들의 정의: 인터페이스는 함수의 시그니처만 정의하며, 구현은 하지 않는다. 이를 구현하는 클래스는 반드시 인터페이스에 정의된 모든 함수를 구현해야 한다. 여기서 출력은 반듯이 되어야하는 것은 아니다.
  2. 강제 구현: 인터페이스를 구현하는 클래스는 인터페이스에 선언된 모든 메서드를 구현해야 한다.. 이를 통해 강제적인 구조를 만들 수 있는 것이다.
  3. 다중 상속: 클래스는 여러 인터페이스를 구현할 수 있다. 이는 C#에서 클래스 다중 상속을 허용하지 않는 대신, 인터페이스를 통해 다중 상속의 효과를 낼 수 있게 한다.
  4. 객체화 불가능: 인터페이스 자체로는 인스턴스를 생성할 수 없다. 오직 인터페이스를 구현한 클래스를 통해서만 객체를 생성할 수 있다.

인터페이스의 주요 용도

  1. 함수 통일화: 인터페이스를 사용하면 서로 다른 클래스들이 동일한 메서드를 구현하도록 강제할 수 있고, 이를 통해 함수의 이름, 파라미터, 반환 타입을 일관성 있게 유지할 수 있게 된다.
  2. 다형성 구현: 인터페이스를 사용하면 같은 인터페이스를 구현한 클래스들은 동일한 메서드 집합을 가지므로, 다형성을 구현할 수 있다.
  3. 유연한 설계: 새로운 클래스가 추가되어도 인터페이스를 구현하기만 하면 기존 코드와 쉽게 통합될 수 있습니다.

인터페이스 구현 시 주의 사항

  • 인터페이스를 구현하는 클래스는 반드시 모든 인터페이스 메서드를 구현해야 합니다.
  • 만약 하나라도 구현하지 않으면 컴파일 에러가 발생한다.
  • 인터페이스는 메서드의 구현을 제공하지 않으므로, 실제 로직은 이를 구현하는 클래스에서 작성해야 한다.
  • 인터페이스 자체로는 인스턴스를 생성할 수 없다. 인터페이스를 구현한 클래스를 통해서만 객체를 생성할 수 있다.

정리하면 interface -> 함수들의 정의를 내려주는 개념 / 이것은 무저껀 있어야 하는 개념

- 함수를 통일화하기 위해서 사용함.

- 인터페이스를 받고 있으면 인터페이스에 선언된 함수들은 무조건 구현되어야한다 (강제상속)

- 인터페이스는 오버라이딩을 꼭 해주어야한다.

그 함수를 상속 받아서 객체화시키지 않으면 그 객체를 활성화하지 못함

이 함수를 오버라이드 해서 써야해 할때 이 함수를 사용함, 객체화도 불가능

 

<수업중 코드>

namespace _20240530
{
    public struct Cat
    {
        public string name;
        public int age;

        public void PrintMyInfo()
        {
            Console.WriteLine("name : " + name + " age : " + age);
        }
    }

    public class Dog
    {
        public string name;
        public int age;

        public void PrintMyInfo()
        {
            Console.WriteLine("name : " + name + " age : " + age);
        }
    }

 

  • Cat 구조체와 Dog 클래스가 정의되어 있다. 둘 다 name과 age라는 두 개의 필드를 가지고 있으며, PrintMyInfo 메서드를 통해 자신의 정보를 출력한다.
  • struct와 class의 주요 차이점은 struct가 값 형식인 반면, class는 참조 형식이라는 것이다

 

    internal class Program
    {
        static void Function1(int k)
        {
            for (int i = k; i < 10; i++)
            {
                Console.WriteLine(i);
            }
        }

        static void Main(string[] args)
        {

 

 

 

  • Function1은 k부터 10까지의 숫자를 출력하는 간단한 함수
  • Main 메서드는 프로그램의 진입점으로, 실행되는 첫 번째 메서드

 

Dog a = new Dog();
a.name = "멍멍이";
a.age = 1;

a.PrintMyInfo();

Dog b = new Dog();
b.name = "깡깡이";
b.age = 3;

b.PrintMyInfo();

Dog c = a;
c.PrintMyInfo();

a.name = "셜록이";
c.PrintMyInfo();

 

 

  • Dog 클래스의 인스턴스 a와 b를 생성하고 각각의 이름과 나이를 설정한 후, PrintMyInfo 메서드를 호출하여 정보를 출력
  • Dog 객체 c는 a를 참조합니다. c는 a와 동일한 객체를 참조하므로, a의 이름을 변경하면 c의 이름도 변경된 것을 볼 수 있다. 이는 class가 참조 형식이기 때문

 

Cat d = new Cat();
d.name = "야옹이";
d.age = 5;

Cat e = d;
d.name = "곱슬이";
e.PrintMyInfo();

d.PrintMyInfo();
        }
    }
}

 

 

  • Cat 구조체의 인스턴스 d를 생성하고 이름과 나이를 설정
  • Cat 구조체 e는 d를 복사합니다. 이때 struct는 값 형식이므로 e는 d의 복사본이 됩니다. 따라서 d의 이름을 변경해도 e의 이름은 영향을 받지 않는다
  • PrintMyInfo 메서드를 호출하여 각각의 정보를 출력

<전체코드>

namespace _20240530
{
    public struct Cat
    {
        public string name;
        public int age;

        public void PrintMyInfo()
        {
            Console.WriteLine("name : " + name + " age : " + age);
        }
    }

    public class Dog
    {
        public string name;
        public int age;

        public void PrintMyInfo()
        {
            Console.WriteLine("name : " + name + " age : " + age);
        }
    }

    internal class Program
    {
        static void Function1(int k)
        {
            for (int i = k; i < 10; i++)
            {
                Console.WriteLine(i);
            }
        }


        static void Main(string[] args)
        {
            Dog a = new Dog();
            a.name = "멍멍이";
            a.age = 1;

            a.PrintMyInfo();

            Dog b = new Dog();
            b.name = "깡깡이";
            b.age = 3;

            b.PrintMyInfo();

            Dog c = a;
            c.PrintMyInfo();

            a.name = "셜록이";
            c.PrintMyInfo();


            //////////////////strcut///////////////////////


            Cat d = new Cat();

            d.name = "야옹이";
            d.age = 5;

            Cat e = d;
            d.name = "곱슬이";
            e.PrintMyInfo();

            d.PrintMyInfo();

        }
    }
}

전체 코드의 동작

  1. Main 메서드가 실행되면서 Dog 객체 a와 b가 생성.
  2. a와 b의 이름과 나이가 설정되고, PrintMyInfo 메서드로 각각의 정보를 출력
  3. c는 a를 참조합니다. a의 이름을 변경하면 c의 출력 결과도 변경된 이름을 반영
  4. Cat 구조체 d가 생성되고, d의 값을 e에 복사한다. d와 e는 독립적인 복사본을 가지므로, d의 이름을 변경해도 e의 값에는 영향을 주지 않는다.
  5. PrintMyInfo 메서드를 통해 Cat 구조체의 정보가 출력된다.

이 코드는 struct와 class의 차이점, 특히 값 형식과 참조 형식의 동작 방식을 명확히 이해하는 데 도움이 된다.

 

 

 

네임스페이스와 인터페이스

namespace _20240530
{
    public interface iSignal
    {
        void Send();
        void Recv();
    }

_20240530 네임스페이스 내에 iSignal 인터페이스가 정의되어 있다. 이 인터페이스는 Send와 Recv라는 두 메서드를 선언하고 있는데 인터페이스는 이를 구현하는 클래스가 두 메서드를 정의하도록 강제하게 된다.

 

public class human
{
    virtual public void Shot()
    {

    }
}

human 클래스는 Shot이라는 가상 메서드를 정의하며, 가상 메서드는 파생 클래스에서 재정의할 수 있다.

 

public class marin : human, iSignal
{
    virtual public void Send()
    {

    }
    virtual public void Recv()
    {

    }

    override public void Shot()
    {

    }
}

public class firebat : human
{
    override public void Shot()
    {

    }
}

 

 

  • marin 클래스는 human 클래스를 상속받고 iSignal 인터페이스를 구현한다. Shot, Send, Recv 메서드를 재정의한다.
  • firebat 클래스는 human 클래스를 상속받고 Shot 메서드를 재정의한다.

또다른 인터페이스와 클래스 정의

public interface AnimalActing
{
    void PrintMyInfo();
}

public class Test1
{

}

public class Test2
{

}

public class Animal : Test1, AnimalActing
{
    public string name;
    public int age;

    virtual public void PrintMyInfo()
    {
        Console.WriteLine("name : " + name + " age : " + age);
    }

    public void NewFunction()
    {
        Console.WriteLine("원본이지렁");
    }
}
 

 

 

  • AnimalActing 인터페이스는 PrintMyInfo 메서드를 선언
  • Animal 클래스는 Test1 클래스를 상속받고 AnimalActing 인터페이스를 구현
  • PrintMyInfo와 NewFunction 메서드를 정의.

 

public class Cat : Animal
{
    override public void PrintMyInfo()
    {
        Console.WriteLine("난 고양이지렁");
    }

    new public void NewFunction()
    {
        Console.WriteLine("재정의 되었지렁");
    }
}

public class Dog : Animal
{
}

public class T : Dog
{

}

 

 

  • Cat 클래스는 Animal 클래스를 상속받고 PrintMyInfo를 재정의하며, NewFunction을 새로 정의
  • Dog 클래스는 Animal 클래스를 상속
  • T 클래스는 Dog 클래스를 상속받음

Main 메서드와 프로그램 실행

internal class Program
{
    public static iSignal signal;

    static void Function1(int k)
    {
        for (int i = k; i < 10; i++)
        {
            Console.WriteLine(i);
        }
    }

    static void Main(string[] args)
    {
        signal = new marin();

        for (int i = 0; i < 10; i++)
        {
            if (signal != null)
            {
                signal.Send();
            }

            signal = null;
        }

 

 

  • Main 메서드는 프로그램의 진입점입니다. signal은 iSignal 타입
  • Function1은 k부터 10까지의 숫자를 출력하는 메서드
  • Main 메서드에서 marin 객체를 signal로 설정한 후, Send 메서드를 호출하고 signal을 null로 설정

 

Dog 클래스의 사용 및 출력

Dog a = new Dog();
a.name = "멍멍이";
a.age = 1;
a.PrintMyInfo();

Dog b = new Dog();
b.name = "깡깡이";
b.age = 3;
b.PrintMyInfo();

Dog c = a;
c.PrintMyInfo();

a.name = "셜록이";
c.PrintMyInfo();

 

 

  • Dog 객체 a, b를 생성하고 각각의 정보를 출력
  • c는 a를 참조하며, a의 이름을 변경한 후 c의 정보를 다시 출력

 

Cat 클래스의 사용 및 출력

Cat d = new Cat();
d.name = "야옹이";
d.age = 5;

Cat e = d;
d.name = "곱슬이";
e.PrintMyInfo();
d.PrintMyInfo();

Cat k = new Cat();
k.PrintMyInfo();

Animal animal = k;
animal.PrintMyInfo();
animal.NewFunction();
k.NewFunction();
((Cat)animal).NewFunction();

Animal animal2 = new Animal();

 

 

  • Cat 객체 d, e를 생성하고 각각의 정보를 출력
  • Animal 타입의 animal 변수는 Cat 객체 k를 참조하며, PrintMyInfo와 NewFunction 메서드를 호출
  • NewFunction 메서드는 Cat 객체에서 재정의된 메서드가 호출

marin과 firebat 객체의 사용

marin marinObj = new marin();
firebat firebatObj = new firebat();

human[] armys = { marinObj, firebatObj };

marinObj.Shot();
firebatObj.Shot();

for (int i = 0; i < armys.Length; i++)
{
    ((iSignal)armys[i]).Send();
}
        }
    }
}

 

 

  • marin과 firebat 객체를 생성하여 각각 Shot 메서드를 호출
  • armys 배열에 marin과 firebat 객체를 저장하고, 각 요소에 대해 Send 메서드를 호출합니다. marin 클래스만 iSignal 인터페이스를 구현하므로, 캐스팅 시 firebat 객체에서 런타임 오류가 발생할 수 있다.

핵심 키워드

 

  • 업캐스팅(upcasting): Cat 객체를 Animal 타입의 변수로 참조
  • 다형성(polymorphism): 부모 클래스 타입의 변수로 자식 클래스의 오버라이드된 메서드를 호출
  • 메서드 숨기기(method hiding): new 키워드를 사용하여 부모 클래스의 메서드를 숨길 수 있습니다. 이 경우, 변수의 타입에 따라 호출되는 메서드가 달라진다.
  • 캐스팅(casting): 부모 클래스 타입의 변수를 자식 클래스 타입으로 캐스팅하면, 자식 클래스의 숨겨진 메서드를 호출할 수 있다.

 

집중적으로 설명하셨던 내용

Animal animal = k; // k는 animal 변수에 참조 되지만 실제 객체는 cat이기 때문에

 

 

  • k는 Cat 클래스의 인스턴스
  • animal 변수는 Animal 타입이지만, 실제로 Cat 객체를 참조
  • 이는 업캐스팅(upcasting)이라고 불리며, 자식 클래스의 인스턴스를 부모 클래스 타입의 변수로 참조하는 것

 

animal.PrintMyInfo(); // 함수를 cat이 오버라이드 해서 printmyinfo가 cat에 있는게 실행된다.

 

 

  • PrintMyInfo 메서드는 Cat 클래스에서 오버라이드
  • animal 변수는 Animal 타입이지만, 실제 객체는 Cat이므로 Cat 클래스의 PrintMyInfo 메서드가 호출
  • 이는 다형성(polymorphism)의 예시로, 참조 변수의 타입이 아니라 실제 객체의 타입에 따라 메서드가 호출
animal.NewFunction(); // new 키워드로 자식에서 덮어 썼지만 얘는 오버라이드 되는게 되는게 아니라

 

 

  • NewFunction 메서드는 Cat 클래스에서 new 키워드를 사용하여 숨겨져있다.
  • animal 변수는 Animal 타입이므로, Animal 클래스의 NewFunction 메서드가 호출
  • new 키워드는 메서드 숨기기를 의미하며, 이는 오버라이딩과 다르다. 오버라이딩은 부모 클래스의 메서드를 재정의하는 것이고, 메서드 숨기기는 부모 클래스의 메서드를 가리는 것이다.
k.NewFunction(); // k를 실행시켜보면 k의 함수가 실행된다.

 

 

  • k는 Cat 타입이므로, Cat 클래스의 NewFunction 메서드가 호출
  • 이는 new 키워드로 숨겨진 메서드가 실제로 호출되는 상황을 보여준다.
((Cat)animal).NewFunction(); // Cat으로 변환 된 오브젝트에 한해서만 new 키워드로 선언한 함수가 사용된다.

 

 

  • animal 변수를 Cat 타입으로 캐스팅하면, Cat 클래스의 NewFunction 메서드가 호출
  • 이는 animal이 실제로 Cat 객체이기 때문에 가능한 것이다.
  • 캐스팅 후에는 Cat 클래스의 NewFunction 메서드가 호출된다.

 

 

 

 

728x90

'C#' 카테고리의 다른 글

C# 1  (0) 2024.05.29