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
- Nullable
- 단축 표기법:
자료형? 변수명 = 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 정의 방법
- 전역 영역에 함수 정의 선언
- 반환 타입 앞에 delegate 키워드 붙이기
- 이때 쓴 이름이 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