닷넷프로그래밍정복 김상형저 가메출판사

1.제네릭

타입인수

제네릭(Generic)은 타입 인수를 사용하여 일반화된 클래스나 메서드를 정의하는 기법이다. 제네릭은 템플릿과 유사하다. 코드의 재사용성을 높이고 타입 안전성을 극대화하는 장점이 있는 반면, 코드의 크기가 커지고 가독성이 떨어지는 단점이 있다. 가독성이 나쁘다는 것은 유지, 보수 비용이 커지고 개발기간을 늘리니까 심한 중첩은 삼가고 적당한 수준에서 사용해야 한다. 공통된 인자를 타입인수(Type Parameter)라 하고 T-임의의-실제 타입을 위한 자리 표시이며 실제 타입은 객체를 생성할때 지정된다. 타입인수는 필요한 모든 곳에 사용가능하다. 타입인수 이름은 가급적 설명이 있는 이름이 좋으나 간단한 것이라면 통상적으로 T, U, V 식으로 간단하게 짓는다.

ex.

class Wrapper<T>

{

T value;

public Wrapper() { Value = default(T);}

public Wrapper(T aVAlue) { Value = aValue; }

public T Data

{

get { return Value;}

set { Value = value;}

}

public void OutValue()

{

Console.WRiteLine(Value);

}

}

class CSTest

{

static void Main()

{

Wrapper<int> gi = new Wrapper<int>(1234);

gi.OutValue();

Wrapper<string> gs = new Wrapper<string>("문자열");

gs.OutValue();

}

}

제약조건 - 제네릭 선언문에 where와 함께 지정할수 있다.

ex.

class Wrapper<T> where T : struct -> T는 값 타입이어야 하고 참조 타입은 쓸수 없다. 단, Nullable타입은 값타입이지만 예외적으로 이 경우에 허용되지 않는다.

{

 //...

}

이 중 가장 실용적인 것은 where T : base인데 Base 및 그 파생클래스를 마음껏 쓸수 있는 장점이 있다. 만약 제약조건이 없다면 default가 object class인데 이것만으론 별 쓸모가 없다.

제네릭컬렉션

가변적인 자료의 집합을 관리하는 컬렉션은 모든 응용 프로그램에 필수적인 자료 구조이다.일반 컬렉션은 요소 타입은 object라 임의의 타입을 저장할수 있다. 어떤 객체든지 컬렉션에 넣을수 있으며 이를 막을수 있는 문법적인 방법이 전혀 없다.

ex.

static void Main()

{

ArrayList ar = new ArrayList(10);

ar.Add(1);

ar.Add(2.34);

ar.Add("string");

}

모든 타입이 object 파생클래스이기에 문법상 오류는 없다. 하지만 이것을 다루기 위해서는

  1. 번거롭게도 어떤 타입의 객체가 저장되는지 리스트에 저장하거나 접근시 어떤 객체 타입인지 확인해줘야 한다.  그렇게 안 하면 당장에 애러가 난다
  2. 혹은 캐스팅을 잘못햇을 경우에 위험하다. 다운위험성도 있다. C#은 이런 경우에 대비해서 is, as 연산자를 제공하지만 실행중에만 쓸수 있고 완전하지 않다.
  3. 값 타입을 컬렉션에 저장할때는 object타입으로 변환하는 박싱이 필요하고 꺼낼때는 언박싱이 필요하다. 이 동작자체는 컴파일러가 해주지만 성능저하를 피할수는 없다.

그래서 제네릭 컬렉션 클래스로 해결하려하는 것이다. 모든 제네릭 컬렉션 클래스는 System.Collections.Generic의 파생클래스이다. 제네릭의 타입체크는 엄격해서 엉뚱한 타입을 넣게 되면 바로 애러처리한다.

ex.

List <string > ar = new List<string > (10);

ar.Add("한가인");

ar.Add("차예련");

ar.Add("남상미");

ar.Add(1234); > string이 아니라서 바로 애러!!

ar.Add(5.678); > string이 아니라서 바로 애러!!

 

2.예외처리

예외(exception)이란 프로그램의 정상적인 흐름을 벗어나게 만드는 잘못된 조건이나 상태를 뜻한다. 일반적으로 조건문을 이용해서 예외처리를 할수도 있지만 원래 코드와 섞여서 좋지 않다.

그래서 특별힌 예외처리 구문을 사용하는 것이 좋다

  • try - 예외가 발생할만한 블록을 지정하며 이 블록 안에 코드를 작성한다. 이블록안에서 예외가 발생하면 catch로 점프하고 발생하지 않으면  catch를 무시하고 실행한다
  • catch - try에서 발생한 예외를 처리하는 블록이다. 발생한 예외객체를 인수로 전달받고 예외 종류별로 여러개의 catch블록을 사용할수 있다.
  • finally - 예외 발생 여부와 상관없이 반드시 처리해야하는 블록이다.

예외처리 구문은 컴파일러가 이전 단계를 찾아 점프하도록 코드를 생성하므로 단계가 아무리 깊어도 상관없다. 또한 중간 단계의 메소드에서 생성한 지역변수들도 모두 자동으로 해제되는데 이 기능을 스택풀기라고 한다.(stack unwinding) 최초 호출원에서 예외를 받아 처리했을때는 메소드를 호출하기 전의 상태로 스택이 그대로 복구되어 다음 명령을 정상적으로 실행할수 있다.

예외객체 - 예외에 대한 정보를 가지는 객체.System.Exception 파생클래스.

예외는 보통 라이브러리의 메소드 실행중에 발생하지만 우리가 직접 예외 객체를 생성하여 던질수도 있다. 예외를 던질때는 throw문을 사용하는데 이때 던져진 예외는 이어지는 catch문 또는 가장 가까운(!) catch문에서 받아 처리할 것이다.

ex.

static void AddKim(string Kim)

{

if(kim[0] != '김')

{

throw new FormatException("김가만 등록할수 있다");

}

Console.WriteLine(Kim + "등록완료");

}

사용자 예외를 정의할때는 ApplicationException을 상속받아야 한다. Exception class와 동일하지만 시스템 예외와 프로그램 예외를 구별하기 위해서 상속받는 것이다.

다중예외처리. - 하나의 코드 블록에서 여러가지 예외가 발생할수 있다. 이럴 때는 try 아래쪽에 발생 가능한 모든 예외에 대해 catch문을 나열하고 발생한 예외에 따라 각각 다르게 처리한다. 단, 가장 하위 예외 클래스가 가장 먼저 와야 한다.

 

3.기타타입

포인터 - C#에서는 포인터를 기본적으로 지원하지 않지만 참조타입으로 포인터를 교모하게 흉내낸다. 참조타입은  Framework에 의해 지원되며 GC가 철저히 관리해서 안전하다.

포인터는 다음 상황에서 필요하다

  1. 다른 언어와 함께 동작할때 - 특히 WInApi나 COM호출시
  2. 디버깅목적으로 메모리내부를 접근할때
  3. 학습목적으로 컴파일러나 프레임워크의 동작을 분석할때
  4. 극단적으로 성능을 높일때는 참조타입보다 포인터를 직접 쓰는 것이 유리하다. 실상 C#자체가 느려서 포인터를 써봐야 성능상 이점이 거의 없다

포인터를 쓰려면 빌드 옵션에서 "안전하지 않은 코드 허용"을 체크해야 하고 포인터 사용에 대한 책임은 전적으로 개발자가 진다. GC 손밖의 문제이다. 문법은 C와 거의 똑같다.

 

널가능타입  - 널은 값이라기 보다는 특정한 상태를 의미한다.

 

Attribute(속성)

컴파일러에게 코드에 대한 여분의 추가정보를 제공하는 선언형식. 적용대상 앞에 []와 속성명을 적고 속성명 다음의 ()안에 인수들을 지정한다.

공통애트리뷰트

Conditional:  namepsace System.Diagnostics

메소드의 컴파일 여부를 조건부로 결정한다. 정의부에만 conditional 애트리뷰트를 작성하면 호출부는 따로 처리하지 않아도 된다. 이 애트리뷰트는 특정 메소드에만 가능하고 클래스나 필드에는 적용할수 없다. 조건이란

  1. 인터페이스의 메소드 선언문에 쓸수 없고 인터페이스 구현메소드에서도 쓸수 없다.
  2. 가상 메소드에서는 쓸수 있지만 재정의 메소드에는 쓸수 없다.
  3. 조건에 따라 호출될수도 있고 아닐수도 있으므로 이 메소드가 없더라도 다른 부분이 영향을 받아서는 안된다. 그래서 중요한 메소드는 대상이 될수 없다. 그래서 리턴타입도 void만 가능하다. 조건부로 컴파일되는 메소드에 의존하는 다른 코드가 있다면 모두 조건부 처리해주어야 한다!

ex. #define TRIAL

 

 [Conditional("TRIAL")]
        static void TrialMessage()
        {

            Console.WriteLine("Trial Messages");
        }


        static void Main(string[] args)
        {
            TrialMessage();
            Console.WriteLine("Other Message");
        }

TRIAL이 define되어 있으면 TrialMessage가 호출되고 그렇지 않으면 호출되지 않는다. 만일 [Conditional("TRIAL"), Conditional("FREE")] 라고 애트리뷰트를 붙인다면 TRIAL이나 FREE가 정의되어 있다면 이란 조건이 된다. 두 조건은 AND로 만들려면 두개의 메소드를 정의한 다음에 한 쪽에서 다른 쪽을 호출하게 만들어야 한다.

ex. [Conditional("TRIAL")]

static void TrialMessage()

{

TrialMessage2();

}

[Conditioanl("FREE")]

static void TrilMessaeg2() {...}

 

Obsolete - 곧 폐기될 구식 코드임을 표시. 이 애트리뷰트가 붙은 요소를 사용하면 경고나 애러를 발생시켜 사용자의 사용을 막을 것을 알린다.

형식 [Obsolete("메세지", error)]

        대상

첫번째 인수는 경고나 오류메세지 문자열이다. 오류의 이유와 배경 등을 설명한다. 두번째 인수는 오류발생여부인데 이 인수가 true이면 애러 처리되어 컴파일이 거부된다. false이거나 생략하면 경고로 처리되는데 경고는 단순한 알림 서비스 일뿐 당분간 계속 쓸수 있다.

 ex.

[Obsolete("앞으로는 이 메소드를 쓰지 마시오", true)]

static public void OutDateMethod() {}

 

DllImport - 외부 DLL 함수를 선언할때 사용한다. COM 라이브러리 함수나 Win32API 함수를 사용하고 싶을때 이 애트리뷰트로 어떤 DLL에 있는 함수인지 밝힌다.

ex

using System.Runtime.InteropServices;

namespace FirstEx1
{
    class Program
    {
        [DllImport("User32.dll")]
        public static extern int MessageBox(int hPrarent, string Message, string CAption, int Type);

        [DllImport("Kernel32.dll")]
        public static extern uint WinExec(string Path, uint nCmdShow);
 

        static void Main(string[] args)
        {
            MessageBox(0, "메모장을 실행합니다", "알림", 0);
            WinExec("notepad.exe", 1);
        }
    }
}

둘 다 닷넷 외부의 DLL 정의된 함수이니까 extern을, 클래스 멤버가 아니니까 static이어야 한다. C 스타일의 메소드는 C#식으로 적당히 인수타입을 조정해야 한다. 문자열포인터는 string타입으로, unsigned는 unint로 바꾼다. 매크로 상수는 C에 정의된 것이니까 레퍼런스를 통해 알아낸 정수상수를 써야 한다.

 

커스텀애트리뷰트 - 컴파일 방식이나 생성되는 기계어 코드에는 전혀 영향을 주지 않으며 실행파일에 메타데이터로 포함될뿐이다. 코드에 설명을 다는 일종의 주석이라 할수 있고 일정화되어 있고 자동화된 처리가 가능하다는 점에서 일반 주석과 다르다.

ex.

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Field, AllowMultiple = true, Inherited= false)]
class ProgrammerAttribute : Attribute
{
    private string Name;
    private string Time;
    public ProgrammerAttribute(string aName)
    {
        Name = aName;
        Time = "기록없음";
    }

    public string When
    {
        get { return Time; }
        set { Time = value; }
    }


}

namespace FirstEx1
{
    class Program
    {
        [Programmer("Kim")]
        static public int Field1 = 0;

        [Programmer("Kim", When = "2007년 6월 29일")]
        static public void Method1() {}

        [Programmer("Lee")]
        static public void Method2() { }

        [Programmer("Park"), Programmer("Choi")]
        static public void Method3() { }

        static void Main(string[] args)
        {
        }
    }
}

 이중에서 Method1의 metadata를 본다면

method public hidebysig static void  Method1() cil managed
{
  .custom instance void ProgrammerAttribute::.ctor(string) = ( 01 00 03 4B 69 6D 01 00 54 0E 04 57 68 65 6E 12   // ...Kim..T..When.
                                                               32 30 30 37 EB 85 84 20 36 EC 9B 94 20 32 39 EC   // 2007... 6... 29.
                                                               9D BC )
  // Code size       2 (0x2)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ret
} // end of method Program::Method1

Method3는

.method public hidebysig static void  Method3() cil managed
{
  .custom instance void ProgrammerAttribute::.ctor(string) = ( 01 00 04 50 61 72 6B 00 00 )                      // ...Park..
  .custom instance void ProgrammerAttribute::.ctor(string) = ( 01 00 04 43 68 6F 69 00 00 )                      // ...Choi..
  // Code size       2 (0x2)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ret
} // end of method Program::Method3

이런 식의 주석이 생기게 되는 것이다.

 

전처리문 - C++의 전처리기와 유사하고 C++형식을 빌려서 만든 문법이다.기계어 코드로 바뀌는 것은 아니지만 컴파일과 소스를 관리하는 방식에 영향을 미친다. C#에서 실제로 전처리 과정이 존재하지 않으며 컴파일러가 처리한다. 관습상 전처리 명령이라 부를뿐이다. 동작이나 용도는 같다. 단, 헤더파일을 include하는 #include는 없는데 C#은 선언 순서를 중요시 여기기 않는 언어이며 헤더파일이 없기때문이다.

ex #define, #undef 

존재만 정의하고 취소할뿐이고 using보다 앞에 와야 한다.

조건부 지시자에는 #if #else #elif #endif 등이 있고

애러처리시엔 #warring, #error를 사용하고

코드 블록을 임의로 묶을때는

#region 과 #endregion을 pair로 쓴다.


 

 

 

이 글은 스프링노트에서 작성되었습니다.

by 무위자연 2008. 8. 26. 14:29