IT_Programming/C#

[펌] C#: C++에서 C#으로의 전환을 위해 알아 두어야 할 사항 - Jesse Libe

JJun ™ 2007. 3. 3. 23:55
 
이 글을 이해하려면 C++에 대한 기본 지식이 있어야 합니다.
난이도     1   2   3 
설명된 소스 코드 다운로드: CtoCsharp(41KB) 
코드 센터에서 이 글에 사용된 코드 찾아보기: C to C#
요약 C#는 C++의 구문과 의미를 바탕으로 설계되었으므로, C 프로그래머는 .NET 및 공용 언어 런타임을 충분히 활용할 수 있습니다. C++에서 C#으로의 전환은 비교적 쉬운 작업이지만, new, struct, 생성자 및 소멸자에 대한 변경 사항을 포함하여 몇 가지 유의해야 할 부분도 있습니다. 이 글에서는 가비지 수집, 속성, foreach 루프 및 인터페이스와 같은 C#에서 새로운 언어 기능을 살펴봅니다. 인터페이스를 다루면서 속성, 배열 및 기본 클래스 라이브러리도 함께 다룰 것입니다. 이어서, 비동기 I/O, 특성 및 검토, 형식 발견, 동적 호출 등에 대해 살펴봅니다.
거의 10년을 주기로 개발자들은 새로운 프로그래밍 기술을 익히기 위해 시간과 노력을 투자하고 있습니다. 1980년대 초에는 Unix와 C, 1990년대 초에는 Windows와 C++, 그리고 지금은 Microsoft .NET Framework와 C#이 대표적입니다. 이러한 과정이 진행되면서 들인 비용보다는 얻는 이익이 훨씬 더 많았습니다. 반가운 소식은 C#과 .NET으로 이루어지는 거의 모든 프로젝트의 분석 및 디자인 단계가 C++과 Windows의 방식에서도 바뀐 점이 별로 없다는 것입니다. 하지만 새로운 환경에서의 프로그래밍 접근 방식에는 상당한 차이가 있습니다. 이 글에서는 C++ 프로그래밍에서 C# 프로그래밍으로 전환할 수 있는 방법과 정보를 소개합니다.
C# 구현의 향상 내용에 대해 소개한 자료는 많으므로 여기에서 다시 반복하지는 않겠습니다(예:
http://www.microsoft.com/korea/msdnmag/issues/0900/csharp/csharp.asp). 대신, C++과 C# 사이에 가장 많이 변경되었다고 생각되는 사항, 즉 관리가 없는 환경에서 관리 가능한 환경으로의 변화에 대해 집중적으로 살펴볼 것입니다. 또한 부주의한 C++ 프로그래머들이 주의해야 할 몇 가지 중요한 함정에 대해 소개하고, 프로그램 구현 방법에 영향을 미치는 새로운 언어 기능에 대해서도 살펴볼 것입니다.

맨 위로


관리 가능한 환경으로 이동

C++은 플랫폼에 구애 받지 않는 하위 단계 수준 의 개체 지향형 프로그래밍 언어로 고안되었습니다. C#은 C++보다 상위 단계 수준 의 구성 요소 지향형 언어로 고안되었습니다. 관리 가능한 환경으로의 이동은 프로그래밍이라는 영역에서는 커다란 변화입니다. C#은 정확한 제어 보다는 전체적인 모습을 볼 수 있는 프레임워크를 제공합니다.
예를 들어, C++의 경우 생성은 물론 개체의 레이아웃에 대해서도 많은 제어 권한을 가집니다. 즉 C++은 배치 연산자 new를 사용하여 개체를 다른 개체 스택 및 힙 위에 또는 메모리의 특정 위치에도 만들 수 있습니다.
.NET의 관리 가능한 환경에서는 이러한 수준의 제어를 포기해야 합니다. 개체의 형식을 선택하면 해당 개체가 어디에 만들어지는지 암시적으로 정해집니다. 일부 형식(int, double 및 long)은 항상 스택(다른 개체 내부에 포함되는 경우 제외)에 만들어지고, 클래스는 항상 힙에 만들어집니다. 개체를 힙의 어디에 만들 것인지 제어할 수 없고, 해당 주소를 얻을 수 없으며, 특정 메모리 위치에 둘 수도 없습니다. 이러한 제약 사항을 해결할 수 있는 방법이 있기는 하지만 이 글의 주제에서 벗어나므로 생략하도록 합니다.
이제는 더 이상 개체의 수명을 제어할 수 없습니다. C#에는 소멸자가 없습니다. 개체 저장 영역에 대한 참조가 더 이상 없을 경우에는 가비지 수집기가 해당 항목의 저장 영역을 회수할 것이지만, 그 시기에 대해서는 알 수 없습니다.
C#의 구조를 통해 기본 프레임워크를 알 수 있습니다. 다중 상속은 관리되거나 가비지 수집이 이루어지는 환경에서는 효과적으로 구현하기가 매우 어렵고, 일반 사항은 프레임워크에서 구현되지 않았기 때문에 다중 상속과 템플릿이 없습니다.
간단한 C# 형식은 하부 CLR(공용 언어 런타임) 형식에 대한 매핑에 불과합니다. 예를 들면, C# int는 System.Int32에 매핑됩니다. C#에서 형식은 언어가 아닌 공용 형식 시스템에 의해 결정됩니다. 사실, 사용자가 Visual Basic 개체에서 C# 개체를 만들어 내는 능력을 보유하려면 모든 .NET 언어에 의해 공유되는 기능인 공용 언어 하위 집합에 종속되어야 합니다.
반면, 관리 가능한 환경 및 CLR를 통해 많은 이점을 얻을 수 있습니다. 가비지 수집 및 모든 .NET 언어에서의 단일 형식 시스템 이외에도, 크게 향상된 구성 요소 기반 언어를 얻게 됩니다. 구성 요소 기반 언어는 버전 관리를 완벽하게 지원하고 리플렉션를 통해 런타임에서 사용 가능한 확장가능 메타 데이터를 제공합니다. 후기 바인딩을 특별히 지원할 필요도 없습니다. 형식 찾기 및 후기 바인딩은 언어에 포함되어 있습니다. C#에서는 열거 및 속성이 언어의 첫 번째 클래스이고 이벤트 및 대리자(형식에 관계 없는 함수 포인터)와 마찬가지로 하부 엔진에 의해 완벽하게 지원됩니다.
하지만 관리 가능한 환경의 최대 장점은 .NET Framework입니다. 프레임워크는 모든 .NET 언어에서 사용할 수 있고, C#은 프레임워크의 풍부한 클래스, 인터페이스 및 개체로 프로그래밍할 수 있도록 최적으로 고안된 언어입니다.

맨 위로


함정

C#은 C++와 크게 달라 보이지 않기 때문에 전환이 쉬울 수 있지만, 여기에는 분명히 유의해야 할 함정이 있습니다. C++에서는 완벽하게 보이는 코드를 작성한 후 컴파일되지 않거나, 심한 경우 예상대로 작동하지 않을 수도 있습니다. C++에서 C#으로의 구문상 변화는 크지 않습니다. 다만 클래스 선언 뒤에 세미콜론이 없고, Main을 대문자로 시작한다는 정도입니다. 구문상의 변화를 쉽게 참고할 수 있도록 이러한 목록을 게시한 웹 페이지를 만들고 있지만, 컴파일러에서 쉽게 다룰 수 있으므로 여기에서는 다루지 않겠습니다. http://www.LibertyAssociates.com/Books&Resources.htm 에서 C# 프로그래밍에 대한 FAQ 참고를 참고하십시오. 그렇다고 해도 문제를 일으킬 수 있는 몇 가지 중요한 변화는 있음을 강조하고 싶습니다.

맨 위로


참조 및 값 형식
C#에서는 값 형식과 참조 형식을 구분합니다. 단순 형식(int, long, double 등) 및 struct는 값 형식이고, 모든 클래스는 개체와 마찬가지로 참조 형식입니다. 값이 참조 형식 내부에 포함되지 않을 경우, 값 형식은 C++의 변수와 같이 스택에서 값을 가집니다. 참조 형식 변수는 스택에 있지만, C++의 포인터와 유사하게 힙에서 개체의 주소를 가집니다. 값 형식은 값(복사본)에 의해 메서드에 전달되는 반면, 참조 형식은 참조에 의해 효율적으로 전달됩니다.

맨 위로


Struct
Struct는 C#에서 많이 다릅니다. C++의 경우 struct는 기본 상속 및 기본 액세스가 전용이 아닌 공용이라는 점만 제외하면 클래스와 동일합니다. 그러나 C#에서의 struct는 클래스와 매우 다릅니다. C#의 struct는 경량 개체를 캡슐화하기 위해 디자인되었습니다. 즉 struct는 참조 형식이 아닌 값 형식이므로 값에 의해 전달됩니다. 또한 클래스에 적용되지 않는 제한 사항이 있습니다. 예를 들어, struct는 봉인되어 있습니다, 즉 struct는 개체에서 파생되는 System.ValueType에서 파생될 수 없으며  그것과 다른 기본 클래스를 가질 수도 없습니다. struct는 기본(parameterless) 생성자를 선언할 수 없습니다.
반면, struct는 클래스보다 더욱 효율적이므로 경량 개체 생성에 가장 적합합니다. struct의 봉인 및 값 의미가 문제가 안 된다면 아주 작은 개체를 만들 때는 클래스보다 struct를 사용하는 것이 훨씬 유리합니다.

맨 위로


모든 것은 개체에서 파생
C#의 경우 모든 것은 결국 개체에서 파생됩니다. 여기에는 생성 클래스는 물론 int 또는 struct와 같은 값 형식이 포함됩니다. 개체 클래스는 ToString과 같은 유용한 메서드를 제공합니다. 예를 들면, C#의 cout라 할 수 있는 System.Console.WriteLine 메서드인 ToString을 사용할 경우입니다. 이 메서드는 오버로드되어 개체의 문자열 및 배열을 가집니다.
WriteLine을 사용하려면 기존 방식의 printf와는 달리 대체 매개 변수를 제공해야 합니다. myEmployee가 사용자 지정된 Employee 클래스의 인스턴스이고 myCounter가 사용자 지정된 Counter 클래스의 인스턴스라고 가정해 보겠습니다. 다음과 같은 코드를 작성할 수 있습니다.
Console.WriteLine("The employee: {0}, the counter value: {1}", 
                  myEmployee, myCounter);
WriteLine은 각 개체에 대해 가상 메서드인 Object.ToString을 호출하여 매개 변수 대신 반환된 문자열로 대체합니다. Employee 클래스가 ToString을 재정의하지 않을 경우, 기본 구현(System.Object에서 파생)이 호출되어 문자열로 클래스의 이름을 반환합니다. Counter는 ToString을 재정의하여 정수 값을 반환할 것입니다. 이 경우, 출력은 다음과 같습니다.
The employee: Employee, the counter value: 12
정수 값을 WriteLine에 전달할 경우는 어떻게 될까요? 정수에 대해 ToString을 호출할 수는 없지만, 컴파일러는 값이 정수 값으로 설정될 개체의 인스턴스에서 int를 암시적으로 가둘 수 있습니다. WriteLine이 ToString을 호출할 경우 개체는 정수의 값을 나타내는 문자열을 반환합니다. (그림 1 참고)

맨 위로


Reference 매개 변수 및 Out 매개 변수
C++과 마찬가지로 C#에서도 메서드는 하나의 반환 값만 가질 수 있습니다. C++의 경우 포인터 또는 참조를 매개 변수로 전달하여 이 제한 사항을 해결할 수 있었습니다. 호출된 메서드가 매개 변수를 변경하고, 호출하는 메서드는 다시 새로운 값을 사용할 수 있습니다.
참조를 메서드에 전달하면 C++에서 참조 또는 포인터 전달을 통해 액세스하는 방식과 동일하게 원래 개체에 액세스할 수 있습니다. 하지만 이런 방식이 값 형식의 경우에는 해당되지 않습니다. 값 형식을 참조로 전달하려는 경우에는 값 형식 매개 변수를 ref 키워드로 표시해야 합니다.
public void GetStats(ref int age, ref int ID, ref int yearsServed)
ref 키워드는 메서드 선언과 메서드에 대한 실제 호출 모두의 경우에 사용해야 한다는 점을 유의하십시오.
Fred.GetStats(ref age, ref ID, ref yearsServed);
이제 호출 메서드에서 age, ID 및 yearsServed를 선언하고 GetStats로 전달하면 변경된 값을 얻게 됩니다.
C#에서는 명확한 할당이 필요합니다. 즉 로컬 변수, age, ID 및 yearsServed는 GetStats를 호출하기 전에 초기화되어야 합니다. 이것은 불필요한 수고입니다. 이것들은 단지 GetStats에서 값을 얻기 위해서만 사용되기 때문입니다. 이 문제를 해결하기 위해 C#에서는 out 키워드도 제공합니다. 이 키워드는 초기화되지 않은 변수를 전달하고 참조에 의해 전달되도록 지정합니다. 다음은 이러한 의도를 명시적으로 나타내는 방법입니다.
public void GetStats(out int age, out int ID, out int yearsServed)
이 때, 호출 메서드는 일치해야 합니다.
Fred.GetStats(out age,out ID, out yearsServed);

맨 위로


New 호출
C++의 경우, 키워드 new는 힙의 개체를 인스턴스화합니다. 하지만 C#의 경우는 그렇지 않습니다. 참조 형식의 경우, new 키워드는 힙의 개체를 인스턴스화합니다. 하지만 struct와 같은 값 형식의 경우 개체는 스택에서 생성되고 생성자가 호출됩니다.
사실, new를 사용하지 않고 스택에 struct를 생성할 수도 있지만 조심해야 합니다. new는 개체를 초기화합니다. new를 사용하지 않을 경우 struct의 값을 사용하기 전에, 즉 메서드에 전달하기 전에 모든 값을 직접 초기화해야 하며, 그렇지 않을 경우 컴파일되지 않습니다. 또한 명확한 할당을 위해서는 모든 개체가 초기화되어야 합니다. (
그림 2 참고)

맨 위로


속성
대부분의 C++ 프로그래머는 멤버 변수를 전용으로 유지하려고 합니다. 이러한 데이터 숨김을 통해 캡슐화가 용이하고 클라이언트가 의존하는 인터페이스를 건드리지 않고도 클래스의 구현을 변경할 수 있기 때문입니다.그러나 클라이언트가 직접 이러한 멤버의 값을 얻거나 설정할 수 있게 하기 위하여, C++ 프로그래머는 전용 멤버 변수의 값을 수정하는 일을 담당하는 접근자 메서드를 생성합니다.
C#에서 속성은 클래스의 첫 번째 클래스 멤버입니다. 클라이언트에게 속성은 멤버 변수로 보이지만, 클래스 구현자에게는 메서드로 보입니다. 이런 개념은 적절하다고 볼 수 있습니다. 즉 프로그래머는 완전한 캡슐화 및 데이터 숨김이 가능하고 클라이언트는 멤버에 쉽게 액세스할 수 있기 때문입니다.
Employee 클래스에 Age 속성을 제공하여 클라이언트가 employee의 age 멤버를 얻고 설정하도록 할 수 있습니다.
public int Age
{
    get
    {
        return age;
    }
    set
    {
        age = value;
    }
}
키워드 값은 속성에서 암시적으로 사용할 수 있습니다. 다음과 같이 작성하는 경우,
Fred.Age = 17;
컴파일러는 값으로 17을 전달합니다.
Set 접근자가 아닌 Get 접근자를 사용하여 YearsServed에 대해 읽기 전용 속성을 만들 수도 있습니다.
public int YearsServed
{
    get
    {
        return yearsServed;
    }
}
이러한 접근자를 사용하기 위해 드라이버 프로그램을 변경할 경우 어떻게 작동하는지 볼 수 있습니다. (그림 3 참고)
속성을 통해 Fred의 age를 얻은 다음, 이 속성을 사용하여 age를 설정할 수 있습니다. YearsServed 속성을 액세스하여 값을 얻을 수 있지만 설정할 수는 없습니다. 마지막 줄에서 주석을 제거하지 않을 경우 프로그램이 컴파일되지 않습니다.
나중에 데이터베이스에서 Employee의 age를 가져오려는 경우 접근자 구현만 변경하면 됩니다. 클라이언트는 아무런 영향을 받지 않습니다.

맨 위로


배열
C#에서는 기존의 C/C++ 배열보다 개선된 버전의 배열 클래스를 제공합니다. 예를 들어, C# 배열의 범위를 벗어나서 작성하는 것은 불가능합니다. 또한 Array에는 더 진보적인 기능의 ArrayList도 있습니다. ArrayList는 프로그램의 크기 변화에 대한 요구 사항을 동적으로 관리할 수 있습니다.
C#에서 배열은 1차원, 다차원 사각형 배열(C++ 다차원 배열과 유사) 및 가변 배열(배열안의 배열)의 세 종류로 사용 가능합니다.
1차원 배열은 다음과 같이 만들 수 있습니다.
int[] myIntArray = new int[5];
또는, 다음과 같이 초기화할 수도 있습니다.
int[] myIntArray = { 2, 4, 6, 8, 10 };
다차원 사각형 배열은 다음과 같이 만들 수 있습니다.
int[,] myRectangularArray = new int[rows, columns];
또는, 다음과 같이 간단히 초기화할 수도 있습니다.
int[,] myRectangularArray =  
{
    {0,1,2}, {3,4,5}, {6,7,8}, {9,10,11}
};
가변 배열은 배열안의 배열이므로 1차원만 제공하면 됩니다.
int[][] myJaggedArray = new int[4][];
그런 다음, 다음과 같이 각 내부 배열을 만듭니다.
myJaggedArray[0] = new int[5]; 
myJaggedArray[1] = new int[2]; 
myJaggedArray[2] = new int[3]; 
myJaggedArray[3] = new int[5]; 
배열은 System.Array 개체에서 파생되므로 Sort 및 Reverse를 포함하여 많은 수의 유용한 메서드와 함께 사용됩니다.

맨 위로


인덱서
자신만의 배열 같은 개체를 만들 수도 있습니다. 예를 들면, 표시가 가능한 문자열 집합을 가진 목록 상자를 만들 수 있습니다. 배열에서 하듯이 인덱스로 상자의 내용을 액세스할 수 있다면 편리할 것입니다.
string theFirstString = myListBox[0];
string theLastString = myListBox[Length-1];
이것은 인덱서를 이용하면 가능합니다. 인덱서는 속성과 비슷한 점이 많지만, 인덱스 연산자의 구문을 지원합니다. 그림 4에서는 속성을 보여주는데 속성의 이름 다음에 인덱스 연산자가 옵니다.
그림 5는 아주 간단한 ListBox 클래스를 구현하고 인덱스 기능을 제공하는 방법을 보여 줍니다.


맨 위로


인터페이스
소프트웨어 인터페이스는 두 형식이 상호 작용하는 방식에 대한 계약입니다. 한 형식이 인터페이스를 등록하면 클라이언트에게 "다음 메서드, 속성, 이벤트 및 인덱서에 대한 지원을 약속한다"는 이야기를 하는 것입니다.
C#은 개체 지향형 언어이므로, 이러한 계약은 인터페이스라는 엔티티로 캡슐화됩니다. 인터페이스 키워드는 계약을 캡슐화하는 참조 형식을 선언합니다.
개념적으로 보면, 인터페이스는 추상 클래스와 유사합니다. 차이가 있다면, 추상 클래스는 파생된 클래스의 패밀리에 대한 기본 클래스로 제공되고, 인터페이스는 다른 상속 트리와 혼합됩니다.


맨 위로


IEnumerable 인터페이스
이전 예제로 다시 돌아가서, 일반 배열로 할 수 있는 것과 마찬가지로 foreach 루프를 사용하여 ListBoxTest 클래스에서 문자열을 출력할 수 있다면 좋을 것입니다. 이것은 클래스에서 IEnumerable 인터페이스를 구현해서 할 수 있고, IEnumerable 인터페이스는 다시 foreach 생성에 의해 암시적으로 사용됩니다. IEnumerable는 열거 및 foreach 루프를 지원하는 모든 클래스에서 구현됩니다.
IEnumerable은 단 하나의 메서드인 GetEnumerator를 가지는데, 이 메서드의 임무는 IEnumerator의 특수한 구현을 반환하는 것입니다. 따라서, Enumerable 클래스의 의미를 통해 Enumerator를 제공할 수 있습니다.
Enumerator는 IEnumerator 메서드를 구현해야 합니다. 이것은 컨테이너 클래스 또는 별도의 클래스에 의해 직접 구현할 수 있습니다. 이 중에서 두 번째 방식이 일반적으로 선호되는데, 그 이유는 컨테이너를 흩어 놓는 대신 Enumerator 클래스에서 해야 할 일을 캡슐화하기 때문입니다.
이미
그림 5에서 보았던 ListBoxTest에 Enumerator를 추가할 것입니다. Enumerator 클래스는 컨테이너 클래스에만 해당되기 때문에(즉 ListBoxEnumerator는 ListBoxTest에 대해 많이 알고 있어야 하므로) ListBoxTest 내에 포함시켜 전용 구현으로 만들 것입니다.
이러한 의미에서 ListBoxTest는 IEnumerable 인터페이스 구현을 위한 것으로 정의할 수 있습니다. IEnumerable 인터페이스는 Enumerator를 반환해야 합니다.
public IEnumerator GetEnumerator()
{
    return (IEnumerator) new ListBoxEnumerator(this);
}
메서드가 현재 ListBoxTest 개체(this)를 열거자에게 전달한다는 점을 알아두십시오. 이를 통해, 열거자가 이 특정 ListBoxTest 개체를 열거할 수 있게 됩니다.
여기에서 Enumerator를 구현하기 위한 클래스는 ListBoxTest 내에서 정의된 전용 클래스인 ListBoxEnumerator로 구현되었습니다. 이 클래스의 임무는 매우 분명합니다.
열거되는 ListBoxTest는 인수로 생상자에게 전달되고, 여기에서 멤버 변수인 myLBT에게 할당됩니다. 또한 생성자는 멤버 변수 인덱스를 -1로 설정하여 개체 나열이 아직 시작되지 않았음을 나타냅니다.
public ListBoxEnumerator(ListBoxTest theLB)
{
    myLBT = theLB;
    index = -1;
}
MoveNext 메서드가 인덱스를 증가시킨 다음 열거하려는 개체의 마지막 부분을 넘어서 실행하지 않도록 확인합니다. 그럴 경우 false가 반환되고, 그렇지 않을 경우 true가 반환됩니다.
public bool MoveNext()
{
    index++;
    if (index >= myLBT.myStrings.Length)
        return false;
    else
        return true;
}
Reset은 인덱스를 -1로 재설정합니다.
Current 속성은 마지막으로 추가된 문자열을 반환하기 위해 구현됩니다. 이것은 임의의 결정입니다. 다른 클래스에서 Current는 디자이너가 결정한 모든 해당 의미를 가지게 됩니다. 하지만 정의될 경우에는 현재 멤버에 대한 액세스는 열거자가 수행하는 임무이므로 모든 열거자가 현재 멤버를 반환할 수 있어야 합니다.
public object Current
{
    get
    {
        return(myLBT[index]);
    }
}
이제 다 되었습니다. foreach에 대한 호출이 열거자를 페칭하고 이것을 사용하여 배열에 걸쳐 열거합니다. foreach는 의미 있는 값을 추가했는지 여부에 상관 없이 모든 문자열을 표시하므로, 관리하기 쉽게 표시 위해 myStrings의 초기화를 여덟 개의 항목으로 변경하였습니다.
myStrings = new String[8]; 

맨 위로


기본 클래스 라이브러리 사용
C#이 C++과 어떻게 다르고, 문제 해결을 위한 접근 방식이 어떻게 바뀌어야 하는지에 대한 이해를 돕기 위해 다소 간단한 예제를 살펴보도록 하겠습니다. 큰 텍스트 파일을 읽고 화면에 해당 내용을 표시하는 클래스를 만들 것입니다. 이것을 다중 스레드 프로그램으로 만들어 데이터를 디스크에서 읽는 동안 다른 작업을 할 수 있도록 할 것입니다.
C++에서는 파일을 읽는 스레드를 만들고, 다른 작업을 수행하는 스레드를 별도로 만들어야 합니다. 이러한 스레드는 독립적으로 작동하지만 동기화가 필요합니다. 이러한 모든 것을 C#에서도 할 수 있지만, 대부분의 경우 자신의 스레드를 작성할 필요는 없습니다. .NET에서 비동기 I/O를 위한 매우 강력한 메커니즘을 제공하기 때문입니다.
비동기 I/O 지원은 CLR에 포함되어 있고 일반적인 I/O 스트림 클래스와 같이 사용이 쉽습니다. 컴파일러에게 많은 수의 System 네임스페이스에서 개체를 사용할 것임을 알리는 것부터 시작합니다.
using System;
using System.IO;
using System.Text;
System을 포함시켜도 그 밑의 모든 하위 네임스페이스가 자동으로 포함되지 않기 때문에 using 키워드를 사용하여 명시적으로 포함시켜야 합니다. I/O 스트림 클래스를 사용할 것이므로 System.IO가 필요하고, 바이트 스트림을 ASCII로 인코딩하기 위해서는 System.Text도 필요 합니다.
이 프로그램 작성 단계는 매우 간단합니다. .NET이 대부분의 작업을 대신해 주기 때문입니다. Stream 클래스의 BeginRead 메서드를 사용할 것입니다. 이 메서드는 비동기 I/O를 제공하고, 버퍼에 가득 찬 데이터를 읽은 다음, 버퍼 프로세싱 준비가 되면 콜백 메서드를 호출합니다.
콜백 메서드에 대한 버퍼 및 대리자로 바이트 배열을 전달해야 합니다. 이 두 가지를 드라이버 클래스의 전용 멤버 변수로 선언합니다.
public class AsynchIOTester
{
    private Stream inputStream;       
    private byte[] buffer;          
    private AsyncCallback myCallBack;
멤버 변수 inputStream은 Stream 형식에 속하고 BeginRead 메서드를 호출하는 개체에 있으며, 대리자(myCallBack) 및 버퍼를 전달됩니다. 대리자는 멤버 함수에 대한 형식에 관계 없는 포인터와 매우 유사합니다. C#에서 대리자는 언어의 첫 번째 클래스 요소입니다.
.NET은 바이트가 디스크의 파일에서 채워져 데이터를 처리할 수 있을 때 대리된 메서드를 호출합니다. 기다리는 동안 다른 작업을 수행할 수 있습니다. (이 경우 정수를 1에서 50,000으로 올릴 수 있지만, 생산 프로그램에서는 사용자와 상호 작용하거나 다른 유용한 작업을 수행할 수 있습니다.)
.NET은 바이트가 디스크의 파일에서 채워져 데이터를 처리할 수 있을 때 대리된 메서드를 호출합니다. 기다리는 동안 다른 작업을 수행할 수 있습니다. (이 경우 정수를 1에서 50,000으로 올릴 수 있지만, 생산 프로그램에서는 사용자와 상호 작용하거나 다른 유용한 작업을 수행할 수 있습니다.)
public delegate void AsyncCallback (IAsyncResult ar);
따라서, 이 대리자는 void를 반환하는 다른 모든 메서드와 연결될 수 있고 매개 변수로 IAsyncResult 인터페이스를 가집니다. CLR은 메서드가 호출되면 런타임에 IAsyncResult 인터페이스를 전달하므로, 다음과 같이 메서드만 선언하면 됩니다.
void onCompletedRead(IAsyncResult asyncResult)
그런 다음, 생성자에서 대리자를 연결합니다.
AsynchIOTester()
{
      ???
     myCallBack = new AsyncCallback(this.OnCompletedRead);
}
이것은 멤버 변수 myCallback(이전에 형식 AsyncCallback에 속하도록 정의)에게 대리자 인스턴스를 할당합니다. 대리자 인스턴스는 AsyncCallback 생성자를 호출하고 대리자와 연결하려는 메서드로 전달함 으로써 만들어집니다.
다음은 전체 프로그램이 단계별로 작동하는 방식입니다. Main에서 클래스의 인스턴스를 만들고 실행되도록 합니다.
public static void Main()
{
    AsynchIOTester theApp = new AsynchIOTester();    
    theApp.Run();
}
new에 대한 호출로 생성자가 실행됩니다. 생성자에서 파일을 열고 Stream 개체를 다시 얻습니다. 그런 다음, 버퍼에 공간을 할당하고 콜백 메커니즘을 연결합니다.
AsynchIOTester()
{
    inputStream = File.OpenRead(@"C:\MSDN\fromCppToCS.txt");
    buffer = new byte[BUFFER_SIZE];
    myCallBack = new AsyncCallback(this.OnCompletedRead);
}
Run 메서드에서 BeginRead를 호출하면 파일의 비동기적 읽기가 이루어집니다.
inputStream.BeginRead(
     buffer,             // where to put the results
     0,                  // offset
     buffer.Length,      // how many bytes (BUFFER_SIZE)
     myCallBack,         // call back delegate
     null);              // local state object
이제 다른 작업으로 이동합니다.
for (long i = 0; i < 50000; i++)        
{
    if (i%1000 == 0)
    {
        Console.WriteLine("i: {0}", i);
    }
}
읽기가 완료되면 CLR이 콜백 메서드를 호출합니다.
void onCompletedRead(IAsyncResult asyncResult)
{
OnCompletedRead에서 첫 번째로 해야 할 일은 Stream 개체의 EndRead 메서드를 호출하여 얼마나 많은 바이트가 읽혀졌는지 알아내고, 공용 언어 런타임에 의해 전달된 IAsyncResult 인터페이스 개체를 전달 하는 것입니다.
int bytesRead = inputStream.EndRead(asyncResult);
EndRead 호출의 결과는 읽은 바이트의 수를 다시 받는 것이고. 수가 0보다 클 경우 버퍼를 문자열로 변환하여 콘솔에 쓴 다음, 다른 비동기 읽기를 유발하기위해 BeginRead를 다시 호출합니다.
if (bytesRead > 0)
{
    String s = Encoding.ASCII.GetString(buffer, 0, bytesRead);
    Console.WriteLine(s);
    inputStream.BeginRead(buffer, 0, buffer.Length,
                          myCallBack, null);
}
이제 읽기가 진행되는 동안 다른 작업(이 경우는 50,000까지 카운트)을 수행할 수도 있지만, 버퍼가 가득 찰 때마다 읽은 데이터를 처리(이 경우는 콘솔에 출력)할 수 있습니다. 이 예제에 대한 전체 소스 코드 AsynchIO.cs는 이 글의 맨 위쪽에 있는 링크에서 다운로드할 수 있습니다.
비동기 I/O의 관리는 전적으로 CLR에서 제공합니다. 네트워크에 대한 다음 내용을 읽어보면 이에 대한 장점을 더 많이 알게 될 것입니다.

맨 위로


네트워크를 통한 파일 읽기
C++에서 네트워크를 통한 파일 읽기는 쉽지 않은 프로그래밍 작업입니다. .NET에서 이점을 폭넓게 지원합니다. 사실, 네트워크를 통한 파일 읽기는 표준 Base Class Library Stream 클래스 사용에 불과합니다.
먼저, TCPListener 클래스의 인스턴스를 만들어 TCP/IP 포트(이 경우는 포트 65000)에서 수신하도록 합니다.
TCPListener tcpListener = new TCPListener(65000);
생성되었으면 TCPListener 개체가 수신을 시작하도록 합니다.
tcpListener.Start();  
이제 클라이언트의 연결 요청을 기다립니다.
Socket socketForClient = tcpListener.Accept();
TCPListener 개체의 Accept 메서드는 Socket 개체를 반환하고, 이 개체는 표준 Berkeley 소켓 인터페이스를 나타내며 특정한 끝 지점(이 경우는 클라이언트)에 바인딩됩니다. Accept는 동기 메서드이고 연결 요청을 받을 때까지는 반환되지 않습니다. 소켓이 연결되면 클라이언트에게 파일을 보낼 준비가 마무리됩니다.
if (socketForClient.Connected)
{
???
그 다음, NetworkStream 클래스를 만들어 소켓을 생성자에게 전달합니다.
NetworkStream networkStream = new NetworkStream(socketForClient);
그런 다음, 이전과 같이 StreamWriter 개체를 만들기는 하지만, 이번에는 파일이 아닌 방금 생성된 NetworkStream에서 만듭니다.
System.IO.StreamWriter streamWriter = 
 new System.IO.StreamWriter(networkStream);
이 스트림에 쓰면 스트림이 네트워크를 통해 클라이언트에게 전송됩니다. 전체 소스 코드 TCPServer.cs도 다운로드가 가능합니다.

맨 위로


클라이언트 생성
클라이언트는 호스트에 TCP/IP 클라이언트 연결을 나타내는 TCPClient 클래스를 인스턴스화합니다.
TCPClient socketForServer;
socketForServer = new TCPClient("localHost", 65000);
이 TCPClient로 NetworkStream을 만들고, 이 스트림에 대해 StreamReader를 만들 수 있습니다.
NetworkStream networkStream = socketForServer.GetStream();
System.IO.StreamReader streamReader = 
    new System.IO.StreamReader(networkStream);
이제 스트림에 데이터가 있는 동안 계속 스트림을 읽고 결과를 콘솔에 출력합니다.
do
{
    outputString = streamReader.ReadLine();
    if( outputString != null )
    {
        Console.WriteLine(outputString);
    }
}
while( outputString != null );
이것을 테스트하기 위해 간단한 테스트 파일을 만듭니다.
This is line one
This is line two
This is line three
This is line four
다음은 서버에서의 출력입니다.
Output (Server)
Client connected
Sending This is line one
Sending This is line two
Sending This is line three
Sending This is line four
Disconnecting from client...
Exiting...
다음은 클라이언트에서의 출력입니다.
This is line one
This is line two
This is line three
This is line four

맨 위로


속성 및 메타데이터
C#과 C++ 사이의 큰 차이점 중 하나는 C#에서는 메타데이터, 즉 클래스, 개체, 메서드 등에 대한 데이터를 기본적으로 지원한다는 것입니다. 특성은 CLR의 일부로 제공되는 특성과 자신의 필요에 맞게 만드는 특성, 두 종류가 있습니다. CLR 특성은 serialization, 마샬링 및 COM 상호 운용성을 지원하기 위해 사용됩니다. CLR을 검색해 보면 매우 많은 특성이 있다는 것을 알 수 있습니다. 이 중 일부 특성은 어셈블리에 적용되고, 다른 일부는 클래스 또는 인터페이스에 적용됩니다. 이러한 적용 대상을 특성 대상이라고 합니다.
특성은 대상 항목 바로 앞에 두고 대괄호로 묶으면 해당 대상에 적용됩니다. 특성은 다음과 같이 다른 특성 위에 두거나,
[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile(".\\keyFile.snk")]
여러 특성을 콤마로 구분함으로써 조합할 수도 있습니다.
[assembly: AssemblyDelaySign(false),
   assembly: AssemblyKeyFile(".\\keyFile.snk")]

맨 위로


'IT_Programming > C#' 카테고리의 다른 글

위임(Delegate) 예제  (0) 2007.12.23
VS2005에서 Framework 1.1로 빌드 하기  (0) 2007.09.02
C#으로 만드는 자바 스크립트, Script#  (0) 2007.01.21
[Effective C#] 개발 지침 50  (0) 2007.01.19
이벤트와 델리게이트  (0) 2006.07.27