기타 기본 기능
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# 에서 알아야 하는 것
- 대부분의 프로그래밍 언어가 제공하는 개념을 C# 은 어떻게 표현하는가 ?
- 데이타 타입, 변수 생성
- 함수 만드는 방법
- 배열, 튜플 만드는 방법, 사용법
- 제어문(조건문 : if, switch, 반복문 4개 : while, do-while, for, foreach)
- 객체지향 언어 문법(C++, C#, Java 등에서 공통으로 사용되는 문법)
- 객체지향 언어 : 필요한 타입을 먼저 만들어서 사용하자는 철학을 가진 언어 (C++, C#, Java 는 100% 객체지향 언어이며, Python, Rust 등은 완벽한 객체지향 언어는 아님)
- 타입을 만드는 방법(class 문법)
- 만들어진 타입을 잘 사용하는 방법(ex : WPF 라이브러리의 Window 타입 사용법등..)
- C#만 가진 특징
- Delegate, Property 등의 핵심 문법
- 라이브러리 활용해서 프로그램 작성
- 기본 라이브러리
- WPF 라이브러리 등
- 대부분의 프로그래밍 언어가 제공하는 개념을 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
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가지로 나뉜다.
- 코드 메모리: 프로그램이 실행되기 위해 필요한 명령어들이 저장되는 메모리
- 데이터 메모리: 프로그램이 실행되면서 필요한 데이터들이 저장되는 메모리 (예: 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 등
- mutable : 객체의 상태를 변경할수 있는 것. 예: int, List
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에 버튼도 들어가고 텍스트도 들어가고 다 되는 거다.