[C#] 예약어(Reserved Word)/키워드(Keyword)

Running boy·2023년 8월 6일
0

컴퓨터 공학

목록 보기
20/36

checked/unchecked

checked는 정수 계열 타입의 산술 연산이나 형변환에서 오버플로우(overflow)가 발생할 경우 에러를 발생시켜 의도하지 않은 동작을 예방한다.

Console.WriteLine(int.MaxValue + 1); // -2147483648

checked
{
    Console.WriteLine(int.MaxValue + 1); // 컴파일 에러
}

unchecked는 checked와는 반대로 에러를 발생시키지 않는다.

Console.WriteLine((short)int.MaxValue); // 컴파일 에러

unchecked
{
    Console.WriteLine((short)int.MaxValue); // -1(int.MaxValue의 하위 2byte)
}

checked를 기본값으로 설정하는 방법

비주얼 스튜디오의 '프로젝트 속성 - 빌드 - 고급'에서 checked를 기본값으로 설정할 수 있다.

이 경우 checked 키워드를 설정하지 않아도 오버플로우시 에러를 발생시킨다.

checked 옵션

마찬가지로 이 설정을 했음에도 에러를 발생시키고 싶지 않다면 unchecked 키워드를 사용하면 된다.


params

파라미터의 갯수를 지정하지 않고 배열로서 취급할 수 있는 키워드이다.

static void Method(params int[] args)
{
    ...
}
Method(1, 2, 3, 4);

반드시 마지막 파라미터여야 한다.
(두 개 이상 지정 불가)

void Method(params int[] args1, params int[] args2) // 컴파일 에러
{
    ...
}

배열을 인자로 넘겨줄 수 있다.

static void Method(params int[] args)
{
    ...
}
Method(new int[4] { 1, 2, 3, 4 });

하지만 배열과 단일 타입을 동시에 넘겨줄 수는 없다.

static void Method(params int[] args)
{
    ...
}
Method(0, new int[4] { 1, 2, 3, 4 }); // 컴파일 에러

extern

외부에서 C/C++과 같은 비관리 코드로 구현된 메서드를 관리 코드에서 선언할 때 사용한다.

extern 키워드의 역할
1. 선언하는 메서드가 외부에서 정의됐음을 알린다.
2. 메서드를 정의하지 않아도 컴파일 에러를 발생시키지 않는다.

extern 구문 작성에 필요한 정보
1. 외부 코드를 제공하는 DLL 이름
2. 외부 메서드의 시그니처

[DllImport("user32.dll")]
static extern int MessageBeep(uint uType);
MessageBeep(10);

unsafe

안전하지 않은 문맥(unsafe context)을 지원하는 키워드이다.

여기서 안전하지 않은 문맥이란 포인터(pointer)를 사용하는 문맥이다.

unsafe static void Sum(int* a, int b, int c)
{
    *a = b + c;
}
int n;

unsafe
{
    Sum(&n, 1, 2);
}

Console.WriteLine(n); // 5

만약 unsafe 구문에서 컴파일 에러가 발생한다면?

문법적으로 문제가 없지만 컴파일 에러가 발생하는 경우 '프로젝트 속성 - 빌드 - 일반'에서 안전하지 않은 코드를 허용하는지 확인해보자.

unsafe 옵션


fixed

해당 키워드는 상황에 따라 두 가지 의미를 가진다.

포인터의 경우

힙 영역에 정의된 참조 형식의 데이터 주소를 고정시키는 키워드이다.

힙 영역은 가비지 수집이 일어날 때마다 값의 주소가 변경되기 때문에 주소를 고정해야 포인터로 접근할 수 있다.

단 C#은 객체 인스턴스 자체를 포인터로 접근할 수 없고, 값 형식이거나 값 형식의 배열인 멤버만 포인터가 허용된다.
(string은 내부적으로 char의 배열이다.)

class TestClass
{
    public int num;
    public string str;

    public TestClass(int num, string str)
    {
        this.num = num;
        this.str = str;
    }
}
TestClass testClass = new TestClass(1, "Test");

Console.WriteLine($"Before num: {testClass.num}, str: {testClass.str}");

unsafe
{
    fixed (int* ptr = &testClass.num)
    {
        *ptr = 10;
    }

    fixed (char* ptr = testClass.str)
    {
        *ptr = 'W';
    }
}

Console.WriteLine($"After num: {testClass.num}, str: {testClass.str}");

// 출력:
// Before num: 1, str: Test
// After num: 10, str: West

string은 불변 타입이다.
하지만 포인터로 접근할 경우 그 값을 수정할 수 있다.

포인터를 이용한 데이터 접근이 unsafe하다는 예시를 보여준다.


버퍼의 경우

고정 크기 버퍼를 갖게 해주는 키워드이다.

// C++
struct CppStructType
{
    public:
        int fields[2];
        __int64 dummy[3];
}
// C#
struct CSharpStructType
{
    public int[] fields;
    public long[] dummy;
}

위 두 코드는 각각 C++, C#에서 동일한 구조체를 정의한 것이다.

하지만 자세히 보면 C++에서의 멤버 배열은 각각 크기가 2, 3으로 정해진 반면 C#에서의 멤버 배열의 크기는 정해지지 않았다.

이는 메모리 구조의 차이로 설명된다.

cpp_struct_type_fixed

struct_memory_csharp


C++에서 배열은 정적으로 크기를 정해줘야 된다.
즉 그 크기를 유추할 수 있기 때문에 구조체의 멤버로 정의됐을 때 연속된 메모리 공간을 차지한다.

반면에 C#의 배열은 동적으로 크기가 정해진다.
그렇기 때문에 임의의 힙 영역에 값을 저장하고 참조하는 형식으로 구현된다.

만약 C++로 작성된 외부 함수를 C#에서 사용하는데 위와 같은 구조체를 정의해야 되는 경우 메모리 구조 차이로 인해 오작동을 일으킬 수 있다.

이를 방지하기 위해 C++과 같이 고정된 크기의 버퍼를 갖게 해주는 역할을 fixed 키워드가 한다.

unsafe struct CSharpStructType // 반드시 unsafe 문맥에서 정의
{
    public fixed int fields[2]; // 문법 역시 C++과 유사하게 바뀐다.
    public fixed long dummy[3];
}

[DllImport("...processItem 구현.dll...", SetLastError = true)]
internal static unsafe extern int ProcessItem(CSharpStructType value);
unsafe
{
    CSharpStructType item = new CSharpStructType();
    ProcessItem(item);
}

stackalloc

값 형식의 배열을 스택 영역에 할당하게 할 수 있는 키워드이다.

스택 영역에 할당되기 때문에 포인터를 사용할지라도 스코프를 벗어나면 할당이 해제된다.
(에러가 발생하진 않지만 더미 값이 들어간다.)

unsafe static int* Method() // 반드시 unsafe 문맥에서 정의
{
    int* ptr = stackalloc int[10];

    for (int i = 0; i < 10; i++)
        *(ptr + i) = i;

    return ptr;
}
unsafe
{
    int* ptr = Method();

    Console.WriteLine(*(ptr + 1)); // 첫 출력은 값이 제대로 나오지만
    Console.WriteLine(*(ptr + 1)); // 이후 더미 값이 나온다.
    Console.WriteLine(*(ptr + 1));
}

// 출력:
// 1
// 153 <- 매 시행마다 값이 바뀜
// 153 <- 매 시행마다 값이 바뀜

왜 굳이 배열을 스택에 할당하려 하는가?

만약 배열을 생성하는 메서드를 계속 호출한다면 힙에 배열이 할당되고 메서드가 반환되면서 배열의 참조를 잃는 과정을 반복하게 된다.

이는 힙의 가비지가 되고 GC의 수집 대상이 되기 때문에 GC의 호출이 잦아짐에 따라 프로그램의 성능에 영향을 준다.

하지만 스택에 배열을 할당할 경우 필요한 연산만 수행하고 부하 없이 할당을 해제할 수 있다.

배열이 단순히 연산에 임시적으로 필요한 타입이라면 스택에 할당하는 것이 프로그램의 성능 향상에 유리하다.


그럼에도 stackalloc을 잘 사용하지 않는 이유는?

스택은 스레드마다 일정한 크기(기본적으로 1MB)를 갖는다.

만약 이 제한된 자원을 남용할 경우 스택 오버플로우(Stack Overflow)가 발생할 수 있다.


참고 자료
시작하세요! C# 10 프로그래밍 - 정성태

profile
Runner's high를 목표로

0개의 댓글