C# 프로그래밍 (2)
포스트
취소

C# 프로그래밍 (2)

기타 기본 기능

1
2
3
4
5
6
7
8
9
10
11
12
int add1(int a, int b) {
    return a + b;
}

int add2(int a, int b) => a + b;

void sayHello() => Console.WriteLine("Hello");

Console.WriteLine( add1(3, 4) );
Console.WriteLine( add2(3, 4) );

sayHello();
  • 기본적인 함수 만들기
    • 함수는 특정한 기능을 수행하는 코드의 집합입니다.
    • 함수를 사용하면 코드를 재사용할 수 있고, 프로그램의 구조를 더 명확하게 만들 수 있습니다.
    • C#에서는 함수를 메서드라고도 부릅니다.
  • 함수의 기본 구조는 다음과 같습니다:
    • return_type function_name(parameters) { … }
    • return_type: 함수가 반환하는 값의 타입입니다. 반환값이 없는 경우 void를 사용합니다.
    • function_name: 함수의 이름입니다. 의미 있는 이름을 사용하는 것이 좋습니다.
    • parameters: 함수가 입력으로 받는 값들입니다. 여러 개의 매개변수를 사용할 수 있으며, 각 매개변수는 타입과 이름으로 구성됩니다.
  • expression bodied: 구현부가 간단한(1줄) 함수는 아래 처럼 만들어도 됩니다.
    • 함수() 뒤에 => 반환값 으로 표현
    • C#에만 있지만 자주 사용됨.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 기본 선언 형식
int[] arr = { 1, 2, 3 };
var tp = (1, 3.4, 'A');

// 요소 접근
Console.WriteLine($"Array with elements {arr[0]} and {arr[1]}.");
Console.WriteLine($"Tuple with elements {tp.Item1} and {tp.Item2}.");

// 출력
Console.WriteLine(tp.ToString());
Console.WriteLine($"Hash code of {tp} is {tp.GetHashCode()}.");

// 이름 지정
var t = (Sum: 4.5, Count: 3);
Console.WriteLine($"Sum of {t.Count} elements is {t.Sum}.");

(double Sum, int Count) d = (4.5, 3);
Console.WriteLine($"Sum of {d.Count} elements is {d.Sum}.");

// 이름 대입
var sum = 4.5;
var count = 3;
var t = (sum, count);
Console.WriteLine($"Sum of {t.count} elements is {t.sum}.");

// 딕셔너리식 이름 지정
var t = (a: 1, b: 2, c: 3);  // 간결해서 보통 권장됨
Console.WriteLine($"The 1st element is {t.Item1} (same as {t.a}).");
Console.WriteLine($"The 2nd element is {t.Item2} (same as {t.b}).");
Console.WriteLine($"The 3rd element is {t.Item3}.");
// Output:
// The 1st element is 1 (same as 1).
// The 2nd element is 2 (same as 2).
// The 3rd element is 3.
  • 배열: 동일한 타입의 여러 값을 저장할 수 있는 자료구조입니다. 배열은 고정된 크기를 가지며, 인덱스를 사용하여 각 요소에 접근할 수 있습니다.
  • 튜플: 여러 값을 하나의 단위로 묶을 수 있는 자료구조입니다. 튜플은 서로 다른 타입의 값을 포함할 수 있으며, 각 요소는 이름이 없거나 이름이 있을 수 있습니다.
  • 배열과 튜플의 차이점:
    • 배열은 동일한 타입의 값만 저장할 수 있지만, 튜플은 서로 다른 타입의 값을 저장할 수 있습니다.
    • 배열은 고정된 크기를 가지지만, 튜플은 가변적인 크기를 가질 수 있습니다.
    • 배열은 인덱스를 사용하여 요소에 접근하지만, 튜플은 요소에 이름으로 접근할 수 있습니다.
  • 배열과 튜플은 모두 데이터를 그룹화하는 데 사용되지만, 용도와 구조가 다르므로 상황에 따라 적절한 자료구조를 선택하는 것이 중요합니다.
  • 튜플은 보통 메소드의 반환값이 여러 개일 때 모아서 반환하기 위해 사용됨
  • 튜플 요소 접근 방식
    • Item1, Item2, … : 튜플 요소에 기본적으로 제공되는 이름입니다. 요소의 순서에 따라 Item1, Item2, …로 접근할 수 있습니다.
    • 이름 지정: 튜플을 선언할 때 각 요소에 이름을 지정할 수 있습니다. 예를 들어, (Sum: 4.5, Count: 3)과 같이 선언하면 Sum과 Count라는 이름으로 요소에 접근할 수 있습니다.
    • 딕셔너리식 이름 지정: 튜플을 선언할 때 각 요소에 딕셔너리식으로 이름을 지정할 수 있습니다. 예를 들어, (a: 1, b: 2, c: 3)과 같이 선언하면 a, b, c라는 이름으로 요소에 접근할 수 있습니다.
    • 이름 대입: 튜플을 선언할 때 변수에 값을 대입하여 이름을 지정하는 방식입니다. 예를 들어, var sum = 4.5; var count = 3; var t = (sum, count);과 같이 선언하면 sum과 count라는 이름으로 요소에 접근할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// #1. construction
int a = 1, b = 2, c = 3;

// 아래 코드는 int 변수 3개로 tuple 을 만든것
// => 즉, 생성(construction)
var t1 = (a, b, c);

// #2. deconstruction
int x, y, z;

// 아래 코드는 t1이라는 tuple 의 값을 각각, x, y, z 에 담은것
// => 분해(파괴, deconstruction 이라는 용어 사용)
x = t1.Item1;
y = t1.Item2;
z = t1.Item3;

// 위 코드를 아래 처럼 한줄로 표현가능
(x, y, z) = t1;

// 위 코드는 변수 x,y,z 를 먼저 선언후 사용한것
// 아래 처럼 선언과 분해를 동시에 해도 됩니다.
(int a, int b, int c) = t1;

int a;
int b;
int c;
(a, b, c) = t1; // 이 코드와 같은 것이 위 한줄의 코드

// #3. 아래 2줄의 차이점은 ?
(int a1, int a2, int a3) t2 = (1, 2, 3);  // t2 라는 tuple 생성, 요소의 이름이 a1, a2, a3
(int b1, int b2, int b3)    = (4, 5, 6);  // (4,5,6) 이라는 tuple 을 분해해서, b1, b2, b3 에 담은것. b1, b2, b3는 변수 이름
t2.a1 = 10;
t2.a2 = 20;
  • 튜플의 포장과 분해
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using static System.Console;

// #1. 함수는 기본 적으로 한개의 값을 반환 합니다.
string Get1() {
    return "john";
}

string ret1 = Get1();
WriteLine($"Returned value: {ret1}");

// #2. 튜플을 이용하면 여러 값을 반환할 수 있습니다.
(string, int) Get2() {
    return ("john", 20);
}

(string name, int age) = Get2();
WriteLine($"Name: {name}, Age: {age}");

// #3. 튜플의 요소에 이름을 지정할 수 있습니다.
(String name, int age) Get3() {
    return ("john", 20);
}

var t = Get3();
WriteLine($"Name: {t.name}, Age: {t.age}");
  • 튜플은 보통 함수가 여러 값을 반환하고 싶을 때 쓴다

  • 중간 정리: C# 에서 알아야 하는 것
    1. 대부분의 프로그래밍 언어가 제공하는 개념을 C# 은 어떻게 표현하는가 ?
      • 데이타 타입, 변수 생성
      • 함수 만드는 방법
      • 배열, 튜플 만드는 방법, 사용법
      • 제어문(조건문 : if, switch, 반복문 4개 : while, do-while, for, foreach)
    2. 객체지향 언어 문법(C++, C#, Java 등에서 공통으로 사용되는 문법)
      • 객체지향 언어 : 필요한 타입을 먼저 만들어서 사용하자는 철학을 가진 언어 (C++, C#, Java 는 100% 객체지향 언어이며, Python, Rust 등은 완벽한 객체지향 언어는 아님)
      • 타입을 만드는 방법(class 문법)
      • 만들어진 타입을 잘 사용하는 방법(ex : WPF 라이브러리의 Window 타입 사용법등..)
    3. C#만 가진 특징
      • Delegate, Property 등의 핵심 문법
    4. 라이브러리 활용해서 프로그램 작성
      • 기본 라이브러리
      • WPF 라이브러리 등

클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Rect {
    public int left = 0;
    public int top = 0;
    public int right = 0;
    public int bottom = 0;

    public int GetArea() {
        return (right - left) * (bottom - top);
    }
}

class Program {
    public static void Main() {
        Rect rc = new Rect();
        rc.left = 1;
        rc.top = 1;
        rc.right = 10;
        rc.bottom = 10;

        int ret = rc.GetArea();

        Console.WriteLine($"{ret}");
    }
}
  • 클래스를 만들자: 클래스의 기본 형태
    • 클래스는 객체의 상태를 나타내는 필드와 객체의 행동을 나타내는 메서드를 가질 수 있습니다.
  • 클래스의 기본 형태는 다음과 같습니다: class ClassName { 필드; 메서드; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Rect {
    public int left = 0;
    public int top = 0;
    public int right = 0;
    public int bottom = 0;

    // 튜플 언패킹으로 생성자 선언 (C#은 보통 이렇게 쓴다)
    public Rect() {}  // 필드에 기본값이 있어서 진짜 빈 생성자로 만들어도 됨
    public Rect(int x1, int y1, int x2, int y2) => (left, top, right, bottom) = (x1, y1, x2, y2);

    public int GetArea() {
        return (right - left) * (bottom - top);
    }
}

class Program {
    public static void Main() {
        // Rect rc = new Rect(1, 1, 10, 10);
        Rect rc = new Rect();

        int ret = rc.GetArea();

        Console.WriteLine($"{ret}");
    }
}
  • 클래스의 기본 - 생성자
  • 생성자란? 객체가 만들어질 때 자동으로 호출되는 함수
  • 객체가 만들어질 때, 객체의 멤버 변수들을 초기화하는 역할을 한다.
  • 생성자는 클래스 이름과 동일한 이름을 가진다.
  • 생성자는 반환형이 없다. (void도 안된다.)
  • 생성자는 여러 개 만들 수 있다. (오버로딩)
  • 생성자는 객체가 만들어질 때 자동으로 호출되기 때문에(new 키워드로 객체를 만들 때), 객체가 만들어질 때 필요한 초기화 작업을 수행할 수 있다.
  • 직접 작성한 생성자가 있을 경우, 컴파일러가 빈 생성자를 자동으로 만들어주지 않기 때문에 빈 생성자도 따로 정의해야 한다.

  • 변수와 객체의 차이
    • 변수(variable)는 값을 저장하는 공간
    • 객체(object)는 변수와 함수를 묶어서 하나의 단위로 만든 것
  • 변수를 객체라고 불러도 틀린 말은 아니지만, 일반적으로는 언어에서 제공하는 타입(primitive type)을 변수라고 부르고, class(struct) 문법으로 만든 타입의 변수를 객체라고 부르는 관례가 있음
  • Rust에서는 모든 것을 변수라고 부름

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Rect {
    public int left = 0;
    public int top = 0;
    public int right = 0;
    public int bottom = 0;

    public int GetArea() {
        return (right - left) * (bottom - top);
    }

    public Rect(int x1, int y1, int x2, int y2) => (left, top, right, bottom) = (x1, y1, x2, y2);

    public Rect() { }
}
class Program {
    public static void Main() {
        // C#의 모든 변수는 "new" 로 만들어야 합니다.
        int    n1 = new int();
        double d1 = new double();
        string s1 = new string();
        Rect   r1 = new Rect();

        // primitive type(언어 자체가 제공하는 타입)은 변수 생성시 "new 를 사용하지 않은 단축 표기법"을 제공
        // 문맥적 달콤함(syntax sugar) 라고 합니다.
        // => 목표 : 다른 언어와 동일하게 사용하게 하기 위해
        int n2 = 0;  // int n2 = new int() 와 동일
        double d2 = 3.4;
        string s2 = "AAA";

        // 단, 사용자 타입은 반드시 new 사용
        // primitive type : 언어 자체가 제공 (해당언어의 컴파일러가 인식) => int, double, char 등... 19page 목록
        // user define type : class(struct) 등의 문법으로 만들어진 타입. 타입 이름을 컴파일러가 인식하는 것이 아니라 class 문법으로 만들어진것

        // WPF 예제에서 사용했던 "Window"도 결국은 class 문법으로 만들어 놓은것 => UDT(User Define Type) 입니다.
    }
}
  • new에 대해 아세요
  • 기본적으로 C#의 모든 변수는 “new”로 만들어야 합니다.
  • 단, primitive type(언어 자체가 제공하는 타입, 컴파일러가 이미 알고 있음)은 변수 생성시 “new를 사용하지 않은 단축 표기법(syntax sugar)”을 제공
    • 이유: 다른 언어와 동일하게 사용하게 하기 위해
    • 사용자 타입(UDT(User Define Type), 커스텀 객체, 컴파일러가 모르는 타입)은 반드시 new 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Bike {
    private int gear = 0;

    public int GetGear() => gear;

    public void SetGear(int value) {
        if (value < 1) { gear = 1; }
        else if (value > 20) { gear = 20; }
        else { gear = value; }
    }
}

class Program {
    public static void Main() {
        Bike b = new Bike();
        b.SetGear(-5);
        Console.WriteLine($"current gear: {b.GetGear()}");
        b.SetGear(5);
        Console.WriteLine($"current gear: {b.GetGear()}");
        b.SetGear(55);
        Console.WriteLine($"current gear: {b.GetGear()}");
        b.SetGear(b.GetGear() - 3);
        Console.WriteLine($"current gear: {b.GetGear()}");
    }
}
  • 타입을 만들 때에는 사용자의 실수를 방지하는 대비가 필요하다 → private를 쓰자
    • 아무나 수정하지 못하게 하면 값이 잘못 들어가진 않겠지
  • 대신 private를 조회하고 수정할 방법을 제공해야 함 → getter/setter
  • 이걸 캡슐화(encapsulation)라고 부른다
    • 상태를 나타내는 필드를 private로 숨기고, public한 메서드로 접근하도록 하는 것
    • 외부의 잘못된 사용으로부터 객체의 상태를 보호할 수 있다
    • 객체의 상태는 메소드를 통해서만 변경 가능하다
  • 파이썬은 private가 없다 (관례적으로 _로 시작하는 이름은 private로 취급한다)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Person1 {
    public int age;
}

class Person2 {
    private int age;

    public int GetAge() => age;

    public void SetAge(int value) {
        if (value > 0)
            age = value;
    }
}

class Program {
    public static void Main() {
        Person1 p1 = new Person1();
        Person2 p2 = new Person2();

        // #1. publie field: 편하고 불안전
        p1.age = 10;
        int n1 = p1.age;

        p1.age = -10;

        // #2. setter/getter 사용: 불편하고 안전
        p2.SetAge(10);
        int n2 = p2.GetAge();

        p2.SetAge(-10);
    }
}
  • public 필드와 getter/setter의 차이를 봐라
    • public 필드는 누구나 수정할 수 있다 → 편하고 불안전, 잘못된 값이 들어갈 수 있다
    • getter/setter는 값을 검증할 수 있다 → 불편하고 안전, 잘못된 값이 들어가는 것을 방지할 수 있다

Property

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
    private int age;

    public int Age {
        get => age;  // expression-bodied에 익숙해져라
        set {  // if문이 필요한 경우에는 block-bodied로 작성해야 함
            if (value > 0)
                age = value;
        }
    }
}

class Program {
    public static void Main() {
        Person p2 = new Person();

        // p2.age = 10;  // error: age는 private이므로 접근 불가
        p2.Age = 10;
        int n2 = p2.Age;

        p2.Age = -10;
    }
}
  • Property 문법
    • C#에만 있음
    • getter/setter를 편하게 사용할 수 있게 해주는 문법
    • public field처럼 보이지만, 실제로는 getter/setter가 존재하는 것
    • 필드도 아니고 메소드도 아니고 그냥 프로퍼티 그 자체
  • 보통 필드는 소문자, 메소드와 프로퍼티는 대문자로 시작함
  • getter/setter는 필요에 따라 하나만 만들 수도 있음
    • getter만 있으면 읽기 전용 프로퍼티
    • setter만 있으면 쓰기 전용 프로퍼티
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
    private int age;

    public int Age {
        get => age;  // expression-bodied에 익숙해져라
        set {  // if문이 필요한 경우에는 block-bodied로 작성해야 함
            if (value > 0)
                age = value;
        }
    }
}

class Program {
    public static void Main() {
        Person p2 = new Person{ Age = 9 };  // 객체 생성 시 property 초기화 가능
        Console.WriteLine(p2.Age);

        p2.Age++;
        Console.WriteLine(p2.Age);
    }
}
  • property에 expression-bodied 문법 사용 가능
    • 식이 아닌 문이 필요한 경우 block-bodied로 작성해야 함
  • 객체 생성 시 property 초기화 가능
    • 객체 초기화 구문(object initializer)을 사용해야 함
    • 보통 선호됨
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System.Windows;
using System.Windows.Controls;

class Program {
    [STAThread]
    public static void Main() {
        Window w = new Window{
            Title = "Hello",
            Width = 300,
            Height = 300
        };
        Button btn = new Button{
            Content = "Don't touch me"
        };

        w.Content = btn;

        Application app = new Application();
        app.Run(w);
    }
}
  • WPF 속성도 프로퍼티로 지정한 거다
1
2
3
4
5
6
7
8
9
10
11
class Person1 {
    public int Age { get; set; } = 0;  // auto-implemented property (자동 구현 속성)
}

class Person2 {
    private int age = 0;
    public int Age {
        get => age;  // getter
        set {if (value >= 0) age = value; }  // setter
    }
}
  • property를 아주 짧게 자동으로 만들어준다. (auto-implemented property)
  • 대신 getter와 setter의 기능을 커스터마이징할 수 없다.
    • 그럼 왜 쓰겠냐? 향후의 확장성을 위해: 나중에 로직을 추가할 수 있도록 일단 프로퍼티가 있음 자체를 선언해두는 정도임
    • auto property를 써두면 나중에 로직이 추가되어도 사용자 코드는 바뀌지 않음

static

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Car {
    private int speed = 0;  // 이건 인스턴스 필드
    public static int cnt = 0; // static 필드
    public static List<Car> cars = new List<Car>(); // static을 이렇게도 쓸 수 있긴 함

    public int Speed {
        get { return speed; }
        set { speed = value; }
    }

    public Car(int s) {
        speed = s;
        cnt++; // static 필드를 증가시킴
        cars.Add(this); // static 필드에 현재 객체를 추가
    }
}

class Program {
    public static void Main() {
        Console.WriteLine($"how many cars are created: {Car.cnt}");  // static 필드에 클래스 이름을 통해 접근
        Car c1 = new Car(50);
        Console.WriteLine($"how many cars are created: {Car.cnt}");
        Car c2 = new Car(80);
        Console.WriteLine($"how many cars are created: {Car.cnt}");

        // 현재까지 생성된 자동차들의 속도를 출력
        Console.WriteLine("Current speeds of all cars:");
        foreach (Car car in Car.cars) {
            Console.WriteLine(car.Speed); // 인스턴스 필드에 접근
        }
    }
}
  • static을 이해하세요
    • static이 붙은 멤버는 객체가 아닌 클래스에 속하는 멤버입니다.
    • static 멤버는 객체를 생성하지 않고도 사용할 수 있습니다.
    • static 멤버는 프로그램 전체에서 하나만 존재하며, 모든 객체가 공유합니다.
    • static 멤버는 클래스 이름을 통해 접근할 수 있습니다.
    • static 멤버는 객체의 상태와 무관하게 동작하므로, 객체의 상태를 변경하지 않는 기능을 구현할 때 유용합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Car {
    private int speed = 0;  // 이건 인스턴스 필드
    private static int cnt = 0; // static 필드
    private static List<Car> cars = new List<Car>(); // static을 이렇게도 쓸 수 있긴 함

    public int Speed {
        get { return speed; }
        set { speed = value; }
    }
    public static int Cnt {
        get { return cnt; }
    }
    public static List<Car> Cars {
        get { return cars; }
    }

    public Car(int s) {
        speed = s;
        cnt++; // static 필드를 증가시킴
        cars.Add(this); // static 필드에 현재 객체를 추가
    }
}

class Program {
    public static void Main() {
        Console.WriteLine($"how many cars are created: {Car.Cnt}");
        Car c1 = new Car(50);
        Console.WriteLine($"how many cars are created: {Car.Cnt}");
        Car c2 = new Car(80);
        Console.WriteLine($"how many cars are created: {Car.Cnt}");

        Console.WriteLine("\nCurrent speeds of all cars:");
        foreach (Car car in Car.Cars) {
            Console.WriteLine(car.Speed);
        }
    }
}
  • static도 private, getter/setter 가능
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
using static System.Console;

class Date {
    private int year;
    private int month;
    private int day;

    public Date(int y, int m, int d) => (year, month, day) = (y, m, d);

    public int Year {
        get { return year; }
        set {
            if (value < 0) { throw new Exception("Year cannot be negative."); }
            year = value;
        }
    }
    public int Month {
        get { return month; }
        set {
            if (value < 1 || value > 12) { throw new Exception("Month must be between 1 and 12."); }
            month = value;
        }
    }
    public int Day {
        get { return day; }
        set {
            if (value < 1 || value > 31) { throw new Exception("Day must be between 1 and 31."); }
            day = value;
        }
    }

    public void ShowDate() {
        WriteLine($"{year}/{month}/{day}");
    }
}

class Program {
    public static void Main() {
        Date date = new Date(2024, 6, 20);
        date.ShowDate();

        date.Year = 2026; // Invalid year
        date.Month = 2;   // Invalid month
        date.Day = 23;     // Invalid day

        date.ShowDate(); // Should still show the original valid date
    }
}
  • 값을 잘못 넣었을 때 오류 던지는 방법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using static System.Console;

class Date {
    private int year;
    private int month;
    private int day;
    private static List<int> dayPerMonth = new List<int> { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

    public Date(int y, int m, int d) => (year, month, day) = (y, m, d);

    public int Year {
        get { return year; }
        set {
            if (value < 0) { throw new Exception("Year cannot be negative."); }
            year = value;
        }
    }
    public int Month {
        get { return month; }
        set {
            if (value < 1 || value > 12) { throw new Exception("Month must be between 1 and 12."); }
            month = value;
        }
    }
    public int Day {
        get { return day; }
        set {
            if (value < 1 || value > dayPerMonth[month]) { throw new Exception($"Day must be between 1 and {dayPerMonth[month]} for month {month}."); }
            day = value;
        }
    }

    public void ShowDate() {
        WriteLine($"{year}/{month}/{day}");
    }
}

class Program {
    public static void Main() {
        Date date = new Date(2024, 6, 20);
        date.ShowDate();

        date.Year = 2026; // Invalid year
        date.Month = 2;   // Invalid month
        date.Day = 23;     // Invalid day

        date.ShowDate(); // Should still show the original valid date
    }
}
  • static의 적절한 활용: 굳이 따로 만들 거 없이 딱 하나만 돌려보면 되는 데이터를 static으로 저장하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using static System.Console;

class Date {
    private int year;
    private int month;
    private int day;
    private static List<int> dayPerMonth = new List<int> { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

    public Date(int y, int m, int d) => (year, month, day) = (y, m, d);

    public static int HowManyDays(int month) {
        if (month < 1 || month > 12) { throw new Exception("Month must be between 1 and 12."); }
        return dayPerMonth[month];
    }
}

class Program {
    public static void Main() {
        WriteLine($"Days in February: {Date.HowManyDays(2)}");
    }
}
  • static의 적절한 활용: 객체가 필요한 게 아니라 기능만 필요할 때 static을 쓰면 굳이 쓸데없는 객체를 만들지 않아도 된다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
using static System.Console;

class Date {
    private int year;
    private int month;
    private int day;
    private static List<int> dayPerMonth = new List<int> { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

    public Date(int y, int m, int d) => (year, month, day) = (y, m, d);

    public int Year {
        get { return year; }
        set {
            if (value < 0) { throw new Exception("Year cannot be negative."); }
            year = value;
        }
    }
    public int Month {
        get { return month; }
        set {
            if (value < 1 || value > 12) { throw new Exception("Month must be between 1 and 12."); }
            month = value;
        }
    }
    public int Day {
        get { return day; }
        set {
            if (value < 1 || value > dayPerMonth[month]) { throw new Exception($"Day must be between 1 and {dayPerMonth[month]} for month {month}."); }
            day = value;
        }
    }

    public void ShowDate() {
        WriteLine($"{year}/{month}/{day}");
    }
    public Date AfterDays(int days) {
        int newDay = day + days;
        int newMonth = month;
        int newYear = year;

        while (newDay > dayPerMonth[newMonth]) {
            newDay -= dayPerMonth[newMonth];
            newMonth++;
            if (newMonth > 12) {
                newMonth = 1;
                newYear++;
            }
        }

        return new Date(newYear, newMonth, newDay);
    }
    public static int HowManyDays(int month) {
        if (month < 1 || month > 12) { throw new Exception("Month must be between 1 and 12."); }
        return dayPerMonth[month];
    }
    public static bool IsLeapYear(int year) {
        return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
    }
    public bool IsLeapYear() {
        return Date.IsLeapYear(year);
    }
}

class Program {
    public static void Main() {
        WriteLine($"Days in February: {Date.HowManyDays(2)}");
        WriteLine($"Is 2026 a leap year? {Date.IsLeapYear(2026)}");
        Date d = new Date(2026, 6, 20);
        WriteLine($"Is 2026 a leap year? {d.IsLeapYear()}");
    }
}
  • static method example 2: 동일 기능을 인스턴스/static 메소드로 모두 제공하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using static System.Console;

class Fn {
    public static int F1(int x) => x * x;
    public int F2(int x) => x + x;
}

class Program {
    public static void Main() {
        WriteLine(Fn.F1(3)); // static method는 클래스 이름으로 호출
        Fn fn = new Fn();
        WriteLine(fn.F2(3)); // instance method는 인스턴스 이름으로 호출
    }
}
  • static과 인스턴스의 비교 이해

스택과 힙

  • 프로그램에서 사용되는 메모리는 크게 2가지로 나뉜다.
    1. 코드 메모리: 프로그램이 실행되기 위해 필요한 명령어들이 저장되는 메모리
    2. 데이터 메모리: 프로그램이 실행되면서 필요한 데이터들이 저장되는 메모리 (예: int num;)
      • static storage : 프로그램이 시작될 때 생성되어 프로그램이 종료될 때까지 유지되는 데이터가 저장되는 메모리
      • stack storage : 지역변수, 매개변수, 리턴값 등과 같이 함수의 실행과 함께 생성되고 소멸되는 데이터가 저장되는 메모리. 작고 빠름.
      • heap storage : 동적으로 할당된 데이터가 저장되는 메모리. 크고 느림.
  • 메모리 용도 구분
    • 크기가 작고 수정을 자주 함 → stack
    • 크기가 크고 수정을 자주 하지 않음 → heap
  • C/C++ : 타입사용자가 stack 을 사용할지 heap 을 사용할지 결정. 일반 개발자에게 모든 권한을 부여. → 포인터를 쓰면 힙, 안 쓰면 스택.
  • C#/Java : 타입설계자가 stack 을 사용할지 heap 을 사용할지 결정 → 클래스(reference type)는 힙, struct(value type)는 스택.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CPoint {
    private int x;
    private int y;
    public CPoint(int a, int b) { x = a; y = b;}
}
struct SPoint {
    private int x;
    private int y;
    public SPoint(int a, int b) { x = a; y = b;}
}
class Program {
    public static void Main() {
        CPoint cp = new CPoint(0, 0);
        SPoint sp = new SPoint(0, 0);
    }
}
  • value type : struct, enum 등의 문법으로 만든 타입 → 모든 필드 자체가 stack 에 생성
  • Reference Type : class, interface 문법으로 만든 타입 → 모든 필드는 Heap 에 생성, Stack 에는 주소를 담는 변수가 생성(Reference 라는 용어 사용)
    • Python : 모든 변수가 Reference인 언어
  • 권장 사항
    • 크기가 작은 타입은 보통 struct
    • 크기가 크고, 자원(파일, 네트워크등)을 많이 사용하면 class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using static System.Console;

class CPoint {
    public int x;
    public int y;
    public CPoint(int a, int b) { x = a; y = b;}
}

struct SPoint {
    public int x;
    public int y;
    public SPoint(int a, int b) { x = a; y = b;}
}

class Program {
    public static void Main() {
        CPoint cp1 = new CPoint(1, 1);
        CPoint cp2 = cp1;  // 주소만 복사됨

        cp1.x = 2;

        WriteLine($"{cp1.x} {cp2.x}");

        SPoint sp1 = new SPoint(1, 1);
        SPoint sp2 = sp1;  // 모든 필드가 복사됨

        sp1.x = 2;

        WriteLine($"{sp1.x} {sp2.x}");
    }
}

  • 참조 타입과 값 타입의 차이
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class CPoint {
    public int x;
    public int y;
    public CPoint(int a, int b) { x = a; y = b;}
}
struct SPoint {
    public int x;
    public int y;
    public SPoint(int a, int b) { x = a; y = b; }
}

class Program {
    public static void Main() {
        CPoint cp1;  // 참조 변수만 생성. x, y 는 없음
        SPoint sp1;  // new 없지만 스택에 x, y 있음. 그러나 생성자 호출안됨
        SPoint sp2 = new SPoint(1, 1);  // ok. stack 객체 만들고 생성자 호출

        // 핵심 : 에러를 모두 찾으세요
        // int a = cp1.x;  // error: 할당되지 않은 변수 사용
        // cp1.x = 2;  // error: 할당되지 않은 변수 사용
        // -> 참조 타입은 new로 객체를 생성해야 사용할 수 있다. (new CPoint(1, 1))

        // int b = sp1.x;  // error: 할당되지 않은 필드 사용, warning: 변수는 선언되었으나 사용되지 않음
        // sp1.x = 2;  // warning: 변수는 선언되었으나 사용되지 않음
        // int c = sp1.x;  // error: 할당되지 않은 필드 사용, warning: 변수는 선언되었으나 사용되지 않음

        int d = sp2.x;  // 됨.
        sp2.x = 2;  // 됨.
    }
}

  • 참조 타입과 값 타입의 차이 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using static System.Console;

int n1 = 10;  // 값 타입
int n2 = n1;  // n1의 값을 n2에 복사

n1 = 20;  // n1의 값을 변경해도 n2에는 영향이 없다.
WriteLine($"{n1} {n2}");  // 20 10

int[] x1 = {1, 2, 3};  // 참조 타입
int[] x2 = x1;  // x1이 참조하는 배열의 주소값이 x2에 복사된다.

x1[0] = 20;  // x1이 참조하는 배열의 첫 번째 요소의 값을 변경하면 x2가 참조하는 배열의 첫 번째 요소의 값도 변경된다.
WriteLine($"{x1[0]} {x2[0]}");  // 20 20

string s1 = "AB";  // 참조 타입이지만 문자열은 불변(immutable)이다. 따라서 s1의 값을 변경하면 s1이 참조하는 문자열 객체가 새로 만들어지고 s1은 새로 만들어진 문자열 객체를 참조하게 된다. 반면 s2는 기존 문자열 객체를 계속 참조한다.
string s2 = s1;  // s1이 참조하는 문자열 객체의 주소값이 s2에 복사된다.

s1 = "XY";  // s1이 참조하는 문자열 객체의 주소값이 s2에 복사되었지만, s1이 참조하는 문자열 객체가 새로 만들어지면서 s1은 새로 만들어진 문자열 객체를 참조하게 된다. 반면 s2는 기존 문자열 객체를 계속 참조한다.
WriteLine($"{s1} {s2}");  // XY AB
  • 참조 타입과 값 타입 구분하기
    • 값 타입은 변수에 실제 값이 저장되고, 참조 타입은 변수에 실제 값이 저장되는 것이 아니라 값이 저장된 메모리 주소가 저장된다. 따라서 참조 타입의 변수를 다른 변수에 할당하면, 두 변수는 같은 메모리 주소를 참조하게 된다. 반면 값 타입의 변수를 다른 변수에 할당하면, 두 변수는 서로 다른 메모리 주소를 참조하게 된다.
  • 타입 선언문을 우클릭해서 “정의로 이동”하면 실제 구현 코드를 볼 수 있다. 용도에 따라 struct 혹은 class로 작성되어 있을 것.
    • int: struct
    • string: class

(im)mutable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// #1. int 타입의 객체는 mutable
int n = 10;
n = 20;  // ok

// #2. string 타입의 객체는 immutable
string s1 = "abcd";

char c = s1[0]; // ok
// s1[0] = 'x';  // error: 'string.this[int]' 속성 또는 인덱서는 읽기 전용이므로 할당할 수 없습니다.

string s2 = s1.ToUpper();  // s1이 바뀌는 게 아니라 s1의 대문자 버전이 새로 만들어지는 것
s2 = "xyz";  // s2를 바꾼 게 아니라 new string("xyz")가 생략된 것

// ---

string s1 = "AB";
string s2 = s1;  // s1과 동일한 레퍼런스를 가리키게 됨.

s1 = "CD";  // == new string("CD") -> 레퍼런스 자체가 새로 생성된 거고, s2에는 영향을 주지 않음.

Console.WriteLine($"s1: {s1}, s2: {s2}");  // s1: CD, s2: AB
  • mutable vs immutable
    • mutable : 객체의 상태를 변경할수 있는 것. 예: int, List, Dictionary<TKey, TValue> 등
    • immutable : 객체의 상태를 변경할수 없는 것(초기화 후 읽기 전용). 예: string, System.DateTime, System.TimeSpan 등
1
2
3
4
5
6
7
8
9
10
11
12
13
CPoint pt = new CPoint(0, 0);
pt.x = 10;  // pt가 가리키는 힙 객체의 속성이 변경됨.
Console.WriteLine($"pt: ({pt.x}, {pt.y})");  // pt: (10, 0)

CPoint pt = new CPoint(0, 0);
pt = new CPoint(10, 20);  // pt가 가리키는 힙 객체가 변경됨. 새 객체를 만든 거임.
Console.WriteLine($"pt: ({pt.x}, {pt.y})");  // pt: (10, 20)

class CPoint {
    public int x;
    public int y;
    public CPoint(int a, int b) { x = a; y = b;}
}
  • immutable 객체와 속성 (클래스로 동일 원리 예시 보기)
    • immutable 객체는 객체가 생성된 이후에 객체의 상태가 변경되지 않는 객체입니다.
    • mutable 객체는 객체가 생성된 이후에도 객체의 상태가 변경될 수 있는 객체입니다.
    • immutable 객체는 객체의 상태가 변경되지 않기 때문에, 객체의 상태를 변경하는 메서드를 제공하지 않습니다. 대신, 객체의 상태를 변경하려면 새로운 객체를 생성해야 합니다.
    • mutable 객체는 객체의 상태가 변경될 수 있기 때문에, 객체의 상태를 변경하는 메서드를 제공할 수 있습니다. 객체의 상태를 변경하려면, 객체의 속성을 직접 변경하거나, 객체의 상태를 변경하는 메서드를 호출하면 됩니다.
1
2
3
4
5
6
7
8
9
10
11
using System.Text;

string s1 = "ABC";  // immutable string
StringBuilder sb1 = new StringBuilder("ABC");  // mutable string. 반드시 new 명시.

Console.WriteLine($"immutable str: {s1}, mutable str: {sb1}");  // ABC

// s1[0] = "1";  // 'string.this[int]' 속성 또는 인덱서는 읽기 전용이므로 할당할 수 없습니다.
sb1[0] = '1';  // StringBuilder는 mutable string이므로 인덱서를 통해 문자 변경 가능

Console.WriteLine($"immutable str: {s1}, mutable str: {sb1}");  // ABC, 1BC
  • immutable string과 mutable string (자바, C#, swift 해당)
1
2
3
4
5
6
7
8
9
string s1 = "AAA";  // string intern pool에 저장됨
string s2 = "AAA";  // string intern pool에 저장됨 (중복 X)
string s3 = new string("AAA");  // string intern pool에 저장되지 않음 (new로 명시했기 때문)
string s4 = new string("AAA");  // string intern pool에 저장되지 않음 (new로 명시했기 때문)
// 이때 메모리에 존재하는 "AAA"의 갯수는? 3개
Console.WriteLine($"\ns1 == s2: {Object.ReferenceEquals(s1, s2)}");  // T
Console.WriteLine($"s1 == s3: {Object.ReferenceEquals(s1, s3)}");  // F
Console.WriteLine($"s1 == s4: {Object.ReferenceEquals(s1, s4)}");  // F
Console.WriteLine($"s3 == s4: {Object.ReferenceEquals(s3, s4)}");  // F
  • immutable / mutable
  • 타입에 따라 new의 명시 여부로 메모리 동작 방식이 달라지는 경우가 있음
    • string은 immutable 타입이지만, new로 명시하면 mutable처럼 동작함
    • string은 immutable이지만, new로 명시하면 mutable처럼 동작하는 이유는 string이 참조 타입이기 때문임
    • string은 참조 타입이기 때문에, new로 명시하면 새로운 객체가 생성되고, string intern pool에 저장되지 않음
    • string intern pool은 string literal이 저장되는 곳으로, 동일한 string literal이 여러 개 존재하더라도 하나의 객체만 저장되고, 동일한 객체를 참조하게 됨
  • 변경 불가한 string
    • 동일 데이터 공유 가능
    • 멀티 코어 최적화, 동시에 여러 CPU가 접근해도 됨
    • 컴파일러가 다양하게 최적화함
  • 웬만하면 변경 불가 string을 쓰는 것을 권장함: StringBuilder는 성능이 나쁘고 동시 접근 시 동기화 필요
  • Rust의 경우 따로 mut를 표기하지 않으면 기본적으로 const로 선언됨

상속

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
    private string name;
    private int age;
}

class Professor : Person {
    private string major;
}
class Student : Person {
    private string id;
}

class Program {
    public static void Main() {
        Student s = new Student();
    }
}

  • 상속 (inheritance)
  • 클래스 간의 관계를 표현하는 방법 중 하나 (클래스만 가능)
  • 부모 클래스(슈퍼 클래스, 베이스 클래스)와 자식 클래스(서브 클래스, 디어 클래스)로 구성됨
  • 자식 클래스는 부모 클래스의 멤버(필드, 메서드)를 상속받아 사용할 수 있음
  • 자식 클래스는 부모 클래스의 멤버를 재정의(override)하여 사용할 수 있음
  • 자식 클래스는 부모 클래스의 멤버를 숨길(hide) 수 있음
  • 자식 클래스들이 갖는 공통적인 속성을 부모 클래스가 정의함으로써 코드의 재사용성을 높이고, 유지보수를 용이하게 함
  • 부모 클래스: Base(기반) class, Super class, Parent class 등의 용어 사용
  • 자식 클래스: Derived(파생) class, Sub class, Child class 등의 용어 사용
1
2
3
4
5
6
7
8
9
10
11
class Car {
}

class Program {
    public static void Main() {
        Car c = new Car();
        string s = c.ToString();  // Car 클래스는 Object 클래스를 상속받았기 때문에, ToString() 메서드를 사용할 수 있음
        int n = 10;
        string s2 = n.ToString();  // int 타입도 Object 클래스를 상속받았기 때문에, ToString() 메서드를 사용할 수 있음
    }
}
  • 거의 모든 타입은 Object 클래스를 상속받음
  • Object 클래스가 가진 모든 메소드는 C#의 거의 모든 변수가 갖기 때문에 각 메소드의 기능을 알 필요가 있다(나중에)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Animal { public int age = 0; }

class Dog : Animal { public int color = 0; }

class Cat : Animal { public int speed = 0; }

class Program {
    public static void Main() {
        Dog r1 = new Dog();
        // int r2 = new Dog();  // 암시적으로 'Dog' 형식을 'int' 형식으로 변환할 수 없습니다.
        Animal r3 = new Dog();  // 기반 클래스 타입의 Reference로 파생 클래스 객체를 가리킬수 있다
        Animal r4 = new Cat();
        // Dog r5 = new Cat();  // 암시적으로 'Cat' 형식을 'Dog' 형식으로 변환할 수 없습니다.
        Console.WriteLine($"r1 type: {r1.GetType()}, r3 type: {r3.GetType()}, r4 type: {r4.GetType()}");  // r1 type: Dog, r3 type: Dog, r4 type: Cat
        // Console.WriteLine($"r1 age: {r1.age}, r3 color: {r3.color}, r4 speed: {r4.speed}");  // r3과 r4의 타입은 Dog와 Cat이라고 출력되긴 하는데, 실제로는 Animal 타입이기 때문에 color와 speed 멤버를 사용할 수 없음
    }
}
  • 업캐스팅(upcasting)
    • 파생 클래스의 객체를 기반 클래스 타입으로 변환하는 것
    • 파생 클래스는 기반 클래스의 모든 멤버를 가지고 있기 때문에, 파생 클래스의 객체를 기반 클래스 타입으로 변환하는 것은 항상 가능
    • 업캐스팅은 명시적으로 형변환을 해주지 않아도 자동으로 일어남
    • 업캐스팅을 하면 기반 클래스 타입으로 변환된 객체는 기반 클래스의 멤버만 사용할 수 있음
    • 업캐스팅을 하면 파생 클래스의 멤버는 사용할 수 없음
    • 업캐스팅을 하면 기반 클래스 타입으로 변환된 객체는 기반 클래스의 멤버만 사용할 수 있기 때문에, 파생 클래스의 멤버를 사용하려면 다운캐스팅(downcasting)을 해야 함
    • 되는 이유: 메모리 상에서 항상 기반 클래스의 데이터가 앞에 나오기 때문에 업캐스팅을 해도 메모리를 찾아가면 필요한 건 다 있게 됨
  • 컴파일러는 컴파일 시간에 업캐스팅된 객체 변수가 가리키는 곳에 있는 객체의 정확한 타입은 알 수 없다. 다만 Reference 자체의 타입(Animal)만 알 수 있다.
  • 그러므로 업캐스팅된 객체는 기반 클래스의 속성만 사용할 수 있음. 컴파일러가 몰라서 허락을 안해주는 거임
    • 정 쓰고 싶다면 다운캐스팅을 해야 함. 다만 확실하게 해당 객체가 가리키는 곳이 그 타입이라는 걸 확신해야 함. 모르겠으면 is 연산자로 조사 후 사용하면 됨
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class Animal {
    public int age = 0;

    public override string ToString() {
        return $"Animal(age: {age})";
    }
}

class Dog : Animal {
    public int color = 0;
    public override string ToString() {
        return $"Dog(age: {age}, color: {color})";
    }
}

class Cat : Animal {
    public int speed = 0;
    public override string ToString() {
        return $"Cat(age: {age}, speed: {speed})";
    }
}

class Program {
    public static void NewYear(Animal ent) {  // 동종 처리 메소드
        ent.age++;
        if (ent is Dog) {  // ent가 Dog 타입인지 확인
            Dog d = (Dog)ent;  // 다운캐스팅
            d.color++;
        }
        else if (ent is Cat) {  // ent가 Cat 타입인지 확인
            Cat c = (Cat)ent;  // 다운캐스팅
            c.speed++;
        }
    }
    public static void Main() {
        Dog r1 = new Dog();
        // int r2 = new Dog();  // 암시적으로 'Dog' 형식을 'int' 형식으로 변환할 수 없습니다.
        Animal r3 = new Dog();
        Animal r4 = new Cat();
        // Dog r5 = new Cat();  // 암시적으로 'Cat' 형식을 'Dog' 형식으로 변환할 수 없습니다.
        Console.WriteLine($"r1 type: {r1.GetType()}, r3 type: {r3.GetType()}, r4 type: {r4.GetType()}");  // r1 type: Dog, r3 type: Dog, r4 type: Cat
        // Console.WriteLine($"r1 age: {r1.age}, r3 color: {r3.color}, r4 speed: {r4.speed}");  // r3과 r4의 타입은 Dog와 Cat이라고 출력되긴 하는데, 실제로는 Animal 타입이기 때문에 color와 speed 멤버를 사용할 수 없음

        NewYear(r1);
        NewYear(r3);
        NewYear(r4);
        Console.WriteLine($"\nr1 age: {r1.age}, r3 age: {r3.age}, r4 age: {r4.age}");
        Console.WriteLine($"\nr1: {r1.ToString()}, r3: {r3.ToString()}, r4: {r4.ToString()}");

        Animal[] arr = new Animal[3];
        arr[0] = new Dog();
        arr[1] = new Cat();
        arr[2] = new Dog();
        Console.WriteLine($"\narr[0] type: {arr[0].GetType()}, arr[1] type: {arr[1].GetType()}, arr[2] type: {arr[2].GetType()}");  // arr[0] type: Dog, arr[1] type: Cat, arr[2] type: Dog
    }
}
  • 동종 처리 메소드
    • 동일 기반 클래스 하위의 파생 클래스 객체를 모두 전달받을 수 있는 메소드. 기반 클래스를 파라미터로 받는다.
    • 파생 클래스 고유의 기능을 사용해야 하는 경우 타입 확인 후 사용하기
  • 그럼 파라미터를 그냥 Object로 두면 안되냐? -> 다시 다운캐스팅해야 하기 때문에 비효율적.
  • 업캐스팅은 배열 선언에도 유용하다. 기반 클래스 배열에는 파생 클래스 요소가 들어갈 수 있지만 반대는 안되기 때문.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;

class Program {
    [STAThread]
    public static void Main() {
        Window w = new Window{
            Title = "Hello",  // 창 제목
            Content = new Button{ Content = "Don't touch me" },  // 창 내용
        };

        // 그림 나타내기
        BitmapImage bm = new BitmapImage();
        bm.BeginInit();
        bm.UriSource = new Uri("https://mblogthumb-phinf.pstatic.net/MjAxODEwMThfNzUg/MDAxNTM5ODQ4MTMyMjEx.OK8q9zLrDU-va2D1qanO3ZGoDXlIJ9x3YfMD0Q1mEgcg.Pj3s17iXyQVGlX8SYD4a1oBZWRqUxHt6qcOmZMFqVksg.JPEG.icecreamtime/2886008653af4f6e8b7.jpg?type=w800"); // 그림 이름 아무것이나
        bm.EndInit();

        Image img = new Image();
        img.Source = bm;

        w.Content = img; // window 이 컨텐츠로 그림 연결

        Application app = new Application();
        app.Run(w);
    }
}
  • 잘 봐라. 윈도우에도 Content 속성이 있고 버튼에도 있다. 그러니까 윈도우의 Content에 버튼도 들어가고 텍스트도 들어가고 다 되는 거다.
이 기사는 저작권자의 CC BY-NC-ND 4.0 라이센스를 따릅니다.

C# 프로그래밍 (1)

C# 프로그래밍 (3)