2017년 1월 19일 목요일

숫자보다 상수를 사용하자

만약 10개의 데이터를 처리하는 함수를 만들었다고 해 봅시다.

void Data::Function()
{
    for(int k = 0; k < 10; ++k)
        Process(data[k]);

    for(int k = 0; k < 10; ++k)
        SendTo(socket, data[k]);
}

Datum Data::GetData(int slot)
{
    if(slot >= 0 && slot < 10)
        return Data[slot];
    return null;
}

그런데 데이터가 15개로 늘었다면 어떻게 해야 할까요?

void Data::Function()
{
    for(int k = 0; k < 15; ++k)
        Process(data[k]);

    for(int k = 0; k < 15; ++k)
        SendTo(socket, data[k]);
}

Datum Data::GetData(int slot)
{
    if(slot >= 0 && slot < 15)
        return Data[slot];
    return null;
}

와 같이 프로그램 전체를 뒤져 위와 같이 Data[]를 참조하는 부분을 다 고쳐야 합니다. 한군데라도 빠뜨리면 그것이 바로 버그로 나타나게 됩니다.

이럴 경우에 C/C++에서는 #define을 사용할 수 있습니다.

#define DATACOUNT    15
void Data::Function()
{
    for(int k = 0; k < DATACOUNT; ++k)
        Process(data[k]);

    for(int k = 0; k < DATACOUNT; ++k)
        SendTo(socket, data[k]);
}

Datum Data::GetData(int slot)
{
    if(slot >= 0 && slot < DATACOUNT)
        return Data[slot];
    return null;
}


와 같이 한다면 이후에 #define문만 수정하면 됩니다.

#define 기능이 부족한 C#이나 아예 다른 언어일 경우에는 어떻게 할까요? 제 경우에는 저런 상수들만 모아놓는 다른 클래스를 선언합니다.*

public class Constant
{
    public const int DataCount = 10;
}

public class Data
{
    public void Function()
    {
        for(int k = 0; k < Constant.DataCount; ++k)
            Process(data[k]);

        for(int k = 0; k < Constant.DataCount; ++k)
            SendTo(socket, data[k]);
    }

    Datum Data::GetData(int slot)
    {
        if(slot >= 0 && slot < Constant.DataCount)
            return Data[slot];
        return null;
    }
}



Constant = {
    DataCount = 10
    }


function Function()
    for k = 1, Constant.DataCount do
        Process(data[k]);
    end

    for k = 1, Constant.DataCount do
        SendTo(socket, data[k]);
    end
end

function GetData(int slot)
    if(slot >= 0 && slot < Constant.DataCount)
        return Data[slot];
    return null;
end

단, Lua처럼 constant기능이 없는 프로그램언어일 경우, 프로그램 실행중 변수값이 바뀌지 않도록 조심해야 합니다.


물론 이렇게 숫자를 그냥 입력하는 것에 비해 상수로 처리하는 것이 더 번거로운 것은 사실입니다. 그냥 간단하게 숫자 두세개 입력하는 것에 비해 #define문을 삽입하고(또는 constant변수를 만들고) 그 변수이름을 입력하는 것이 귀찮을 수도 있습니다.
그 때문에 '여기서만 사용할 것이다', '어차피 바뀌지 않을 값이다' 등 여러 핑계로 숫자를 그대로 입력하고 싶어지는 경우가 많죠(저역시 그런 유혹을 가끔 느끼곤 합니다)

하지만 그런 상황에서도 숫자를 직접 입력하는 것보다 #define/const를 사용하는 것이 좋습니다. 만약

internal class DataClass
{
    private Datum[] = new Datum[100];

    internal void Function()
    {
        for(int k = 0; k < 10; ++k)
            Send(socket, Datum[k]);
    }
}

이런 코드가 있다면 어떨까요? Function()의 10이란 숫자가 원래 크기 100을 잘못 쓴 버그라고 생각할 가능성이 큽니다. 하지만

internal class Constant
{
    internal const int DataSize = 100;
    internal const int FirstElementSend = 10;
}

internal class DataClass
{
    private Datum[] = new Datum[Constant.DataSize];

    internal void Function()
    {
        for(int k = 0; k < Constant.FirstElementSend; ++k)
            Send(socket, Datum[k]);
    }
}

라고 하면 원래부터 앞쪽 일부 데이터만 보내는 것임을 확실히 알 수 있을 것입니다.



* 사실 C/C++에서도 #define을 쓰는 것보다 const 변수를 사용하는 것이 좋습니다. 왜냐하면 #define과 달리 const변수는 디버깅정보로 들어가기 때문입니다.
그때문에 const변수를 사용한다면 디버깅중에 상수값을 확인할 수도, 중간계산값을 확인할 수도 있습니다. #define을 사용하면 디버깅시 최종값만을 확인할 수 있습니다.

2017년 1월 7일 토요일

bool보다 enum이 좋다

프로그래밍을 하다보면 여러 조건들을 합쳐 하나의 조건으로 만들어야 하는 경우가 있습니다.
이를테면

    bool CanMoveTo(int x, int y)  // x, y위치로 갈 수 있는가
    {
        if(IsStun())              // 스턴걸린 상태라면
           return false;
        if(IsObstacle(x, y)       // x, y위치에 장애물이 있다면
           return false;
        if(IsThereEnemy(x, y)     // x, y위치에 적이 있다면
           return false;
        return true;
    }

    void TryMoveTo(int x, y)
    {
        if(CanMoveTo(x, y))
            Enter(x, y);
        else
            EnterFail();    // 로그
    }

와 같이 말입니다. x, y 위치로 갈 수 있는지 없는지 파악해서 true/false를 리턴하는 함수입니다.
그런데 테스트해보니, 분명히 갈 수 있는 위치이며 갈 수 있는 상태인데 false를 리턴한다면 어떨까요?
만약 다른 테스터의 테스트중이나 외부에서 디버그로그를 받아야 하는 상황이라서 디버거를 사용 못한다면 저 세 함수를 다 분석해서 버그가 어디 있는지 찾아야 합니다.

이럴 때는 bool보다 enum을 사용하는 것이 낫습니다.

    enum WhyCannotMoveTo
    {
        NoReason,           // 이유없음 - 들어갈 수 있음
        Stun,
        Obstacle,
        Enemy,
    }

    WhyCannotMoveTo CanMoveTo(int x, int y)  // x, y위치로 갈 수 있는가
    {
        if(IsStun())              // 스턴걸린 상태라면
           return Stun;
        if(IsObstacle(x, y)       // x, y위치에 장애물이 있다면
           return Obstacle;
        if(IsThereEnemy(x, y)     // x, y위치에 적이 있다면
           return Enemy;
        return NoReason;
    }

    void TryMoveTo(int x, y)
    {
        WhyCannotMoveTo reason = CanMoveTo(x, y);
        if(reason == NoReason)
            Enter(x, y);
        else
            EnterFail(reason);    // 로그
    }

즉 reason값을 확인하면 저 세 함수들 중 어느 부분에 버그가 있는지 확인할 수 있으므로 좀더 쉬운 디버깅이 가능합니다.