달력

5

« 2025/5 »

  • 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
2016. 6. 14. 22:43

스레드 프로그래밍/C#2016. 6. 14. 22:43

스레드

https://msdn.microsoft.com/ko-kr/library/ms173178.aspx

1. 스레딩을 사용하면 Visual Basic 또는 C# 프로그램에서 동시 처리(concurrent processing)를 수행하여 한 번에 여러 작업을 진행할 수 있습니다.

2. 예를 들어, 스레딩을 사용하여 사용자의 입력을 모니터링하고 백그라운드 작업을 수행하고 입력의 동시 스트림(simultaneous streams)을 처리할 수 있습니다.

3. 스레드에는 다음과 같은 속성이 있습니다.

    1) 스레드를 사용하면 프로그램에서 동시 처리 작업을 수행할 수 있습니다.

    2) .NET Frameowrk System.Threading 네임스페이스를 사용하면 스레드를 더 쉽게 사용할 수 있습니다.

    3) 스레드는 응용 프로그램의 리소스를 공유합니다.

    4) 자세한 내용은 Using Threads and Threading를 참조하십시오.

4. 기본적으로(By default) Visual Basic 또는 C# 프로그램에는 하나의 스레드가 있습니다.

5. 그러나 기본 스레드와 함께 병렬 방식으로(in parallel with the primary thread) 코드를 실행하는 데 사용할 보조 스레드를 만들 수도 있습니다.

6. 이러한 스레드를 일반적으로 작업자 스레드(worker threads)라고 합니다.

7. 작업자 스레드를 사용하면 기본 스레드를 사용(tie up)하지 않고도( 시간이 오래 걸리는(time-consuming) 작업이나 빨리 끝내야 할 작업을 수행할 수 있습니다.

8. 예를 들어, 작업자 스레드는 이전 요청이 완료되기를 기다리지 않고 다른 들어오는 요청(incoming requests)을 처리(fulfill)해야 하는 서버 응용 프로그램에 자주 사용됩니다.

9. 작업자 스레드는 데스크톱 응용 프로그램에서 "백그라운드" 작업을 수행하는 데도 사용됩니다.

10. 이렇게 하면 사용자 인터페이스 요소를 관리하는 기본 스레드가 사용자 작업에 계속 응답(responsive)할 수 있습니다.

11. 스레딩을 사용하면 처리량(throughput)과 응답성(responsiveness)에 관련된 문제를 해결할 수 있지만 교착 상태(deadlock)나 경쟁 조건(race conditions) 등의 리소스 공유 문제(resource-sharing issues)가 새로 발생할 수도 있습니다.

12. 다중 스레드는 파일 핸들과 네트워크 연결 등의 서로 다른 리소스를 필요로 하는 작업에 가장 적합합니다.

13. 한 리소스에 여러 스레드를 할당하면 동기화 문제(synchronization issues)가 발생할 수 있으며 다른 스레드의 작업이 완료되기를 기다리느라 스레드가 자주 차단되며 다중 스레드를 사용하는 의의가 사라집니다.

14. 일반적으로 작업자 스레드는 다른 스레드에 사용되는 리소스를 많이 필요로 하지 않으며 시간이 오래 걸리거나 빠른 시간 안에 끝내야 할 작업을 수행하는 데 사용됩니다.

15. 대개의 경우(Naturally) 프로그램의 일부 리소스는 여러 스레드에서 액세스해야 합니다.

16. 이러한 경우에 대비하여 System.Threading 네임스페이스에서는 스레드를 동기화하기 위한 클래스를 제공합니다.

17. 이러한 클래스에는 Mutex, Monitor, Interlocked, AutoResetEventManualResetEvent가 있습니다.

18. 이러한 클래스의 일부 또는 전체를 사용하여 여러 스레드의 작업을 동기화할 수 있지만 다중 스레딩에 대한 일부 지원 기능은 Visual Basic 및 C# 언어를 통해 지원됩니다.

19. 예를 들어, Visual Basic SyncLock Statement과 C# Lock 문Monitor를 암시적으로 사용하여 동기화 기능을 제공합니다.

System_CAPS_note참고

.NET Framework 4에서는 System.Threading.Tasks.Parallel 및 System.Threading.Tasks.Task 클래스, Parallel LINQ (PLINQ),System.Collections.Concurrent 네임스페이스의 새 동시 컬렉션 클래스, 스레드 개념이 아닌 작업 개념을 기반으로 하는 새 프로그래밍 모델 등을 통해 매우 간단하게 수행할 수 있습니다. 자세한 내용은 Parallel Programming in the .NET Framework을 참조하십시오.

제목

설명

다중 스레드 응용 프로그램(C# 및 Visual Basic)

스레드를 만들고 사용하는 방법에 대해 설명합니다.

다중 스레드 프로시저의 매개 변수 및 반환 값(C# 및 Visual Basic)

다중 스레드 응용 프로그램에서 매개 변수를 전달하고 반환하는 방법에 대해 설명합니다.

연습: BackgroundWorker 구성 요소를 사용한 다중 스레딩(C# 및 Visual Basic)

간단한 다중 스레드 응용 프로그램을 만드는 방법을 보여 줍니다.

스레드 동기화(C# 및 Visual Basic)

스레드의 상호 작용을 제어하는 방법을 설명합니다.

스레드 타이머(C# 및 Visual Basic)

개별 스레드에서 프로시저를 고정 간격으로 실행하는 방법에 대해 설명합니다.

스레드 풀링(C# 및 Visual Basic)

시스템에서 관리하는 작업자 스레드의 풀을 사용하는 방법을 설명합니다.

방법: 스레드 풀 사용(C# 및 Visual Basic)

스레드 풀에 있는 여러 스레드의 동기화된 사용을 보여 줍니다.

Managed Threading

.NET Framework에서 스레딩을 구현하는 방법을 설명합니다.




다중 스레드 응용 프로그램


1. Visual Basic과 C#에서 동시에 여러 작업을 수행하는 응용 프로그램을 만들 수 있습니다.

2. 다른 작업을 지연시킬 가능성이 작업은 별도의 스레드에서 실행되는데, 이러한 프로세스를 다중 스레딩 또는 자유 스레딩이라고 합니다.

3. 다중 스레딩을 사용하는 응용 프로그램에서는 프로세서를 많이 사용하는 작업이(processor-intensive tasks) 별도의 스레드에서 실행되는 동안에도 사용자 인터페이스가 계속 활성 상태에 있기 때문에(stay active) 사용자 입력에 빠르게 응답합니다(more responsive).

4. 또한 다중 스레딩을 사용하면 작업 부하(workload)가 늘어날 때마다 스레드를 추가할 수 있으므로 확장 가능한(scalable) 응용 프로그램을 만드는 경우에도 유용합니다.

참고 참고

Visual Studio 2010 및 .NET Framework 4에서는 새로운 런타임, 새로운 클래스 라이브러리 형식 및 새로운 진단 도구(new diagnostic tools)를 제공하여 병렬 프로그래밍(parallel programming)에 대한 지원이 향상(enhance)되었습니다. 자세한 내용은 .NET Framework의 병렬 프로그래밍을 참조하십시오.

<BackgroundWorker 구성 요소 사용>

5. 다중 스레드 응용 프로그램을 만드는 가장 좋은 방법(The most reliable way)은 BackgroundWorker 구성 요소(component)를 사용하는 것입니다.

6. 이 클래스는 지정하는 메서드만 처리하는 별도의 스레드를 관리합니다.

7. 예제를 보려면 연습: BackgroundWorker 구성 요소를 사용한 다중 스레딩을 참조하십시오.

8. 백그라운드 작업을 시작하려면 BackgroundWorker를 만들고, 작업 진행률(the progress)을 보고하고 작업이 완료되면 신호를 보내는 이벤트를 수신합니다.

9. BackgroundWorker 개체는 프로그래밍 방식으로(programmatically) 또는 도구 상자의 구성 요소 탭에서 폼으로 끌어서 만들 수 있습니다.

10. 폼 디자이너에서 BackgroundWorker를 만들면 구성 요소 트레이에(Component Tray) 표시되며 해당 속성은 속성 창에 표시됩니다.

<백그라운드 작업에 대한 설정>

11. 백그라운드 작업에 대해 설정하려면(set up for) DoWork 이벤트에 대한 이벤트 처리기를 추가해야 합니다.

12. 완료하는 데 시간이 많이 걸리는 작업(time-consuming)을 이 이벤트 처리기로 호출합니다.

13. 작업을 시작하려면 RunWorkerAsync를 호출합니다.

14. 진행률 업데이트 알림(notifications of progress updates)을 받으려면 ProgressChanged 이벤트를 처리합니다.

15. 작업이 완료될 때 알림을 받으려면 RunWorkerCompleted 이벤트를 처리합니다.

16. ProgressChangedRunWorkerCompleted 이벤트를 처리하는 메서드에서 응용 프로그램의 사용자 인터페이스에 액세스할 수 있는데 그 이유는 이러한 이벤트가 RunWorkerAsync메서드를 호출한 스레드에서 발생하기 때문입니다.

17. 그러나 DoWork 이벤트 처리기는 백그라운드 스레드에서 실행되므로 사용자 인터페이스 개체를 다룰 수 없습니다.

<스레드 만들기 및 사용>

18. 응용 프로그램 스레드를 보다 효과적으로 제어하기 위해 스레드를 직접 관리할 수 있습니다.

19. 그러나 올바른 다중 스레드 응용 프로그램을 작성하기가 어려울 수 있습니다.

20. 응용 프로그램이 응답하지 않거나 경합 상태(race conditions)로 인해 일시적인 오류(transient errors)가 발생할 수 있습니다.

21. 자세한 내용은 스레드로부터 안전한 구성 요소를 참조하십시오.

22. Thread 형식의 변수를 선언하고, 새 스레드에서 실행할 프로시저 또는 메서드의 이름을 제공하여 생성자를 호출하면 새 스레드를 만들 수 있습니다.


System.Threading.Thread newThread =
    new System.Threading.Thread(AMethod);


<스레드 시작 및 중지>

23. 새 스레드를 실행하려면 다음 코드와 같이 Start 메서드를 사용합니다.


newThread.Start();


24. 스레드의 실행을 중지하려면 다음 코드와 같이 Abort 메서드를 사용합니다.


newThread.Abort();


25. 스레드의 시작과 중지 이외에도 Sleep 또는 Suspend 메서드를 호출하여 스레드를 일시 중지(pause)하거나, Resume 메서드를 사용하여 일시 중지된 스레드를 다시 시작하거나, Abort 메서드를 사용하여 스레드를 소멸시킬 수 있습니다.

<스레드 메서드>

26. 다음 표에서는 각 스레드를 제어하는 데 사용할 수 있는 몇 가지 메서드를 보여 줍니다.


메서드

동작

Start

스레드 실행을 시작하도록 합니다.

Sleep

지정한 시간 동안 스레드를 일시 중지합니다.

Suspend

스레드가 안전한 지점에 도달하면 스레드를 일시 중지합니다.

Abort

스레드가 안전한 지점에 도달하면 스레드를 중지합니다.

Resume

일시 중지된 스레드를 다시 시작합니다.

Join

현재 스레드에서 다른 스레드가 끝나기를 기다리도록 만듭니다. 이 메서드는 제한 시간 값(a time-out value)과 함께 사용할 경우 스레드가 할당된 시간 안에 끝나면 True를 반환합니다.

<안전한 지점>

27. 이러한 메서드의 대부분은 이름 그대로이므로 설명이 필요 없지만(self-explanatory) 안전한 지점(safe points)이라는 개념은 사용자에게 새로울 수 있습니다.

28. 안전한 지점은 공용 언어 런타임이 사용되지 않은 변수를 해제하고 메모리를 회수(reclaim)하는 프로세스인 자동 가비지 수집을 수행하기에 안전한 코드 위치를 말합니다.

29. 스레드의 Abort 또는 Suspend 메서드를 호출하면 공용 언어 런타임에서 코드를 분석하여 스레드 실행을 중지할 적절한 위치를 결정합니다.

<스레드 속성>

Property

IsAlive

스레드가 활성 상태이면 True 값을 포함합니다.

IsBackground

스레드가 백그라운드 스레드인지 또는 백그라운드 스레드이어야 하는지 여부를 나타내는 부울 값을 가져오거나 설정합니다. 백그라운드 스레드는 포그라운드 스레드와 유사하지만 프로세스가 중지되는 것은 막지 않습니다. 한 프로세스에 속하는 포그라운드 스레드가 모두 중지되고 나면 공용 언어 런타임에서는 아직 활성화되어 있는 백그라운드 스레드에 대해 Abort 메서드를 호출하여 해당 프로세스를 끝냅니다.

Name

스레드의 이름을 가져오거나 설정합니다. 이 속성은 대개 디버깅할 때 개별 스레드를 찾는 데 사용됩니다.

Priority

운영 체제에서 스레드 예약(scheduling)의 우선 순위를 결정(prioritize)하는 데 사용되는 값을 가져오거나 설정합니다.

ApartmentState

특정 스레드에 대해 사용되는 스레딩 모델을 가져오거나 설정합니다. 스레드가 비관리 코드를 호출할 때는 스레딩 모델이 중요합니다.

ThreadState

스레드의 상태를 설명하는 값을 포함합니다.

<스레드 우선 순위>

30. 모든 스레드에는 해당 스레드가 실행되는 데 걸리는 프로세서 시간을 결정하는 우선 순위 속성이 있습니다.

31. 운영 체제에서는 우선 순위가 낮은 스레드(low-priority threads)보다 높은 스레드(high-priority)에 더 긴 시간 간격(slices)을 할당합니다.

32. 새 스레드는 Normal 값으로 만들어지지만 ThreadPriority 열거형(enumeration)에 있는 다른 값으로 Priority 속성을 변경할 수 있습니다.

33. 다양한 스레드 우선 순위에 대한 자세한 내용(a detailed description)은 ThreadPriority를 참조하십시오.

<포그라운드 및 백그라운드 스레드>

34. 포그라운드 스레드는 무기한(indefinitely) 실행되는 반면(whereas) 백그라운드 스레드는 마지막 포그라운드 스레드가 중지되면 곧바로 중지됩니다.

35. IsBackground 속성을 사용하면 스레드의 백그라운드 상태를 확인(determine)하거나 변경할 수 있습니다.

<폼 및 컨트롤에 다중 스레딩 사용>

36. 다중 스레딩은 프로시저와 클래스 메서드를 실행하는 데 가장 적합하지만(best suited to) 폼 및 컨트롤에도 사용될 수 있습니다.

37. 이 경우 다음 사항을 주의해야 합니다.

    1) 가능한 한 컨트롤을 만들 때 컨트롤이 생성된 스레드에서만 컨트롤의 메서드를 실행합니다. 컨트롤 메서드를 다른 스레드에서 호출해야 하는 경우에는 Invoke를 사용하여 메서드를 호출해야 합니다.

    2) 컨트롤이나 폼을 조작(manipulate)하는 스레드는 SyncLock(Visual Basic) 또는 lock(C#) 문을 사용하여 잠그지 않습니다. 컨트롤의 메서드와 폼의 메서드는 종종 호출 프로시저로 콜백되기 때문에 실수로(inadvertently) 교착 상태(두 개의 스레드가 서로 다른 쪽의 스레드 잠금이 해제되기를 기다림으로써 응용  프로그램이 중단되는 상태)가 발생할 수 있습니다.





다중 스레드 프로시저의 매개 변수 및 반환 값


1. 다중 스레드 응용 프로그램에서 값을 지정하고 반환하는 것은 복잡합니다.

2. 그 이유는 사용하지 않고 값을 반환하지도 않는 프로시저에 데한 참조가 스레드 클래스에 대한 생성자에 전달되어야 하기 때문입니다.

3. 다음 단원(sections)에서는 별도의 스레드에 있는 프로시저에서 매개 변수를 지정하고 값을 반환하는 방법에 대해 간단하게 설명합니다.

<다중 스레드 프로시저에 대한 매개 변수 지정>

4. 다중 스레드 메서드의 호출에 대해 매개 변수를 지정하는 가장 좋은 방법 클래스의 대상 메서드를 래핑(wrap)하고 새 스레드에 대해 매개 변수로 사용될 해당 클래스에 대한 필드를 정의하는 것입니다.

5. 이 방법을 사용하면 새 스레드를 시작할 때마다 클래스의 새 인스턴스와 해당 매개 변수를 만들 수 있습니다.

6. 예를 들어 다음과 같이 삼각형(triangle)의 면적을 계산하는 함수가 있다고 가정합니다.


double CalcArea(double Base, double Height)
{
    return 0.5 * Base * Height;
}


7. 이 경우 다음과 같이 CalcArea 함수를 래핑하고 입력 매개 변수를 저장하기 위한 필드를 만드는 클래스를 작성할 수 있습니다.


class AreaClass
{
    public double Base;
    public double Height;
    public double Area;
    public void CalcArea()
    {
        Area = 0.5 * Base * Height;
        MessageBox.Show("The area is: " + Area.ToString());
    }
}


8. AreaClass를 사용하려면 AreaClass 개체를 만들고, 다음 코드와 같이 Base 속성과 Height 속성을 설정합니다.


protected void TestArea()
{
    AreaClass AreaObject = new AreaClass();

    System.Threading.Thread Thread =
        new System.Threading.Thread(AreaObject.CalcArea);
    AreaObject.Base = 30;
    AreaObject.Height = 40;
    Thread.Start();
}


9. 이 때 TestArea 프로시저는 CalcArea 메서드를 호출한 후 Area 필드의 값을 확인하지 않습니다.

10. CalcArea는 별도의 스레드에서 실행되므로 Thread.Start를 호출한 직후에 Area 필드를 확인하면 Area 필드가 설정되어 있지 않을 수도 있습니다.

12. 다음 단원에서는 다중 스레드 프로시저에서 값을 반환할 수 있는 좀 더 유용한 방법에 대해 설명합니다.

<다중 스레드 프로시저에서 값 반환>

13. 프로시저는 함수가 될 수 없으며 ByRef 인수를 사용할 수도 없기 때문에 별도의 스레드에서 실행되는 프로시저에서 값을 반환하는 것은 복잡합니다(complicated).

14. 값을 반환하는 가장 쉬운 방법(the easiest way)은 BackgroundWorker 구성 요소를 사용하여 스레드를 관리하고, 작업이 완료되면 이벤트를 발생시키고(raise an event), 이벤트 처리기로 처리하는 것입니다.

15. 다음 예제에서는 별도의 스레드에서 실행되는 프로시저에서 이벤트를 발생시키는 방법으로 값을 반환합니다.


class AreaClass2
{
    public double Base;
    public double Height;
    public double CalcArea()
    {
        // Calculate the area of a triangle.
        return 0.5 * Base * Height;
    }
}

private System.ComponentModel.BackgroundWorker BackgroundWorker1
    = new System.ComponentModel.BackgroundWorker();

private void TestArea2()
{
    InitializeBackgroundWorker();

    AreaClass2 AreaObject2 = new AreaClass2();
    AreaObject2.Base = 30;
    AreaObject2.Height = 40;

    // Start the asynchronous operation.
    BackgroundWorker1.RunWorkerAsync(AreaObject2);
}

private void InitializeBackgroundWorker()
{
    // Attach event handlers to the BackgroundWorker object.
    BackgroundWorker1.DoWork +=
        new System.ComponentModel.DoWorkEventHandler(BackgroundWorker1_DoWork);
    BackgroundWorker1.RunWorkerCompleted +=
        new System.ComponentModel.RunWorkerCompletedEventHandler(BackgroundWorker1_RunWorkerCompleted);
}

private void BackgroundWorker1_DoWork(
    object sender,
    System.ComponentModel.DoWorkEventArgs e)
{
    AreaClass2 AreaObject2 = (AreaClass2)e.Argument;
    // Return the value through the Result property.
    e.Result = AreaObject2.CalcArea();
}

private void BackgroundWorker1_RunWorkerCompleted(
    object sender,
    System.ComponentModel.RunWorkerCompletedEventArgs e)
{
    // Access the result through the Result property.
    double Area = (double)e.Result;
    MessageBox.Show("The area is: " + Area.ToString());
}


16. QueueUserWorkItem 메서드의 선택적 상태 개체(state-object) 변수인 ByVal을 사용하면 스레드 풀 스레드에 매개 변수와 반환 값을 제공할 수 있습니다.

17. 스레드 타이머 스레드에서도 이러한 용도의 상태 개체를 지원합니다.

18. 스레드 풀링 및 스레드 타이머에 대한 자세한 내용은 스레드 풀링스레드 타이머를 참조하십시오.





연습: BackgroundWorker 구성 요소를 사용한 다중 스레딩


1. 이 연습에서는 텍스트 파일에서 특정 단어를 검색하는 다중 스레드 응용 프로그램을 만드는 방법을 설명합니다.

    1) BackgroundWorker 구성 요소에서 호출할 수 있는 메서드로 클래스 정의

    2) BackgroundWorker 구성 요소에서 발생한(raise) 이벤트 처리

    3) BackgroundWorker 구성 요소를 시작하여 메서드 실행

    4) BackgroundWorker 구성 요소를 중지하는 Cancel 단추 구현

<사용자 인터페이스를 만들려면>

2. Visual Basic 또는 C# Windows 응용 프로그램 프로젝트를 새로 열고 Form1이라는 폼을 만듭니다.

3. Form1에 두 개의 단추와 네 개의 텍스트 상자를 추가합니다.

4. 다음 표에 표시된 대로 개체를 명명합니다.

object

Property

설정

첫 번째 단추

NameText

Start, Start

두 번째 단추

NameText

Cancel, Cancel

첫 번째 텍스트 상자

NameText

SourceFile, ""

두 번째 텍스트 상자

NameText

CompareString, ""

세 번째 텍스트 상자

NameText

WordsCounted, "0"

네 번째 텍스트 상자

NameText

LinesCounted, "0"

5. 각 텍스트 상자 옆에 레이블을 추가합니다.

6. 다음 표와 같이 각 레이블에 Text 속성을 설정합니다.

object

Property

설정

첫 번째 레이블

Text

소스 파일

두 번째 레이블

Text

Compare String

세 번째 레이블

Text

Matching Words

네 번째 레이블

Text

Lines Counted

<BackgroundWorker 구성 요소를 만들고 해당 이벤트를 구독하려면>

7. 도구 상자의 구성 요소 섹션에 있는 BackgroundWorker 구성 요소를 폼에 추가합니다

8. 이 구성 요소가 폼의 구성 요소 트레이에 나타납니다.

9. Visual Basic의 BackgroundWorker1 개체나 C#의 backgroundWorker1 개체에 대해 다음 속성을 설정합니다.

Property

설정

WorkerReportsProgress

True

WorkerSupportsCancellation

True

10. C#에서만 backgroundWorker1 개체의 이벤트를 구독합니다.

11. 속성 창의 위쪽에서(at the top of the Properties window) 이벤트 아이콘을 클릭합니다.

12. RunWorkerCompleted 이벤트를 두 번 클릭하여 이벤트 처리기 메서드(event handler method)를 만듭니다.

13. ProgressChanged 및 Dowork 이벤트에 대해 동일한 작업을 수행합니다.

<개별 스레드에서 실행되는 메서드를 정의하려면>

14. 프로젝트 메뉴에서 클래스 추가를 선택하여 프로젝트에 클래스를 추가합니다.

15. 새 항목 추가(Add New Item) 대화 상자가 표시됩니다.

16. 템플릿 창에서 클래스를 선택하고 이름 필드에 Words.vb나 Words.cs를 입력합니다.

17. 추가를 클릭합니다.

18. Words 클래스가 표시됩니다.

19. Words 클래스에 다음 코드를 추가합니다.


public class Words
{
    // Object to store the current state, for passing to the caller.
    public class CurrentState
    {
        public int LinesCounted;
        public int WordsMatched;
    }

    public string SourceFile;
    public string CompareString;
    private int WordCount;
    private int LinesCounted;

    public void CountWords(
        System.ComponentModel.BackgroundWorker worker,
        System.ComponentModel.DoWorkEventArgs e)
    {
        // Initialize the variables.
        CurrentState state = new CurrentState();
        string line = "";
        int elapsedTime = 20;
        DateTime lastReportDateTime = DateTime.Now;

        if (CompareString == null ||
            CompareString == System.String.Empty)
        {
            throw new Exception("CompareString not specified.");
        }

        // Open a new stream.
        using (System.IO.StreamReader myStream = new System.IO.StreamReader(SourceFile))
        {
            // Process lines while there are lines remaining in the file.
            while (!myStream.EndOfStream)
            {
                if (worker.CancellationPending)
                {
                    e.Cancel = true;
                    break;
                }
                else
                {
                    line = myStream.ReadLine();
                    WordCount += CountInString(line, CompareString);
                    LinesCounted += 1;

                    // Raise an event so the form can monitor progress.
                    int compare = DateTime.Compare(
                        DateTime.Now, lastReportDateTime.AddMilliseconds(elapsedTime));
                    if (compare > 0)
                    {
                        state.LinesCounted = LinesCounted;
                        state.WordsMatched = WordCount;
                        worker.ReportProgress(0, state);
                        lastReportDateTime = DateTime.Now;
                    }
                }
                // Uncomment for testing.
                //System.Threading.Thread.Sleep(5);
            }

            // Report the final count values.
            state.LinesCounted = LinesCounted;
            state.WordsMatched = WordCount;
            worker.ReportProgress(0, state);
        }
    }


    private int CountInString(
        string SourceString,
        string CompareString)
    {
        // This function counts the number of times
        // a word is found in a line.
        if (SourceString == null)
        {
            return 0;
        }

        string EscapedCompareString =
            System.Text.RegularExpressions.Regex.Escape(CompareString);

        System.Text.RegularExpressions.Regex regex;
        regex = new System.Text.RegularExpressions.Regex( 
            // To count all occurrences of the string, even within words, remove
            // both instances of @"\b" from the following line.
            @"\b" + EscapedCompareString + @"\b",
            System.Text.RegularExpressions.RegexOptions.IgnoreCase);

        System.Text.RegularExpressions.MatchCollection matches;
        matches = regex.Matches(SourceString);
        return matches.Count;
    }

}


<스레드에서 이벤트를 처리하려면>

20. 기본 폼에 다음 이벤트 처리기를 추가합니다.


private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
// This event handler is called when the background thread finishes.
// This method runs on the main thread.
if (e.Error != null)
    MessageBox.Show("Error: " + e.Error.Message);
else if (e.Cancelled)
    MessageBox.Show("Word counting canceled.");
else
    MessageBox.Show("Finished counting words.");
}

private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    // This method runs on the main thread.
    Words.CurrentState state =
        (Words.CurrentState)e.UserState;
    this.LinesCounted.Text = state.LinesCounted.ToString();
    this.WordsCounted.Text = state.WordsMatched.ToString();
}


<WordCount 메서드를 실행하는 새 스레드를 시작하여 호출하려면>

21. 프로그램에 다음 프로시저를 추가합니다.


private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    // This event handler is where the actual work is done.
    // This method runs on the background thread.

    // Get the BackgroundWorker object that raised this event.
    System.ComponentModel.BackgroundWorker worker;
    worker = (System.ComponentModel.BackgroundWorker)sender;

    // Get the Words object and call the main method.
    Words WC = (Words)e.Argument;
    WC.CountWords(worker, e);
}

private void StartThread()
{
    // This method runs on the main thread.
    this.WordsCounted.Text = "0";

    // Initialize the object that the background worker calls.
    Words WC = new Words();
    WC.CompareString = this.CompareString.Text;
    WC.SourceFile = this.SourceFile.Text;

    // Start the asynchronous operation.
    backgroundWorker1.RunWorkerAsync(WC);
}


22. 해당 폼의 Start 단추에서 StartThread 메서드를 호출합니다.


private void Start_Click(object sender, EventArgs e)
{
    StartThread();
}


<스레드를 중지하는 Cancel 단추를 구현하려면>

23. Cancel 단추에 대한 Click 이벤트 핸들러에서 StopThread 프로시저를 호출합니다.


private void Cancel_Click(object sender, EventArgs e)
{
    // Cancel the asynchronous operation.
    this.backgroundWorker1.CancelAsync();
}


<테스트>

24. 이제 응용 프로그램을 테스트하여 제대로 작동되는지 확인할 수 있습니다(make sure it works correctly).

<응용 프로그램을 실행하려면>

    1) F5 키를 눌러 응용 프로그램을 실행합니다.

    2) 폼이 표시되면 sourceFile 상자에 테스트하려는 파일의 파일 경로를 입력합니다.

    3) 예를 들어, 테스트 파일 이름이 Test.txt일 경우 C:\Test.txt라고 입력합니다.

    4) 두 번째 텍스트 상자에 텍스트 파일에서 응용 프로그램이 검색할 단어(a word)나 구(a phrase)를 입력합니다.

    5) Start 단추를 클릭합니다.

    6) LinesCounted 단추의 숫자가 즉시 증가됩니다.

    7) 이 작업이 끝나면 응용 프로그램은 "Finished Counting" 메시지를 표시합니다.

<Cancel 단추를 테스트하려면>

    1) F5 키를 눌러 응용 프로그램을 시작하고 이전 프로시저에서 설명한 대로 파일 이름과 검색 단어를 입력합니다.

    2) 이 작업이 끝나기 전에 프로시저를 취소할 수 있는 시간이 있는지를 확인하기 위해 선택한 파일의 크기가 충분한지 확인합니다.

    3) Start 단추를 클릭하여 응용 프로그램을 시작합니다.

    4) Cancel 단추를 클릭합니다.

    5) 응용 프로그램이 즉시 카운트를 중지합니다.

<다음 단계>

25. 이 응용 프로그램에는 기본적인 오류 처리 기능(some basic error handling)이 포함되어 있습니다.

26. 빈 검색 문자열을 검색합니다(detect).

27. 카운트할 수 있는 최대 단어 수(the maximum number)나 줄 수를 초과하는 오류 등을 포함한 여러 다른 오류들을 처리하여 이 프로그램을 보다 강력하게(robust) 만들 수 있습니다.





스레드 동기화


1. 다음 단원에서는 다중 스레드 응용 프로그램에서 리소스에 대한 액세스를 동기화하는 데 사용할 수 있는 기능과 클래스에 대해 설명합니다.

2. 응용 프로그램에서 다중 스레드를 사용할 때의 이점(benefits) 중 하나는 각 스레드가 비동기적으로 실행된다는 점입니다.

3. Windows 응용 프로그램의 경우 이렇게 하면 응용 프로그램 창과 컨트롤의 응답 가능 상태를 유지한 채(remain resonsive) 시간이 오래 걸리는 작업을 백그라운드에서 수행할 수 있습니다.

4. 서버 응용 프로그램의 경우 다중 스레딩을 사용하면 들어오는 각 요청을  서로 다른 스레드로 처리할 수 있습니다.

5. 그렇지 않으면(otherwise), 이전 요청이 완전히 처리될 때까지(fully satisfied) 새로운 각 요청의 처리를 시작할 수 없습니다(not get serviced).

6. 그러나 스레드의 비동기적 특성(nature)으로 인해 파일 핸들, 네트워크 연결, 메모리 등과 같은 리소스에 대한 액세스를 조정(coordinate)해야 한다는 문제가 있습니다.

7. 그렇지 않으면 두 개  이상의 스레드에서 각각 다른 스레드의 작업을 인식하지 못한 채(unaware of) 동시에(at the same time) 동일한 리소스에 액세스할 수 있습니다.

8. 그 결과로 예기치 않은(unpredictable) 데이터 손상(data corruption)이 발생할 수 있습니다.

9. 정수 숫자 데이터 형식(integral numeric data types)에 대한 간단한 연산의 경우 Interlocked 클래스의 멤버를 통해 스레드를 동기화할 수 있습니다.

10. 다른 모든 데이터 형식과 스레드로부터 안전하게 보호되지 않는 리소스의 경우 다중 스레딩을 안전하게 수행하려면 이 항목에서 설명하는 구문(the constructs)을 사용해야 합니다.

11. 다중 스레드 프로그래밍에 대한 배경 지식은 다음을 참조하십시오.

    1) 관리되는 스레딩 기본 사항

    2) 스레드 및 스레딩 사용

    3) 관리되는 스레딩을 구현하는 최선의 방법

<잠금 및 SyncLock 키워드>

12. lock(C#) 및 SyncLock(Visual Basic) 문을 사용하면 다른 스레드의 방해(interruption)를 받지 않은 채 코드 블록의 실행을 완료(run to completion)할 수 있습니다.

13. 이를 위해서는 코드 블록을 진행하는 동안 지정된 개체에 대한 상호 배타적 잠금(a mutual-exclusion lock)을 유지해야 합니다.

14. lock 또는 SyncLock 문은 개체를 인수로 지정하며 뒤에 한 번에 하나의 스레드에서만 실행할 코드 블록이 나옵니다.


public class TestThreading
{
    private System.Object lockThis = new System.Object();

    public void Process()
    {

        lock (lockThis)
        {
            // Access thread-sensitive resources.
        }
    }

}


15. lock 키워드에 제공되는 인수는 참조 형식을 기반으로 한 개체여야  하고 이 개체는 잠금 범위를 정의하는 데 사용됩니다.

16. 위 예제에서 함수 외부에 lockThis 개체에 대한 참조가 없으므로 잠금 범위는 이 함수로 제한됩니다.

17. 이와 같은 참조가 있는 경우에만 잠금 범위가 해당 개체애 맞게 확대됩니다(extend).

18. 엄밀하게 말해서(strictly speaking), 제공된 객체는 여러 스레드 간에 공유되는 리소스를 고유하게(uniquely) 식별하는 데만(identify) 사용되므로 이는 임의의(arbitrary) 클래스 인스턴스가 될 수 있습니다.

19. 그러나 실제로(In practice) 코드를 작성하는 경우 이 개체는 일반적으로 스레드 동기화(thread synchronization)가 필요한 리소스를 나타냅니다.

20. 예를 들어, 컨테이너 개체를 여러 스레드에서 사용해야 하는 경우 이 컨테이너를 lock 키워드에 전달하고 동기화된 코드 블록을 그 뒤에 추가하여 컨테이너에 액세스할 수 있습니다.

21. 다른 스레드는 동일한 컨테이너에 대해 잠긴 상태이므로 이 개체에 액세스할 수 없고 개체에 대한 액세스가 안전하게 동기화됩니다.

22. 일반적으로 public 형식이나 사용자 응용 프로그램의 제어 범위 밖에 있는 개체 인스턴스에 대해서는 잠금을 사용하지 않는 것이 좋습니다.

23. 예를 들어, 인스턴스에 공용으로 액세스할 수 있는 경우 lock(this)을 사용하면 문제가 발생할 수 있습니다(problematic).

24. 제어 범위 밖에 있는 코드마저 개체에 대해 잠길 수 있기 때문입니다.

25. 이 경우 동일한 개체가 해제되기를(the release of the same object) 두 개 이상의 스레드가 기다리는 교착 상태(deadlock situations)가 발생할 수 있습니다.

26. 개체와 달리(as opposed to) 공용 데이터 형식에 대해 잠금을 수행하는 경우에도 동일한 이유로 인해 문제가 발생할 수 있습니다.

27. 리터럴 문자열에 대해 잠금(Locking on literal strings)을 수행하는 경우는 특히 위험(risky)합니다.

28. 리터럴 문자열은 CLR(공용 언어 런타임)에서 사용하도록 의도되어 있기 때문입니다.

29. 즉, 전체 프로그램에서 임의의 지정된 문자열에 대한 인스턴스가 하나 있으며 정확하게 동일한 개체는 실행 중인 모든 응용 프로그램 도메인에서 모든 스레드에 대해 이 리터럴을 나타냅니다.

30. 그 결과, 응용 프로그램 프로세스에서 내용이 동일한 문자열을 잠그면 응용 프로그램에서 해당 문자열의 인스턴스가 모두 잠깁니다.

31. 따라서 잠금은 억류(intern)되지 않은 private 또는 protected버에 대해 수행하는 것이 좋습니다.

32. 일부 클래스는 잠금을 위한 특별한 멤버를 제공합니다.

33. 예를 들어 Array 형식은 SyncRoot를 제공합니다.

34. 대부분의 SyncRoot 멤버도 제공합니다.

35. lock 및 SyncLock 문에 대한 자세한 내용은 다음 항목을 참조하십시오.

    1) lock 문

    2) SyncLock 문

    3) Monitor

<Monitor>

36. lock 및 SyncLock 키워드와 마찬가지로 monitor를 사용하면 코드 블록이 여러 스레드에서 동시에 실행(simultaneous execution)되지 않도록 방지(prevent)할 수 있습니다.

37. Enter 메서드를 사용하면 스레드 하나만 다음 문으로 진행하도록(proceed into) 허용할 수 있습니다.

38. 모든 다른 스레드는 현재 실행 중인 스레드가 Exit를 호출할 때까지 차단됩니다.

39. 이는 lock 키워드를 사용할 때와 동일합니다.


lock (x)
{
    DoSomething();
}


System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
    DoSomething();
}
finally
{
    System.Threading.Monitor.Exit(obj);
}


40. 일반적으로 Monitor 클래스를 직접 사용하는 것보다 lock(C#) 또는 SyncLock(Visual Basic) 키워드를 사용하는 것이 더 좋습니다.

41. lock or SyncLock 키워드를 사용하면 코드를 더 간결하게(more concise) 작성할 수 있고 lock 또는 SyncLock 키워드의 경우 보호된 코드에서 예외를 throw 하더라도 내부 모니터를(the underlying monitor) 해제할 수 있기 때문입니다.

42. 이를 수행하는 데는 finally 키워드가 사용됩니다.

43. 이 키워드는 예외가 throw되었는지 여부와 상관없이 관련 코드 블록을 실행합니다.

<동기화 이벤트 및 대기 핸들>

44. lock 또는 monitor를 사용하면 스레드가 중요한 부분을 차지하는 코드 블록이 동시에 실행되지 않도록 방지할 수 있지만 이러한 구문을 사용하면 한 스레드가 다른 스레드에 이벤트를 전달(communicate)할 수 없습니다.

45. 이 문제를 해결하기 위해서는 스레드를 활성화(activate)하거나 일시 중단(suspend)하는 데 사용할 수 있고 신호를 받은(signaled) 상태 및 신호를 받지 않은(un-signaled) 상태 중 한 가지 상태가 지정되는 개체인 동기화 이벤트(synchronization events))가 필요합니다.

46. 스레드를 일시 중단하려면 신호를 받지 않은 상태의 동기화 이벤트에서 스레드를 대기시키고, 스레드를 활성화하려면 신호를 받은 상태로 이벤트 상태를 변경합니다.

47. 이미 신호를 받은 상태의 이벤트에서 스레드를 대기시키려고 하면 스레드가 지연 시간 없이 계속 실행됩니다.

48. 동기화 이벤트에는 AutoResetEventManualResetEvent라는 두 가지 종류가 있습니다.

49. 이 둘 사이의 유일한 차이는 AutoResetEvent의 경우 스레드를 활성화할 때마다 신호를 받은 상태에서 신호를 받지 않은 상태로 자동으로(automatically) 변경된다는 점입니다.

50. 반대로(Conversely), ManualResetEvent를 사용하면 신호를 받은 상태를 통해 스레드를 그 수에 상관없이(any number of threads) 활성화할 수 있고 해당 Reset 메서드를 호출한 경우에만 신호를 받지 않은 상태로 되돌릴 수(revert) 있습니다.

51. 스레드는 WaitOne, WaitAny 또는 WaitAll 등의 대기 메서드 중 하나를 호출하여 이벤트에 대기할 수 있도록 합니다.

52. WaitHandle.WaitOne()단일 이벤트가 신호를 받을 때까지(become signaled) 스레드를 대기시키고, WaitHandel.WaitAny()하나 이상의 지정된(indicated) 이벤트가 신호를 받을 때까지 스레드를 차단하며 WaitHandle.WaitAll()지정된 모든 이벤트가 신호를 받을 때까지 스레드를 차단합니다.

53. 이벤트는 해당 Set 메서드가 호출되면 신호를 받은 상태로 변경됩니다.

54. 다음 예제에서는 Main 함수를 사용하여 스레드를 만들고 시작합니다.

55. 새 스레드는 WaitOne 메서드를 사용하여 이벤트에서 대기합니다.

56. Main 함수를 실행하는 기본 스레드(primary thread)를 통해 이벤트가 신호를 받은 상태가 될 때까지 이 스레드는 일시 중단됩니다.

57. 이벤트가 신호를 받은 상태가 되면 보조 스레드(the auxiliary thread)가 반환됩니다.

58. 이 경우(In this case), 이벤트는 스레드 하나의 활성화에만 사용되므로 AutoResetEvent 또는 ManualResetEvent 클래스를 사용할 수 있습니다.


using System;
using System.Threading;

class ThreadingExample
{
    static AutoResetEvent autoEvent;

    static void DoWork()
    {
        Console.WriteLine("   worker thread started, now waiting on event...");
        autoEvent.WaitOne();
        Console.WriteLine("   worker thread reactivated, now exiting...");
    }

    static void Main()
    {
        autoEvent = new AutoResetEvent(false);

        Console.WriteLine("main thread starting worker thread...");
        Thread t = new Thread(DoWork);
        t.Start();

        Console.WriteLine("main thread sleeping for 1 second...");
        Thread.Sleep(1000);

        Console.WriteLine("main thread signaling worker thread...");
        autoEvent.Set();
    }
}


<뮤텍스 개체>

59. 뮤텍스는 monitor와 비슷합니다.

60. 이는 한 번에(at a time) 여러 스레드에서(more than one thread) 코드 블록이 동시에 실행되는 것을 방지합니다.

61. 사실(In fact), "뮤텍스"라는 용어(name)는 "상호 배타적(mutually exclusive)"이라는 표현의 줄임말(a shortened form)입니다.

62. 그러나 monitor와 달리 뮤텍스를 사용하면 프로세스 간에 스레드를 동기화(synchronize)할 수 있습니다.

63. 뮤텍스는 Mutex 클래스로 표현(represented)됩니다.

64. 프로세스간 동기화(inter-process synchronization)에 사용되는 뮤텍스를 명명된 뮤텍스라고 합니다.

65. 이는 다른 응용 프로그램에 사용하기 위한 것이며 전역 또는 정적 변수를 통해 공유할 수 없기 때문입니다.

66. 두 응용 프로그램에서 모두 동일한 뮤텍스 개체에 액세스할 수 있도록 이 뮤텍스에 이름을 지정해야 합니다.

67. 프로세스 내의 스레드(intra-process thread synchronization)를 동기화하는 데 뮤텍스를 사용할 수도 있지만 일반적으로 Monitor를 사용하는 것이 더 좋습니다.

68. monitor는 .NET Framework용으로 특별히 디자인 되었으며 리소스를 더 효율적으로 활용(make better use)하기 때문입니다.

69. 반면(In contrast) Mutex 클래스는 Win32 구문에 대한 래퍼(a wrapper to a Win32 construct)입니다.

70. 이는 monitor 보다 더 강력하지만 뮤텍스를 사용하려면 Monitor 클래스에 필요한 것보다 더 처리가 복잡한(more computationally expensive) interop 전환(transition)이 필요합니다.

71. 뮤텍스를 사용하는 방법의 예제는 뮤텍스를 참조하십시오.

<Interlocked 클래스>

72. Interlocked 클래스의 메서드를 사용하면 여러 스레드에서 같은 값을 동시에 업데이트 하거나 비교하려고 할 때 발생할 수 있는 문제를 방지할 수 있습니다.

73. 이 클래스의 메서드를 사용하면 모든 스레드에서 값을 안전하게 늘리거나(increment), 줄이거나(decrement), 교환(exchange)하거나 비교(compare)할 수 있습니다.

<ReadWriter 잠금>

74. 일부 경우에는(In some cases) 데이터를 쓰고 있을 때만 리소스를 잠그고 데이터를 업데이트하지 않을 때는 여러 클라이언트에서 동시에 데이터를 읽을 수 있도록 할 수 있습니다.

75. ReadWriterLock 클래스를 사용하면 스레드에서 리소스를 수정하는 동안은 리소스를 단독으로 사용하고(exclusive access), 리소스를 읽을 때는 여러 스레드에서 동시에 사용하도록(non-exclusive access) 할 수 있습니다.

76. ReaderWriter 잠금은 해당 스레드에서 데이터를 업데이트할 필요가 없는 경우에도 다른 스레드를 대기하도록 만드는 단독 잠금 대신 사용할 수 있는 유용한 기능입니다.

<교착 상태>

77. 스레드 동기화는 다중 스레드 응용 프로그램에서 매우 중요하지만(invaluable) 여러 스레드가 서로를 대기하여 응용 프로그램이 중지되는(come to a halt) deadlock이 발생할 위험이 항상 존재합니다.

78. 교착 상태는 교차로에서(at a four-way stop) 자동차들이 서로 다른 자동차가 가기를 기다리며 모두 멈춰있는 상황과 비슷합니다(is analogous to).

79. 따라서 교착 상태를 방지하는 것이 중요하며 이를 위해서는 철저한 계획(careful planning)을 세워야 합니다.

80. 종종 코딩을 시작하기 전에 다중 스레드 응용 프로그램의 다이어그램을 작성하면(by diagramming) 교착 상태를 예측할 수 있습니다.

<관련 단원>

1. 방법: 스레드 풀 사용

2. 방법: Visual C# .NET을 사용하여 다중 스레딩 환경에서 공유 리소스에 대한 액세스 동기화

3. 방법: Visual C# .NET을 사용하여 스레드 만들기

4. Visual C#을 사용하여 작업 항목을 스레드 풀에 제출하는 방법

5. 방법: Visual C# .NET을 사용하여 다중 스레딩 환경에서 공유 리소스에 대한 액세스 동기화





스레드 타이머


1. System.Threading.Timer 클래스는 작업을 별도의 스레드에서 정기적으로(periodically) 실행하는 데 유용합니다.

2. 예를 들어, 데이터베이스의 상태(status) 및 무결성(integrity)을 검사하거나 중요한(critical) 파일을 백업할 때 스레드 타이머를 사용할 수 있습니다.

3. 다음 예제에서는 2초마다 작업을 시작하고 플래그를 사용하여 타이머를 중지하는 Dispose 메서드를 시작합니다(initiate).

4. 이 예제에서는 출력 창(the output window)에 상태(status)를 게시(post)합니다.


private class StateObjClass
{
    // Used to hold parameters for calls to TimerTask.
    public int SomeValue;
    public System.Threading.Timer TimerReference;
    public bool TimerCanceled;
}

public void RunTimer()
{
    StateObjClass StateObj = new StateObjClass();
    StateObj.TimerCanceled = false;
    StateObj.SomeValue = 1;
    System.Threading.TimerCallback TimerDelegate =
        new System.Threading.TimerCallback(TimerTask);

    // Create a timer that calls a procedure every 2 seconds.
    // Note: There is no Start method; the timer starts running as soon as 
    // the instance is created.
    System.Threading.Timer TimerItem =
        new System.Threading.Timer(TimerDelegate, StateObj, 2000, 2000);

    // Save a reference for Dispose.
    StateObj.TimerReference = TimerItem;  

    // Run for ten loops.
    while (StateObj.SomeValue < 10) 
    {
        // Wait one second.
        System.Threading.Thread.Sleep(1000);  
    }

    // Request Dispose of the timer object.
    StateObj.TimerCanceled = true;  
}

private void TimerTask(object StateObj)
{
    StateObjClass State = (StateObjClass)StateObj;
    // Use the interlocked class to increment the counter variable.
    System.Threading.Interlocked.Increment(ref State.SomeValue);
    System.Diagnostics.Debug.WriteLine("Launched new thread  " + DateTime.Now.ToString());
    if (State.TimerCanceled)    
    // Dispose Requested.
    {
        State.TimerReference.Dispose();
        System.Diagnostics.Debug.WriteLine("Done  " + DateTime.Now.ToString());
    }
}


4. 스레드 타이머는 콘솔 응용 프로그램을 개발할 때와 같이 System.Windows.Forms.Timer 개체를 사용할 수 없는 경우에 특히 유용합니다.





스레드 풀링


1. 스레드 풀은 백그라운드에서 여러 가지 작업을 수행하는 데 사용할 수 있는 스레듸의 컬렉션입니다.

2. 자세한 내용은 스레딩을 참조하십시오.

3. 스레드 풀을 사용하면 기본 스레드(the primary thread)에서 다른 작업을 비동기적으로 수행할 수 있습니다.

4. 스레드 풀은 대개 서버 응용 프로그램에 사용됩니다.

5. 들어오는 각 요청은(Each incoming request) 스레드 풀의 스레드에 할당되므로 기본 스레드를 사용할 수 있을 때까지 기다리거나 이후의 요청을(subsequent requests) 처리하는 데 시간을 지연하지 않고 요청을 비동기적으로 처리할 수 있습니다.

6. 풀에 있는 스레드가 해당 작업을 완료하고 대기 스레드의 큐(a queue of waiting threads)로 반환되면 여기서 해당 스레드를 다시 사용(reuse)할 수 있습니다.

7. 이와 같이 스레드를 다시 사용하면 응용 프로그램에서 각 작업에 대해 새 스레드를  만드느라 리소스를 낭비하지 않아도 됩니다.

8. 스레드 풀에는 일반적으로 스레드의 최대 수가 지정되어 있습니다.

9. 모든 스레드에서 작업을 수행 중이면 다른 작업은 사용 가능한 스레드가 생길 때까지 큐에 배치됩니다.

10. 고유한 스레드 풀을 구현할 수도 있지만 ThreadPool 클래스를 통해 .NET Framework에서 제공하는 스레드 풀을 사용하는 것이 더 간편합니다.

11. 스레드 풀링을 사용하는 경우 실행할 프로시저의 대리자를 사용하여 ThreadPool.QueueUserWorkItem 메서드를 호출하면 Visual Basic 또는 C#에서 스레드를 만들고 프로시저를 실행합니다.

<스레드 풀링 예제>

12. 다음 예제에서는 스레드 풀링를 사용하여 여러 가지 작업을 시작하는 방법을 보여 줍니다.


public void DoWork()
{
    // Queue a task.
    System.Threading.ThreadPool.QueueUserWorkItem(
        new System.Threading.WaitCallback(SomeLongTask));
    // Queue another task.
    System.Threading.ThreadPool.QueueUserWorkItem(
        new System.Threading.WaitCallback(AnotherLongTask));
}

private void SomeLongTask(Object state)
{
    // Insert code to perform a long task.
}

private void AnotherLongTask(Object state)
{
    // Insert code to perform a long task.
}


13. 스레드 풀링의 한 가지 장점(One advantage)은 상태 개체의 인수를 작업 프로시저에 전달할 수 있다는 점입니다.

14. 호출할 프로시저에 둘 이상의 인수가 필요한 경우 클래스의 인스턴스 또는 구조체를 Object 데이터 형식으로 캐스팅할 수 있습니다.

<스레드 풀 매개 변수 및 반환 값>

15. 스레드 풀 스레드에서 값을 반환하는 것은 간단하지 않습니다(straightforward).

16. Sub 프로시저가 스레드 풀의 큐에 들어갈 수 있는 유일한 프로시저 형식이므로 함수 호출에서 값을 반환하는 일반적인 방법은 사용할 수 없습니다.

17. 매개 변수를 제공하고 값을 반환할 수 있는 한 가지 방법은 다중 스레드 프로시저의 매개 변수 및 반환 값에 설명된대로 매개 변수, 반환 값 및 메서드를 래퍼 클래스에 래핑하는 것입니다.

18. QueueUserWorkItem 메서드의 선택적 ByVal 상태 개체 변수를 사용하면 보다 쉽게 매개 변수를 제공하고 값을 반환할 수 있습니다.

19. 이 변수를 사용하여 클래스의 인스턴스에 대한 참조를 전달하면 스레드 풀 스레드에서는 이 인스턴스의 멤버를 수정하게 되고 이 인스턴스 멤버가 반환 값으로 사용됩니다.

20. 처음에는(At first) 값으로 전달된 변수에서 참조하는 개체를 수정할 수 있다는 것이 이해가 되지 않을 수 있습니다.

21. 이는 개체 참조만이 값으로 전달되기 때문에 가능합니다.

22. 개체 참조에서 참조하는 개체의 멤버를 변경하면 변경 내용이 실제 클래스 인스턴스에 적용됩니다.

23. 상태 객체 내에서 값을 반환할 때는 구조체(Structures)를 사용할 수 없습니다.

24. 구조체는 값 형식이므로 비동기 프로세스에서 수행한 변경 내용이 원본 구조체의 멤버를 변경하지 않습니다.

25. 반환 값이 필요하지 않을 경우에 매개 변수를 제공하려면 구조체를 사용합니다.





방법: 스레드 풀 사용


1. 스레드 풀링은 작업이 큐에 추가된 다음 스레드가 만들어질 때 자동으로 시작되는 다중 스레딩의 한 형태입니다.

2. 자세한 내용은 스레드 풀링을 참조하십시오.

3. 다음 예제에서는 .NET Framework 스레드 풀을 사용하여 20과 40 사이의 숫자 10개(ten numbers between 20 and 40)에 대한 Fibonacci 결과를 계산합니다.

4. 각 Fibonacci 결과를 Fibonacci 클래스로 표현됩니다.

5. 이 클래스는 계산을 수행하는 ThreadPoolCallback이라는 메서드를 제공합니다.

6. 각 Fibonacci 값을 나타내는 개체가 작성되고 ThreadPoolCallback 메서드가 QueueUserWorkItem에 전달되면 이 메서드를 실행하기 위해 풀의 사용 가능한 스레드가 할당됩니다.

7. 각 Fibonacci 개체에는 계산을 위해 어느 정도 임의(semi-random)로 지정되는 값이 할당되고 각 스레드는 프로세서 시간을 할당 받기 위해 경쟁(compete)하므로 10개의 결과를 모두 계산하는 데 얼마나 오래 걸릴지 미리(in advance) 알 수 없습니다.

8. 생성 과정에서 각 Fibonacci 개체가 ManualResetEvent 클래스의 인스턴스에 전달되는 이유는 바로 여기에 있습니다.

9. 각 개체는 해당 계산이 완료되면 제공된 이벤트 개체에 신호를 보내므로(signal) 10개의 Fibonacci 개체가 모두 결과를 계산할 때까지 기본 스레드에서 WaitAll을 사용한 실행을 차단할 수 있습니다.

10. 그런 다음 Main 메서드에서 각 Fibonacci 결과를 표시합니다.


using System;
using System.Threading;

public class Fibonacci
{
    private int _n;
    private int _fibOfN;
    private ManualResetEvent _doneEvent;

    public int N { get { return _n; } }
    public int FibOfN { get { return _fibOfN; } }

    // Constructor.
    public Fibonacci(int n, ManualResetEvent doneEvent)
    {
        _n = n;
        _doneEvent = doneEvent;
    }

    // Wrapper method for use with thread pool.
    public void ThreadPoolCallback(Object threadContext)
    {
        int threadIndex = (int)threadContext;
        Console.WriteLine("thread {0} started...", threadIndex);
        _fibOfN = Calculate(_n);
        Console.WriteLine("thread {0} result calculated...", threadIndex);
        _doneEvent.Set();
    }

    // Recursive method that calculates the Nth Fibonacci number.
    public int Calculate(int n)
    {
        if (n <= 1)
        {
            return n;
        }

        return Calculate(n - 1) + Calculate(n - 2);
    }
}

public class ThreadPoolExample
{
    static void Main()
    {
        const int FibonacciCalculations = 10;

        // One event is used for each Fibonacci object.
        ManualResetEvent[] doneEvents = new ManualResetEvent[FibonacciCalculations];
        Fibonacci[] fibArray = new Fibonacci[FibonacciCalculations];
        Random r = new Random();

        // Configure and start threads using ThreadPool.
        Console.WriteLine("launching {0} tasks...", FibonacciCalculations);
        for (int i = 0; i < FibonacciCalculations; i++)
        {
            doneEvents[i] = new ManualResetEvent(false);
            Fibonacci f = new Fibonacci(r.Next(20, 40), doneEvents[i]);
            fibArray[i] = f;
            ThreadPool.QueueUserWorkItem(f.ThreadPoolCallback, i);
        }

        // Wait for all threads in pool to calculate.
        WaitHandle.WaitAll(doneEvents);
        Console.WriteLine("All calculations are complete.");

        // Display the results.
        for (int i= 0; i<FibonacciCalculations; i++)
        {
            Fibonacci f = fibArray[i];
            Console.WriteLine("Fibonacci({0}) = {1}", f.N, f.FibOfN);
        }
    }
}


launching 10 tasks...
thread 0 started...
thread 1 started...
thread 1 result calculated...
thread 2 started...
thread 2 result calculated...
thread 3 started...
thread 3 result calculated...
thread 4 started...
thread 0 result calculated...
thread 5 started...
thread 5 result calculated...
thread 6 started...
thread 4 result calculated...
thread 7 started...
thread 6 result calculated...
thread 8 started...
thread 8 result calculated...
thread 9 started...
thread 9 result calculated...
thread 7 result calculated...
All calculations are complete.
Fibonacci(38) = 39088169
Fibonacci(29) = 514229
Fibonacci(25) = 75025
Fibonacci(22) = 17711
Fibonacci(38) = 39088169
Fibonacci(29) = 514229
Fibonacci(29) = 514229
Fibonacci(38) = 39088169
Fibonacci(21) = 10946
Fibonacci(27) = 196418






'프로그래밍 > C#' 카테고리의 다른 글

String 클래스  (0) 2016.06.22
Windows Forms 컨트롤의 다중 스레딩  (0) 2016.06.21
보간된 문자열(Interpolated strings)  (0) 2016.06.14
제네릭  (0) 2016.06.11
C# 기타  (0) 2016.06.10
:
Posted by 지훈2
2016. 6. 14. 06:12

보간된 문자열(Interpolated strings) 프로그래밍/C#2016. 6. 14. 06:12

보간된 문자열(Interpolated Strings)

https://msdn.microsoft.com/ko-kr/library/dn961160.aspx



1. 문자열을 생성하는데 사용됩니다.

2. 보간된 문자열 식은 식이 포함된 템플릿 문자열과 유사합니다.

3. 보간된 문자열 식은 포함된 식을 식 결과의 ToString 표현(represenations)으로 대체하여 문자열을 만듭니다.

4. 보간된 문자열은 인수 측면에서(with respect to) 복합 형식 지정(Composite Formatting)보다 이해하기 쉽습니다.

5. 다음은 보간된 문자열의 예입니다.


Console.WriteLine($"Name = {name}, hours = {hours:hh}")


6. 보간된 문자열의 구조(structure)는 다음과 같습니다.


$ " <text> { <interpolation-expression> <optional-comma-field-width> <optional-colon-format> } <text> ... } "


7. 문자열 리터럴을 사용할 수 있는 곳이면 어디든지 보간된 문자열을 사용할 수 있습니다.

8. 프로그램 실행 시 보간된 문자열 리터럴을 사용하여 코드가 실행되는 경우 코드에서 보간 식을 평가하여 새 문자열 리터럴을 계산합니다.

9. 이 계산(computation)은 보간된 문자열을 포함하는 코드가 실행될 때마다 발생합니다.

10. 보간된 문자열에 중괄호(a curly brace)("{" or "}")를 포함하려면 두 개의 중괄호 "{{" 또는 "}}"를 사용합니다.

11. 자세한 내용은 암시적 변환 섹션을 참조하세요.

<암시적 변환>

12. 보간된 문자열에서 다음과 같은 암시적 형식 변환을 수행할 수 있습니다.


var s = $"hello, {name}" System.IFormattable s = $"Hello, {name}" System.FormattableString s = $"Hello, {name}"


13. 첫번째 예제에서는 모든 문자열 보간 값이 계산된 string 값을 생성합니다.

14. 최종 결과이며 문자열 형식입니다.

15. 나타나는 모든 이중 중괄호("{{" 및 "}}")가 단일 중괄호로 변환됩니다.

16. 두 번째 예제에서는 고정 컨텍스트(invariant context)를 포함하는 문자열을 변환할 수 있는 IFormattable 변수를 생성합니다.

17. 이 기능은 숫자 및 데이터 형식을 다양한 언어에서 올바르게 표시하는 데 유용합니다.

18. 나타나는 모든 이중 중괄호("{{" 및 "}}")는 ToString을 사용하여 문자열의 형식을 지정할 때까지 이중 중괄호로 유지됩니다.

19. 포함된 보간 식은 모두{0}, {1} 등으로 변환됩니다.


s.ToString(null, System.Globalization.CultureInfo.InvariantCulture);


20. 세 번째 예제에서는 보간 계산 결과로 얻은 개체를 검사할 수 있는 FormattableString을 생성합니다.

21. 예를 들어 개체와 개체가 문자열로 렌더링되는 방식을 검사(inspect)하면 쿼리를 빌드하는 경우 삽입 공격(injection attack)으로부터 보호하는 데 도움이 됩니다.

22. FormattableString을 사용하면 InvariantCulture 및 CurrentCulture 문자열결과를 편리하게 생성할 수 있습니다.

23. 나타나는 모든 이중 중괄호 ("{{" 및 "}}")는 형식을 지정할 때까지 이중 중괄호로 유지됩니다.


$"Name = {name}, hours = {hours:hh}" var s = $"hello, {name}" System.IFormattable s = $"Hello, {name}" System.FormattableString s = $"Hello, {name}" $"{person.Name, 20} is {person.Age:D3} year {(p.Age == 1 ? "" : "s")} old."


24. 보간된 문자열 식은 $로 시작하고 컴파일러가 쉼표, 콜론, 닫는 중괄호를 발견할 때까지 포함된 보간 식을 균형 있는 텍스트(balanced text)로 검색(scan)하기 때문에 포함된 보간 식 내에서 인용 문자(quotation characters)를 따옴표로 묶을(quote) 필요가 없습니다.

25. 동일한 이유로, 마지막 예제에서는 괄호를 사용하여 형식 사양을 시작하는 콜론 없이 보간 식 내에 조건식(p.Age == 1 ? "" : "s")을 포함할 수 있게 합니다.

26. 포함된 보간 식 외부(하지만 보간된 문자열 식 내부)에서는 평소대로 인용 문자를 이스케이프합니다.


expression: interpolated-string-expression interpolated-string-expression: interpolated-string-start interpolations interpolated-string-end interpolations: single-interpolation single-interpolation interpolated-string-mid interpolations single-interpolation: interpolation-start interpolation-start : regular-string-literal interpolation-start: expression expression , expression


예제 사이트

http://www.dotnetperls.com/string-interpolation


1. 문자열 : $"Hello {변수}"

2. 배열 : $"Hello {변수[0]}"

3. 계산 식 : $"Hello {1+2}"

4. 메서드 호출 : $"Hello {함수()}

376.5875 ms,    String interpolation
293.1515 ms,    Concat (+)
369.2315 ms,    String Format


'프로그래밍 > C#' 카테고리의 다른 글

Windows Forms 컨트롤의 다중 스레딩  (0) 2016.06.21
스레드  (0) 2016.06.14
제네릭  (0) 2016.06.11
C# 기타  (0) 2016.06.10
C# 컨텍스트 키워드  (0) 2016.06.06
:
Posted by 지훈2
2016. 6. 11. 17:40

제네릭 프로그래밍/C#2016. 6. 11. 17:40

제네릭

https://msdn.microsoft.com/ko-kr/library/512aeb7t.aspx

1. 제네릭이 2.0 버전의 C# 언어와 CLR(공용 언어 런타임)에 추가되었습니다.

2. 제네릭을 통해 .NET Framework에 형식 매개 변수(type parameters)라는 개념이 처음 소개되었습니다.

3. 형식 매개 변수를 사용하면 클라이언트 코드에서 클래스나 메서드를 선언하고 인스턴스화할 때까지 하나 이상의 형식 지정(specification)을 연기(defer)하는 클래스와 메서드를 디자인할 수 있습니다.

4. 예를 들어 다음과 같이 제네릭 형식 매개 변수 T를 사용하면 런타임 캐스트나 boxing 작업에 따른 비용이나 위험을 초래(incur)하지 않은 채 다른 클라이언트에서 사용 가능한 단일 클래스를 작성할 수 있습니다.


// Declare the generic class.
public class GenericList<T>
{
    void Add(T input) { }
}
class TestGenericList
{
    private class ExampleClass { }
    static void Main()
    {
        // Declare a list of type int.
        GenericList<int> list1 = new GenericList<int>();

        // Declare a list of type string.
        GenericList<string> list2 = new GenericList<string>();

        // Declare a list of type ExampleClass.
        GenericList<ExampleClass> list3 = new GenericList<ExampleClass>();
    }
}


<제네릭 개요>

5. 제네릭 형식을 사용하면 코드 재사용(code reuse), 형식 안전성, 성능(performance)을 최대화할 수 있습니다.

6. 제네릭의 가장 일반적인 용도(the most common use)는 컬렉션 클래스를 만드는 것입니다.

7. .NET Framework 클래스 라이브러리에는 System.Collections.Generic 네임스페이스의 여러 가지 새로운 제네릭 컬렉션 클래스가 포함되어 있습니다.

8. 이러한 클래스는 가능한 경우 항상 System.Collections 네임스페이스의 ArrayList 같은 클래스 대신 사용해야 합니다.

9. 고유한 제네릭 인터페이스, 클래스, 메서드, 이벤트 및 대리자를 만들 수 있습니다.

10. 특정 데이터 형식의 메서드에만 액세스하도록 제네릭 클래스를 제한(constrain)할 수 있습니다.

11. 리플레션을 사용하면 런타임에 제네릭 데이터 형식에 사용되는 형식과 관련된정보를 얻을 수 있습니다.

<관련 단원>

12. 자세한 내용은 다음을 참조하십시오.

    1) 제네릭 소개

    2) 제네릭의 장점

    3) 제네릭 형식 매개 변수

    4) 형식 매개 변수에 대한 제약 조건

    5) 제네릭 클래스

    6) 제네릭 인터페이스

    7) 제네릭 메서드

    8) 제네릭 대리자

    9) 제네릭 코드의 default 키워드

    10) C++ 템플릿과 C# 제네릭의 차이

    11) 제네릭 및 리플렉션

    12) 런타임의 제네릭

    13) .NET Framework 클래스 라이브러리의 제네릭





제네릭 소개


1. 제네릭 클래스 및 메서드를 사용하면 제네릭이 아닌 형식(counterparts)에서는 불가능한 재사용성, 형식 안전성 및 효율성을 달성할 수 있습니다.

2. 제네릭은 컬렉션 및 컬렉션을 다루는 메서드에서 가장 자주 사용됩니다.

3. .NET Framework 클래스라이브러리 버전 2.0에서는 System.Collections.Generic이라는 새로운 네임스페이스를 제공하며, 이 네임스페이스에는 새로운 제네릭 기반 컬렉션 클래스가 여러 개 있습니다.

4. .NET Framework 2.0 이상을 대상으로하는 모든 응용 프로그램에서는 이전의 ArrayList 같은 제네릭이 아닌 형식 대신 새로운 제네릭 컬렉션 클래스를 사용하는 것이 좋습니다.

5. 자세한 내용은 .NET Framework 클래스 라이브러리의 제네릭을 참조하십시오.

6. 물론 사용자 고유의 형식이 안전하고 효율적인 일반화 솔루션 및 디자인 패턴을 제공하기 위해 사용자 지정 제네릭 형식 및 메서드를 만들 수도 있습니다.

7. 다음 코드 예제에서는 이해를 돕기 위해(for demonstration purposes) 간단한 제네릭 연결 리스트 클래스를 보여 줍니다.

8. 대부분의 경우에는 사용자가 이러한 클래스를 직접 만드는 대신 .NET Framework 클래스 라이브러리에서 제공하는 List<T> 클래스를 사용해야 합니다.

9. 목록에 저장되는 항목의 형식을 지정하기 위해 일반적으로는(ordinarily) 구체적인(concrete) 형식이 사용될 여러 위치에 형식 매개 변수 T가 사용되고 있습니다.

10. 구체적으로는 다음과 같이 사용되고 있습니다.

    1) AddHead 메서드의 메서드 매겨 변수 형식

    2) 공용 메서드인 GetNext의 반환 형식 및 중첩 Node 클래스의 Data 속성

    3) 중첩 클래스에 있는 전용 멤버 데이터의 형식

11. 중첩 Node 클래스에서도 T를 사용할 수 있습니다.

12. GenericList<int>와 같이 GenericList<T>를 구체적 형식을 사용하여 인스턴스화하면 모든 T가 int로 대체됩니다.


// type parameter T in angle brackets
public class GenericList<T> 
{
    // The nested class is also generic on T.
    private class Node
    {
        // T used in non-generic constructor.
        public Node(T t)
        {
            next = null;
            data = t;
        }

        private Node next;
        public Node Next
        {
            get { return next; }
            set { next = value; }
        }

        // T as private member data type.
        private T data;

        // T as return type of property.
        public T Data  
        {
            get { return data; }
            set { data = value; }
        }
    }

    private Node head;

    // constructor
    public GenericList() 
    {
        head = null;
    }

    // T as method parameter type:
    public void AddHead(T t) 
    {
        Node n = new Node(t);
        n.Next = head;
        head = n;
    }

    public IEnumerator<T> GetEnumerator()
    {
        Node current = head;

        while (current != null)
        {
            yield return current.Data;
            current = current.Next;
        }
    }
}


13. 다음 코드 예제에서는 클라이언트 코드에서 제네릭 GenericList<T> 클래스를 사용하여 정수 목록을 만드는 방법을 보여 줍니다.

14. 형식 매개 변수를 변경하는 간단한 방법으로 다음 코드를 문자열이나 기타 사용자 지정 형식의 목록을 만들도록 쉽게 변경할 수 있습니다.


class TestGenericList
{
    static void Main()
    {
        // int is the type argument
        GenericList<int> list = new GenericList<int>();

        for (int x = 0; x < 10; x++)
        {
            list.AddHead(x);
        }

        foreach (int i in list)
        {
            System.Console.Write(i + " ");
        }
        System.Console.WriteLine("\nDone");
    }
}





제네릭의 장점


1.  제네릭을 사용하면 이전 버전의 공용 언어 런타임과 C# 언어에 적용되었던 제한 사항(limitation)을 해결할 수 있습니다.

2. 이전 버전에서는 유니버설 기본 형식인 Object와 형식 사이의 캐스팅을 통해 일반화를 수행(accomplish)했습니다.

3. 제네릭 클래스를 만들면 컴파일 타임에 안전한 컬렉션을 만들 수 있습니다.

4. 제네릭이 아닌 컬렉션 클래스를 사용하는 경우의 제한 사항을 보여 주는 예로는 .NET Framework 클래스 라이브러리에서 ArrayList 컬렉션 클래스를 사용하는 간단한 프로그램을 작성하는 경우를 들 수 있습니다.

5. ArrayList는 참조나 값 형식을 저장하기 위해 수정(modification)하지 않고도 사용할 수 있는 매우 편리한 컬렉션 클래스(a highly convenient collection)입니다.


// The .NET Framework 1.1 way to create a list:
System.Collections.ArrayList list1 = new System.Collections.ArrayList();
list1.Add(3);
list1.Add(105);

System.Collections.ArrayList list2 = new System.Collections.ArrayList();
list2.Add("It is raining in Redmond.");
list2.Add("It is snowing in the mountains.");


6. 그러나 이러한 편리함에는 상응하는 대가가 따릅니다(But this convenience comes at a cost).

7. ArrayList에 추가되는 모든 참조나 값 형식은 Object에 암시적으로 업캐스팅(upcast)됩니다.

8. 항목(items)이 값 형식이면 이를 목록에 추가할 때 boxing해야 하고 이를 검색할 때 unboxing해야 합니다.

9. 캐스팅이나 boxing 및 unboxing 작업은 모두 성능을 저하시킵니다.

10. 큰 컬렉션을 반복해야 하는 시나리오에서는 boxing과 unboxing의 영향을 결코 무시할 수 없습니다.

11. 다른 제한 사항으로는 컴파일 타임에 형식을 검사할 수 없다는 점을 들 수 있습니다(is lack of).

12. ArrayList는 Object에 모든 항목을 캐스팅하므로 컴파일 타임에 클라이언트 코드가 다음과 같은 작업을 수행하지 못하도록 막을 수 없습니다.


System.Collections.ArrayList list = new System.Collections.ArrayList();
// Add an integer to the list.
list.Add(3);
// Add a string to the list. This will compile, but may cause an error later.
list.Add("It is raining in Redmond.");

int t = 0;
// This causes an InvalidCastException to be returned.
foreach (int x in list)
{
    t += x;
}


13. 유형이 다른 컬렉션(a heterogeneous collection)을 만드는 경우 규칙을 정확히 따르는(perfectly acceptable) 의도적인(intentional) 선택일지라도 문자열과 ints를 단일 ArrayList에 결합하면 프로그래밍 오류가 발생할 확률이 더  커지고 이러한 오류는 런타임 이전에 발견할 수 없습니다.

14. 버전 1.0 및 1.1의 C# 언어에서는 고유한 형식별 컬렉현을 작성하는 방법으로만 .NET Framework 기본 클래스 라이브러리 컬렉션 클래스에서 코드를 일반화하는 위험(the dangers of generalized code)을 방지할 수 있었습니다.

15. 물론 이러한 클래스는 여러 데이터 형식에 다시 사용(reusable)할 수 없으므로 일반화의 이점(the benefits of generalization)이 사라지고 저장하려는 각 형식에 대해 클래스를 다시 작성해야만 합니다.

16. ArrayList 및 기타 유사한 클래스에 실제로 필요한 것은 클라이언트 코드에서 사용하려는 특정 데이터 형식을 인스턴스별로(on a per-instance basis) 지정할 수 있는 방법입니다.

17. 이렇게 하면 T:System.Object로 업캐스팅할 필요가 없어지고 컴파일러에서 형식을 검사할 수도 있게 됩니다.

18. 즉, ArrayList에는 형식 매개 변수가 필요합니다.

19. 제네릭은 바로 이러한 요구 사항을 충족시킵니다.

20. N:System.Collections.Generic 네임스페이스의 제네릭 List<T> 컬렉션에서는 컬렉션에 항목을 추가하는 동일한 작업을 다음과 같이 수행할 수 있습니다.


// The .NET Framework 2.0 way to create a list
List<int> list1 = new List<int>();

// No boxing, no casting:
list1.Add(3);

// Compile-time error:
// list1.Add("It is raining in Redmond.");


21. 클라이언트 코드에서 ArrayList와 비교할 때 List<T>을 사용하여 유일하게 추가된 구문은 선언과 인스턴스화의 형식 인수(the type argument)입니다.

22. 이와 같이 코딩이 약간 더 복잡해진 대신(In return for this slightly more coding complexity) ArrayList보다 안전하면서도 속도가 훨씬 빠른 목록을 만들 수 있습니다.

23. 이러한 차이는 목록 항목이 값 형식인 경우에 특히 잘 드러납니다.





제네릭 형식 매개 변수


1. 제네릭 형식 또는 메서드 정의에서 형식 매개 변수(a type parameters)는 클라이언트가 제네릭 형식의 변수를 인스턴스화할 때 지정하는 특정 형식에 대한 자리 표시자(placeholder)입니다.

2. 제네릭 소개에 나열된(listed) GenericList<T> 등의 제네릭 클래스는 실제로 형식이 아니고 형식에 대한 청사진(blueprint)과 같으므로 있는 그대로(as-is) 사용할 수는 없습니다.

3. 클라이언트 코드에서 GenericList<T>를 사용하려면 꺾쇠괄호(the angle brackets) 내에 형식 매개 변수를 지정하는 방법으로 생성된 형식을 선언하고 인스턴스화해야 합니다.

4. 이 특정 클래스(this particular class)에 대한 형식 매개 변수의 형식은 컴파일러에서 인식하는 모든 형식이 될 수 있습니다.

5. 만들 수 있는 생성된 형식 인스턴스의 수에는 제한이 없고(Any number of), 각 인스턴스에서는 다음과 같이 서로 다른 형식 매개 변수를 사용할 수 있습니다.


GenericList<float> list1 = new GenericList<float>();
GenericList<ExampleClass> list2 = new GenericList<ExampleClass>();
GenericList<ExampleStruct> list3 = new GenericList<ExampleStruct>();


<형식 매개 변수 명명 지침>

6. 필수적. 단일 문자 이름으로도 자체 설명이 가능하여(completely self explanatory) 설명적인 이름(descriptive names)을 굳이 사용할 필요가 없는 경우가 아니면 제네릭 형식 매개 변수 이름을 설명적인 이름으로 지정하십시오.


public interface ISessionChannel<TSession> { /*...*/ }
public delegate TOutput Converter<TInput, TOutput>(TInput from);
public class List<T> { /*...*/ }


7. 선택적. 단일 문자 형식 매개변수를 사용하는 형식에는 형식 매개 변수 이름(the type parameter name)으로 T를 사용하십시오.


public int IComparer<T>() { return 0; }
public delegate bool Predicate<T>(T item);
public struct Nullable<T> where T : struct { /*...*/ }


8. 필수적. 설명적인(descriptive) 형식 매개 변수 이름 앞에 "T"를 붙이십시오.


public interface ISessionChannel<TSession>
{
    TSession Session { get; }
}


9. 선택적. 매개 변수 이름 안에는 형식 매개 변수에 적용되는 제약 조건(constraints)을 나타내십시오.

10. 예를 들어 ISession으로 제한되는 매개 변수의 이름은 TSession이 될 수 있습니다.





제네릭 형식 매개 변수에 대한 제약 조건


1. 제네릭 클래스를 정의하는 경우 클래스를 인스턴스화할 때 클라이언트 코드에서 형식 인수에 사용할 수 있는 형식의 종류에 제약 조건(restrictions)을 적용할 수 있습니다.

2. 클라이언트 코드가 제약 조건에서 허용하지 않는 형식을 사용하여 클래스를 인스턴스화하려고 하면 컴파일 타임 오류가 발생합니다.

3. 이러한 제한(restrictions)을 제약 조건(constraints)이라고합니다.

4. 제약 조건은 컨텍스트 키워드 where를 사용하여 지정합니다.

5. 다음 표에서는 여섯 가지의 형식 제약 조건을 보여 줍니다.

제약 조건

설명

where T: struct

형식 인수가 값 형식이어야 합니다. Nullable 를 제외한 임의의 값 형식을 지정할 수 있습니다. 자세한 내용은 Nullable 형식 사용(C# 프로그래밍 가이드)를 참조하십시오.

where T : class

형식 인수가 참조 형식이어야 합니다. 이는 모든 클래스, 인터페이스, 대리자 또는 배열 형식에도 적용됩니다.

where T : new()

형식 인수가 매개 변수 없는 공용 생성자를 가지고 있어야 합니다. 다른 제약 조건과 함께 사용하는 경우 new() 제약 조건은 마지막에 지정해야 합니다.

where T : <기본 클래스 이름>

형식 인수가 지정된 기본 클래스이거나 지정된 기본 클래스에서 파생되어야 합니다.

where T : <인터페이스 이름>

형식 인수가 지정된 인터페이스이거나 지정된 인터페이스를 구현해야 합니다. 여러 인터페이스 제약 조건을 지정할 수 있습니다. 제한하는 인터페이스는 제네릭이 될 수도 있습니다.

where T : U

T에 대해 지정한 형식 인수가 U에 대해 지정한 인수이거나 이 인수에서 파생되어야 합니다.

<제약 조건 사용 이유>

6. 제네릭 목록의 항목을 검사(examine)하여 그 유효성 여부를 확인하거나 이 항목을 다른 항목과 비교하려는 경우, 컴파일러는 클라이언트 코드에서 지정할 수 있는 모든 형식 인수에 대해 호출해야 할 메서드나 연산자가 지원되는지 확인해야 합니다.

7. 하나 이상의 제약 조건을 제네릭 클래스 정의에 적용하면 이러한 확인(guarantee)이 가능합니다.

8. 예를 들어, 기본 클래스 제약 조건에서는 이 형식의 개체와 이 형식에서 파생된 개체만이 형식 인수로 사용될 수 있음을 컴파일러에 알립니다.

9. 컴파일러에서 이를 확인하면 사용자는 제네릭 클래스에서 해당 형식의 메서드를 호출할 수 있습니다.

10. 제약 조건은 컨텍스트 키워드 where를 사용하여 적용됩니다.

11. 다음 코드 예제에서는 기본 클래스 제약 조건을 적용하여 제네릭 소개의 GenericList<T> 클래스에 추가할 수 있는 기능을 보여 줍니다.


public class Employee
{
    private string name;
    private int id;

    public Employee(string s, int i)
    {
        name = s;
        id = i;
    }

    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    public int ID
    {
        get { return id; }
        set { id = value; }
    }
}

public class GenericList<T> where T : Employee
{
    private class Node
    {
        private Node next;
        private T data;

        public Node(T t)
        {
            next = null;
            data = t;
        }

        public Node Next
        {
            get { return next; }
            set { next = value; }
        }

        public T Data
        {
            get { return data; }
            set { data = value; }
        }
    }

    private Node head;

    public GenericList() //constructor
    {
        head = null;
    }

    public void AddHead(T t)
    {
        Node n = new Node(t);
        n.Next = head;
        head = n;
    }

    public IEnumerator<T> GetEnumerator()
    {
        Node current = head;

        while (current != null)
        {
            yield return current.Data;
            current = current.Next;
        }
    }

    public T FindFirstOccurrence(string s)
    {
        Node current = head;
        T t = null;

        while (current != null)
        {
            //The constraint enables access to the Name property.
            if (current.Data.Name == s)
            {
                t = current.Data;
                break;
            }
            else
            {
                current = current.Next;
            }
        }
        return t;
    }
}


12. 제약 조건을 사용하면 형식 T의 모든 항목이 항상 Employee 개체이거나 Employee에서 상속된 개체이므로 제네릭 클래스에서 Employee.Name 속성을 사용할 수 있습니다.

13. 여러 제약 조건을 동일한 형식 매개 변수에 적용할 수 있고 제약 조건 자체가 제네릭 형식일 수도 있습니다.


class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new()
{
    // ...
}


14. 형식 매개 변수를 제한하면, 허용되는 작업의 수 및 제한하는 형식과 해당 상속 계층 구조(its inheritance hierarchy)의 모든형식에서 지원하는 메서드에 대해 허용되는 호출의 수를 늘릴 수 있습니다.

15. 따라서 제네릭 클래스나 메서드를 디자인할 때 System.Object에서 지원하지 않는 메서드를 호출하거나 제네릭 멤버에 대해 단순한 할당 이상의 작업을 수행하려는 경우에는 형식 매개 변수에 제약조건을 적용해야 합니다.

16. where T : class 제약 조건을 적용할 때는 형식 매개 변수에 대해 == 및 != 연산자를 사용하지 않아야 합니다.

17. 이러한 연산자에서는 값이 같은지(value equality) 확인하는 대신 참조가 동일한지만(reference identity) 테스트하기 때문입니다.

18. 인수로 사용되는 형식에서 이러한 연산자를 오버로드한 경우에도 마찬가지입니다(this is the case).

19. 다음 코드에서는 이러한 경우(this point)의 예를 보여 줍니다.

20. String 클래스에서 == 연산자를 오버로드해도 false가 출력됩니다.


public static void OpTest<T>(T s, T t) where T : class
{
    System.Console.WriteLine(s == t);
}
static void Main()
{
    string s1 = "target";
    System.Text.StringBuilder sb = new System.Text.StringBuilder("target");
    string s2 = sb.ToString();
    OpTest<string>(s1, s2);
}


21. 이러한 방식으로 동작하는 이유는 컴파일 시 컴파일러에는 T가 참조 형식이라는 정보만 제공되므로 모든 참조 형식에 유효한 default 연산자가 사용되기 때문입니다.

22. 값이 동일한지 테스트하려면 where T : IComparable<T> 제약 조건을 함께 적용하고 제네릭 클래스를 생성하는데 사용되는 모든 클래스에서 해당 인터페이스를 구현하는 것이 좋습니다.

23. 다음 예제와 같이 여러 매개 변수에 제약 조건을 적용하고 단일 매개 변수에 여러 제약 조건을 적용할 수 있습니다.


class Base { }
class Test<T, U>
    where U : struct
    where T : Base, new() { }

<바인딩되지 않은 형식 매개 변수>

24. public 클래스 SampleClass<T>{}의 T와 같이 제약 조건이 없는 형식 매개 변수를 바인딩되지 않은 형식(unbounded type) 매개 변수라고 합니다.

25. 바인딩 되지 않은 형식 매개 변수에는 다음과 같은 규칙이 적용됩니다.

    1) != 및 == 연산자를 사용할 수 없습니다. 구체적인 형식 인수(concrete type argument)에서 이러한 연산자를 지원하리라는 보장이 없기(there is no guarantee) 때문입니다.

    2) 바인딩 되지 않은 형식 매개 변수와 System.Object 사이에 변환하거나 이 매개 변수를 임의의 인터페이스 형식으로 명시적으로 변환할 수 있습니다.

    3) 바인딩 되지 않은 형식 매개 변수를 null과 비교할 수 있습니다. 바인딩되지 않은 매개 변수를 null과 비교하는 경우 형식 인수가 값 형식이면 비교 결과로 항상 false가 반환됩니다.

<제약 조건으로서의 형식 매개 변수>

26. generic 형식을 제약조건으로 사용하면 다음 예제에서와 같이 자체 형식 매개 변수가 있는 멤버 함수에서 해당 매개 변수를 포함 형식의 형식 매개 변수로 제한해야 하는 경우에 유용합니다.

27. 위 예제에서 T는 Add 메서드의 컨텍스트에서는 형식 제약 조건이고 List  클래스의 컨텍스트에서는 바인딩되지 않은 형식 매개 변수입니다.

28. 형식 매개 변수는 제네릭 클래스 정의에서 제약 조건으로도 사용할 수 있습니다.

29. 형식 매개 변수는 다른 형식의 매개 변수와 함께 꺾쇠 괄호 안에 선언되어야 합니다.


//Type parameter V is used as a type constraint.
public class SampleClass<T, U, V> where T : V { }


30. 컴파일러에서는 형식 매개 변수가 System.Object에서 파생된다는 점을 제외하고는 이 제약 조건에 대해 어떠한 정보도 알 수 없으므로 형식 매개 변수와 제네릭 클래스를 함께 사용할 필요는 거의 없습니다.

31. 제네릭 클래스에 대해 형식 매개 변수를 제약 조건으로 사용하는 경우로는 두 형식 매개 변수 사이에 반드시 상속 관계가 있도록 정의하는 경우를 들 수 있습니다.





제네릭 클래스


1. 제네릭 클래스는 특정 데이터 형식(a particular data type)에 고유하지 않은 작업을 캡슐화합니다.

2. 제네릭 클래스는 연결된 목록, 해시 테이블, 스택, 큐, 트리 등의 컬렉션에 가장 일반적으로 사용됩니다.

3. 컬렉션에서 항목을 추가하고 제거하는 등의 작업은 저장되는 데이터의 형식에 관계없이(regardless of) 기본적으로 동일한 방식으로 수행됩니다.

4. 컬렉션 클래스가 필요한 대부분의 시나리오에서는 .NET Framework  클래스 라이브러리에서 제공하는  컬렉션을 사용하는 것이 좋습니다.

5. 이러한 클래스 사용에 대한 자세한 내용은 .NET Framework 클래스 라이브러리의 제네릭을 참조하십시오.

6. 일반적으로(typically) 기존의 구체적인 클래스로(an existing concrete class) 시작하여 일반성(generalization)과 편의성(usability)의 균형이 맞을 때까지 형식을 하나씩 형식 매개 변수로 바꾸는 방법으로 제네릭 클래스를 만듭니다.

7. 사용자 고유의 제네릭 클래스를 만들 때는 다음과 같은 사항을 고려해야 합니다.

    1) 형식 매개 변수로 일반화할 형식:

        a) 일반적으로는(as a rule), 매개 변수화(parameterize)할 수 있는 형식이 많을수록 코드의 융통성(flexible)과 재사용 가능성(reusable)이 향상됩니다.

        b) 그러나 코드를 지나치게 일반화하면 다른 개발자가 읽거나 이해하기가 어려워집니다.

    2) 형식 매개 변수에 적용할 제약 조건(형식 매개 변수에 대한 제약 조건) 참조

        a) 필요한 형식을 처리할 수 있는 범위 내에서 최대한 많은 제약 조건을 적용하는 것이 좋습니다.

        b) 예를 들어 사용자 제네릭 클래스를 참조 형식으로만 사용하려는 경우에는 클래스 제약 조건을 적용합니다.

        c) 이렇게 하면 클래스를 값 형식으로 잘못 사용하는 것을(unintended use)) 막을 수 있고, T에 as 연산자를 사용하여 null 값 여부를 확인할 수 있습니다.

    3) 제네릭 동작을 기본 클래스와 서브클래스로 분할할지 여부.

        a) 제네릭 클래스는 기본 클래스가 될 수 있으므로(serve as) 제네릭이 아닌 클래스에 적용되는 디자인 고려 사항(considerations)이 동일하게 적용됩니다.

        b) 자세한 내용은 이 항목의 뒷부분에서 설명하는 제네릭 기본 클래스에서 상속하는 데 대한 규칙을 참조하십시오.

    4) 제네릭 인터페이스를 하나 이상 구현할지 여부

        a) 예를 들어 제네릭 기반 컬렉션(a generics-based collection)의 항목을 만드는 데 사용될 클래스를 디자인하는 경우 IComparable<T>(T는 사용자 클래스 형식)과 같은 인터페이스를 구현해야 할 수 있습니다.

8. 간단한 제네릭  클래스의 예제를 보려면 제네릭 소개를 참조하십시오.

9. 형식 매개 변수와 제약 조건에 대한 규칙은 제네릭 클래스 동작에서 특히 상속과 멤버 액세스 가능성에 대해 몇 가지 영향(several implications)을 줍니다.

10. 계속하려면(proceeding) 몇 가지 용어(some terms)를 이해하고 있어야 합니다.

11. Note<T>, 제네릭 클래스의 경우 클라이언트 코드에서는 형식 인수를 지정하여 폐쇄형 생성(a closed constructed type)(Node<int>)을 만드는 방법으로 클래스를 참조하거나, 

12. 제네릭 기본 클래스를 지정하는 경우와 같이 형식 매개 변수를 지정하지 않고 개방형 생성 형식(Node<T>)을 만드는 방법으로 클래스를 참조할 수 있습니다.

13. 제네릭 클래스는 구체적인 클래스, 폐쇄형 구성 클래스 또는 개방형 구성 기본 클래스에서 상속할 수 있습니다.


class BaseNode { }
class BaseNodeGeneric<T> { }

// concrete type
class NodeConcrete<T> : BaseNode { }

//closed constructed type
class NodeClosed<T> : BaseNodeGeneric<int> { }

//open constructed type 
class NodeOpen<T> : BaseNodeGeneric<T> { }


14. 제네릭이 아닌 구체적인 클래스는 폐쇄형 생성 기본 클래스에서는 상속할 수 있지만 개방형 생성 클래스 또는 형식 매개 변수에서는 상속할 수 없습니다.

15. 이는 런타임에 클라이언트 코드에서 기본 클래스를 인스턴스화할 때 필요한 형식 인수를 제공할 수 없기 때문이다.


//No error
class Node1 : BaseNodeGeneric<int> { }

//Generates an error
//class Node2 : BaseNodeGeneric<T> {}

//Generates an error
//class Node3 : T {}


16. 개방형 생성 형식에서 상속하는 제네릭 클래스에서는 다음 코드와 같이 상속하는 클래스에서 공유하지 않는 모든 클래스 형식 매개 변수에 대해 형식 인수를 제공해야 합니다.


class BaseNodeMultiple<T, U> { }

//No error
class Node4<T> : BaseNodeMultiple<T, int> { }

//No error
class Node5<T, U> : BaseNodeMultiple<T, U> { }

//Generates an error
//class Node6<T> : BaseNodeMultiple<T, U> {} 


17. 개방형 생성 형식에서 상속하는 제네릭 클래스에서는 기본 형식에 대한 제약 조건(a superset of)을 포함하거나 암시적인 제약 조건을 지정해야 합니다.


class NodeItem<T> where T : System.IComparable<T>, new() { }
class SpecialNodeItem<T> : NodeItem<T> where T : System.IComparable<T>, new() { }


18. 제네릭 형식은 다음과 같이 여러 형식 매개 변수 및 제약 조건을 사용할 수 있습니다.


class SuperKeyType<K, V, U>
    where U : System.IComparable<U>
    where V : new()
{ }


19. 개방형 생성 형식 및 폐쇄형 생성 형식은 메서드 매개 변수로 사용될 수 있습니다.


void Swap<T>(List<T> list1, List<T> list2)
{
    //code to swap items
}

void Swap(List<int> list1, List<int> list2)
{
    //code to swap items
}


20. 제네릭 클래스에서 인터페이스를 구현하면 이 클래스의 모든 인스턴스를 해당 인터페이스에 캐스팅할 수 있습니다.

21. 제네릭 클래스는 비가변적(invariant)입니다.

22. 즉, 입력 매개 변수에서 List<BaseClass>를 지정하면 List<DerivedClass>를 제공하려고 할 때 컴파일 타임 오류가 발생합니다.





제네릭 인터페이스


1. 제네릭 컬렉션 클래스 또는 컬렉션의 항목을 나타내는 제네릭 클래스에 대한 인터페이스를 정의하는 것이 유용한 경우가 많습니다.

2. 값 형식에 대해 boxing 및 unboxing 연산을 하지 않으려면 제네릭 클래스에서 IComparable 대신 IComparable<T> 같은 제네릭 인터페이스를 사용하는 것이 좋습니다(the preference).

3. .NET Framework 클래스 라이브러리에는 System.Collections.Generic 네임스페이스의 컬렉션 클래스에 사용할 제네릭 인터페이스가 여러 개 정의되어 있습니다.

4. 인터페이스를 형식 매개 변수에 대한 제약 조건으로 지정한 경우 이 인터페이스를 구현하는 형식만 사용할 수 있습니다.

5. 다음 코드 예제에서는 GenericList<T>에서 파생되는 SortedList<T> 클래스를 보여 줍니다.

6. 자세한 내용은 제네릭 소개를  참조하십시오.

7. SortedList<T>는 where T : IComparable<T> 제약 조건을 추가합니다

8. 따라서 SortedList<T>의 BubbleSort 메서드에서는 목록 요소에 대해 제네릭 CompareTo 메서드를 사용할 수 있습니다.

9. 이 예제에서 목록 요소는 IComparable<Person>을 구현하는 단순 클래스인 Person입니다.


//Type parameter T in angle brackets.
public class GenericList<T> : System.Collections.Generic.IEnumerable<T>
{
    protected Node head;
    protected Node current = null;

    // Nested class is also generic on T
    protected class Node
    {
        public Node next;
        private T data;  //T as private member datatype

        public Node(T t)  //T used in non-generic constructor
        {
            next = null;
            data = t;
        }

        public Node Next
        {
            get { return next; }
            set { next = value; }
        }

        public T Data  //T as return type of property
        {
            get { return data; }
            set { data = value; }
        }
    }

    public GenericList()  //constructor
    {
        head = null;
    }

    public void AddHead(T t)  //T as method parameter type
    {
        Node n = new Node(t);
        n.Next = head;
        head = n;
    }

    // Implementation of the iterator
    public System.Collections.Generic.IEnumerator<T> GetEnumerator()
    {
        Node current = head;
        while (current != null)
        {
            yield return current.Data;
            current = current.Next;
        }
    }

    // IEnumerable<T> inherits from IEnumerable, therefore this class 
    // must implement both the generic and non-generic versions of 
    // GetEnumerator. In most cases, the non-generic method can 
    // simply call the generic method.
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

public class SortedList<T> : GenericList<T> where T : System.IComparable<T>
{
    // A simple, unoptimized sort algorithm that 
    // orders list elements from lowest to highest:

    public void BubbleSort()
    {
        if (null == head || null == head.Next)
        {
            return;
        }
        bool swapped;

        do
        {
            Node previous = null;
            Node current = head;
            swapped = false;

            while (current.next != null)
            {
                //  Because we need to call this method, the SortedList
                //  class is constrained on IEnumerable<T>
                if (current.Data.CompareTo(current.next.Data) > 0)
                {
                    Node tmp = current.next;
                    current.next = current.next.next;
                    tmp.next = current;

                    if (previous == null)
                    {
                        head = tmp;
                    }
                    else
                    {
                        previous.next = tmp;
                    }
                    previous = tmp;
                    swapped = true;
                }
                else
                {
                    previous = current;
                    current = current.next;
                }
            }
        } while (swapped);
    }
}

// A simple class that implements IComparable<T> using itself as the 
// type argument. This is a common design pattern in objects that 
// are stored in generic lists.
public class Person : System.IComparable<Person>
{
    string name;
    int age;

    public Person(string s, int i)
    {
        name = s;
        age = i;
    }

    // This will cause list elements to be sorted on age values.
    public int CompareTo(Person p)
    {
        return age - p.age;
    }

    public override string ToString()
    {
        return name + ":" + age;
    }

    // Must implement Equals.
    public bool Equals(Person p)
    {
        return (this.age == p.age);
    }
}

class Program
{
    static void Main()
    {
        //Declare and instantiate a new generic SortedList class.
        //Person is the type argument.
        SortedList<Person> list = new SortedList<Person>();

        //Create name and age values to initialize Person objects.
        string[] names = new string[] 
        { 
            "Franscoise", 
            "Bill", 
            "Li", 
            "Sandra", 
            "Gunnar", 
            "Alok", 
            "Hiroyuki", 
            "Maria", 
            "Alessandro", 
            "Raul" 
        };

        int[] ages = new int[] { 45, 19, 28, 23, 18, 9, 108, 72, 30, 35 };

        //Populate the list.
        for (int x = 0; x < 10; x++)
        {
            list.AddHead(new Person(names[x], ages[x]));
        }

        //Print out unsorted list.
        foreach (Person p in list)
        {
            System.Console.WriteLine(p.ToString());
        }
        System.Console.WriteLine("Done with unsorted list");

        //Sort the list.
        list.BubbleSort();

        //Print out sorted list.
        foreach (Person p in list)
        {
            System.Console.WriteLine(p.ToString());
        }
        System.Console.WriteLine("Done with sorted list");
    }
}


10. 단일 형식에 다음과 같이 여러 인터페이스를 제약 조건으로 지정할 수 있습니다.


class Stack<T> where T : System.IComparable<T>, IEnumerable<T>
{
}


11. 인터페이스에서는 다음과 같이 두 개 이상의 형식 매개 변수를 정의할 수  있습니다.


interface IDictionary<K, V>
{
}


12. 클래스에 적용되는 상속 규칙이 인터페이스에도 적용됩니다.


interface IMonth<T> { }

interface IJanuary     : IMonth<int> { }  //No error
interface IFebruary<T> : IMonth<int> { }  //No error
interface IMarch<T>    : IMonth<T> { }    //No error
//interface IApril<T>  : IMonth<T, U> {}  //Error


13. 제네릭 인터페이스에 반공변성(contra-variant)이 있는 경우, 즉 형식 매개 변수만 반환 값으로 사용하는 경우 제네릭 인터페이스는 제네릭이 아닌 인터페이스에서 상속할 수 있습니다.

14. .NET Framework 클래스 라이브러리에서 IEnumerable<T>GetEnumerator의 반환 값 및 Current 속성 getter에 T만 사용하므로 IEnumerable<T>IEnumerable에서 상속합니다.

15. 구체적인 클래스에서는 다음과 같이 폐쇄형 구성 인터페이스를 구현할 수 있습니다.


interface IBaseInterface<T> { }

class SampleClass : IBaseInterface<string> { }


16. 제네릭 클래스는 클래스 매개 변수 목록에서 인터페이스에 필요한 모든 인수를 제공하는 경우 다음과 같이 제네릭 인터페이스 또는 폐쇄형 구성 인터페이스를 구현할 수 있습니다.


interface IBaseInterface1<T> { }
interface IBaseInterface2<T, U> { }

class SampleClass1<T> : IBaseInterface1<T> { }          //No error
class SampleClass2<T> : IBaseInterface2<T, string> { }  //No error


17. 제네릭 클래스, 제네릭 구조체 또는 제네릭 인터페이스 내의 메서드에는 메서드 오버로드를 제어하는 규칙이 동일하게 적용됩니다.

18. 자세한 내용은 제네릭 메서드를 참조하십시오.





제네릭 메서드


1. 제네릭 메서드는 다음과 같이 형식 매개 변수를 사용하여 선언한 메서드입니다.


static void Swap<T>(ref T lhs, ref T rhs)
{
    T temp;
    temp = lhs;
    lhs = rhs;
    rhs = temp;
}


2. 다음 코드 예제에서는 형식 인수에 int를 사용하여 메서드를 호출하는 방법을 보여 줍니다.


public static void TestSwap()
{
    int a = 1;
    int b = 2;

    Swap<int>(ref a, ref b);
    System.Console.WriteLine(a + " " + b);
}


3. 형식 인수를 생략하고 컴파일러에서 이를 자동으로 유추하도록 할 수도 있습니다.

4. Swap에 대한 다음 호출은 위 예제의 호출과 동일한 작업을 수행합니다.


Swap(ref a, ref b);


5. 정적 메서드와 인스턴스 메서드에는 형식 유추(inference)를 위한 동일한 규칙이 적용됩니다.

6. 컴파일러에서는 사용자가 전달하는  메서드 인수를 기준으로 형식 매개 변수를 유추할 수 있지만 제약 조건이나 반환 값만으로는 형식 매개 변수를 유추할 수 없습니다.

7. 따라서 매개 변수가 없는 메서드에 대해서는 형식 유추가 실행되지 않습니다.

8. 형식 유추는 컴파일러에서 오버로드된 메서드 시그니처를 확인(resolve)하려고 하기 전에 컴파일 타임에 진행됩니다.

9. 컴파일러는 동일한 이름을 공유하는 모든 제네릭 메서드에 형식 유추 논리를 적용합니다.

10. 오버로드 확인(resolution) 단계(step)에서 컴파일러는 형식 유추에 성공한 제네릭 메서드만 포함합니다.

11. 제네릭 클래스 내에서 제네릭이 아닌 메서드는 다음과 같이 클래스 수준 형식 매개 변수에 액세스할 수 있습니다.


class SampleClass<T>
{
    void Swap(ref T lhs, ref T rhs) { }
}


9. 포함하는 클래스(containing class)와 동일한 형식 매개 변수를 사용하는 제네릭 메서드를 정의하면 컴파일러에서 CS0693 경고가 발생합니다.

10. 이는 메서드 범위 내에서 내부 T에 대해 제공한 인수가 외부 T에 대에 제공한 인수를 숨기기 때문입니다.

11. 클래스를 인스턴스화할 때 제공한 인수가 아닌 다른 형식 인수를 사용하여 제네릭 클래스 메서드를 호출할 수 있으려면 다음 예제의 GenericList2<T>에서와 같이 메서드의 형식 매개 변수에 다른 식별자를 제공하는 것이 좋습니다.


class GenericList<T>
{
    // CS0693
    void SampleMethod<T>() { }
}

class GenericList2<T>
{
    //No warning
    void SampleMethod<U>() { }
}


12. 메서드에서 형식 매개 변수에 대한 더 구체적인 작업(more specialized operations)을 수행하려면 제약 조건을 사용합니다.

13. SwapIfGreater<T>라는 이 버전의 Swap<T>은 IComparable<T>을 구현하는 형식 인수와만 함께 사용할 수 있습니다.


void SwapIfGreater<T>(ref T lhs, ref T rhs) where T : System.IComparable<T>
{
    T temp;
    if (lhs.CompareTo(rhs) > 0)
    {
        temp = lhs;
        lhs = rhs;
        rhs = temp;
    }
}


14. 제네릭 메서드는 여러 형식 매개 변수에 대해 오버로드할 수 있습니다.

15. 예를 들어 다음 메서드에서는 모두 동일한 클래스에 지정할 수 있습니다.


void DoWork() { }
void DoWork<T>() { }
void DoWork<T, U>() { }





제네릭 대리자


1. 대리자는 자체 형식 매개 변수를 정의할 수 있습니다.

2. 제네릭 대리자를 참조하는 코드에서는 제네릭 클래스를 인스턴스화하거나 제네릭 메서드를 호출할 때와 마찬가지로 다음 예제와 같이 형식 매개 변수를 지정하여 폐쇄형 구성 형식을 만들 수 있습니다.


public delegate void Del<T>(T item);
public static void Notify(int i) { }

Del<int> m1 = new Del<int>(Notify);


3. C#  버전 2.0에는 메서드 그룹 변환이라는 새로운 기능이 있습니다.

4. 이 기능은 제네릭 대리자뿐만 아니라 구체적인 대리자에도 적용되며, 이를 통해 위 코드를 다음과 같이 단순화된 구문으로 작성할 수 있습니다.


Del<int> m2 = Notify;


5. 제네릭 클래스 내에 정의된 대리자에서는 클래스 메서드와 마찬가지로 제네릭 클래스 형식 매개 변수를 사용할 수 있습니다.


class Stack<T>
{
    T[] items;
    int index;

    public delegate void StackDelegate(T[] items);
}

6. 대리자를 참조하는 코드에서는 다음과 같이 포함하는 클래스의 형식 매개 변수를 지정해야 합니다.


private static void DoWork(float[] items) { }

public static void TestStack()
{
    Stack<float> s = new Stack<float>();
    Stack<float>.StackDelegate d = DoWork;
}


7. sender 인수에 강력한 형식을 사용할 수 있고 Object 사이에서 캐스팅할 필요가 없으므로 제네릭 대리자는 특히 일반적인 디자인 패턴을 기반으로 이벤트를 정의할 때 유용합니다.


delegate void StackEventHandler<T, U>(T sender, U eventArgs);

class Stack<T>
{
    public class StackEventArgs : System.EventArgs { }
    public event StackEventHandler<Stack<T>, StackEventArgs> stackEvent;

    protected virtual void OnStackChanged(StackEventArgs a)
    {
        stackEvent(this, a);
    }
}

class SampleClass
{
    public void HandleStackChange<T>(Stack<T> stack, Stack<T>.StackEventArgs args) { }
}

public static void Test()
{
    Stack<double> s = new Stack<double>();
    SampleClass o = new SampleClass();
    s.stackEvent += o.HandleStackChange;
}





제네릭 코드의 default 키워드


1. 제네릭 클래스 및 메서드에서 다음과 같은 내용을 미리 알지 못하는 경우 매개 변수화된 형식 T에 기본값을 할당하는 방법에 대한 문제가 발생합니다(arise).

    1) T가 참조 형식인지 값 형식인지 여부

    2) T가 값 형식인 경우 숫자 값인지 구조체인지 여부

2. t가 매개 변수화된 형식 T의 변수인 경우 "t = null"과 같은 구문은 T가 참조 형식인 경우에만 유효하고 "t = 0"은 구조체가 아닌 숫자 값 형식인 경우에만 사용할 수 있습니다.

3. 참조 형식에 대해서는 null을 반환하고 숫자 값 형식에는 0을 반환하는 default 키워드를 사용하면 이 문제를 해결할 수 있습니다.

4. 구조체에 대해서는 멤버가 값 형식인지 참조 형식인지 여부에 따라 0 또는 null로 초기화된 구조체의 각 멤버가 반환됩니다.

5. null 허용 값 형식의 경우 default는 다른 구조체와 같이 초기화되는 System.Nullable<T>을  반환합니다.

6. GenericList<T> 클래스에 있는 다음 예제에서는 default 키워드를 사용하는 방법을 보여 줍니다.

7. 자세한 내용은 제네리 개요(Overview)를 참조하십시오.


namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            // Test with a non-empty list of integers.
            GenericList<int> gll = new GenericList<int>();
            gll.AddNode(5);
            gll.AddNode(4);
            gll.AddNode(3);
            int intVal = gll.GetLast();
            // The following line displays 5.
            System.Console.WriteLine(intVal);

            // Test with an empty list of integers.
            GenericList<int> gll2 = new GenericList<int>();
            intVal = gll2.GetLast();
            // The following line displays 0.
            System.Console.WriteLine(intVal);

            // Test with a non-empty list of strings.
            GenericList<string> gll3 = new GenericList<string>();
            gll3.AddNode("five");
            gll3.AddNode("four");
            string sVal = gll3.GetLast();
            // The following line displays five.
            System.Console.WriteLine(sVal);

            // Test with an empty list of strings.
            GenericList<string> gll4 = new GenericList<string>();
            sVal = gll4.GetLast();
            // The following line displays a blank line.
            System.Console.WriteLine(sVal);
        }
    }

    // T is the type of data stored in a particular instance of GenericList.
    public class GenericList<T>
    {
        private class Node
        {
            // Each node has a reference to the next node in the list.
            public Node Next;
            // Each node holds a value of type T.
            public T Data;
        }

        // The list is initially empty.
        private Node head = null;

        // Add a node at the beginning of the list with t as its data value.
        public void AddNode(T t)
        {
            Node newNode = new Node();
            newNode.Next = head;
            newNode.Data = t;
            head = newNode;
        }

        // The following method returns the data value stored in the last node in
        // the list. If the list is empty, the default value for type T is
        // returned.
        public T GetLast()
        {
            // The value of temp is returned as the value of the method. 
            // The following declaration initializes temp to the appropriate 
            // default value for type T. The default value is returned if the 
            // list is empty.
            T temp = default(T);

            Node current = head;
            while (current != null)
            {
                temp = current.Data;
                current = current.Next;
            }
            return temp;
        }
    }
}





C++ 템플릿과 C# 제네릭의 차이점


1. C# 제네릭과 C++ 템플릿은 모두 매개 변수가 있는 형식을 지원하는 언어 기능(language features)입니다.

2. 그러나 이 둘 사이에는 여러 가지 차이점이 있습니다.

3. 구문이라는 측면에서(At the syntax level) C# 제네릭은 C++ 템플릿처럼 복잡하지 않으며 매개 변수가 있는 형식을 보다 간편하게 다룰 수 있습니다.

4. 또한 C#에서는 C++ 템플릿이 제공하는 모든 기능을 제공하지는 않습니다.

5. 구현이라는 측면에서 가장 큰 차이점은, C# 제네릭 형식 대체(substitutions)는 런타임에 수행되므로 인스턴스화된 개체에서 제네릭 형식 정보가 유지(preserve)된다는 점입니다.

6. 자세한 내용은 런타임의 제네릭을 참조하십시오.

7. 다음은 C# 제네릭과 C++ 템플릿 간의 주요 차이점(the key differences)입니다.

    1) C# 제네릭은 C++ 템플릿과 같은 정도의 융통성(flexibility)을 제공하지 않습니다. 예를 들어, C# 제네릭 클래스에서 사용자 정의 연산자를 호출할 수는 있지만 산술 연산자를 호출할 수는 없습니다.

    2) C#에서는 template C<int i> {} 같은 비형식 템플릿 매개 변수(non-type template parameters)를 허용하지 않습니다.

    3) C#에서는 명시적 특수화(explicit specialization)를 지원하지 않습니다. 즉, 특정 형식에 대한 템플릿을 사용자 지정하여 구현할 수 없습니다.

    4) C#에서는 부분 특수화(partial specialization)를 지원하지 않습니다. 즉, 형식 인수의 하위 집합(subset)을 사용자 지정하여 구현할 수 없습니다.

    5) C#에서는 형식 매개 변수를 제네릭 형식에 대한 기본 클래스로 사용할 수 없습니다.

    6) C#에서는 형식 매개 변수에 기본 형식을 사용할 수 없습니다.

    7) C#에서는 생성된 형식을 제네릭으로 사용할 수는 있지만 제네릭 형식 매개 변수 자체가 제네릭이 될 수는 없습니다. C++에서는 템플릿 매개 변수를 허용합니다.

    8) C++에서는 템플릿의 모든 형식 매개 변수에 대해서는 유효하지 않을 수 있는 코드가 허용되며 이러한 코드는 형식 매개 변수로 사용되는 특정 형식에 대해 검사를 받습니다. C#에서는 제약 조건을 충족하는 모든 형식과 함께 사용할 수 있도록 클래스의 코드를 작성해야 합니다. 예를 들어 C++의 경우 형식 매개 변수의 개체에 대해 + 및 - 산술 연산자를 사용하는 함수를 작성할 수 있습니다. 이 경우 이러한 연산자를 지원하지 않는 형식으로 템플릿을 인스턴스화할 때 오류가 발생합니다. C#에서는 이러한 함수를 작성할 수 없고(disallow) 제약 조건에서 추론 가능한(deduce) 언어 구문(consructs)만 사용할 수 있습니다.





제네릭 및 리플렉션


1. CLR(공용 언어 런타임)에서는 런타임에 제네릭 형식 정보에 액세스할 수 있으므로 리플렉션(reflection)을 사용하면 제네릭이 아닌 형식에 대해서와 동일한 방식으로 제네릭 형식에 대한 정보를 얻을 수 있습니다.

2. 자세한 내용은 런타임의 제네릭을 참조하십시오.

3. .NET Framework 2.0에서는 제네릭 형식에 대한 런타임 정보를 사용할 수 있도록 여러 가지 새로운 멤버가 Type 클래스에 추가되었습니다.

4. 이러한 메서드와 속성의 사용 방법에 대한 자세한 내용은 해당 클래스에 대한 문서를 참조하십시오.

5. System.Reflection.Emit 네임스페이스에도 제네릭을 지원하는 새 멤버가 추가되었습니다.

6. 자세한 내용은 방법: 리플렉션 내보내기를 사용하여 제네릭 형식 정의를 참조하십시오.

7. 제네릭 리플렉션에 사용되는 용어에 대한 고정 조건(invariant conditions) 목록은 IsGenericType 속성 설명을 참조하십시오.


System.Type 멤버 이름

설명

IsGenericType

형식이 제네릭인 경우 true를 반환합니다.

GetGenericArguments

제네릭 형식 정의의 형식 매개 변수 또는 생성된 형식에 대해 제공되는 형식 인수를 나타내는 Type 개체의 배열을 반환합니다.

GetGenericTypeDefinition

현재 생성된 형식의 내부(underlying) 제네릭 형식 정의를 반환합니다.

GetGenericParameterConstraints

현재 제네릭 형식 매개 변수에 대한 제약 조건을 나타내는 Type 개체의 배열을 반환합니다.

ContainsGenericParameters

특정 형식이 지정되지 않은 형식 매개 변수가 형식 또는 형식의 바깥쪽 형식이나 메서드에 포함되어 있는 경우 true를 반환합니다.

GenericParameterAttributes

현재 제네릭 형식 매개 변수의 특수 제약 조건을 설명하는 GenericParameterAttributes 플래그의 조합을 가져옵니다.

GenericParameterPosition

형식 매개 변수를 나타내는 Type 개체에 대해 형식 매개 변수를 선언한 제네릭 메서드 정의 또는 제네릭 형식 정의의 형식 매개 변수 목록에서 형식 매개 변수가 차지하는 위치를 가져옵니다.

IsGenericParameter

현재 Type이 제네릭 형식이나 메서드 정의의 형식 매개 변수를 표시하는지 여부를 나타내는 값을 가져옵니다.

IsGenericTypeDefinition

현재 Type이 다른 제네릭 형식을 생성하는 데 사용할 수 있는 제네릭 형식 정의를 표시하는지 여부를 나타내는 값을 가져옵니다. 형식이 제네릭 형식의 정의를 표시하는 경우 true를 반환합니다.

DeclaringMethod

현재 제네릭 형식 매개 변수를 정의한 제네릭 메서드를 반환합니다. 형식 매개 변수가 제네릭 메서드를 통해 정의되지 않은 경우 null을 반환합니다.

MakeGenericType

현재 제네릭 형식 정의의 형식 매개 변수에 대한 형식 배열의 요소를 대체하고, 생성된 결과 형식을 나타내는 Type 개체를 반환합니다.

8. 또한 제네릭 메서드에 대한 런타임 정보를 사용할 수 있도록 새로운 멤버가 MethodInfo 클래스에 추가되었습니다.

9. 제네릭 메서드 리플렉션에 사용되는 용어에 대한 고정 조건의 목록을 보려면 IsGenericMethod 속성 설명(remarks)을 참조하십시오.


System.Reflection.MemberInfo 멤버 이름

설명

IsGenericMethod

메서드가 제네릭인 경우 true를 반환합니다.

GetGenericArguments

생성된 제네릭 메서드의 형식 인수나 제네릭 메서드 정의의 형식 매개 변수를 나타내는 Type 개체의 배열을 반환합니다.

GetGenericMethodDefinition

현재 생성된 메서드의 내부 제네릭 메서드 정의를 반환합니다.

ContainsGenericParameters

특정 형식이 지정되지 않은 형식 매개 변수가 메서드 또는 메서드의 바깥쪽 형식에 포함되어 있는 경우 true를 반환합니다.

IsGenericMethodDefinition

현재 MethodInfo가 제네릭 메서드의 정의를 나타내는 경우 true를 반환합니다.

MakeGenericMethod

현재 제네릭 메서드 정의의 형식 매개 변수를 형식 배열의 요소로 대체하고, 생성된 메서드를 나타내는MethodInfo 개체를 반환합니다.




런타임의 제네릭


1. 제네릭 형식 또는 메서드가 MSIL(Microsoft Intermediate Language)로 컴파일되면 자체적으로 형식 매개 변수를 갖는 것으로 식별하는(identify) 메타데이터가 포함됩니다.

2. 제네릭 형식의 MSIL이 사용되는 방식은 제공된 형식 매개 변수가 값 형식인지 참조 형식인지에 따라 다릅니다.

3. 값 형식을 매개 변수로 사용하여 제네릭 형식을 처음 생성할 경우 런타임에서는 제공된 매개 변수를 MSIL의 해당 위치에 대체하여 특수화된 제네릭 형식을 만듭니다.

4. 고유한 값 형식이 매개 변수로 사용될 때마다 특수화된 제네릭 형식이 만들어집니다.

5. 예를 들어 프로그램 코드에서 다음과 같이 정수로 구성된 스택을 선언한다고 가정합니다.


Stack<int> stack;


6. 이 시점에서 런타임은 매개 변수를 정수로 적절히 대체하여 특수화된 버전의 Stack<T> 클래스를 생성합니다.

7. 이제부터 프로그램에서 정수 스택을 사용하여 런타임은 생성된 특수화 Stack<T> 클래스를 다시 사용합니다.

8. 다음 예제에서는 Stack<int> 코드의 단일 인스턴스를 공유하는 정수 스택의 두 인스턴스를 만듭니다.


Stack<int> stackOne = new Stack<int>();
Stack<int> stackTwo = new Stack<int>();


9. 그러나 코드의 다른 지점에서 long과 같이 값 형식이 다르거나, 사용자 정의된 구조체를 매개 변수로 사용하는 다른 Stack<T> 클래스를 만들었다고 가정(suppose)해 보십시오.

10. 이 경우 런타임에서는 제네릭 형식의 다른 버전을 생성하여 MSIL의 적절한 위치에 long을 대체합니다.

11. 특수화된 각 제네릭 클래스에는 내부적으로 값 형식이 포함되므로 변환은 더 이상 필요하지 않습니다.

12. 참조 형식에 대해서는 제네릭의 작동 방식이 조금(somewhat) 다릅니다.

13. 참조 형식을 사용하여 제네릭 형식이 처음 생성될 때 런타임에서는 MSIL의 매개 변수를 개체 참조(object references)로 대체하여 특수화된 제네릭 형식을 만듭니다.

14. 이후 참조 형식에 관게없이 참조 형식을 매개 변수로 사용하여 생성된 형식이 인스턴스화될 때마다 런타임에서는 이전에 만든 특수화된 버전의 제네릭 형식을 다시 사용합니다.

15. 이는 모든 참조의 크기가 동일하기 때문에 가능합니다.

16. 예를 들어 Customer 클래스와 Order 클래스라는 두 참조 형식이 있고 Customer 형식의 스택을 만들었다고 가정합니다.


class Customer { }
class Order { }


Stack<Customer> customers;


17. 이 시점에서 런타임은 데이터를 저장하는 대신 이후에 채워질 개체 참조를 저장하는 특수화된 버전의 Stack<T> 클래스를 생성합니다.

18. 코드의 다음 줄에서 Order라는 다른 참조 형식의 스택을 만든다고 가정해 봅니다.


Stack<Order> orders = new Stack<Order>();


19. 값 형식과는 달리, Order 형식에 대한 또 다른 특수화된 버전의 Stack<T> 클래스는 만들어지지 않습니다.

20. 대신 특수화된 버전의 Stack<T> 클래스 인스턴스가 만들어지고 orders 변수가 이 인스턴스를 참조하도록 설정됩니다.

21. 이후에 Customer 형식의 스택을 만드는 코드 줄이 나타난다고 가정해 봅니다.


customers = new Stack<Customer>();


22. Order 형식을 사용하여 만든 Stack<T> 클래스를 사용한 경우와 마찬가지로 특수화된 Stack<T> 클래스의 다른 인스턴스가 생성됩니다.

23. 여기에(therein) 포함된 포인터는 Customer 형식과 크기가 같은 메모리 영역을 참조하도록 설정됩니다.

24. 참조 형식의 수는 프로그램마다 크게(widly) 다를 수(vary) 있으므로, 제네릭을 C# 방식으로 구현하면 컴파일러가 참조 형식의 제네릭 클래스에 대해 만드는 특수화된 클래스의 수가 1개로 줄어들어 코드가 매우 간결해집니다.

25. 뿐만 아니라(moreover) 값 형식 또는 참조 형식 매개 변수를 사용하여 제네릭 C# 클래스가 인스턴스화되면 런타임에 리플렉션을 통해 쿼리(query)할 수 있고 실제 형식과 형식 매개 변수를 모두 조사(ascertain)할 수 있습니다.





.NET Framework 클래스 라이브러리의 제네릭


1. .NET Framework 클래스 라이브러리 버전 2.0에서는 System.Collections.Generic이라는 새로운 네임스페이스를 제공하며, 이 네임스페이스에는 바로 사용할 수 있는(several ready-to-use) 제네릭 컬렉션 클래스 및 관련(associated) 인터페이스가 있습니다.

2. System 같은 기타 네임스페이스에서도 IComparable<T> 같은 새로운 제네릭 인터페이스를 제공합니다.

3. 이러한 클래스 및 인터페이스는 이전 버전의 .NET Framework에서 제공하던 제네릭이 아닌 컬렉션 클래스보다 효율적이고 형식 안전적입니다.

4. 사용자 지정 컬렉션 클래스를 디자인하고 구현가지 전에 .NET Framework 클래스 라이브러리에서 제공하는 클래스를 사용하거나 파생시킬 수 있는지 고려해 보십시오.



'프로그래밍 > C#' 카테고리의 다른 글

스레드  (0) 2016.06.14
보간된 문자열(Interpolated strings)  (0) 2016.06.14
C# 기타  (0) 2016.06.10
C# 컨텍스트 키워드  (0) 2016.06.06
C# 키워드 T~Z  (0) 2016.06.03
:
Posted by 지훈2