2017년 9월 3일 일요일

do while(false)

어떤 작업을 하다가 문제가 생겼을 때 원래대로 복원하고 리턴해야 할 경우가 있습니다.
메모리 관리가 필요한 C++에서

MyClass MemoryAllocate()
{
    ClassA a = new ClassA();
    if(a == null)
        return NULL;

    ClassB b = new ClassB();
    if(b == null)
    {
        delete a;
        return NULL;
    }

    ClassC c = new ClassC();
    if(c == null)
    {
        delete a;
        delete b;
        return NULL;
    }

    MyClass mc = new MyClass(a, b, c);
    if(mc != null)
    {
        delete a;
        delete b;
        delete c;
        return NULL;
    }
    return mc;
}

그런데 이런 식으로 한다면 메모리할당에 문제가 생겼을때 처리하는 루틴이 많이 분산되어 있네요. 에러처리하는 루틴은 한군데 모아놓는 것이 낫죠.

MyClass MemoryAllocate()
{
    ClassA a = NULL;
    ClassB b = NULL;
    ClassC c = NULL;
    MyClass mc = NULL;

    a = new ClassA();
    if(a == NULL)
        goto ErrRtn;

    b = new ClassB();
    if(b == NULL)
        goto ErrRtn;

    ClassC c = new ClassC();
    if(c == NULL)
        goto ErrRtn;

    MyClass mc = new MyClass(a, b, c);
    if(mc != NULL)
        goto ErrRtn;
    return mc;

ErrRtn:
    if(a != NULL)
        delete a;
    if(b != NULL)
        delete b;
    if(c != NULL)
        delete c;
    if(mc != NULL)
        delete mc;
    return NULL;
}

그런데 goto문을 쓴다는 것에 거부감을 느끼는 사람이 많습니다. 저 역시 goto문은 쓰지 않으려고 하고 있거든요.
이런 경우에 do while(false문을 사용하면 구조적으로 해결할 수 있습니다.


MyClass MemoryAllocate()
{
    ClassA a = NULL;
    ClassB b = NULL;
    ClassC c = NULL;
    MyClass mc = NULL;

    do
    {
        a = new ClassA();
        if(a == NULL)
            break;

        b = new ClassB();
        if(b == NULL)
            break;

        ClassC c = new ClassC();
        if(c == NULL)
            break;

        MyClass mc = new MyClass(a, b, c);
        if(mc != NULL)
            break;
        return mc;
    } while(0);

    if(a != NULL)
        delete a;
    if(b != NULL)
        delete b;
    if(c != NULL)
        delete c;
    if(mc != NULL)
        delete mc;
    return NULL;
}



2017년 7월 27일 목요일

c# enum 마지막값 얻기

using System;
using System.Linq;

public class Test
{
    internal enum ttt
    {
        QAZ,
        XSW,
        EDC,
        VFR,
    }

    public static void Main()
    {
        var first = Enum.GetValues(typeof(ttt)).Cast<ttt>().First();
        var last = Enum.GetValues(typeof(ttt)).Cast<ttt>().Last();
        Console.WriteLine(first);
        Console.WriteLine(last);
    }
}
QAZ
VFR


C# 소멸자와 Dispose

다음과 같은 C++ 프로그램을 봅시다

#include <iostream>
using namespace std;

class Test
{
    public :
    Test()
    {
        puts(" Constructor");
    }
    ~Test()
    {
        puts(" Destructor");
    }
};

int main()
{
    puts("Program Start");
    {
        Test t;
        puts("  Do with Test");
    }
    puts("Program End");
    return 0;
}
Program Start
 Constructor
  Do with Test
 Destructor
Program End

블럭 안에서 Test를 선언해 사용하면, 블럭을 빠져나갈때 자동으로 소멸자(Detstructor, ~Test())가 호출되어 Test에 대한 마무리를 할 수 있습니다.

이 프로그램을 C#으로 포팅해서 실행해 보겠습니다.

using System;

public class Test
{
    public Test()
    {
        Console.WriteLine(" Constructor");
    }
    ~Test()
    {
        Console.WriteLine(" Destructor");
    }
}

public class Program
{
    public static void Main()
    {
        Console.WriteLine("Program Start");
        {
            Test t = new Test();
            Console.WriteLine("  Do with Test");
        }
        Console.WriteLine("Program End");
    }
}
Program Start
 Constructor
  Do with Test
Program End
 Destructor

조금 다른 결과가 나왔군요. 소멸자가 호출되긴 하는데, 그 시점이 블럭을 빠져나갈 때가 아니라 프로그램 종료된 후입니다.

C++에서는 t라는 객체가 스택 위에 잡힙니다. 그래서 블록이 닫히고 스택이 해제될 때 해당 객체의 소멸자를 호출할 수 있습니다.
반면 C#의 경우 t 객체는 스택이 아니라 힙(heap) 위에 동적할당의 형태로 잡힙니다. 그래서 스택과는 달리 해제될 시점을 알 수 없는 것이죠.
실제로 C++에서도

        Test *t = new Test();

형태로 동적할당을 하면 블럭을 빠져나갈 때가 아니라 t를 해제할때 소멸자가 호출됩니다.
다만 C#에서는 메모리관리를 프로그래머가 아니라 시스템(Gabage Collection)에서 하므로 메모리해제할 시점을 프로그래머가 확정할 수 없습니다. 위 예제에서처럼 프로그램 종료할 때라든지, 아니면 메모리가 부족해져 가비지 콜렉션을 할때 실행됩니다.

이런 경우 C#에서는 소멸자 대신 Dispose를 사용하는 것이 좋습니다.

using System;

public class Test : IDisposable
{
    public Test()
    {
        Console.WriteLine(" Constructor");
    }
    public void Dispose()
    {
        Console.WriteLine(" Dispose");
    }
}

public class Program
{
    public static void Main()
    {
        Console.WriteLine("Program Start");
        using(Test t = new Test())
        {
            Console.WriteLine("  Do with Test");
        }
        Console.WriteLine("Program End");
    }
}
Program Start
 Constructor
  Do with Test
 Dispose
Program End

Dispose는 인터페이스인 IDisposable의 멤버이므로 반드시 public으로 정의되어야 합니다.
그리고 using문에서 선언된 객체는 using블럭을 빠져나갈때 그 객체의 Dispose() 메소드를 호출합니다. 그러므로 Dispose()로서 소멸자와 동일한 효과를 낼 수 있습니다.

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값을 확인하면 저 세 함수들 중 어느 부분에 버그가 있는지 확인할 수 있으므로 좀더 쉬운 디버깅이 가능합니다.