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

C# 프로그래밍 (4)

table of contents

접근 지정자

코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Shape {
    internal int color = 0;

    public void SetColor(int c) {
        color = c;
    }
}

class Rect : Shape {
    public void Draw() {
        int c = color; // ?
    }
}

class Program {
    public static void Main() {
        Shape s = new Shape();
        s.color = 10;
    }
}
  • protected: 기반 클래스와 파생 클래스에서만 접근 가능
  • internal: C#에서 추가된 접근 지정자. 동일 모듈(같은 컴파일 단위)에서만 접근 가능

nullable

코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// #1. reference type 의 변수는 null 로 초기화 될수 있습니다.
string s1 = "hello";
string s2 = null;  // ok

// #2.value type 의 변수는 null 로 초기화  될수 없습니다.
int n1 = 0;
// int n2 = null;  // error: 'int'은(는) null을 허용하지 않는 값 형식이므로 null을 이 형식으로 변환할 수 없습니다.

Nullable<int> n3 = null;
Nullable<double> n4 = null;
// Nullable<string> n5 = null; // error: 제네릭 형식 또는 메서드 'Nullable<T>'에서 'string' 형식을 'T' 매개 변수로 사용하려면 해당 형식이 null을 허용하지 않는 값 형식이어야 합니다.

Nullable<int> n5 = null;
int? n6 = null;  // 단축 표기법

// Nullable 의 원리 - 55 page n1, n2 그림 참고
int n7 = 10;
Nullable<int> n8 = null;
Nullable<int> n9 = 10;
  • nullable type: null 값을 가질수 있는 타입
    • reference type은 nullable type (값 없음 표현)
    • value type은 nullable type이 아님
      • int, double, bool 등은 null 값을 가질수 없다.
      • 하지만 C#에서는 value type도 null 값을 가질수 있도록 nullable type을 제공함
        • Nullable 또는 T? 형태로 선언할 수 있음 (value type만 사용 가능)
        • 예: int? n = null; // ok
    • 단축 표기법: 자료형? 변수명 = null; (예: int? n = null;)
코드 보기
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
// 58 page

// int  : 정수 한개 보관
// int? : 정수 한개 + bool 보관(값 있음/없음)

// #1. int? = int
// 5바이트 <- 4바이트
int n = 0;
int? n1 = n; // ok. (hasValue = true, value = 10)

// #2. int = int?
// 4바이트 <- 5바이트(value + bool)
// int n2 = n1; // error

int n2 = (int)n1; // ok.
// 1. n1 != null 이면 아무 문제 없음
// 2. n1 이 null 이었다면 예외 발생

// #3. int? 에 의 값을 안전하게 int 로 옮기기
if (n1 != null) {
    int n3 = (int)n1; // 조사했으므로 항상 안전
}
if (n1 is not null) {
    int n3 = (int)n1; // 조사했으므로 항상 안전
}

int n4 = n1.GetValueOrDefault(9);
// n1 == null이면 9 반환, n1 != null이면 value 반환
int n5 = n1.GetValueOrDefault();
// n1 == null이면 0 반환, n1 != null이면 value 반환
  • nullable에서 non-nullable로 캐스팅 불가
  • nullable은 기존 자료형과 달리 그 값 자체와 값의 존재 유무를 함께 저장한다 (용량이 더 큼).
  • nullable ← non-nullable 캐스팅은 불가 (애초에 할당된 용량이 부족하기 때문).
  • non-nullable ← nullable 캐스팅은 일부 가능
    • 만약 nullable에 값이 있었다면 캐스팅 가능하지만(bool만 지우면 되니까), 값이 없었을 경우 캐스팅 불가.
  • 안전하게 캐스팅하려면 if로 확인하거나(2가지 방식) GetValueOrDefault() 메서드 사용
    • if (someNullable == null)
    • if (someNullable is null)
코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// null-coalescing operator (?? 연산자)

int?   n1 = null;
// n1 : int? 타입

int n2 = n1;  // error

int n3 = n1.GetValueOrDefault();  // ok
int n4 = n1 ?? 0;  // 위와 동일 (실전에서 주로 사용)

// string 은 reference type 이므로 ? 가 없어도 null 가능
string s1 = null;
string s2 = s1;  // s2 도 null
string s3 = s1 ?? "Unknown";  // s3 은 "Unknown"
  • null-coalescing operator (?? 연산자)
    • ?? 연산자는 왼쪽 피연산자가 null이 아니면 그 값을 반환하고, null이면 오른쪽 피연산자의 값을 반환한다.
    • nullable 타입에서 null이 아닌 값을 int로 안전하게 변환할 때 유용하게 사용된다.
    • string과 같은 참조형 타입에서도 null 대신 기본값을 제공할 때 사용된다.
코드 보기
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
// null conditional operator ( ?, ?[])

string s1 = "hello";
string s2 = null;

var ret1 = s1.ToString();  // ok. 객체가 존재.
var ret2 = s2.ToString();  // 런타임에러(예외 발생)

// 아래처럼 메소드 호출하면 안전
// 1. s2 != null이면 ret3에 메소드 반환 결과
// 2. s2 == null이면 메소드 호출 안되고, ret3도 null(아래 초기화 때문)
string ret3 = null;

if ( s2 != null ) {
    ret3 = s2.ToString();
}
// 아래 한 줄이 위 코드와 완벽히 동일
string ret4 = s2?.ToString();  // 아주 널리 사용되는 코드
// 1. s2 != null 이면 ToString() 메소드 호출
// 2. s2 == null 이면 ToString() 메소드 호출 안하고, null 반환

// 배열도 reference type
// => "?." 아니라 "?[]" 도 가능
int[] arr = null;

int n1 = arr[0];  // 에러. 현재 배열 자체가 없음(null)

int n2 = arr?[0];
// 1. arr != null 이면 arr[0] 반환
// 2. arr == null 이면 arr[0] 접근 안하고, null 반환
// 그러나 int n2 에는 null을 담을 수 없다.

int? n3 = arr?[0]; // ok

if (n3 != null) { }
  • null conditional operator (?, ?[])
  • null conditional operator는 객체가 null인지 체크하면서 메소드 호출하거나 배열 요소에 접근할 때 사용한다
  • null conditional operator는 객체가 null이면 메소드 호출 안하고, null 반환한다
  • 배열도 nullable이 될 수 있다
    • 다만 배열 요소가 value type이고 null이면, 다른 변수에 대입할 때 nullable value type에 대입해야 한다
코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
string name1 = "ABC";
string name2 = null;

var n1 = name1.Length;  // ok
var n2 = name2.Length;  // 실행시간 에러 발생

// null 인 객체를 사용하는 것은 "아주 위험합니다"
// 안전한 코드를 사용하려면 null 을 사용하지 말고, 항상 값을 가지게 하면 됩니다

string s3 = null;
// ~ C#8.0 까지는 아무 문제 없는 코드
// C#9.0 부터는 null 불가능 문자열

string? s4 = null; // null 가능   문자열

// C# 9.0 이후에 Reference 타입도 ? 를 사용하자는 개념 추가
// => 사용하지 않아도 에러는 아님.
// => 경고로 처리
  • non-nullable한 reference type
    • reference type도 null 가능, 불가능을 선택할 수 있다
    • C#9.0부터는 reference type도 ? 표기를 하지 않으면 경고가 나옴(바로 터지는 오류는 아님)
    • 이 문법을 사용하지 않고 싶다면 프로젝트 설정에서 “" 항목을 disable로 변경하기

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
/*
class Object
{
    // 2개의 static method
    public static bool Equals(object? objA, object? objB) { ... };
    public static bool ReferenceEquals(object? objA, object? objB) { ...};

    // 2개의 non-virtual method
    public Type GetType() { ... };
    protected object MemberwiseClone() { ... };

    // 3개의 virtual method
    public virtual bool Equals(object? obj) { ... };
    public virtual int GetHashCode() { ... };
    public virtual string? ToString() { ... };
}
*/

class Car {  // class Car : Object
}

class Program {
    public static void Main() {
        Car c = new Car();
        var s = c.ToString();
    }
}
  • Object 클래스
    • C#의 거의 모든 타입은 Object 클래스를 상속받는다.
    • Object 클래스에는 7개의 메소드가 있다.
      • 2개의 static method : Equals, ReferenceEquals
      • 2개의 non-virtual method : GetType, MemberwiseClone
      • 3개의 virtual method : Equals, GetHashCode, ToString
코드 보기
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
class Program {
    public static void Main() {
        int n = 10;
        double d = 3.14;

        Foo(n);
        Foo(d);
        Foo("abc");

        PrintHierachy(n);
        PrintHierachy(d);
        PrintHierachy("A");
    }
    // 메소드 인자가 object 타입이면
    // "모든 타입의 변수를 받을수 있다."
    public static void Foo(object obj) {  // obj 가 어떤 타입인지 알고 싶다
        // #1. is 연산자 : obj 가 int 타입인지 조사
        if ( obj is int ) {
        }

        // #2. GetType() 메소드 사용
        Type t = obj.GetType();

        // t 가 obj 변수의 타입정보를 가진변수
        Console.Write("{0} ->", t.Name);
        Console.Write("{0} ->", t.BaseType.Name);

        Console.WriteLine(""); // 개행
    }
    // 변수의 클래스 계층도 출력
    public static void PrintHierachy(object obj) {
        Type t = obj.GetType();

        while (true) {
            Console.Write("{0} ->", t.Name);
            if (t.Name == "Object") break;
            t = t.BaseType;
        }

        Console.WriteLine(""); // 개행
    }
}
  • Object 타입 활용
    • Object 타입은 모든 타입의 조상이다.
    • 따라서 Object 타입의 변수는 모든 타입의 변수를 참조할 수 있다.
    • 메소드가 Object 타입을 인자로 받는다면 모든 타입의 변수를 인자로 전달할 수 있다.
  • 어떤 변수가 어떤 타입인지 알아내기
    • is 연산자 : 어떤 변수가 특정 타입인지 조사하는 연산자
    • GetType() 메소드 : 어떤 변수의 타입정보를 얻어오는 메소드
      • Name : 타입의 이름
      • BaseType : 부모 타입의 정보
  • Type: 타입의 정보를 관리하는 타입
  • 클래스 계층 확인
    • 클래스 계층 : 클래스의 상속 관계
    • 가장 기반 클래스인 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System.Windows;
using System.Windows.Controls;

class MainWindow : Window {
    public MainWindow() {
        this.Title = "WPF 첫 번째 프로그램";
        this.Width = 400;
        this.Height = 300;
    }
}

class Program {
    [STAThread]
    public static void Main() {
        MainWindow w = new MainWindow();
        Application app = new Application();

        Button b1 = new Button();
        Slider s1 = new Slider();

        string cont = "";

        cont += PrintHierachy(w);
        cont += PrintHierachy(app);
        cont += PrintHierachy(b1);
        cont += PrintHierachy(s1);

        w.Content = cont;

        w.Show();
        app.Run();
    }
    public static string PrintHierachy(object obj) {
        string result = "- ";
        Type t = obj.GetType();

        while (true) {
            result += $"{t.Name}";
            if (t.Name == "Object") break;
            else result += " → ";
            t = t.BaseType;
        }
        result += "\n";
        Console.WriteLine(result);
        return result;
    }
}
  • WPF의 클래스 계층 구조를 확인해보세요

ToString

코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using static System.Console;

class Point {
    private int x = 0;
    private int y = 0;

    public Point(int a, int b) => (x, y) = (a, b);

    public override string ToString() => $"Point({x}, {y})";
}

class Program {
    public static void Main() {
        Point p = new Point(1, 2);
        WriteLine(p.ToString());
    }
}
  • 객체를 편하게 출력하고 싶다
    • ToString() 메서드 : 객체를 문자열로 표현하는 메서드
    • 모든 클래스는 Object 클래스를 상속받으며, Object 클래스에는 ToString() 메서드가 정의되어 있다: 기본적으로 자신의 타입을 문자열로 반환하도록 되어 있고, virtual임 -> 맘에 안들면 다시 만들라는 거임
    • ToString() 메서드를 오버라이드하여 객체를 편하게 출력할 수 있다
    • 보통 디버깅 목적임

동일성

코드 보기
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
using static System.Console;

class Point {
    private int x = 0;
    private int y = 0;
    public Point(int a, int b) => (x, y) = (a, b);
    public override bool Equals(object? obj) {
        if (obj is Point p) {  // 타입 확인과 캐스팅을 동시에 했음
            return x == p.x && y == p.y;
        }
        return false;
    }
}

class Program {
    public static void Main() {
        // 객체의 동일성에는 2가지 개념이 있습니다.
        // 1. 객체 자체가 동일한가 ?
        // 2. 객체는 다르지만 상태가 동일한가 ?

        Point p1 = new Point(1,2);
        Point p2 = p1;

        Point p3 = new Point(1,2);
        Point p4 = new Point(1,2);

        WriteLine($"{p1 == p2}");  // T
        WriteLine($"{p3 == p4}");  // F

        WriteLine($"{p1.Equals(p2)}");  // T
        WriteLine($"{p3.Equals(p4)}");  // F

        bool is_eq = (p3 == p4) || p3.Equals(p4);  // 참조 동일성 먼저 확인하고, 참조가 다르면 상태 동일성 확인
        WriteLine($"p3 vs p4: {is_eq}");
        is_eq = Equals(p3, p4);  // Object.Equals()는 내부적으로 참조 동일성 먼저 확인하고, 참조가 다르면 상태 동일성 확인
        WriteLine($"p3 vs p4: {is_eq}");
        is_eq = ReferenceEquals(p3, p4);  // 참조 동일성만 확인
        WriteLine($"p3 vs p4: {is_eq}");
    }
}
  • 객체의 동일성 개념
  • 객체 자체가 동일한가? (참조 동일성)
    • == 연산자
    • Object.ReferenceEquals: 원래부터 참조 동일성을 비교하기 위해 구현된 메소드.
  • 객체는 다르지만 상태가 동일한가? (값 동일성)
    • Object.Equals 메서드: 사실 내부적으로 == 연산자 사용함. 대신 virtual임.
      • Object는 파생 클래스의 상태가 어떻게 구현될지 알 수 없기 때문에 레퍼런스 기준으로 먼저 구현을 했지만, 파생 클래스가 맘대로 수정해서 상태 동일성을 확인할 수 있게 한거임
    • 두 객체가 동일 상태인지 확인하는 효율적인 방법은 일단 참조 동일성을 확인한 후에 서로 다를 경우에만 Equals를 호출하는 것. 참조가 같으면 비교할 필요가 없고, 참조 비교는 주소만 보면 되기 때문에 상당히 빠름. → 이와 유사한 메소드를 Object.Equals()가 제공함. 보통 권장됨.
코드 보기
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 Point {
    private int x = 0;
    private int y = 0;
    public Point(int a, int b) => (x, y) = (a, b);
    public static bool operator ==(Point p1, Point p2) {
        return p1.x == p2.x && p1.y == p2.y;
    }
    public static bool operator !=(Point p1, Point p2) {
        return !(p1 == p2);
    }
}

class Program {
    public static void Main() {
        Point p3 = new Point(1,2);
        Point p4 = new Point(1,2);
        WriteLine($"{p3 == p4}");
    }
}
  • 연산자 오버라이딩
  • == 연산자를 재정의할 수 있으나, 만약 했다면 !=도 해야 함.
  • 재정의한 경우 당연히 원래 연산자의 동작 결과와 달라질 수 있음
코드 보기
1
2
3
4
5
6
7
8
string s1 = "AAA";
string s2 = "AAA";
string s3 = new string("AAA");

Console.WriteLine(s1 == s2);  // true
Console.WriteLine(s1 == s3);  // true
Console.WriteLine(s1.Equals(s3));  // true
Console.WriteLine(ReferenceEquals(s1, s3));  // false
  • 동일성 연산에 대한 일반적인 사용자의 기대
  • 보통 == 연산자는 참조 동일성을 비교하지만, 문자열의 경우 내용만 같으면 같은 것으로 나온다 → string 클래스가 재정의했다는 말임
  • 이 경우 ReferenceEquals로 참조 동일성을 비교할 수 있다

인터페이스

코드 보기
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;

class Camera {
    public void Take() { WriteLine("take picture"); }
}

class HDCamera {
    public void Take() { WriteLine("take HD picture"); }
}

class Person {
    public void UseCamera(Camera c) { c.Take(); }
    public void UseCamera(HDCamera c) { c.Take(); }
}

class Program {
    public static void Main() {
        Person p = new Person();
        Camera c = new Camera();
        HDCamera hdc = new HDCamera();

        p.UseCamera(c);
        p.UseCamera(hdc);
    }
}
  • 만약 이 세상에 인터페이스가 없다면?
  • 어떤 클래스 A는 다른 클래스인 B1을 이용한다.
  • 그런데 어느 날 클래스 B1의 상위 버전인 B2가 나타났고, 클래스 A는 B2도 이용하고 싶다.
  • 인터페이스가 없다면 클래스 A는 B1을 위한 코드와 B2를 위한 코드를 각각 작성해야 한다.
  • 이것은 그 예시로 똑같은 코드를 굳이 중복해서 써야 하게 된다. 객체지향의 관점에서는 잘못됐다는 거임.
코드 보기
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
using static System.Console;

interface ICamera {
    void Take();
}

class Person {
    public void UseCamera(ICamera c) { c.Take(); }
}

class Camera : ICamera {
    public void Take() { WriteLine("take picture"); }
}

class HDCamera : ICamera {
    public void Take() { WriteLine("take HD picture"); }
}

class UHDCamera : ICamera {
    public void Take() { WriteLine("take Ultra HD picture"); }
}

class Program {
    public static void Main() {
        Person p = new Person();
        Camera c = new Camera();
        HDCamera hdc = new HDCamera();
        UHDCamera uhdc = new UHDCamera();

        p.UseCamera(c);
        p.UseCamera(hdc);
        p.UseCamera(uhdc);
    }
}
  • 인터페이스
    • 인터페이스는 클래스와 비슷하지만, 멤버로 메서드 시그니처만 가질 수 있다.
    • 인터페이스는 객체의 행동을 정의하는데 사용된다. 모든 파생 클래스가 지켜야 하는 원칙을 설계하는 것임.
    • interface IClassname { … }의 형태로 선언되며, 이때 정의되는 메소드는 접근 지정자를 표기하지 않는다.
  • 그럼 일반적인 기반 클래스나 추상 클래스와의 차이는?
    • 인터페이스는 다중 상속이 가능하다. 클래스는 단일 상속만 가능하다.
      • 다중 상속: 클래스가 여러 개의 부모 클래스로부터 상속을 받는 것
    • 인터페이스는 구현이 없는 메서드 시그니처만 포함할 수 있다. 클래스는 구현이 있는 메서드를 포함할 수 있다.
    • 인터페이스는 객체의 행동을 정의하는데 사용된다. 클래스는 객체의 상태와 행동을 모두 정의하는데 사용된다.
    • 추상 클래스는 지켜야 하는 규칙에 더해 상태도 정의되고, 일부 메소드는 직접 정의할 수도 있지만 인터페이스는 오로지 껍데기만 갖는다. 그 껍데기도 변수는 가질 수 없고 메소드 이름만 가질 수 있다.
    • 추상 클래스는 “상속한다” 표현하지만, 인터페이스는 “구현한다”고 표현한다.
  • C++은 추상 클래스와 인터페이스를 굳이 구분하지 않지만 자바와 C#은 완전히 구분한다.
  • 물론 추상 클래스를 인터페이스처럼 써도 되지만, 그렇게 되면 필요 없는 변수나 다른 멤버를 같이 상속해야만 하는 경우가 생긴다.
  • 결합의 정도
    • 강한 결합(tightly coupling): 하나의 클래스가 다른 클래스 사용시 클래스 이름을 직접 사용하는 것. 확장성 없는 경직된 디자인.
    • 약한 결합(loosely coupling): 하나의 클래스가 다른 클래스 사용시 클래스 이름을 직접 사용하지 말고 규칙을 담은 인터페이스 이름을 사용하는 것. 확장성 있고 유연한 디자인.
  • C#, Java 는 “모든 것이 인터페이스” 일 정도로 널리 사용
코드 보기
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
class Label : IComparable {
    private string title;
    public Label(string s) => title = s;
    public int CompareTo(object obj) {
        Label other = (Label)obj;
        return title.CompareTo(other.title);
    }
}

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

        string s1 = "AAA";
        string s2 = "BBB";

        // 두 변수의 크기를 비교하는 방법.

        // #1. 비교 연산자(<, >,...) 사용
        // => 수치 타입만 가능. string 타입 안됨
        bool b1 = n1 < n2; // ok
        bool b2 = s1 < s2; // error

        // #2. CompareTo 메소드 사용
        // => 수치타입및 string 모두 제공
        // => 크기 비교가 가능한 모든 타입에는 CompareTo 있음
        int ret1 = n1.CompareTo(n2);
        // n1 > n2 : 양수(1)
        // n1 < n2 : 음수(-1)
        // n1 == n2 : 0
        int ret2 = s1.CompareTo(s2);

        Label d1 = new Label("GOOD");
        Label d2 = new Label("BAD");

        // 사용자 정의 타입인 Label 도 크기 비교가 되도록 해봅시다.
        int ret = d1.CompareTo(d2);
    }
}
  • IComparable 인터페이스
    • 객체의 크기를 비교하는데 사용되는 인터페이스
    • CompareTo 메서드가 정의되어 있다.
    • CompareTo 메서드는 두 객체의 크기를 비교하여 양수, 음수, 또는 0을 반환한다.
    • 수치 타입과 string 타입은 이미 CompareTo 메서드를 제공한다.
    • 다른 C# 타입과 동일한 비교 인터페이스를 제공하기 위해 커스텀 클래스에도 구현하면 좋다.
  • 인터페이스 무한 구현 참말사건
    • 인터페이스는 클래스가 구현해야 하는 규칙을 정의하는데 사용된다. 클래스는 여러 개의 인터페이스를 구현할 수 있다.
    • 인터페이스는 다중 상속이 가능하다. 클래스는 여러 개의 인터페이스를 구현할 수 있지만, 클래스는 하나의 클래스만 상속할 수 있다.

this

코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Point {
    private int x;
    private int y;

    public Point(int x, int y) => (this.x, this.y) = (x, y);
    public void Set(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

class Program {
    public static void Main() {
        Point p1 = new Point(1, 2);
        Point p2 = new Point(1, 2);

        p1.Set(10, 20);
        p2.Set(10, 20);
    }
}
  • this 키워드
    • this는 클래스의 인스턴스 자신을 가리키는 참조 변수입니다. 클래스의 멤버에 접근할 때 사용됩니다.
    • this는 클래스의 인스턴스 메서드나 생성자에서 사용할 수 있으며, 클래스의 멤버와 지역 변수 또는 매개변수의 이름이 충돌할 때 구분하기 위해 사용됩니다.
    • 파이썬의 경우 self로 표기되며, 수동으로 표시해야 함
코드 보기
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
class Point {
    private int x;
    private int y;

    public Point(int x, int y) => (this.x, this.y) = (x, y);
    public void Set(int x, int y) {
        this.x = x;
        this.y = y;
    }
    public Point SetX(int x) {
        this.x = x;
        return this;
    }
    public Point SetY(int y) {
        this.y = y;
        return this;
    }
}

class Program {
    public static void Main() {
        Point p1 = new Point(1, 2);

        p1.Set(10, 20);
        p1.SetX(10).SetY(20).SetX(5);
    }
}
  • method chaining
    • 메소드 체이닝은 객체의 메소드를 연속적으로 호출하는 프로그래밍 패턴입니다. 각 메소드는 객체 자신을 반환하여 다음 메소드를 호출할 수 있도록 합니다.
    • 메소드 체이닝을 사용하면 코드가 더 간결하고 읽기 쉬워집니다. 예를 들어, 여러 개의 설정 메소드를 연속적으로 호출하여 객체를 구성할 때 유용합니다.

delegate

코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using static System.Console;

delegate void Poo(int arg);

class Program {
    public static void Main() {
        Poo temp = Foo;
        temp(10);
        temp.Invoke(20);
    }

    public static void Foo(int arg) {
        WriteLine($"Foo : {arg}");
    }
}
  • 메소드 자체를 변수처럼 다루기
    • 메소드의 호출정보(주소)를 보관하는 타입
    • C 포인터 개념
    • C#에서는 delegate라고 한다
  • delegate 정의 방법
    1. 전역 영역에 함수 정의 선언
    2. 반환 타입 앞에 delegate 키워드 붙이기
    3. 이때 쓴 이름이 delegate의 타입이 된다
  • 위와 같이 선언하면 내부적으로는 delegate 이름으로 MulticastDelegate를 상속한 클래스가 만들어지는 것. 구현은 자동.
  • 사용할 때는 기본 자료형 선언할 때처럼 Delegatename 변수명 = new Delegatename(메소드명);과 같이 선언하거나 단축 표기로 직접 대입해도 된다.
  • delegate 실행은 메소드 사용과 똑같이 바로 괄호로 해도 되고, .Invoke()를 해도 된다
코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using static System.Console;

delegate void Poo(int arg);

class Program {
    public static void Main() {
        Poo temp = Foo;
        temp += Foo2;
        temp(10);

        temp -= Foo;
        temp.Invoke(20);

        temp = Foo;
        temp(10);
    }

    public static void Foo(int arg) {
        WriteLine($"Foo : {arg}");
    }
    public static void Foo2(int arg) {
        WriteLine($"Foo2 : {arg}");
    }
}
  • delegate 변수에는 메소드를 여러 개 담을 수 있다. (MulticastDelegate)
  • delegate 변수에 메소드를 담는 방법은 += 연산자를 이용하는 것. = 연산자로 제거도 가능.
  • 등록한 순서대로 메소드가 실행된다.
  • 대입 연산자로 기존의 등록을 모두 무시하고 새로 등록할 수 있다.

delegate method

코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using static System.Console;

delegate void DeFunc(int arg);

class Test {
    public static void SMethod(int arg) => WriteLine("Test.SMethod");
    public void IMethod(int arg) => WriteLine("Test_Object.IMethod");
}

class Program {
    public static void Main() {
        Test t = new Test();

        t.IMethod(1);    // instance method는 객체이름으로 호출
        Test.SMethod(1); // static method는 클래스 이름으로 호출

        DeFunc f1 = Test.SMethod; // static method는 클래스 이름으로 대입
        DeFunc f2 = t.IMethod;   // instance method는 객체 이름으로 대입

        f1(10);
        f2(10);
    }
}
  • 메소드 타입에 따른 delegate 대입 차이
    • static method는 클래스 이름으로 대입
    • instance method는 객체 이름으로 대입
코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Point {
    private int x;
    private int y;

    public Point(int x, int y) => (this.x, this.y) = (x, y);
    public void Set(int x, int y) {
        this.x = x;
        this.y = y;
    }
    public static Foo(int a, int b) {
        // this.x = 10;
    }
}

class Program {
    public static void Main() {
        Point p1 = new Point(1, 2);
        Point.Foo(20, 20);
    }
}
  • this는 인스턴스 메소드에서만 사용 가능
    • static 메소드에서는 this 사용 불가 → Main은 static이기 때문에 this 사용 불가, 자기자신 클래스의 인스턴스 메소드도 꼭 객체를 따로 선언해야만 하는 것.
코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using static System.Console;

delegate void DeFunc(int arg);

class Program {
    public static void SMethod(int arg) => WriteLine("SMethod");
    public void IMethod(int arg) => WriteLine("IMethod");

    public static void Main() {
        Program.SMethod(1); // static은 클래스 이름으로 호출이 원칙인데 자기 자신 안에서는 생략 가능

        Program p1 = new Program();
        p1.IMethod(2); // main(실행부)에서 인스턴스 메소드를 쓸 때는 객체 반드시 필요 <- 여긴 static이기 때문
    }

    public void Poo() {
        // 본인 클래스의 인스턴스 메소드 안에서 인스턴스 메소드를 쓸 때는 this를 쓰거나 생략 가능
        this.IMethod(3); // this 생략 가능

        // static 메소드는 타입(클래스) 이름으로 씀
        Program.SMethod(4); // 클래스 이름으로 호출
    }
}
  • 호출 위치와 정의 타입에 따른 메소드 호출 방식
    • static 메소드는 클래스 이름으로 호출하는 것이 원칙이지만, 자기 자신 안에서는 생략 가능
    • 인스턴스 메소드는 객체가 필요하지만, 자기자신 클래스의 메소드 안에서는 this를 쓰거나 생략 가능 (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
class Program {
    private int color = 0;
    public void F1(int a) {
        color = 10;

        Program.SF(0);
        SF(0);
    }
    public void F2(int a) {
        this.F1(0);
        F1(0);

        Program.SF(0);
        SF(0);
    }
    public static void SF(int a) { }
    public static void Main() {
        Program pg = new Program();
        pg.F1(1);
        pg.F2(0); // F2(pg, 0)

        Program.SF(0);
        SF(0);
    }
}
  • static Main()에서 this가 안되는 이유
    • 컴파일 시 가장 먼저 실행되는 부분임
    • static은 객체 생성이 필요 없음
    • 아직 그 어떤 객체도 생성하지 않은 상태라는 거임
    • 그러니 this를 할 객체도 없음
    • 이는 인스턴스 메소드를 호출할 때에는 반드시 객체가 필요하게 되는 이유와 같음
    • 하지만 static 메소드를 호출할 때에는 반대 이유로 별다른 명시 없이 호출 가능
  • 인스턴스 메소드는 기본적으로 객체에 의해 호출되기 때문에, 해당 메소드가 실행될 때에는 객체가 존재함이 보장된다
    • 그러니까 this 가능
  • 메소드와 delegate
    • 인스턴스 메소드의 안에서 delegate에 인스턴스 메소드를 담을 때에는 this 생략 가능 → 인스턴스 메소드가 호출된 시점에서 이미 객체가 존재함이 보장될 것이기 때문

event

코드 보기
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 static System.Console;

delegate void Handler();

class Button {
    public Handler Click = null;
    public void UserPressButton() {
        if (Click != null) Click();  // 등록된 함수 호출
    }
}

class Program {
    public static void Foo() { WriteLine("Foo"); }
    public static void Goo() { WriteLine("Goo"); }
    public static void Main() {
        Button btn1 = new Button(); // 이순간 GUI 버튼이 만들어 지고
        Button btn2 = new Button();

        // 버튼 누를때 호출될 함수 등록
        btn1.Click = Foo;
        btn1.Click += Goo;
        btn2.Click = Goo;

        btn1.UserPressButton();  // 등록된 함수 호출
        btn2.UserPressButton();
    }
}
  • 버튼을 누르면 해야할 일을 Button 클래스의 UserPressButton() 메소드에서 직접 한다면
    • 모든 버튼(btn1, btn2) 가 동일한 일을 하게 됩니다.
  • 버튼마다 다른 일을 하도록 하기 위해서 이벤트와 델리게이트를 사용합니다.
코드 보기
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 static System.Console;

delegate void Handler();

class Button {
    public event Handler Click = null;
    public void UserPressButton() {
        if (Click != null) Click();  // 등록된 함수 호출
    }
}

class Program {
    public static void Foo() { WriteLine("Foo"); }
    public static void Goo() { WriteLine("Goo"); }
    public static void Main() {
        Button btn1 = new Button(); // 이순간 GUI 버튼이 만들어 지고
        Button btn2 = new Button();

        // 버튼 누를때 호출될 함수 등록
        // btn1.Click = Foo;
        btn1.Click += Goo;
        // btn2.Click = Goo;

        btn1.UserPressButton();  // 등록된 함수 호출
        btn2.UserPressButton();
    }
}
  • event와 delegate
    • delegate: 대입, 가산, 감산 모두 가능
    • event: 대입은 불가능, 가산과 감산만 가능
    • event는 delegate의 기능을 제한한 것. 실수로 대입하여 앞서 등록된 함수를 모두 지우는 것을 방지함.
코드 보기
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
using System.Windows;
using System.Windows.Controls;

class MainWindow : Window {
    private Button btn1;
    private Button btn2;
    private TextBox tb;
    private Slider sd;
    private Thickness margin;
    public MainWindow() {
        StackPanel sp = new StackPanel();
        this.Content = sp;  // this 없어도 됩니다.

        margin = new Thickness(10);
        btn1 = new Button() { Content = "버튼1", FontFamily = new System.Windows.Media.FontFamily("D2Coding"), FontSize = 20 };
        btn2 = new Button() { Content = "버튼2", FontFamily = new System.Windows.Media.FontFamily("D2Coding"), FontSize = 20 };
        tb = new TextBox() { Text = "텍스트박스", Width = 550, Height = 100, IsReadOnly = true, FontFamily = new System.Windows.Media.FontFamily("D2Coding"), FontSize = 20 };
        sd = new Slider() { Minimum = 10, Maximum = 40, Value = 15, Width = 550 };

        // 요소 간 패딩 설정
        sp.Margin = margin;
        btn1.Margin = margin;
        btn2.Margin = margin;
        tb.Margin = margin;
        sd.Margin = margin;

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

        btn1.Click += Btn1_Click;
        btn2.Click += Btn2_Click;
        sd.ValueChanged += Sd_ValueChanged;
    }

    private void Btn1_Click(object sender, RoutedEventArgs e) {
        string s = tb.Text;
        this.Title = s;
    }
    private void Btn2_Click(object sender, RoutedEventArgs e) {
        // 슬라이더의 값을 받아 텍스트박스의 폰트 크기로 설정
        tb.FontSize = sd.Value;
    }
    private void Sd_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) {
        tb.Text = $"슬라이더의 값이 {(int)sd.Value}로 변경되었습니다.";
    }
}

class Program {
    [STAThread]
    public static void Main() {
        MainWindow w = new MainWindow();
        Application app = new Application();

        w.Show();
        app.Run();
    }
}
  • 이벤트 활용한 WPF
이 기사는 저작권자의 CC BY-NC-ND 4.0 라이센스를 따릅니다.

C# 프로그래밍 (3)

C# 프로그래밍 (5)