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

C# 프로그래밍 (5)

다른 글은 코드 -> 요약 순서인데 이건 요약 -> 코드 순서로 써봄. 뭐가 더 보기 좋을지 몰라서 바꿔봤다.

예외 처리

  • 프로그램이 실패했을 때의 처리
    1. 실패한 함수에서 프로세스 종료
      • 비추천. 보통 호출자에게 보고하는 것이 좋음.
    2. 실패한 함수가 실패 코드 반환
      • 1번보다는 나은데, 보고를 받은 쪽에서 무시하고 킵고잉할 수 있음 → 나중에 더 큰 오류 생김
      • 호출자가 반드시 오류를 처리하도록 강제하기 위해, 오류를 처리하지 않으면 프로그램 강제종료
    3. 직접 예외를 던져 → catch 안 하면 죽어버릴거야
      • 호출자가 모르게 프로세스를 종료하지 않으면서, 처리하지 않으면 진행을 못하게 할 수 있음
      • catch 후에 직접 종료하면 사용자는 적어도 왜 꺼졌는지는 볼 수 있음
    4. 수제 커스텀 예외로 친절하게 던지기
      • 예외 클래스 만들어서, 예외에 대한 설명과 정보를 담아서 던지기
      • catch에서 예외 종류에 따라 다른 처리 가능
  • 객체지향 언어의 예외 처리 방식
    • 함수가 실패하면 예외를 던진다 → 호출자가 catch해주지 않으면 프로그램은 죽어버릴거야 못지나가
    • catch 후 처리 못하겠으면 직접 종료하기
    • 특정 경로에 로그 남기기
  • 예외 클래스 Exception
    • System.Exception 클래스는 모든 예외의 기본 클래스
    • 예외 클래스는 보통 예외에 대한 설명을 담는 Message 프로퍼티를 가짐
    • 파생 클래스 만들어서 쓰기
코드 보기
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
using static System.Console;
using System.Diagnostics;

class DBBackupException : Exception {
    public string backupfilename = "temp.txt";
    public DBBackupException(string message) : base(message) { }
}

class NetworkException : Exception {
    public string url = "127.0.0.1";
    public NetworkException(string message) : base(message) { }
}

class Database {
    public Database(string dbname) { }

    // 실패한 함수에서 프로세스 종료
    public void Backup1() {
        Process p = Process.GetCurrentProcess();
        p.Kill();
    }
    // 실패한 함수가 실패 코드 반환
    public bool Backup2() {
        bool success = false;
        if (!success)
            return false;
        return true;
    }
    // 직접 예외를 던져
    public void Backup3() {
        bool success = false;
        if (!success) {
            throw new Exception("백업 실패");
        }
    }
    // 수제 커스텀 예외로 친절하게 던지기
    public void Backup4() {
        bool success = false;
        if (!success) {
            throw new DBBackupException("백업 실패");
        }
        else
            throw new NetworkException("네트워크 오류");
    }
    public void Remove() => WriteLine("Remove DB");
}

class Program {
    public static void Main() {
        Database db = new Database("product.db");

        // db.Backup1();

        // bool ret = db.Backup2();
        // if (!ret) {
        //     WriteLine("백업 실패");
        //     return;
        // }

        // db.Backup3();  // 예외 안받아줘서 죽어버릴거야

        // try {
        //     db.Backup3();
        // }
        // catch (Exception ex) {
        //     WriteLine("DB 백업 실패");
        //     WriteLine(ex.Message);
        //     Process.GetCurrentProcess().Kill();
        // }

        try {
            db.Backup4();
        }
        catch (DBBackupException ex) {
            WriteLine("DB 백업 실패");
            WriteLine(ex.Message);
            WriteLine(ex.backupfilename);
            Process.GetCurrentProcess().Kill();
        }
        catch (NetworkException ex) {
            WriteLine("네트워크 오류");
            WriteLine(ex.Message);
            WriteLine(ex.url);
            Process.GetCurrentProcess().Kill();
        }
        catch (Exception ex) {
            WriteLine("알 수 없는 오류");
            WriteLine(ex.Message);
            Process.GetCurrentProcess().Kill();
        }

        db.Remove();
    }
}
  • 사용자의 실수 처리하기
    • 똑바로 해올 때까지 다시 시켜
코드 보기
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
int age = 0;

while (true) {
    Console.WriteLine("나이 입력");

    string s = Console.ReadLine();

    // age = int.Parse(s);  // 사용자가 숫자 말고 다른 걸 쓰면 프로그램이 죽어버림

    try {
        age = int.Parse(s);
    }
    catch (FormatException ex) {
        Console.WriteLine("숫자만 써라");
        Console.WriteLine(ex.Message);
        continue;
    }
    catch (Exception ex) {
        Console.WriteLine("알 수 없는 오류");
        Console.WriteLine(ex.Message);
    }

    break;
}

Console.WriteLine($"당신의 나이는 {age}살입니다.");

parameter modifier

  • 참조 전달과 값 전달
  • C#에서 매개변수는 기본적으로 값 전달입니다. 즉, 메서드에 인수를 전달할 때, 해당 인수의 값을 복사하여 메서드로 전달합니다. 따라서 메서드 내에서 매개변수의 값을 변경해도 원래의 변수에는 영향을 미치지 않습니다.
  • 예를 들어, 다음과 같은 코드가 있다고 가정해봅시다:
코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using static System.Console;

class MyMath {
    public static void Inc1(int x) {
        ++x;
    }
    public static void Inc2(ref int x) {
        ++x;
    }
}

class Program {
    public static void Main() {
        int n1 = 0;
        int n2 = 0;

        WriteLine($"init n1: {n1}, n2: {n2}");
        MyMath.Inc1(n1);
        MyMath.Inc2(ref n2);
        WriteLine($"n1: {n1}, n2: {n2}");
    }
}
  • 위 코드에서 Inc1 메서드는 매개변수 x를 증가시키지만, n1의 값은 여전히 0입니다. 이는 xn1의 값을 복사하여 전달받았기 때문입니다. 따라서 Inc1 메서드 내에서 x를 변경해도 n1에는 영향을 미치지 않습니다.
  • 만약 매개변수를 참조로 전달하고 싶다면, ref 키워드를 사용할 수 있습니다. 이렇게 하면 메서드 내에서 매개변수의 값을 변경하면 원래의 변수에도 영향을 미치게 됩니다. ref 키워드는 함수 정의와 호출 모두에 표기되어야 합니다.
  • 위 코드에서 Inc2 메서드는 ref 키워드를 사용하여 매개변수 x를 참조로 전달받습니다. 따라서 Inc2 메서드 내에서 x를 증가시키면 n1의 값도 변경되어 1이 됩니다.
  • C/C++에서 포인터로 표기되는 것과 같은 개념임.

  • in/ref/out parameter
    • in parameter: main에서 보낸 값을 함수 안에서 사용하는 파라미터. 파라미터 전달 시 기본값.
    • ref parameter: main에서 참조 전달로 보낸 파라미터에 값을 담아줌. 읽고 쓰기를 전제하기 때문에 전달 시 초기화는 되어 있어야 함.
      • ++ 같은 연산자는 사실 x = x + 1과 동일하기 때문에 읽고 쓰기가 동반됨.
    • out parameter: main에서 참조 전달로 보낸 파라미터에 값을 담아줌. 쓰기 전용이기 때문에 전달 시 초기화는 필요 없음.
      • 대입 이외의 연산이 필요한 경우 따로 초기화해줄 필요가 있음.
      • 함수 호출 시 func(out type varname)과 같이 선언과 동시에 전달할 수 있음
코드 보기
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
using static System.Console;

class MyMath {
    public static int AddSub1(int a, int b, ref int ret) {
        ret = a - b;
        return a + b;
    }
    public static int AddSub2(int a, int b, out int ret) {
        ret = a - b;
        return a + b;
    }
}

class Program {

    public static void Main() {
        int ret1 = 0;
        // int ret1_1;

        int ret2 = MyMath.AddSub1(5, 3, ref ret1);
        int ret2_1 = MyMath.AddSub2(5, 3, out int ret1_1);

        WriteLine($"ret1(a - b): {ret1}\nret2(a + b): {ret2}\n");
        WriteLine($"ret1_1(a - b): {ret1_1}\nret2_1(a + b): {ret2_1}");
    }
}
  • 좀 더 제한적이고 간결한 예외 처리
    • int.Parse() : 문자열을 정수로 변경. 실패 시 예외 발생
    • int.TryParse() : 문자열을 정수로 변경. 실패 시 false 반환
  • 좀 더 일반적인 정리
    • Something(): 실패 시 예외 발생
    • TrySomething(): 실패 시 false 반환 → try/catch 없이 if로 추가 처리 가능
      • 쓰기만 하면 된다면 out으로 전달
코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using static System.Console;

// try parse
class Program {
    public static void Main() {
        // #1. int.Parse() : 문자열을 정수로 변경
        int n1 = int.Parse("10");    // ok. 성공
        // int n2 = int.Parse("Hello"); // 실패. 예외 발생

        // #2. int.TryParse()
        int n3;
        bool result1 = int.TryParse("10", out n3); // ok. 성공
        WriteLine($"result1: {result1}, n3: {n3}");

        bool result2 = int.TryParse("Hello", out int n4); // 실패. false 반환
        WriteLine($"result2: {result2}, n4: {n4}");
    }
}
  • 파라미터 수정자 활용
    • ref 키워드로 참조에 의한 전달을 구현할 수 있다.
    • ref 키워드로 참조에 의한 전달을 구현할 때는 메서드 정의와 메서드 호출 양쪽 모두에서 ref 키워드를 사용해야 한다.
코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using static System.Console;

class MyUtil {
    public static void Swap(ref int x, ref int y) {
        int temp = x;
        x = y;
        y = temp;
    }
}

class Program {
    public static void Main() {
        int x = 1;
        int y = 2;

        MyUtil.Swap(ref x, ref y);
        WriteLine($"{x}, {y}");	// 2, 1
    }
}
  • 파라미터 수정자 종류별 사용 시 차이
    1. no modifier parameter: 일반적인 파라미터로, 초기화된 상태로 전달됩니다. 따라서 메서드 내에서 해당 파라미터의 값을 읽을 수 있습니다.
    2. out parameter: out 키워드로 선언된 파라미터는 초기화되지 않은 상태로 전달됩니다. 따라서 메서드 내에서 해당 파라미터의 값을 읽을 수 없습니다. 대신, 메서드 내에서 해당 파라미터에 값을 할당해야 합니다.
    3. ref parameter: ref 키워드로 선언된 파라미터는 초기화된 상태로 전달됩니다. 따라서 메서드 내에서 해당 파라미터의 값을 읽을 수 있습니다. 또한, 메서드 내에서 해당 파라미터에 값을 할당할 수 있습니다.
코드 보기
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 Example {
    public static void no_modifier_parameter(int x) {
        int n = x;  // no modifier parameter는 초기화된 상태로 전달되므로, x의 값을 읽을 수 있습니다. 따라서 이 줄은 정상적으로 컴파일됩니다.
        x = 0;
    }

    public static void out_parameter(out int x) {
        // int n = x;  // out parameter는 초기화되지 않은 상태로 전달되므로, x의 값을 읽을 수 없습니다. 따라서 이 줄은 컴파일 오류를 발생시킵니다.
        x = 0;
    }

    public static void ref_parameter(ref int x) {
        int n = x;  // ref parameter는 초기화된 상태로 전달되므로, x의 값을 읽을 수 있습니다. 따라서 이 줄은 정상적으로 컴파일됩니다.
        x = 0;
    }
}

class Program {
    public static void Main() {
        int n1;
        int n2 = 0;

        Example.out_parameter(out n1);
        Example.out_parameter(out n2);

        // Example.ref_parameter(ref n1);  // ref parameter는 초기화된 상태로 전달되어야 하므로, n1은 초기화되지 않은 상태로 전달됩니다. 따라서 이 줄은 컴파일 오류를 발생시킵니다.
        // 다만 현재 코드를 순서대로 실행하면 앞서 초기화되기 때문에 실행이 되긴 함.
        Example.ref_parameter(ref n2);

        Example.out_parameter(out int n3);
    }
}
  • 클래스와 구조체의 파라미터 전달
  • 클래스는 참조형이므로, 메서드에 전달할 때 참조값이 전달된다.
  • 구조체는 값형이므로, 메서드에 전달할 때 값이 복사되어 전달된다.
코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CPoint { public int X { get; set; } = 0; public int Y { get; set; } = 0; }

struct SPoint { public int X { get; set; } public int Y { get; set; } }

class Program {
    public static void F1(CPoint pt) { pt.X = 10; pt.Y = 20; }
    public static void F2(SPoint pt) { pt.X = 10; pt.Y = 20; }
    public static void Main() {
        CPoint cpt = new CPoint{X = 0, Y = 0};
        SPoint spt = new SPoint{X = 0, Y = 0};

        F1(cpt); Console.WriteLine($"class edited: {cpt.X}, {cpt.Y}");
        F2(spt); Console.WriteLine($"struct edited: {spt.X}, {spt.Y}");
    }
}

오버로딩

  • 메서드 오버로딩(Method Overloading)
    • 같은 이름의 메서드를 여러 개 정의하는 것
    • 매개변수의 타입, 개수, 순서가 다르면 같은 이름의 메서드를 여러 개 정의할 수 있습니다.
    • 메서드 오버로딩을 사용하면 코드의 가독성이 향상되고, 유지보수가 쉬워집니다.
    • 내부적으로는 함수가 여러 개 정의되었지만 사용자 입장에서는 함수가 1개로 보이기 때문에 일관된 라이브러리 구축에 좋음.
  • C#, C++, 자바, swift는 오버로딩이 되지만 C, 파이썬, rust는 안된다. 사유는 모든 코드는 명확해야 한다는 철학 때문 (C는 너무 오래 전에 나와서 당시엔 문법 자체가 없었음).
    • 의외로 파이썬은 진짜 이런 방식의 오버로딩은 불가하고, 대신 가변인자(*args, **kwargs)를 이용해서 비슷한 효과를 낼 수는 있음. 혹은 multipledispatch 라이브러리 활용.
코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Math {
    public int Square(int x) {
        return x * x;
    }
    public double Square(double x) {
        return x * x;
    }
}

class Program {
    public static void Main() {
        Math m = new Math();

        var ret1 = m.Square(3);
        var ret2 = m.Square(3.3);
    }
}

네임드 파라미터

  • 파라미터에 이름 짓기
    • 메서드의 매개변수에 이름을 붙이는 기능
    • 매개변수의 이름을 명시적으로 지정하여 가독성을 높일 수 있습니다.
    • 매개변수의 순서를 바꿔서 호출할 수 있습니다.
    • 선택적 매개변수와 함께 사용할 때 유용합니다.
    • 일부 매개변수만 이름을 표기해도 되지만, 반드시 이름이 표기되지 않은 매개변수가 먼저 와야 한다.
코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Rect {
    public void Set(int x, int y, int width, int height) => Console.WriteLine($"x: {x}, y: {y}, width: {width}, height: {height}");
}

class Program {
    public static void Main() {
        Rect rc = new Rect();

        rc.Set(1, 1, 10, 10);
        rc.Set(x: 1, y: 1, width: 10, height: 10);
        rc.Set(width: 10, height: 10, x: 1, y: 1);
        rc.Set(1, 1, width: 10, height: 10);
    }
}
  • 선택적 매개변수
    • 메서드의 매개변수에 기본값을 지정하는 기능
    • 메서드를 호출할 때 해당 매개변수를 생략할 수 있습니다.
    • 생략된 매개변수는 기본값으로 초기화됩니다.
    • 선택적 매개변수는 반드시 매개변수 목록의 마지막에 위치해야 합니다.
  • 오버로딩과 구조가 겹칠 경우, 컴파일러가 구분할 수 있으면 실행되고 구분하지 못하면 비정상 종료
코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using static System.Console;

class Example {
    public void M1(int a, int b = -1, int c = -1) {
        WriteLine($"a: {a}, b: {b}, c: {c}");
    }
}

class Program {
    public static void Main() {
        Example e = new Example();

        e.M1(1, 2, 3);
        e.M1(1, 2);
        e.M1(1);
    }
}

generic

  • 로직이 전부 똑같은데 타입만 다르다고 굳이 똑같은 코드를 복붙해서 오버로딩을 해야겠니?
  • 제네릭을 사용하면, 타입에 상관없이 로직이 똑같은 메서드를 하나만 작성할 수 있다.
  • 제네릭 메소드 호출 시 타입을 명시적으로 지정할 수도 있고, 컴파일러가 호출 시 전달되는 인자의 타입을 보고 자동으로 유추하도록 할 수도 있다.
  • C++에서는 템플릿이라고 부른다. C#에서는 제네릭이라고 부른다.
  • 하나의 제네릭 메소드 안에 여러 타입이 필요한 경우 T1, T2, … 등으로 여러 타입 매개변수를 사용할 수 있다. T는 Type의 약자이다.
코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Program {
    public static void Main() {
        int n1 = 10, n2 = 20;
        double d1 = 1.1, d2 = 2.3;
        string s1 = "Hello", s2 = "World";

        Swap<int>(ref n1, ref n2);
        Swap(ref d1, ref d2);
        Swap(ref s1, ref s2);

        PrintSomething<int, string>(123, "abc");
        PrintSomething(3.14, 123);
    }
    public static void Swap<T>(ref T a, ref T b) {
        T tmp = a;
        a = b;
        b = tmp;
    }
    public static void PrintSomething<T1, T2>(T1 a, T2 b) {
        Console.WriteLine($"a: {a}({a.GetType()}), b: {b}({b.GetType()})");
    }
}
  • 제너릭 클래스
  • 변수 타입도 제너릭으로 선언할 수 있다
  • 대신 기본값 초기화도 제너릭으로 해야 한다
코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Point<T> {
    private T x = default(T);
    private T y = default(T);

    public Point(T a, T b) {
        (x, y) = (a, b);
    }
}

class Program {
    public static void Main() {
        Point<int> p1 = new Point<int>(1, 2);
        Point<double> p2 = new Point<double>(1.1, 2.1);
        Point<string> p3 = new Point<string>("1", "2");
    }
}

제네릭 제약조건

  • 제네릭 제약조건(Generic Constraint)
    • 제네릭 타입 매개변수에 대한 제약조건을 설정할 수 있다.
    • 제네릭 메서드에도 제약조건을 설정할 수 있다.
  • 제네릭 제약조건의 종류
    • where T : struct - T는 값 형식이어야 한다.
    • where T : class - T는 참조 형식이어야 한다.
    • where T : class? - T는 참조 형식이되, null 가능
    • where T : notnull - T는 null 불가능 타입이어야 한다.
    • where T : unmanaged - T는 언매니지드 타입이어야 한다.
    • where T : new() - T는 매개변수가 없는 생성자가 있어야 한다.
    • where T : - T는 특정 클래스의 파생 클래스여야 한다.
    • where T : - T는 특정 인터페이스를 구현해야 한다.
  • 사용자 타입을 제네릭 제약조건에 맞게 만들면 같이 사용할 수 있다.
코드 보기
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
using static System.Console;

class Point : IComparable<Point> {
    public int X{ set; get; } = 0;
    public int Y{ set; get; } = 0;
    public Point(int x, int y) => (X, Y) = (x, y);

    public int CompareTo(Point other) {
        if (other == null) return 1;
        int result = X.CompareTo(other.X);
        if (result == 0) {
            result = Y.CompareTo(other.Y);
        }
        return result;
    }
}

class List<T> where T : struct {  // T 는 value type 이어야 한다.
    //
}

class Program {
    public static void Main() {
        WriteLine($"{Max(10, 20)}");
        WriteLine($"{Max("AAA", "CC")}");

        List<int>    c1 = new List<int>();  // ok
        // List<string> c2 = new List<string>();  // error: string 은 value type이 아니다.

        Nullable<int> n = null;
    }
    public static T Max<T>(T a, T b) where T : IComparable<T> {
        return a.CompareTo(b) > 0 ? a : b;
    }
}

partial

  • partial class : 클래스의 정의를 여러 파일로 나누어 작성할 수 있도록 하는 기능
  • 기본적으로 여러 개의 파일에 여러 개의 클래스를 정의해도 Main 진입점이 하나만 존재한다면 프로그램이 정상적으로 실행된다.
  • 하나의 클래스 구현 사항이 너무 많아질 때, 또는 여러 명/사람+기계가 협업하여 하나의 클래스를 구현할 때, partial class를 사용하여 클래스 정의를 여러 파일로 나누어 작성할 수 있다.
    • 다만 partial class로 나누어 작성된 클래스는 컴파일 시 하나의 클래스로 합쳐지므로, 클래스의 멤버 변수나 메서드가 중복되지 않도록 주의해야 한다.
코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// partial1.cs

class Button { }
partial class Window {
    public void Show() {
        Console.WriteLine("Window is shown");
    }
}

class Program {
    static void Main() {
        Button b1= new Button();
        Slider s1 = new Slider();

        Window w = new Window();
        w.Show();
        w.Hide();
    }
}
1
2
3
4
5
6
7
8
9
10
// partial2.cs
class Slider { }

class TextBox { }

partial class Window {
    public void Hide() {
        Console.WriteLine("Window is hidden");
    }
}

namespace

  • 네임스페이스 (Namespace)
    • 목적
      • 다른 소스와의 이름 충돌 방지
      • 클래스, 인터페이스, 구조체, 열거형 등을 그룹화
    • 문법
      • 정의: namespace GroupName { ... } (중괄호 안에 타입 정의)
      • 중첩: namespace Outer { namespace Inner { ... } }
      • 참조: Outer.Inner.TypeName
    • using 지시문
      • 파일 최상단에 작성
      • 참조 시 호출에서 네임스페이스 이름 생략 가능
      • 참조된 타입 간 이름이 겹치면 네임스페이스를 명시해야 함
    • C# 표준 클래스는 기본적으로 System 네임스페이스에 속함.
코드 보기
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
using Graphic;
using Graphic3D;

namespace Graphic {
    class Point {
    public int X { get; set; } = 0;
    public int Y { get; set; } = 0;
    public Point(int a, int b) => (X, Y) = (a, b);
    public override string ToString() => $"{GetType().Name}({X}, {Y})";
    }

    namespace Engine {
        class Card {
            public static void Test() {
                Console.WriteLine("Card.Test");
            }
        }
    }
}

namespace Graphic3D {
    class Point {
    public int X { get; set; } = 0;
    public int Y { get; set; } = 0;
    public int Z { get; set; } = 0;
    public Point(int a, int b, int c) => (X, Y, Z) = (a, b, c);
    public override string ToString() => $"{GetType().Name}({X}, {Y}, {Z})";
    }
}

class Program {
    static void Main() {
        Graphic.Point p1 = new Graphic.Point(1, 2);
        Graphic3D.Point p2 = new Graphic3D.Point(1, 2, 3);

        Console.WriteLine(p1.ToString());
        Console.WriteLine(p2.ToString());

        Graphic.Engine.Card c1 = new Graphic.Engine.Card();
        Graphic.Engine.Card.Test();
    }
}

비주얼 스튜디오의 솔루션 관리 방식

  • 하나의 솔루션 하위에 여러 프로젝트가 들어갈 수 있다.
  • slnx 파일만 잘 쓰면 비주얼 스튜디오가 아니어도 솔루션은 만들 수 있다.
  • 예를 들면 다음과 같이
코드 보기
1
2
3
4
5
6
7
8
9
10
<!-- DAY5.slnx -->
<Solution>
  <Project Path="CodeOnly1/CodeOnly1.csproj" />
  <Project Path="lecture1/lecture1.csproj" />
  <Project Path="WpfSample1/WpfSample1.csproj" />
  <Project Path="WpfSample2/WpfSample2.csproj" Id="f62729d9-918f-4b17-9cb7-2ba1e36580d1" />
  <Project Path="WpfSample3/WpfSample3.csproj" Id="f85307cf-4f8e-4259-9451-4d7ff34ff746" />
  <Project Path="WpfSample4/WpfSample4.csproj" Id="be5cd5cb-cbb8-40fe-ba3e-2999bd5d4ab6" />
  <Project Path="WpfSample5/WpfSample5.csproj" Id="630f37ac-3497-4ded-9658-197b2e11851b" />
</Solution>

WPF 똑바로 만들기

  • 좀 더 똑바로 WPF를 만들어보자
  • Window: 프로그램의 주 윈도우를 만들때 사용. UI 담당
    • xaml 파일로 UI 작성해서 불러올 수 있음. (XamlReader.Load)
  • Application: 프로그램의 시작, 끝, event 루프를 담당. 프로그램의 life cycle 주기에서 사용자가 하고 싶은 구현 작성
코드 보기
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
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.IO;

class MainWindow : Window {
    public MainWindow() {
        this.Title = "Hello WPF";

        // 기본적인 code only UI 만들기
        StackPanel sp = new();
        this.Content = sp;

        Button btn1 = new Button{Content = "OK 1"};
        Button btn2 = new Button{Content = "OK 2"};

        sp.Children.Add(btn1);
        sp.Children.Add(btn2);

        btn1.Click += Btn1_Click;
        btn2.Click += Btn2_Click;

        // 파일로 UI 만들기
        FileStream fs = new FileStream("UI1.xaml", FileMode.Open, FileAccess.Read);
        Button btn3 = (Button)System.Windows.Markup.XamlReader.Load(fs);
        fs.Close();

        sp.Children.Add(btn3);
    }

    private void Btn1_Click(object sender, RoutedEventArgs e) {
        MessageBox.Show("버튼1이 클릭되었습니다.");
    }
    private void Btn2_Click(object sender, RoutedEventArgs e) {
        MessageBox.Show("버튼2가 클릭되었습니다.");
    }
}
class APP : Application {
    protected override void OnStartup(StartupEventArgs e) {
        base.OnStartup(e);
        Console.WriteLine("프로그램 시작");
    }
    protected override void OnExit(ExitEventArgs e) {
        base.OnExit(e);
        Console.WriteLine("프로그램 종료");
    }
    [STAThread]
    public static void Main()
    {
        MainWindow w = new MainWindow();
        w.Show();

        APP app = new APP();
        app.Run();
    }
}
1
<Button xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Content="UI1 Button" Background="LightBlue" FontSize="32" FontFamily="D2Coding"></Button>
  • xaml로 모든 UI를 작성할 수 있다면 굳이 Window 클래스를 새로 구현할 필요가 없지
코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.IO;

class APP : Application {
    [STAThread]
    public static void Main()
    {
        FileStream fs = new FileStream("UI2.xaml", FileMode.Open, FileAccess.Read);
        Window w = (Window)System.Windows.Markup.XamlReader.Load(fs);
        fs.Close();

        w.Show();

        APP app = new APP();
        app.Run();
    }
}
1
2
3
4
5
6
7
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    <StackPanel>
        <Button Content="OK 1"></Button>
        <Button Content="OK 2"/>
        <Button>OK 3</Button>
    </StackPanel>
</Window>
  • xaml로 UI를 작성하고, 기능은 C# 코드로 구현.
    • 다만 이 코드는 오류가 있어서(해결 못해서) 주석처리해둠
  • Window를 따로 구현한 경우 namespace를 활용해서 xaml에서 Window 클래스를 참조할 수 있도록 해야 함.
코드 보기
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;
using System.IO;

namespace CustomUI {
    class MainWindow : Window {
        // public void Foo(object sender, System.Windows.Input.MouseButtonEventArgs e) {
        //     MessageBox.Show("Hello World!");
        // }
    }
}

class APP : Application {
    [STAThread]
    public static void Main()
    {
        FileStream fs = new FileStream("UI3.xaml", FileMode.Open, FileAccess.Read);
        Window w = (Window)System.Windows.Markup.XamlReader.Load(fs);
        fs.Close();

        w.Show();

        APP app = new APP();
        app.Run();
    }
}
1
2
3
4
5
6
7
8
<local:MainWindow xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:local="clr-namespace:CustomUI"><!-- MouseLeftButtonDown="Foo" -->
    <StackPanel>
        <Button Content="OK 1"></Button>
        <Button Content="OK 2"/>
        <Button>OK 3</Button>
    </StackPanel>
</local:MainWindow>

본디 WPF는 xaml과 cs가 2쌍 필요하다

  • 비주얼 스튜디오 WPF 프로젝트 만들어서 이해하기
  • WPF는 윈도우와 APP 각각 cs와 xaml의 쌍으로 구성된다. 고로 2쌍의 cs와 xaml이 필요한 것.
코드 보기
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
// MainWindow.xaml.cs

using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfSample1 {
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window {
        public MainWindow() {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e) {
            MessageBox.Show("Button Clicked");
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- MainWindow.xaml -->
<Window x:Class="WpfSample1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfSample1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Button Content="Button" HorizontalAlignment="Center" Margin="0,197,0,0" VerticalAlignment="Top" Click="Button_Click"/>
    </Grid>
</Window>
  • xaml에는 다양한 방식으로 요소를 선언할 수 있고, 태그 안에 속성을 선언해서 요소의 속성을 설정할 수 있다. 또한, 태그 안에 다른 요소를 중첩시켜서 계층 구조를 만들 수도 있다.
  • WPF에서는 이벤트 핸들러를 사용하여 사용자 상호 작용에 응답할 수 있다. 예를 들어, 버튼 클릭 이벤트에 대한 핸들러를 작성하여 버튼이 클릭될 때 특정 작업을 수행하도록 할 수 있다.
  • WPF는 데이터 바인딩(Name)을 지원하여 UI 요소와 데이터 소스 간의 연결을 쉽게 할 수 있다. 이를 통해 UI 요소의 속성을 데이터 소스의 값에 바인딩하여 자동으로 업데이트되도록 할 수 있다.
코드 보기
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
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfSample2 {
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window {
        public MainWindow() {
            InitializeComponent();
        }

        private void Button1_Click(object sender, RoutedEventArgs e) {
            MessageBox.Show("Button 1 clicked");
            btn3.Content = "Clicked!";
        }
        private void Button4_Click(object sender, RoutedEventArgs e) {
            MessageBox.Show("Button 4 clicked");
            btn3.Content = "NO";
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<Window x:Class="WpfSample2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfSample2"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel>
        <Button Content="OK 1" Click="Button1_Click" FontFamily="D2Coding" FontSize="20"></Button>
        <Button Content="OK 2" FontFamily="D2Coding" FontSize="20"/>
        <Button Name="btn3" FontFamily="D2Coding" FontSize="20">OK 3</Button>
        <Button Content="OK 4" Click="Button4_Click" FontFamily="D2Coding" FontSize="20"></Button>
    </StackPanel>
</Window>
  • 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
25
26
27
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfSample3 {
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window {
        public MainWindow() {
            InitializeComponent();
        }

        private void fontsld_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) {
            if (fontbtn != null) {
                fontbtn.FontSize = e.NewValue;
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<Window x:Class="WpfSample3.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfSample3"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel>
        <Slider Name="fontsld" Minimum="10" Maximum="200" Value="12" ValueChanged="fontsld_ValueChanged"/>
        <Button Name="fontbtn" Content="OK" FontSize="12" Height="Auto"/>
    </StackPanel>
</Window>
  • 창 하나당 cs와 xaml의 쌍 하나씩. xaml은 창의 레이아웃과 디자인을 정의하는 파일이고, cs는 그 창의 동작과 이벤트 처리를 담당하는 코드 파일입니다.
  • 여러 개의 창을 정의하고 각각의 창을 바꿔 로드하는 예제.
코드 보기
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
72
73
// WindowNavigator.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;

namespace WpfSample4 {
    public partial class WindowNavigator : UserControl {
        private readonly List<WindowTypeItem> _windowTypes = new();

        public WindowNavigator() {
            InitializeComponent();
            Loaded += OnLoaded;
        }

        private void OnLoaded(object sender, RoutedEventArgs e) {
            var currentWindow = Window.GetWindow(this);
            var currentType = currentWindow?.GetType();

            var allTypes = typeof(App).Assembly.GetTypes()
                .Where(t => typeof(Window).IsAssignableFrom(t))
                .Where(t => !t.IsAbstract)
                .Where(t => t.Namespace == typeof(App).Namespace)
                .Where(t => t != currentType)
                .OrderBy(t => t.Name);

            _windowTypes.Clear();
            foreach (var type in allTypes) {
                _windowTypes.Add(new WindowTypeItem(type.Name, type));
            }

            var hasItems = _windowTypes.Count > 0;

            WindowComboBox.ItemsSource = _windowTypes;
            WindowComboBox.DisplayMemberPath = nameof(WindowTypeItem.DisplayName);
            WindowComboBox.SelectedIndex = hasItems ? 0 : -1;
            WindowComboBox.IsEnabled = hasItems;
            NavigateButton.IsEnabled = hasItems;

            UpdateSelectedText();
        }

        private void WindowComboBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) {
            UpdateSelectedText();
        }

        private void NavigateButton_OnClick(object sender, RoutedEventArgs e) {
            var item = GetSelectedItem();
            if (item is null) {
                return;
            }

            var nextWindow = (Window)Activator.CreateInstance(item.Type)!;
            nextWindow.Show();

            var currentWindow = Window.GetWindow(this);
            currentWindow?.Close();
        }

        private void UpdateSelectedText() {
            var item = GetSelectedItem();
            SelectedWindowText.Text = item is null ? "전환할 창이 없습니다." : item.DisplayName;
        }

        private WindowTypeItem? GetSelectedItem() {
            return WindowComboBox.SelectedItem as WindowTypeItem;
        }

        private sealed record WindowTypeItem(string DisplayName, Type Type);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- WindowNavigator.xaml -->

<UserControl x:Class="WpfSample4.WindowNavigator"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d"
             d:DesignHeight="200" d:DesignWidth="400">
    <Border Padding="16" CornerRadius="8" Background="#22000000">
        <StackPanel>
            <TextBlock Text="이동할 창 선택" FontSize="16" FontWeight="SemiBold" Foreground="White"/>
            <TextBlock x:Name="SelectedWindowText" Margin="0,8,0,8" FontSize="14" Foreground="White"/>
            <ComboBox x:Name="WindowComboBox"
                      MinWidth="240"
                      SelectionChanged="WindowComboBox_OnSelectionChanged"/>
            <Button x:Name="NavigateButton"
                    Margin="0,12,0,0"
                    Padding="12,6"
                    Content="확인"
                    Click="NavigateButton_OnClick"/>
        </StackPanel>
    </Border>
</UserControl>
1
2
<!-- 실행되는 모든 창에 다음의 요소 추가 -->
<local:WindowNavigator HorizontalAlignment="Center" VerticalAlignment="Center"/>
  • 흰 배경에 빨간색으로 마우스 그림그리기
코드 보기
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
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfSample5 {
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window {
        public MainWindow() {
            InitializeComponent();
        }
        private Point ptfrom;

        private void canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
            //    MessageBox.Show("LBUTTON");

            // 마우스 클릭시 좌표 보관
            ptfrom = e.GetPosition(this);
        }

        private void canvas_MouseMove(object sender, MouseEventArgs e) {
            // WPF 는 아주 잘만든 객체지향 라이브러리
            // => 선을 그린다는것은
            // => 선 객체를 만들어서 canvas 의 자식으로

            if (e.LeftButton == MouseButtonState.Pressed) {
                Line line = new Line();
                line.Stroke = new SolidColorBrush(Colors.Red);
                line.StrokeThickness = 5;

                Point to = e.GetPosition(this);

                line.X1 = ptfrom.X;
                line.Y1 = ptfrom.Y;
                line.X2 = to.X;
                line.Y2 = to.Y;

                // Canvas 에 자식으로 추가
                canvas.Children.Add(line);

                // 현재 점을 다시 시작점으로
                ptfrom = to;
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
<Window x:Class="WpfSample5.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfSample5"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Canvas Background="White" Name="canvas"
            MouseLeftButtonDown="canvas_MouseLeftButtonDown" MouseMove="canvas_MouseMove">
    </Canvas>
</Window>

raw 환경에서 C# 쓰는 방법

  1. 메모장+컴파일러
    • 텍스트 파일을 직접 만들고 저장
    • developer command prompt 실행
    • 코드가 있는 폴더로 이동
    • csc filename.cs로 빌드
    • filename.exe로 실행
    • 다만 외부 라이브러리 사용 제한됨
  2. dotnet tools 사용 (권장됨)
    • 폴더 만들기
    • developer command prompt 실행 (사실 이건 아무 터미널에서나 해도 됨)
    • 폴더 이동
    • dotnet new console 명령 실행 → 비주얼 스튜디오에서도 쓸 수 있는 C# 프로젝트 생성됨
      • dotnet new wpf 하면 WPF 프로젝트 만들어짐
    • vscode나 원하는 IDE로 알아서 쓰면 됨 ← vscode는 비주얼 스튜디오처럼 보기 좋은 편집기는 아님
    • dotnet build 하면 빌드만 되고 dotnet run 하면 빌드 후 실행됨
이 기사는 저작권자의 CC BY-NC-ND 4.0 라이센스를 따릅니다.

C# 프로그래밍 (4)

머신 비전 시스템 구현 (1)