[C# 7.1] 2. C# 예약어 (extern, ref, params, checked) 전처리기, 특성

RisingJade의 개발기록·2022년 5월 20일
0

C# 7.1 정리

목록 보기
2/4

전처리기

컴파일러에는 별도의 전처리기가 없지만, 이 단원에 설명된 지시문은 전처리기가 있는 것처럼 처리됩니다. 조건부 컴파일에서 지시문을 유용하게 사용할 수 있습니다. C 및 C++ 지시문과 달리, 매크로를 만드는 데는 해당 지시문을 사용할 수 없습니다. 전처리기 지시문은 한 줄에서 유일한 명령이어야 합니다.

  • if: 지정된 기호가 정의된 경우에만 코드가 컴파일되는 조건부 컴파일을 엽니다.
  • elif: 앞에 있는 조건부 컴파일을 닫고 지정된 기호가 정의되었는지에 따라 새 조건부 컴파일을 엽니다.
  • else: 앞에 있는 조건부 컴파일을 닫고 이전 지정된 기호가 정의되지 않은 경우 새 조건부 컴파일을 엽니다.
  • endif: 앞에 있는 조건부 컴파일을 닫습니다.

C# 컴파일러는 #if 지시문을 찾은 후 마지막으로 #endif 지시문을 찾으면 지정된 기호가 정의된 경우에만 지시문 사이에 있는 코드를 컴파일합니다. C 및 C++와 달리, 기호에 숫자 값을 할당할 수 없습니다. C#의 #if 문은 부울이고, 기호가 정의되었는지 여부만 테스트합니다.

if, endif, else, elif, define

어떤 전처리기 기호가 정의가 되었는지 판별하여 조건에 맞는 코드를 실행한다.

예시

#define __X86__
#undef OUTPUT_LOG

using System;

class Program{
	static void Main(string[] args)
    {
#if OUTPUT_LOG
		Console.WriteLine("OUTPUT_LOG 정의됨");
#else
		Console.WriteLine("OUTPUT_LOG 정의 안 됨");
#if __X86__
		Console.WriteLine("__x86__ 정의됨");
#elif __X86__
		Console.WriteLine("__x86__ 정의 안 됨");
#endif
    }
}

특성

닷넷의 어셈블리 파일레는 해당 어셈블리 스스로를 기술하는 메타데이터가 포함돼 있다. 예를 들어, 어셈블리 내에서 구현하고 있는 타입, 그 타입 내에 구현된 멤버 등의 정보가 메타데이터에 해당한다.
특성은 이런 메타데이터에 함께 호함되며, 원하는 데이터를 보관하는 특성을 자유롭게 정의해서 사용 할 수 있다.

예시

class AuthorAttribute : System.Attribute{
	string name;
    public AuthorAttribute(string name){
     	this.name = name;
	}
}

[Author("Anders")]
class Program{
	~~~
}

예약어

연산 범위 확인: checked, unchecked

정수 계열 타입의 산술 연산을 하거나, 서로 다은 정수 타입 간의 형변환을 하게 되면 표현 가능한 숫자의 범위를 넘어서는 경우가 존재(오버플로우, 언더플로우). 보통 이러한 문제는 개발자가 의도한 경우가 아니다.
따라서 C#으로 하여금 이런 문제가 생기면 오류를 발생시키라고 명시할 수 있는데, 이때 checked 예약어가 그와 같은 역할을 한다.

예시

short c = 32767;
int n = 32768;
-----------------------------------------
checked
{
	c++; // 예외 발생! (System.OverflowException 발생.)
}
------------------------------------------
unchecked
{
	c++; // 예외 발생 X
}
  • 단순히 코드 곳곳에 넣어서 확인하는 것이 아니라 전체 소스코드에 걸쳐 강제로 적용하고 싶다면 /checked 옵션을 컴팡일러에 적용하면 된다.

가변 매개변수: params

메서드를 정의할 때 몇 개의 인자를 받아야 할지 정할 수 없을 때가 있다. 이때 쓰는 것이 params 예약어이다.

예시

static int Add(params int[] values){
	int result =0;
    for (int i=0; i < values.length; i++{
    	result += values[i];
    }
    return result;
}

static void Main(string[] args){
	Console.WriteLine(Add(1,2,3,4,5));
    Console.WriteLine(Add(2,3,4,5,6,7,8,9));
}

Win32 API 호출: extern

닷넷 호환 언어로 만들어진 managed code에서 C/C++ 같은 언어로 만들어진 unmaged code의 기능을 사용하는 수단으로 플랫폼 호출(P/Invoke: platform invocation)이 있다. extern 예약어는 PInvoke 호출을 정의하는 구문에 사용된다.

Extern 구문에는 3가지 정보가 필요하다.

  • 비관리 코드를 제공하는 DLL 이름
  • 비관리 코드의 함수 이름
  • 비관리 코드의 함수 형식(signature)

예시

using System.Runtime.InteropServices;
class Program{
	[DllImport("user32.dll")]
    static extern int MessageBeep(uint uType);
    
    static int TestMethod(uint type)/// 비교를 위한 정적 메소드
    {
    	return 0;
    }
    
    static void Main(string[] args){
    	MessageBeep(0);
    }
}
  • 사실 extern 예약어 자체는 메서드에 코드가 없어도 컴파일되게 하는 역할만 한다. 해당 Win32 API와 C#코드를 연결하는 역할은 DllImport특성을 적용해야만 이용할 수 있다.
  • 닷넷 CLR은 DLLImport특성으로 전달된 DLL파일명에 extern 예약어가 지정된 메서드와 시그니처가 동일한 Win32 API를 연결한다. 이렇게 정의된 extern 정적 메서드를 사용하는 방법은 일반적인 정적 메서드를 사용하는 방법과 동일하다.

안전하지 않은 컨텍스트: unsafe

C#의 독특한 특징 중 하나는 기존 네이티브 언어(C/C++)와의 호환성을 위한 기능이 추가됐다는 점이다. Win32 API를 직접 호출할 수 있는 extern 예약어도 그런 사례 중 하나다.
C/C++와의 호환성을 높이기 위해 존재하는 또 한 가지 사례는 바로 안전하지 않은 컨텍스트(unsafe context)에 대한 지원이다.
unsafe context란 말 그대로 안전하지 않는 코드를 포함한 영역을 의미하며, 안전하지 않은 코드란 역시 포인터(pointer)가 되겠다.

즉, C#은 C/C++의 포인터를 지원하며 unsafe 예약어는 포인터를 쓰는 코드를 포함하는 클래스나 그것의 멤버 또는 블록에 사용한다.

예제

unsafe static void GetAddResult(int* p, int a, int b)
{
	*p = a + b;
}
static void Main(string[] args)
{
	int i;
    unsafe
    {
    	GetAddResult(&i, 5, 10);
    }
    
    Console.WriteLine(i);
}

이런 안전하지 않는 소스코드는 반드시 컴파일러 옵션으로 /unsafe를 지정해야한다.

참조 형식의 멤버에 대한 포인터: fixed

unsafe 문맥에서 포인터를 사용할 수 있는 곳은 스택에 데이터가 저장된 변수에 한해 적용된다. 즉, 지역 변수나 매서드의 매개변수 타입이 값 형식인 경우에만 포인터 연산자(*,&)를 사용할 수 있다.

  • 당연하게도 참조 형식의 데이터는 직접적인 포인터 연산을 지원할 수 없는데, 왜냐하면 참조 형식의 인스턴스는 힙에 할당되고 그 데이터는 GC가 동작할 때마다 위치가 바뀔 수 있기 때문이다. 이로 인해 포인터를 이용해 그 위치를 가리키면 가비지 수집 이후 엉뚱한 메모리를 가리킬 수 있다는 위험이 생긴다.
  • 이를 해결하기 위해 fixed라는 예약어를 도입했다. 이 예약어는 힙에 할당된 참조 형식의 인스턴스를 가비지 수집기가 움직이지 못하도록 고정시킴으로써 포인터가 가리키는 메모리를 유효하게 만드는 것이다.

예시

class Managed
{
	public int cnt;
    public string name;
}

class Program
{
	unsafe static void Main(string[] args)
    {
    	Managed inst = new Managed();
        
        inst.cnt = 5;
        inst.name = "text";
        
        fixed(int* pValue = &inst.cnt)
        {
			*pValue = 6;        
        }
        fixed(char* pChar = inst.Name.ToCharArray())
        {
        	for(int i =0; i < inst.Name.Length; i++)
            {
            	Console.WriteLine(*(pChar+i));
            }
        }
    }
}

주의!
C#은 객체 인스턴스의 포인터를 가져오는 것을 허용하지 않는다. 대신 해당 객체가 가진 멤버 데이터가 값 형식이거나 값 형식의 배열인 경우에만 포인터 연산을 할 수 있다.
하지만 fixed되는 대상은 객체의 데이터를 포함한 객체가 된다. 따라서 프로그램 실행이 fixed 블록의 끝에 다다를 때까지는 GC가 해당 객체를 이동시킬 수 없다.

보통 fixed된 포인터는 관리 프로그램의 힙에 할당된 데이터를 관리되지 않은 프로그램에 넘기는 용도로 쓰인다.

스택을 이용한 값 형식 배역: stackalloc

값 형식은 스택에 할당되고 참조 형식은 힙에 할당된다. 그런데 값 형식임에도 그것이 배열로 선언되면 힙에 할당된다.
stackalloc 예약어는 값 형식의 배열을 힙이 아닌 스택에 할당하게 한다.

  • 스택에 배열을 만들고 싶은 이유는 힙을 사용하지 않으므로 GC의 부하가 없다는 장점이 있다.
    게임 프로그램을 만들 때 유용하게 사용되는데, 일정주기마다 호출되는 Update 메서드 내에 힙에 메모리를 할당하면 GC로 인해 끊김 현상이 발생할 수 있기 때문에 stackalloc을 통해 GC의 호출 빈도를 조금이나마 낮출 수 있다.
    +그러나 스택은 기본적으로 스레드마다 1MB(윈도우 기준)정도 밖에 안되는 크기이기 때문에 이런 제한된 자원을 남용하면 프로그램 실행에 오류를 발생시킬 수 있으므로 사용시 신중을 기하자. (따라서 잘 안쓰이긴 한다.)
profile
언제나 감사하며 살자!

0개의 댓글