http://www.microsoft.com/Korea/MSDN/MSDNMAG/ISSUES/2001/ctocsharp/default.aspx
C++ -> C#: C++에서 C#으로의 전환을 위해 알아 두어야 할 사항 |
Jesse Liberty |
이 글을 이해하려면 C++에 대한 기본 지식이 있어야 합니다. |
난이도? ?? 1?? 2?? 3? |
요약 :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# 구현의 향상 내용에 대해 소개한 자료는 많으므로 여기에서 다시 반복하지는 않겠습니다. 대신, 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을 대문자로 시작한다는 정도입니다. 구문상의 변화를 쉽게 참고할 수 있도록 이러한 목록을 게시한 웹 페이지를 만들고 있지만, 컴파일러에서 쉽게 다룰 수 있으므로 여기에서는 다루지 않겠습니다. 그렇다고 해도 문제를 일으킬 수 있는 몇 가지 중요한 변화는 있음을 강조하고 싶습니다.참조 및 값 형식 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")] 사용자 지정 특성 사용자 지정 특성을 자유롭게 만들고 적당한 때 런타임에 사용할 수 있습니다. 예를 들어, 코드의 섹션을 연결된 문서의 URL로 태그를 추가하는 문서 특성을 만들 수 있습니다. 또는, 코드를 코드 검토 주석이나 버그 수정 주석으로 태그를 추가할 수 있습니다. 개발팀에서 버그 수정에 대한 기록을 유지하려는 경우를 생각해 보겠습니다. 모든 버그에 대한 데이터베이스를 유지하기 위해, 코드의 특정 수정 사항에 대해 버그 보고서를 연관시키려고 할 경우에는 코드에 다음과 같은 주석을 추가할 수 있을 것입니다. // Bug 323 fixed by Jesse Liberty 1/1/2005. 이렇게 해두면 소스 코드에서 쉽게 볼 수는 있겠지만, 이러한 정보를 보고서로 추출하거나 데이터베이스로 만들어 검색할 수 있도록 하면 더 좋을 것입니다. 또한 모든 버그 보고서 표시 방식을 동일한 구문으로 사용하면 좋을 것입니다. 이 때 필요한 것이 사용자 지정 특성입니다. 그러기 위해서는 주석을 다음과 같이 바꿀 수 있습니다.[BugFix(323,"Jesse Liberty","1/1/2005") Comment="Off by one error"] 특성도 C#의 클래스에 해당합니다. 사용자 지정 특성을 만들려면 System.Attribute에서 파생된 새로운 사용자 지정 특성을 만듭니다.public class BugFixAttribute : System.Attribute 컴파일러에게 이 특성이 어떤 종류의 요소(특성 대상)와 함께 사용될 수 있는지 말해 주어야 합니다. 이것을 특성으로 지정합니다. (너무 당연한가요?)[AttributeUsage(AttributeTargets.ClassMembers, AllowMultiple = true)] AttributeUsage는 특성에 적용되는 특성(메타 특성)입니다. 이것은 메타 메타데이터, 즉 메타데이터에 대한 데이터를 제공합니다. 이 경우는 두 개의 인수를 전달하게 되는데, 하나는 대상(이 경우는 클래스 멤버)이고 다른 하나는 주어진 요소가 이러한 특성을 둘 이상 받을 수 있는지 여부를 나타내는 플래그입니다. AllowMultiple은 true로 설정되어 있는데, 이것은 클래스 멤버에 둘 이상의 BugFixAttribute가 지정될 수 있음을 의미합니다.Attribute 대상을 조합하려는 경우에는 OR를 사용할 수 있습니다. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true)] 이것은 특성이 Class 또는 Interface에 연결될 수 있도록 합니다.새로운 사용자 지정 특성의 이름은 BugFixAttribute입니다. 표기법은 단어 Attribute를 해당 특성 이름에 붙이는 것입니다. 특성을 요소에 지정하면, 컴파일러에서 특성을 짧은 버전의 이름으로 호출할 수 있도록 지원합니다. 따라서 다음과 같이 작성할 수 있습니다. [BugFix(123, "Jesse Liberty", "01/01/05", Comment="Off by one")] 컴파일러는 먼저 BugFix라는 이름의 특성을 찾고, 이것을 찾지 못할 경우 BugFixAttribute를 찾습니다.모든 특성은 적어도 하나의 생성자를 가지고 있어야 합니다. 특성은 두 가지의 형식의 매개 변수를 가지는데, 하나는 위치 매개 변수이고 다른 하나는 이름 매개 변수입니다. 이전 예제에서 버그 ID, 프로그래머 이름 및 날짜는 위치 매개 변수이고, 주석은 이름 매개 변수에 해당합니다. 위치 매개 변수는 생성자를 통해 전달되고 생성자에서 선언된 순서대로 전달되어야 합니다. public BugFixAttribute(int bugID, string programmer, string date) { this.bugID = bugID; this.programmer = programmer; this.date = date; } 이름 매개 변수는 속성과 같이 구현됩니다.특성 사용 특성을 테스트하기 위해 MyMath라는 이름의 간단한 클래스를 만들고 두 개의 함수를 부여합니다. 그런 다음, 클래스에 버그 수정 특성을 지정합니다. [BugFixAttribute(121,"Jesse Liberty","01/03/05")] [BugFixAttribute(107,"Jesse Liberty","01/04/05", Comment="Fixed off by one errors")] public class MyMath 이러한 특성은 메타데이터와 함께 저장됩니다. 그림 6에 전체 소스 코드가 제공되어 있습니다. 다음은 출력입니다.Calling DoFunc(7). Result: 9.3333333333333339 보는 바와 같이, 특성은 출력에 대해 전혀 영향을 미치지 않고, 특성을 만들어도 성능에는 아무런 영향이 없습니다. 사실, 특성이 존재한다는 사실도 제 말을 통해 믿을 수 밖에 없을 것입니다. 하지만 그림 7에서 보는 바와 같이 ILDASM을 사용한 메타데이터를 잠깐 살펴보면 특성이 있다는 사실을 알 수 있습니다.검토 검토가 유용하려면, 이상적으로는 런타임 동안 메타데이터에서 특성에 액세스하는 방법이 필요합니다. C#은 메타데이터 검사를 위한 검토를 지원합니다. 먼저 MemberInfo 형식의 개체를 초기화합니다. System.Reflection 네임스페이스의 이 개체는 멤버의 특성을 발견하고 메타데이터에 대한 액세스를 위해 제공합니다. System.Reflection.MemberInfo inf = typeof(MyMath); MyMath 형식에 대해 typeof 연산자를 호출합니다. 그러면 MemberInfo에서 파생된 형식 Type의 개체가 반환됩니다.다음 단계는 이 MemberInfo 개체에 대해 GetCustomAttributes를 호출하여 찾고자 하는 특성의 형식을 전달하는 것입니다. 이렇게 되면 개체 배열을 얻을 수 있으며, 여기서 이들 각 개체는 형식 BugFixAttribute에 속합니다. object[] attributes; attributes = Attribute.GetCustomAttributes(inf, typeof(BugFixAttribute)); 이제 그림 8과 같이 이 배열에 걸쳐 반복하여 BugFixAttribute 개체의 속성을 출력할 수 있습니다. 이 대체 코드를 그림 6의 코드에 삽입하면 메타데이터가 표시됩니다.형식 찾기 검토를 사용하여 어셈블리의 내용을 찾고 검사할 수 있습니다. 어셈블리에 대한 정보를 표시할 도구를 만들거나 어셈블리에서 메서드를 동적으로 호출하려는 경우 특히 유용합니다. 스크립팅 엔진을 개발하여 사용자가 스크립트를 생성하고 프로그램을 통해 실행하도록 하려는 경우에도 해당합니다. 검토를 통해, 모듈과 연관된 형식, 메서드, 필드, 속성 및 이벤트와 연관된 형식은 물론이고, 각 형식 메서드의 서명, 형식에 의해 지원되는 인터페이스 및 형식의 슈퍼클래스도 찾을 수 있습니다. 우선, 어셈블리를 Assembly.Load 정적 메서드로 동적으로 로드합니다. 이 메서드에 대한 서명은 다음과 같습니다. public static Assembly.Load(AssemblyName) 그런 다음, 코어 라이브러리를 전달해야 합니다.Assembly a = Assembly.Load("Mscorlib.dll"); 어셈블리가 로드되었으면 GetTypes를 호출하여 Type 개체의 배열을 반환할 수 있습니다. Type 개체는 검토의 핵심입니다. Type은 형식 선언, 즉 클래스, 인터페이스, 배열, 값 및 열거를 나타냅니다.Type[] types = a.GetTypes(); 어셈블리는 foreach 루프에 표시될 수 있는 형식의 배열을 반환합니다. 여기에서의 출력으로 많은 페이지가 채워집니다. 다음은 출력의 일부입니다.Type is System.TypeCode Type is System.Security.Util.StringExpressionSet Type is System.Text.UTF7Encoding$Encoder Type is System.ArgIterator Type is System.Runtime.Remoting.JITLookupTable 1205 types found 코어 라이브러리의 형식으로 가득 찬 배열을 얻었고 이것을 하나씩 출력했습니다. 출력에서 보는 바와 같이 배열에는 1,205개 항목이 포함되어 있습니다.형식에 대한 검토 어셈블리의 단일 형식에 대해서도 검토할 수 있습니다. 이렇게 하려면, GetType 메서드로 어셈블리에서 한 형식을 추출합니다. public class Tester { public static void Main() { // examine a single object Type theType = Type.GetType("System.Reflection.Assembly"); Console.WriteLine("\nSingle Type is {0}\n", theType); } } 출력은 다음과 같습니다.Single Type is System.Reflection.Assembly 멤버 찾기 그림 9에서 보는 바와 같이, 모든 해당 멤버에 대해 이 형식을 요청하여 모든 메서드, 속성 및 필드를 나열할 수 있습니다. 출력이 상당히 길어지긴 했지만 다음 출력의 일부에서 확인할 수 있듯이 출력 안에서 필드, 메서드, 생성자 및 속성을 볼 수 있습니다. System.String s_localFilePrefix is a Field Boolean IsDefined(System.Type) is a Method Void .ctor() is a Constructor System.String CodeBase is a Property System.String CopiedCodeBase is a Property 메서드만 찾기 필드, 속성 등은 제외하고 메서드에 대해서만 관심이 있을 수 있습니다. 이렇게 하려면, GetMembers에 대한 호출을 제거합니다. MemberInfo[] mbrInfoArray = theType.GetMembers(BindingFlags.LookupAll); 그런 다음, GetMethods에 대한 호출을 추가합니다.mbrInfoArray = theType.GetMethods(); 이제 출력에는 메서드밖에 없습니다.Output (excerpt) Boolean Equals(System.Object) is a Method System.String ToString() is a Method System.String CreateQualifiedName(System.String, System.String) is a Method System.Reflection.MethodInfo get_EntryPoint() is a Method 특정 멤버 찾기 마지막으로, 더욱 범위를 좁히기 위해 FindMembers 메서드를 사용하여 형식의 특정 멤버를 찾을 수 있습니다. 예를 들어, 그림 10에서 보는 바와 같이 문자 "Get"으로 이름이 시작하는 메서드에 대한 검색으로 제한할 수 있습니다. 출력의 일부는 다음과 같습니다. System.Type[] GetTypes() is a Method System.Type[] GetExportedTypes() is a Method System.Type GetType(System.String, Boolean) is a Method System.Type GetType(System.String) is a Method System.Reflection.AssemblyName GetName(Boolean) is a Method System.Reflection.AssemblyName GetName() is a Method Int32 GetHashCode() is a Method System.Reflection.Assembly GetAssembly(System.Type) is a Method System.Type GetType(System.String, Boolean, Boolean) is a Method 동적 호출 메서드를 찾았으면 검토를 사용하여 호출할 수 있습니다. 예를 들어, 각도의 코사인 값을 반환하는 System.Math의 Cos 메서드를 호출하려는 경우를 생각해 보겠습니다. 이렇게 하려면, 다음과 같이 System.Math 클래스에 대한 Type 정보를 얻습니다. Type theMathType = Type.GetType("System.Math"); 이 형식 정보로 해당 클래스의 인스턴스를 동적으로 로드할 수 있습니다.Object theObj = Activator.CreateInstance(theMathType); CreateInstance는 개체 인스턴스화를 위해 사용할 수 있는 Activator 클래스의 정적 메서드입니다.System.Math의 인스턴스가 있으면 Cos 메서드를 호출할 수 있습니다. 이렇게 하려면, 매개 변수의 형식을 설명하는 배열을 준비해야 합니다. Cos는 하나의 매개 변수(코사인 값이 필요한 각도)를 가지므로 하나의 멤버를 가진 배열이 필요합니다. 이 배열에 Cos에서 예상하는 매개 변수 형식인 System.Double 형식에 대한 Type 개체를 둡니다. Type[] paramTypes = new Type[1]; paramTypes[0]= Type.GetType("System.Double"); 이제 원하는 메서드의 이름과 이전에 얻은 형식 개체의 GetMethod 메서드에게 매개 변수의 형식을 설명하는 이 배열을 전달할 수 있습니다.MethodInfo CosineInfo = theMathType.GetMethod("Cos",paramTypes); 이제 메서드를 호출할 수 있는 형식 MethodInfo의 개체를 가지고 있습니다. 메서드를 호출하려면, 배열에서 다시 매개 변수의 실제 값을 전달해야 합니다.Object[] parameters = new Object[1]; parameters[0] = 45; Object returnVal = CosineInfo.Invoke(theObj,parameters); 이제 두 개의 배열을 만들었습니다. 하나는 매개 변수의 형식을 가진 paramTypes이고, 다른 하나는 실제 값을 가진 매개 변수입니다. 메서드가 두 개의 인수를 가지고 있는 경우 이러한 배열이 두 값을 가지도록 선언하면 됩니다. 메서드가 값을 가지고 있지 않은 경우에도 배열을 만들 수 있지만, 크기를 0으로 부여해야 합니다.Type[] paramTypes = new Type[0]; 조금 이상하게 보이지만 이것이 맞습니다. 그림 11에 전체 코드가 나와 있습니다.결론 부주의한 C++ 프로그래머가 혼란스러워 할 수 있는 몇 가지 함정이 있기는 하지만 C#의 구문은 C++과 크게 다르지 않고 새로운 언어로의 전환은 비교적 쉽다고 볼 수 있습니다. C#으로 작업하면서 느끼는 흥미로운 점은, 이전에 직접 작성해야 했던 많은 기능을 제공하는 새로운 공용 언어 런타임 라이브러리를 항상 사용하게 된다는 점입니다. 이 글에서는 몇 가지 눈에 띄는 사항만 간단히 다루었습니다. CLR 및 .NET Framework는 스레드, 마샬링, 웹 응용 프로그램 개발, Windows 기반 응용 프로그램 개발 등을 위한 폭넓은 기능을 지원합니다. 언어 기능과 CLR 기능간의 구분이 모호할 때가 많은 것이 사실이지만, 이 두 가지는 분명 강력한 개발 도구인 것입니다. |
? 최종수정일: 2003년 1월 6일
반응형
'프로그래밍(Programming) > C#' 카테고리의 다른 글
C# 프로그래밍 가이드 - microsoft (0) | 2015.08.25 |
---|---|
C# station 괜찮은 사이트 (0) | 2013.06.07 |
C# 일관성 없는 액세스 가능성 (0) | 2013.01.22 |
C#강의 51~55 끝 (0) | 2012.10.31 |
c#강의 41~50 (0) | 2012.10.31 |