이벤트(event)는 어떤 사건이 발생했을 때 통보를 받고 해당 사건에 대응하여 처리를 수행하는 프로그래밍 패턴이다. 예를 들어, 버튼 클릭, 키 입력, 타이머 만료 등과 같은 상황을 이벤트라고 할 수 있다. 이벤트는 특정 사건이 발생했을 때 델리게이트를 통해 해당 사건을 처리하는 메서드를 호출한다.
< 이벤트 생성하기 >
이벤트를 사용하기 위해서는 먼저 이벤트를 정의해야 한다.
이벤트를 정의할 때는 event 키워드를 사용한다.
publicdelegatevoidMyEventHandler(string message); // 델리게이트 정의publicevent MyEventHandler MyEvent; // 이벤트 정의
< 이벤트 구독 및 처리하기 >
이벤트가 발생했을 때 실행할 메서드를 작성하고, 이 메서드를 이벤트에 연결한다. 이를 이벤트 구독(subscription)이라고 한다.
아래의 Form1과 Form2 코드가 있다.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespaceWindowsFormsApp2
{
publicpartialclassForm1 : Form
{
publicForm1()
{
InitializeComponent();
}
privatevoidbutton1_Click(object sender, EventArgs e)
{
Form2 form2 = new Form2();
form2.Show();
form2.OnCount += MyCount;
}
int c = 0;
//자식클래스가 보낸데이터 수신(구독)publicvoidMyCount(int myCount)
{
c += myCount;
label1.Text = c.ToString();
}
}
}
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespaceWindowsFormsApp2
{
publicpartialclassForm2 : Form
{
publicdelegatevoidCountHandler(int addCount);
publicevent CountHandler OnCount;
publicForm2()
{
InitializeComponent();
}
privatevoidbutton1_Click(object sender, EventArgs e)
{
//이 예제에서 매개변수로 int를 전달하지만.//개발자가 만든Class의 객체,string도 가능
OnCount(1);
}
}
}
[Form2 (이벤트 생성자)]
먼저 Form2에서는 델리게이트를 정의한다. 이 델리게이트는 int형 매개변수를 받는 메서드를 참조한다.
델리게이트를 사용하여 이벤트를 정의한다. 이 이벤트는 Form2의 버튼이 클릭될 때 발생시킬 것이다.
publicdelegatevoidCountHandler(int addCount);
button1_Click 메서드에서는 이벤트를 발생시킨다. 이 예제에서는 1이라는 값을 매개변수로 전달한다.
publicevent CountHandler OnCount;
[Form1 (이벤트 구독자)] Form1에서는 Form2의 인스턴스를 생성하고, Form2의 OnCount 이벤트에 대한 구독을 설정한다. 이때 Form1 내의 MyCount 메서드가 구독자로 동작한다.
MyCount 메서드는 Form2로부터 받은 값을 c에 더하고, 이를 레이블에 표시한다. 이 메서드는 Form2의 OnCount 이벤트에 의해 호출된다.
int c = 0;
publicvoidMyCount(int myCount)
{
c += myCount;
label1.Text = c.ToString();
}
이렇게 Form2에서 발생한 이벤트가 Form1에서 처리된다. Form2의 버튼을 클릭할 때마다 Form1의 레이블에 표시되는 값이 증가한다. 이벤트를 사용함으로써 두 폼 사이에 느슨한 결합(loose coupling)을 유지할 수 있고, 코드의 유지 보수가 용이해진다.
멀티스레딩을 사용하면 한 프로세스 내에서 여러 개의 작업을 동시에 처리할 수 있다. 하지만 이런 스레드들을 잘 관리하지 않으면 리소스가 해제되지 않는 문제가 발생할 수 있다.
이는 시스템 자원을 불필요하게 점유하게 되어 프로그램의 성능을 저하시키거나 메모리 누수 같은 문제를 발생시킬 수 있다.
< 스레드의 자원을 제대로 해제하는 두 가지 방법 >
[Background 속성 이용] 스레드는 기본적으로 Foreground 스레드로 생성된다. 때문에 메인(UI) 스레드가 종료되어도 해당 스레드는 계속 실행된다. 하지만 Background 스레드로 설정하면 메인 스레드가 종료될 때 해당 스레드도 함께 종료되어 자원을 해제하게 된다.
Thread.IsBackground = true;를 통해 해당 스레드를 Background 스레드로 설정할 수 있다.이렇게 하면 해당 스레드가 프로그램이 종료되는 시점에서 함께 종료되도록 한다.
[Form Closing] Form이 종료되는 시점에 작동하는 이벤트인 Form Closing을 이용해 스레드의 자원을 해제할 수도 있다. 만약 Form이 닫히는 시점에 생성된 스레드가 아직 실행 중이라면 해당 스레드를 종료하고 자원을 해제하는 코드를 작성한다. 이를 통해 스레드가 더 이상 필요하지 않은 시점에 정확하게 스레드를 종료하고 자원을 회수할 수 있다.
< 실습 >
아래와 같이 코드를 작성하고 솔루션 빌드 후, 실행을 하고 Form1 창을 닫았다. 그러면 정말 종료가 된 것일까?
상점에서 손님1이 계산을 하고 있으면, 손님2는 손님1이 계산을 끝내기를 기다려야 한다. 이처럼 한 작업이 끝나야 다음 작업이 시작되는 것을 동기 방식이라고 한다.
< 비동기(Asynchronous) >
[특징] a. 여러 작업을 동시에 처리할 수 있다. b. 작업들 간에 서로 기다리지 않고 병렬로 진행된다.
[예시] 요리사가 여러 음식을 동시에 준비하는 것과 비슷하다. 비빔면을 조리하는 동안, 짜파게티도 함께 조리할 수 있다.
< 장점과 단점 >
동기 : 장점: 코드가 간단하며, 이해하기가 쉽다. 단점: 리소스를 효율적으로 사용하지 못하고, 대기 시간이 길어질 수 있다. (위에서 설명한 것과 같이, 비슷한 예시로 편의점을 생각해봐도 좋다. 계산하시는 분은 원래 한 분이시니, 계산대에 사람이 줄을 서서 기다리고 있는 모습을 상상해보자.) 비동기 : 장점: 리소스를 효율적으로 활용하고, 대기 시간을 줄일 수 있다. 단점: 코드가 복잡해질 수 있고, 디버깅이 어려울 수 있다.
< C# 대표적인 비동기,동기 함수 >
C#에서 대표적인 비동기 함수는 Async await가 있고 동기 함수는 Sync가 있다.
비동기 개념중 중요한 개념엔 Task가 있다.
[Async await] :
여러 작업을 동시에 처리할 수 있는 방식.
using System;
using System.Threading.Tasks;
classProgram
{
staticasync Task Main()
{
// 커피 만들기 시작var coffeeTask = MakeCoffeeAsync();
// 사용자 입력 받기var userInputTask = GetUserInputAsync();
// 커피 만들기 작업과 사용자 입력 받기 작업이 모두 완료될 때까지 기다리기.await Task.WhenAll(coffeeTask, userInputTask);
// 커피가 만들어지고 사용자 입력이 완료된 후에 실행되는 코드.
Console.WriteLine("커피가 준비되었고 사용자 입력이 완료되었습니다.");
}
staticasync Task MakeCoffeeAsync()
{
Console.WriteLine("Coffee making process started.");
// 물을 끓이는 작업
Console.WriteLine("Boiling water...");
await Task.Delay(2000); // 2초 동안 대기 (물 끓이는 시간)// 커피를 내리는 작업
Console.WriteLine("Brewing coffee...");
await Task.Delay(2000); // 2초 동안 대기 (커피 내리는 시간)
}
staticasync Task GetUserInputAsync()
{
Console.WriteLine("성함 :"); // 사용자에게 이름을 입력받는 안내 메시지를 출력.string name = await Task.Run(() => Console.ReadLine()); // 사용자 입력을 비동기적으로 받아오기.
Console.WriteLine($"Hello, {name}!"); // 사용자의 이름을 출력.
}
}
[Sync] :
한 번에 하나의 작업만 처리하는 방식.
usingSystem;
usingSystem.Threading;
classProgram
{
staticvoidMain()
{
Console.WriteLine("Coffee making process started.");
MakeCoffee();
Console.WriteLine("Coffee is ready.");
}
staticvoidMakeCoffee()
{
Console.WriteLine("Boiling water...");
Thread.Sleep(2000);
Console.WriteLine("Brewing coffee...");
Thread.Sleep(2000);
}
}
[Task] :
async await 구문을 사용할 때 Task를 함께 사용한다.
using System;
using System.Threading.Tasks;
classProgram
{
staticasync Task Main(string[] args)
{
// 케이크 굽기 시작 (비동기적으로)
Task bakeCakeTask = BakeCakeAsync();
// 파스타 만들기 시작 (비동기적으로)
Task makePastaTask = MakePastaAsync();
// 샐러드 만들기 시작 (비동기적으로)
Task makeSaladTask = MakeSaladAsync();
// 모든 요리가 완료될 때까지 기다림await Task.WhenAll(bakeCakeTask, makePastaTask, makeSaladTask);
Console.WriteLine("모든 요리가 준비되었습니다!");
}
staticasync Task BakeCakeAsync()
{
Console.WriteLine("케이크 굽는 중...");
await Task.Delay(5000); // 케이크 굽는데 5초 걸린다고 가정
Console.WriteLine("케이크가 완성되었습니다!");
}
staticasync Task MakePastaAsync()
{
Console.WriteLine("파스타 만드는 중...");
await Task.Delay(3000); // 파스타 만드는데 3초 걸린다고 가정
Console.WriteLine("파스타가 완성되었습니다!");
}
staticasync Task MakeSaladAsync()
{
Console.WriteLine("샐러드 만드는 중...");
await Task.Delay(2000); // 샐러드 만드는데 2초 걸린다고 가정
Console.WriteLine("샐러드가 완성되었습니다!");
}
}
요리사가 비빔면을 조리하면서 동시에 짜파게티를 조리하는 코드
using System;
using System.Threading.Tasks;
classProgram{
staticasync Task Main(string[] args)
{
Task cookJjapagettiTask = CookJjapagettiAsync();
Task cookBibimmyeonTask = CookBibimmyeonAsync();
await Task.WhenAll(cookJjapagettiTask, cookBibimmyeonTask);
Console.WriteLine("모든 요리가 완료되었습니다.");
}
staticasync Task CookJjapagettiAsync()
{
Console.WriteLine("짜파게티 요리를 시작합니다.");
await BoilWaterAsync("짜파게티");
Console.WriteLine("짜파게티 면을 삶습니다.");
await Task.Delay(2000); // 면을 삶는데 시간이 걸림
Console.WriteLine("짜파게티를 양념합니다.");
await Task.Delay(1000); // 양념하는데 시간이 걸림
Console.WriteLine("짜파게티 요리가 완료되었습니다.");
}
staticasync Task CookBibimmyeonAsync()
{
Console.WriteLine("비빔면 요리를 시작합니다.");
await BoilWaterAsync("비빔면");
Console.WriteLine("비빔면 면을 삶습니다.");
await Task.Delay(2000); // 면을 삶는데 시간이 걸림
Console.WriteLine("비빔면을 양념합니다.");
await Task.Delay(1000); // 양념하는데 시간이 걸림
Console.WriteLine("비빔면 요리가 완료되었습니다.");
}
staticasync Task BoilWaterAsync(string dishName)
{
Console.WriteLine($"{dishName}을(를) 위해 물을 끓이는 중...");
await Task.Delay(1000); // 물 끓이는데 시간이 걸림
}
}
그럴 때, 스레드 동기화를 해주면 둘 이상의 스레드가 서로의 작업을 덮어쓰지 않고 공유 자원에 안전하게 엑세스 할 수 있는 방법이다.
출처 : C#.NET 0.5년차~3년차(파트1)
처음 a값에 변수 5를 가지고 있는데 Thread2에서 a 값을 4로 변경하고 getValue(a)를 통해서 4값을 동기화를 해주면, 4값을 얻을 수 있는데, 만약 동기화가 안되어 있다면 getValue(a)에서 4를 기대했으나, Thread1에 의해서 3으로 변경한 것이 영향을 받게 되어, 3의 값을 얻게 되버린다.
Thread2가 a=4;로 선언했으면 Thread2에서 getValue(a)를 했을 시, 4를 얻을 수 있는 영역을 임계영역(Critical section)이라고 한다. 임계 영역은 공유 자원에 Thread Safety하게 접근하는 영역이다.
a=5;를 하고
Thread2 에서 a=4;로 변경을 요청한다.
그런데 Thread2을 임계영역으로 두게 된다면 Thread1의 a=3;요청은 뒤로 미뤄지게 되고, Thread2가 독점적으로 임계영역으로 사용하게 되고, Thread2에서 getValue(a)를 하게 되면 4가 출력된다. 그리고 임계 영역이 끝나면, 공유자원에 대한 독점을 해제 함으로써, Thread1이 a를 3으로 변경하게 되는 작업을 하게 된다.
< 스레드 동기화 문법 >
[lock 키워드]
C#에서는 lock 키워드를 통해 공유 자원에 대해서 서로 작업을 스레드끼리 충돌이 안나게끔 해줄 수가 있게 된다.
먼저 동기화에 사용할 객체를 정의해줘야 한다.ex) object lockObj = new object();
그 다음에 lock 키워드를 적고 나서 생성한 객체를 소괄호 안에 넣어주면 만든 object에 대해서 상호 베재 잠금을 획득하게 된다.
< 실습 >
아래와 같이 코드를 작성하게 되면, Thread2에서는 Thread1의 영향을 받아, Message Box에서는 3이 출력되는 상황이 벌어진다.
이런 상황에서 서로간에 영향을 안받게 할려면 lock() 키워드를 사용하게 된다는 것이다.
lock 키워드 안에는 잠금에 사용할 객체를 만들어 줘야 한다.
잠금에 사용할 객체 생성상호배제 잠금하기
여기서 While문을 통해서 이런 작업이 반복적으로 일어나도록 할 수 있다.
위와 같이 코드를 작성하면, 의도한 대로 Thread1에 영향을 받지 않고 4가 출력이 된다는 것을 확인할 수 있다.
Form1 실행 후 4초 뒤 4가 출력되고 확인을 누르면 또 4초뒤에 반복적으로 해당 메세지가 출력된다.
< 코드의 실행 순서 >
Message.show에서 3초 후에 나타나는 줄 알았는데 Thread1의 Thread.Sleep의 시간도 같이 합해지는 이유를 몰랐었다.
이에 대해 친구에게 매우 상세하게 답변을 얻었다.
우선 상상을 해야한다.
두 명의 사람이 한 대의 컴퓨터를 쓰려고 하는 상황이다.
컴퓨터는 한 번에 한 사람만 사용할 수 있고, 각자 쓰고자 하는 프로그램이 있다고 가정해보자.
사람 1은 컴퓨터를 쓰려고 자리에 앉는다 그리고선 컴퓨터를 사용하기 시작하는데, 갑자기 2분 동안 쉬기로 결정한다.
하지만 사람1은 컴퓨터를 점유한 상태다. 사람 2는 컴퓨터를 사용하고 싶지만, 사람 1이 여전히 컴퓨터 앞에 앉아 있으므로 기다려야 하는 상황이다. 사람 1이 쉬는 것을 끝내고 컴퓨터를 떠나면, 사람 2는 컴퓨터를 사용하기 시작한다. 그러나 이제 사람 2가 컴퓨터를 사용하면서 3분 동안 쉬기로 결정한다.
이 예제에서, 사람 1은 WorkThread 메서드를, 사람 2는 Work2 메서드를, 컴퓨터는 공유 자원인 변수 a를 상징한다.
사람들이 쉬는 시간은 Thread.Sleep 메서드에 해당한다.
MessageBox.Show가 나타나는 시점은 사람 2가 컴퓨터를 사용하기 시작한 이후다.
그러나 사람 2는 사람 1이 쉬는 시간 동안 기다려야 했기 때문에, 실제로는 사람 1의 쉬는 시간 + 사람 2의 쉬는 시간 만큼 기다린 후에 MessageBox.Show가 나타난다.
즉, Work2 스레드는 WorkThread 스레드가 lock을 해제할 때까지 기다려야 하고, WorkThread는 2초 동안 쉰다.
그 후 Work2가 실행되고, 추가로 3초를 기다린 후에 MessageBox가 표시된다. 따라서 총 대기 시간은 2초 + 3초인 5초가 되는 것이다.
이런 lock 키워드는 파일 읽고쓰기,네트워크 전송,수신,DB작업 등과 같은 곳에 사용할 수 있다.
※< lock keyword 사용할 때 주의점 >
lock(obj)
{
…
lock(obj)
{
…
}
…
}//상호배제잠금을해제
위와 같이 lock안에 동일한 객체에 잠금을 요청하게 된다면, lock을 해제 하기 전에 lock을 또 시도하기 때문에
교착상태(DeadLock) 발생할 수 있다.
(위와 같은 코드를 사용한 프로그램 멈출 가능성이 높다. try catch에
잡히지 않기에 원인도 파악하기 힘들다.)
< AutoResetEvent >
스레드를 동기화 하는 방법 중 두 번째는 AutoResetEvent가 있다.
AutoResetEvent를 사용하게 된다면 Thread간의 실행 순서를 제어할 수 있게 된다.
예를 들어 Thread1이 작업이 끝나야지만 Thread2가 동작이 되게 하게 끔을 할 수 있는 것이다.
Thread간의 순서를 둬야할 경우, 위의 그림과 같이 DB에 데이터를 먼저 쓰고 나서
데이터를 읽는 작업을 Thread2에서 한다거나 할 때 AutoResetEvent를 사용하면 효과적이다.
또 DB데이터 쓰는 작업이 계속해서 반복 될 때 오래걸리게 되면, Thread2가 오래걸리는 작업을 영향을 안받고 데이터를 읽을 수 있다. 이렇듯 서로의 작업에 영향을 받지 않고 작업을 하는데 단지 먼저 데이터가 쓰여야지만, 읽을 수 있게끔 방법을 만들 수 있다.
이 코드를 실행하면 autoResetEvent.WaitOne();이 신호를 받을 때 까지 기다리고 있다가, Thread.Sleep(5000);에 의해 5초를 기다리고 autoResetEvent.set();을 통해 신호가 되었다고 신호를 보내주면, 이제서야 WaitOne();이 신호를 받고,
MessageBox.Show(""+a);를 통해 a메세지가 출력되는 것이다. 즉, Set()을 보내기 전까지 Thread2의 Set()의 윗코드가 실행된다.
또한
이 AutoResetEvent를 true로 바꾸게 되면, 첫번째 신호는 받은 상태로 변경된다. 즉 실행하자마자 변수가 나타나고, 이후 Sleep()등을 통해, 작동 된다.
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespaceWinFormsApp1
{
publicpartialclassForm1 : Form
{
int a = 5; //공유 자원object lockObj = newobject();
AutoResetEvent autoResetEvent = new AutoResetEvent(true);
Thread thread = null;
Thread thread2 = null;
publicForm1()
{
InitializeComponent();
}
privatevoidForm1_Load(object sender, EventArgs e)
{
thread = new Thread(new ThreadStart(WorkThread));
thread.IsBackground = true;
thread.Priority = ThreadPriority.Normal;
thread.Start();
thread2 = new Thread(new ThreadStart(Work2));
thread2.IsBackground=true;
thread2.Priority = ThreadPriority.Normal;
thread2.Start();
}
privatevoidWorkThread()
{
while (true) //반복문 시작
{
autoResetEvent.WaitOne(); //신호를 받을 때 까지 대기하게 된다.
MessageBox.Show("" + a);
}
}
privatevoidWork2()
{
while (true)
{
a++;
Thread.Sleep(5000);
autoResetEvent.Set(); //신호를 보내서, WaitOne()을 실행할 수 있게 된다.
}
}
}
}
위와 같이 코드를 작성하게 되면, 코드를 빌드하자마자 바로 5가 출력된다. 이후, a값이 1씩 증가할 수 있는 것을 확인할 수 있다.
Thread thread = null; : Thread 객체를 선언한다. 여기서 Thread는 새로운 작업 흐름을 나타낸다
thread = new Thread(new ThreadStart(GetItemThread)); 새로운 Thread 객체를 생성하고, 이 Thread에서 실행할 메서드(GetItemThread)를 지정한다.
thread.IsBackground = true; : 이 Thread를 백그라운드로 설정한다. 백그라운드 스레드는 프로그램이 종료될 때 자동으로 종료된다.
thread.Priority = ThreadPriority.Normal : Thread가 OS 자원을 얼마나 자주 할당받을 것인지 (보통 normal, above를 자주 쓴다.)
thread.Start(); thread를 시작하게 된다. 시작하게 되면 ThreadStart(GetItemThread));에서 GetItemThread 함수가 가장 먼저 실행된다.
< 실습해 보기 >
위의 코드를 실행하면 Form1의 "AAAA테스트" MessageBox가 5초 후에 나오는 것을 확인할 수 있다.
만약 Thread.Sleep()을 Form_Load함수에 넣으면 어떻게 될까?
위 코드를 실행하게 되면 BBB테스트가 먼저 출력이 되고, 확인을 누르면(안 누르면 그다음으로 안 넘어간다.) AAAA테스트 메시지가 찍히고 나서 Form1창이 뜨는 것을 직접 확인할 수 있다.
실행하면 2초후에 BBB테스트BBB테스트 누른 후 4초후 AAAA테스트이후 Form1창이 출력된다.
< Thread 1개 더 생성해 보기 >
이렇게 코드를 작성하게 된다면
Form1이 Load 되고 나서 Thread안에 있는 동작이 이루어지게 된다. BBB테스트가 2초 후에 출력되고, 확인을 누를 필요 없이, Form1 Load 됨과 동시에 5초 후에 AAA테스트 메시지가 나타난다. 즉, 각각의 스레드는 별개로(병렬적) 동작한다는 것을 알 수 있다.
이러한 Thread는 병렬작업과 오래 걸리는 작업에 유용하게 사용된다.(예 네트워크전송, 수신, DB작업)
또한 로딩창, 로딩바를 호출해서 사용자 편의성을 증가시킬 수도 있다.
< try catch로 감싸기 >
Thread의 함수들을 try catch로 감싸서 오류내용을 메모장 같은 곳에 기록을 할 수 있다.
또한 반복문을 통해서, 어떠한 작업이 반복적으로 일어나게 할 수도 있다. 그런데 While문에서 오류를 만나면 While문에서 빠져나오게 된다. 그때 계속 동작을 시키고 싶다면, try catch()로 감싸주면 While문을 빠져나가지 않고 계속 동작할 수 있도록 할 수 있다.
< 주의사항 >
Form2가 있다고 가정했을 때,
Form1_Load 함수에
Form2 form2 = new Form2();
form2.Show(); 이렇게 작성하면 form2가 개별 Thread로 동작할 것 같지만, 동작하지 않는다.
프로퍼티는 공개 데이터 멤버처럼 사용할 수 있지만, 실제로는 접근자(Accessor)라는 특별한 메소드들 이다.
< 프로퍼티의 구조 >
프로퍼티는 get , set Accessor로 구성된다. get은 프로퍼티의 값을 반환하는데 사용된다. set은 새로운 값을 할당하는데 사용된다. (C# 9 이후로는 init 액세서를 사용하여 객체 생성 시에만 값을 할당할 수 있다.) set 또는 init에서는 value 키워드를 사용하여 할당되는 값을 정의한다.
< 백킹 필드와 프로퍼티 >
프로퍼티를 구현하는 기본 패턴 중 하나는 private 백킹 필드를 사용하여 프로퍼티 값을 설정하고 검색하는 것이다.
프로퍼티(Property)는 객체의 상태를 나타내는데 사용되며, 일반적으로 메서드를 통해 값을 가져오거나 설정하는 역할을 한다. 허나 백킹 필드는 이 프로퍼티의 값을 실제로 저장하는 변수라고 할 수 있다.
프로퍼티의 get 또는 set 접근자를 사용하여 값을 가져오거나 설정할 때, 이 접근자들은 내부적으로 백킹 필드를 사용하여 작업을 수행한다. 백킹 필드는 프로퍼티의 값을 저장하고, 프로퍼티의 get 접근자는 이 값을 읽어오며, set 접근자는 이 값을 변경한다.
이러한 구조는 프로퍼티가 단순히 값을 반환하거나 설정하는 것 이상의 로직을 수행할 수 있게 해준다. 예를 들어, 값을 설정하기 전에 유효성 검사를 수행하거나, 프로퍼티 값이 변경될 때 이벤트를 발생시키는 등의 작업이 가능하다.
public class Person { private string name; // 백킹 필드
public string Name // 프로퍼티 { get { return name; } set { if (!string.IsNullOrEmpty(value)) { name = value; } } } }
위 예제에서 name 변수는 백킹 필드이며, Name은 프로퍼티라고 할 수 있다. Name 프로퍼티를 통해 값을 설정하면, 유효성 검사를 거친 후 name 백킹 필드에 값을 저장한다. 또한, 값을 가져올 때는 name 백킹 필드에서 값을 읽어오는 것.
아래 예시에서 TimePeriod 클래스는 시간 간격을 나타낸다. 내부적으로 이 클래스는 _seconds라는 private 필드를 사용하여 시간 간격을 초 단위로 저장한다. Hours라는 read-write 프로퍼티를 통해 사용자가 시간 간격을 시간 단위로 지정할 수 있다.
publicclassTimePeriod
{
privatedouble _seconds;
publicdouble Hours
{
get { return _seconds / 3600; }
set
{
if (value < 0 || value > 24)
thrownew ArgumentOutOfRangeException(nameof(value),
"The valid range is between 0 and 24.");
_seconds = value * 3600;
}
}
}
C# 11부터는 required 키워드를 사용하여 클라이언트 코드가 프로퍼티 또는 필드를 초기화하도록 강제할 수 있다.
publicclassSaleItem
{
public required string Name { get; set; }
public required decimal Price { get; set; }
}
< 메서드, 필드, 프로퍼티의 차이점 >
메서드(Method) : 코드 블록을 캡슐화하여 동작을 수행한다.
필드(Field) : 클래스의 상태를 저장하는 변수.
프로퍼티(Property) : 필드의 값을 조작하거나 반환하는 메서드 쌍(getter, setter)으로, 이를 통해 필드에 직접 접근하는 대신 필요한 로직을 캡슐화할 수 있다.
< 무명 형식 (Anonymous Types) >
무명 형식(Anonymous Types)은 명시적으로 타입을 정의하지 않고도 읽기 전용 프로퍼티들을 하나의 객체로 캡슐화하는 간편한 방법을 제공한다. 이 타입의 이름은 컴파일러에 의해 생성되며 소스 코드 수준에서는 사용할 수 없다. 각 프로퍼티의 타입은 컴파일러에 의해 추론된다.
무명 형식은 new 연산자와 객체 초기화자(object initializer)를 함께 사용하여 생성된다. 다음 예제는 Amount와 Message라는 두 개의 프로퍼티로 초기화된 무명 형식을 보여준다.
var v = new { Amount = 108, Message = "Hello" };
Console.WriteLine(v.Amount + v.Message);
무명 형식은 주로 쿼리 표현식의 select 절에서 사용된다. 이를 통해 소스 시퀀스의 각 객체에서 프로퍼티의 부분 집합을 반환할 수 있다. 무명 형식은 하나 이상의 public 읽기 전용 프로퍼티를 포함한다. 메소드나 이벤트와 같은 다른 종류의 클래스 멤버는 허용되지 않는다. 프로퍼티를 초기화하는 데 사용되는 표현식은 null, 익명 함수, 또는 포인터 타입이 될 수 없다.
var productQuery =
from prod in products
selectnew { prod.Color, prod.Price };
foreach (var v in productQuery)
{
Console.WriteLine("Color={0}, Price={1}", v.Color, v.Price);
}
무명 형식은 var를 사용하여 암시적으로 타입이 지정된 로컬 변수로 초기화하는 경우가 일반적이다.
무명 형식의 이름은 컴파일러만 알 수 있으므로 변수 선언에서 타입 이름을 지정할 수 없다.
var apple = new { Item = "apples", Price = 1.35 };
var onSale = apple with { Price = 0.79 };
무명 형식을 필드, 프로퍼티, 이벤트 또는 메서드의 반환 타입으로 선언할 수 없다. 무명 형식을 메서드에 인수로 전달하거나 저장해야 하는 경우, 일반적으로 명명된 struct 또는 클래스를 사용하는 것이 좋다.
무명 형식은 Equals 및 GetHashCode 메서드를 프로퍼티의 메서드를 기반으로 정의하기 때문에, 모든 프로퍼티가 같은 경우에만 같은 무명 형식의 두 인스턴스가 같다.
< 인터페이스와 추상 클래스에서의 프로퍼티 >
[ 인터페이스에서의 프로퍼티 ]
인터페이스에서 프로퍼티를 선언할 때는 구현을 제공하지 않고, get과 set 접근자만 지정한다.
interfaceIPerson {
string Name { get; set; }
int Age { get; set; }
}
[ 추상 클래스에서의 프로퍼티 ]
추상 클래스에서는 프로퍼티에 기본 구현을 제공할 수도 있고, 추상 프로퍼티로 선언하여 하위 클래스에서 구현하도록 할 수도 있다.
abstractclassAbstractPerson {
publicabstractstring Name { get; set; }
publicint Age { get; set; }
}
Reference:
Microsoft Docs - Properties (C# Programming Guide)
C#에서 메소드 오버라이딩은 상속받은 메소드를 자식 클래스에서 재정의하는 것을 의미한다. 이를 위해서는 기본 클래스의 메소드에 virtual 키워드를 사용하고, 파생 클래스에서는 override 키워드를 사용해야 한다.
publicclassAnimal
{
publicvirtualvoidsound()
{
Console.WriteLine("This is the sound of an animal");
}
}
publicclassDog : Animal
{
publicoverridevoidsound()
{
Console.WriteLine("This is the sound of a dog");
}
}
classProgram
{
staticvoidMain()
{
Animal myAnimal = new Animal();
Animal myDog = new Dog();
myAnimal.sound();
myDog.sound();
}
}
< Partial 클래스>
파샬 partial class : 두개 이상의 파일이 클래스를 나뉘어서 개밝하는 방식 컴파일시 자동으로 결합이 되고, 코드가 길어질 경우 관리를 수월하게 할 수가 있고, 하나의 클래스를 여러명에서 동시에 작성할 수 있다. partial class 클래스명 { //코드 }
C#에서 추상 클래스(abstract class)는 인스턴스를 만들 수 없고, 하나 이상의 추상 메서드(정의되지 않은 메서드, 즉 프로토타입만 있는 메서드)를 가질 수 있는 클래스를 말한다. 이 클래스는 반드시 상속을 통해 사용되며, 상속받은 클래스에서 추상 메서드를 구현(override)해야 한다.
abstract 추상클래스:
상속을 해주기 위한 클래스
abstractclass 추상클래스
{
abstractpublicvoidmessage(); //상속받은 클래스에서 기능 구현
}
class 자식클래스 : 추상클래스
{
publicoverridevoidmessage()
{
Console.WriteLine(“코드구현”);
}
}
여기서 "Hello,World!" 다음 새로운 줄에서 "Hello,C#"가 출력되는 것을 볼 수 있다.
< 콘솔에 입력받기 >
C#에서 사용자로부터 입력을 받기 위해서는 'Console.ReadLine()' 메소드를 사용한다.
Console.WriteLine("Enter your name:");
string name = Console.ReadLine();
Console.WriteLine($"Hello, {name}!");
위 코드를 실행하면 콘솔이 사용자로부터 이름을 입력받고, 입력받은 이름을 사용하여 메세지를 출력한다.
< 데이터 타입 알아보기 >
int number = 10;
double pi = 3.141592;
char character = 'A';
bool isTrue = true;
string name = "C#";
< 데이터타입 사용방법과 변수 상수 선언 >
C#에서의 변수와 상수 선언
C#에서 변수는 '변수타입 변수명 = 초기값;' 형태로 선언한다. 변수는 값이 변경될 수 있다.
상수는 'const 상수타입 상수명 = 초기값;'의 형태로 선언한다. 상수는 한번 선언되면 값을 변경할 수 없다.
string name = "홍길동";
name = "1";
name = "0.5"; //string은 숫자가 아닌 문자열 이다.char letter = 'A'; //char는 ''로 한다. ""로 할 시, 오류int price = 100;
// price = 0.5; //소숫점은 넣을 수 없다.double b = 0.05;
decimal sale = Convert.ToDecimal(1.5); //decimal은 Convert.ToDecimal()로 형변환을 알려주고 넣거나decimal sale_price;
sale_price = price - (price * sale); //선언 하고 난 뒤 결과 값을 받을 수 있다.
Console.WriteLine("할인된 가격은 {0}", sale_price);
Console.ReadLine();
conststring str = "홍길동"; //const는 상수로서 변경 불가능 하다.
< Object Var Dynamic 타입과 연산자 >
C#은 Object, Var, Dynamic이라는 특별한 타입을 가지고 있다.
Object: C#의 모든 타입은 Object 타입에서 파생되므로, 어떤 타입의 데이터도 저장할 수 있다.
Var: 컴파일 타임에 타입이 결정되며, 선언 시 초기화를 해주어야 한다.
Dynamic: 런타임에 타입이 결정되며, 선언 시 초기화를 하지 않아도 된다.
이들 타입과 함께 하는 사용하는 연산자에는 사칙연산자(+,-,*,/),비교연산자(==,!=,<,>,<=,>=)등이 있다.
object aa = 11;
aa = "감자";
int a = Convert.ToInt32(aa); //object에서는 다른 형으로 재할당 시, Convert.Toint32(aa);로 해야 한다. object aaa = "a";
Console.WriteLine("aa값은: {0}", aa);
var bb = 22;
//bb = "b"; //var type은 처음에 숫자가 들어가면, 그 다음에 문자로 바꿀 수 없다.
Console.WriteLine("bb값은: {0}", bb);
dynamic cc = 33;
cc = "감자";//형을 바꿔서 넣을 수 있다. dynamic ccc = "c";
cc = "한글";
int c = cc;//실행되는 시점에서 검사를 하기에, 실행하는 시점에서만 오류인지 아닌지 알 수 있다.
Console.WriteLine("cc값은: {0}", cc);
Console.ReadLine();
//연산자 int a = 10;
int b = 5;
int c = a + b;
Console.WriteLine("a+b={0}", c);
Console.WriteLine("a+b={0}", a + b);
Console.WriteLine("a+b={0}", a - b);
Console.WriteLine("a*b={0}", a * b);
Console.WriteLine("a/b={0}", a / b);
Console.ReadLine();
int d;
d = 5 / 2;
Console.WriteLine("d의 값은? {0}", d);
double e;
e = 5 / 2;
Console.WriteLine("e의 값은? {0}", e);
double f;
f = 5.0 / 2;
Console.WriteLine("f의 값은? {0}", f);
float g;
g = 5f / 2f;
Console.WriteLine("g의 값은? {0}", g);
Console.ReadLine();
< 증감연산자,산술연산자 >
C#에서는 증감연산자와 산술연산자를 지원한다.
증감연산자 :
++ : 값을 1 증가 시킨다.
-- : 값을 1 감소시킨다.
산술연산자 :
+ : 더하기 연산
- : 빼기 연산
* : 곱하기 연산
/ : 나누기 연산
% : 나머지 연산
/*
연산자 종류
증감연산자 ++,--
산술연산자 +,-,*,/,%
*/int a = 1;
int b = 2;
int c;
c = ++a;
Console.WriteLine("선행연산자 a의 값 {0} c의 값{1}", a, c); //a의 값 2 c의 값 2
a = 1;
c = a++;
Console.WriteLine("선행연산자 a의 값 {0} c의 값{1}", a, c); //a의 값 2 c의 값 1
a = 1;
c = --a;
Console.WriteLine("선행연산자 a의 값 {0} c의 값 {1}", a, c); //a의 값 0 c의 값 0
c = a--;
Console.WriteLine("선행연산자 a의 값 {0} c의 값 {1}", a, c); //a의 값 -1 c의 값 0
Console.WriteLine("나머지값 {0}", 5 % 2); //나머지 값 1
< IF문 관계연산자 논리연산자 >
C#에서는 if문,관계연산자,논리연산자를 사용하여 조건에 따라 코드를 실행할 수 있다.
if문 :
if문은 조건식이 참일 때 해당 코드 블록을 실행한다.
관계연산자:
==:같다.
!= : 다르다.
< : 작다
> : 크다.
<= : 작거나 같다.
>= : 크거나 같다.
논리 연산자 :
&& : 논리 AND 연산, 모든 조건이 참일 때, 참을 반환한다.
|| : 논리 OR 연산, 조건 중 하나 이상이 참일 때 참을 반환한다.
! : 논리 NOT 연산 , 조건의 반대를 반환한다.
/*
관계 연산자 <,>,==,!=,<=,>=
논리 연산자 &&,||,!
*/int a = 3;
if (a == 1)
{
Console.WriteLine("1과 같음");
}
elseif (a >= 1)
{
Console.WriteLine("1보다 크거나 같음");
}
elseif (a < 1)
{
Console.WriteLine("1보다 작음");
}
else
{
Console.WriteLine("해당없음");
}
int b, c;
b = 100;
c = 200;
if (b == 100 && c == 300)
{
Console.WriteLine("1");
}
elseif (b == 100 || c == 300)
{
Console.WriteLine("1");
}
elseif (b > 100 || c == 300)
{
Console.WriteLine("3");
}
elseif (b != 100 || c != 300)
{
Console.WriteLine("4");
}
< 형변환 암시적 명시적 변환 >
C#에서의 데이터 타입간 암시적 변환과 명시적 변환을 사용할 수 있다.
암시적 변환: 데이터 손실이 없는 경우, 컴파일러가 자동으로 변환을 수행한다.
명시적 변환: 데이터 손실이 있는 경우, 개발자가 명시적으로 변환을 지시해야 한다.
/*
타입 자동 변환되는 유형
byte short,ushort,int,unit,long,ulong,float,double,decimal
sbyte short,int,long,float,double,decimal
short int,long,float,double,decimal
ushort int,unit,long,ulong,float,double,decimal
int long,float,double,decimal
uint long,ulong,float,double,decimal
long float,double,decimal
ulong float,double,decimal
float double
char ushort,int,unit,long,ulong,float,double,decimal
var 암시적 변환 타입
*/char a1 = 'a';
char a2 = 'A';
Console.WriteLine("a1의 값:" + a1);
Console.WriteLine("a2의 값:" + a2);
ushort change_a1, change_a2;
change_a1 = a1;
change_a2 = a2;
Console.WriteLine("change_a1의 값:" + change_a1);
Console.WriteLine("change_a2의 값:" + change_a2);
byte ch_byte_a1, ch_byte_a2;
ch_byte_a1 = (byte)a1;
ch_byte_a2 = Convert.ToByte(a2);
Console.WriteLine("ch_byte_a1의 값:" + ch_byte_a1);
Console.WriteLine("ch_byte_a2의 값:" + ch_byte_a2);
< 소수점 자리수 조절, 문자열 코드 >
C#에서는 소수점 자리수 조절을 위해 'Math.Round()', 문자열 조절을 위해 'string.Format()'을 사용할 수 있다.
double number = 3.141592;
number = Math.Round(number, 2); // 소수점 아래 두 자리까지 반올림, number는 3.14string str = string.Format("{0:N2}", number); // 숫자를 문자열로 변환하며 소수점 아래 두 자리까지 표현, str은 "3.14"
< For문 배열 메소드 사용 방법 >
C#에서 'for'문을 사용하여 반복 작업을 처리하고, 배열을 사용하여 어러 값을 저장 하며, 메소드를 사용하여 작업을 재사용할 수 있다.
C#에서 전역변수는 클래스 내 어디서는 접근 가능한 변수이며, 지역변수는 특정 블록(메소드,if문 등) 내에서만 사용하능한 변수이다. 열거형(Enum)은 여러 개의 상수를 하나의 타입으로 묶어놓은 것이다.
using System;
namespaceConsoleApp2
{
internalclassProgram
{
staticint globalVar = 10; // 전역 변수enum Days { Sun, Mon, Tue, Wed, Thu, Fri, Sat }; // 열거형staticvoidMain(string[] args)
{
int localVar = 20; // 지역 변수
Console.WriteLine(globalVar);
Console.WriteLine(localVar);
Days day = Days.Mon;
Console.WriteLine(day);
}
}
}
< 구조체 Struct >
C#에서 구조체(Struct)는 관련된 데이터를 하나의 복합 데이터 타입으로 그룹화하는 방법을 제공한다.
struct Point
{
publicint x, y;
publicPoint(int x, int y)
{
this.x = x;
this.y = y;
}
publicvoidDisplayPoint()
{
Console.WriteLine($"Point: ({x},{y})");
}
}
staticvoidMain()
{
Point point = new Point(3, 4);
point.DisplayPoint();
}
< Switch문 >
C#에서 'switch'문을 사용하면 어러 가지 경우에 따른 작업을 처리할 수 있다.
staticvoidMain()
{
int number = 2;
switch(number)
{
case1:
Console.WriteLine("Number is 1");
break;
case2:
Console.WriteLine("Number is 2");
break;
default:
Console.WriteLine("Number is not 1 or 2");
break;
}
}
< While문 Do While문 >
C#에서 'while'문과 'do while'문을 사용하여 조건에 따라 반복 작업을 처리할 수 있다.
staticvoidMain()
{
int number = 1;
while(number <= 5)
{
Console.WriteLine(number);
number++;
}
number = 1;
do
{
Console.WriteLine(number);
number++;
}
while(number <= 5);
}
< Foreach문 >
C#에서 foreach문을 사용하여 컬렉션의 모든 요소를 순회할 수 있다..
staticvoidMain()
{
int[] numbers = newint[5] {1, 2, 3, 4, 5};
foreach(int number in numbers)
{
Console.WriteLine(number);
}
}
< 클래스 Class 필드 속성 >
C#에서 클래스(Class)는 객체를 생성하는 템플릿이며, 필드는 클래스 내의 변수를, 속성은 클래스 내의 메소드를 정의하는 데 사용된다.
classProgram
{
publicclassCar
{
privatestring color; // 필드publicstring Color // 속성
{
get { return color; }
set { color = value; }
}
}
staticvoidMain()
{
Car car = new Car();
car.Color = "Red";
Console.WriteLine(car.Color);
}
}
< 클래스 정적 Static 상수 Const >
C#에서 클래스 내의 멤버를 'static'으로 선언하면, 그 멤버는 클래스의 인스턴스가 아닌 클래스 자체에 속하게 된다. 'const'는 상수를 선언하는 데 사용되며, 선언과 동시에 초기화 되어야 하고, 그 값을 변경할 수 없다.
classProgram
{
publicclassCircle
{
publicconstdouble PI = 3.14; // 상수publicstaticint count = 0; // 정적 필드publicCircle()
{
count++;
}
}
staticvoidMain()
{
Console.WriteLine($"PI: {Circle.PI}");
Circle c1 = new Circle();
Circle c2 = new Circle();
Console.WriteLine($"Created {Circle.count} circles");
}
}
< 클래스 메소드 정적 메소드 참조형 Out >
C#에서 클래스 메소드는 특정 클래스의 인스턴스에 연결되어 작동하며, 정적 메소드는 클래스 자체에 연결되어 작동한다. out 키워드는 메소드에서 여러 값을 반환할 때 사용된다.
classProgram
{
publicclassCalculator
{
publicstaticvoidMultiply(int a, int b, outint result)
{
result = a * b;
}
}
staticvoidMain()
{
int result;
Calculator.Multiply(3, 4, out result);
Console.WriteLine(result);
}
}
< 속성 정의 >
C#에서 속성(Property)은 클래스, 구조체, 인터페이스의 멤버로, 외부에서 접근할 수 있는 값이다. get과 set 접근자를 사용하여 속성 값을 읽거나 변경할 수 있다.
classProgram
{
publicclassStudent
{
privatestring name;
publicstring Name
{
get { return name; }
set { name = value; }
}
}
staticvoidMain()
{
Student student = new Student();
student.Name = "John Doe";
Console.WriteLine(student.Name);
}
}