루아(코로나)의 테이블의 요소(field)는 숫자를 키(key)로 가지는 것들과 문자열을 키로 가지는 것들로 구분할 수 있다. 예를 들어서


          local tA = { 11, x=160, “hello"35, y=200, true}


위 테이블은 숫자키를 가지는 요소가 색으로(굵게) 표시된 것들이며 아래와 완전히 동일하다.


          local tA = {

                    [1]=11-- 숫자키

                    [“x”]=160,

                    [2]=“hello"-- 숫자키

                    [3]=35-- 숫자키

                    [“y”]=200,

                    [4]=true-- 숫자키

          }

즉, 숫자키를 가지는 요소는 네 개이고 문자열키를 가지는 요소는 두 개다. 특이한 것은 루아는 아무 키도 지정하지 않은 요소는 자동으로 숫자키가 1부터 시작해서 할당되며, 또한 사용자가 아무 숫자로 키를 지정할 수 있다는 것이다. 0으로 지정할 수도 있고 음수도 가능하며 심지어 실수도 가능하다.


          local tB = { [-10]=11, 21, 33, x=50, 44 }


  한 가지 오해하기 쉬운 것은 위의 예에서 두 번째 요소인 21의 숫자키가 무엇일까 하는 것이다. -9라고 생각하기 쉬운데 1이다. 앞에서도 언급했듯이 아무 키도 지정되지 않는 요소는 항상 숫자키 1로 부터 시작해서 1씩 증가시킨다.


          local tC = { [2]=11, 22, 33, 44 }


위의 예에서 11의 숫자키가 2로 지정되었는데 33도 2가 되서 숫자키가 겹치게 되므로 보통은 이렇게 정의하지 않을 것이다.


  주의할 점은 테이블의 크기를 구하는 #연산자를 사용할 때이다. #연산자는 자연수(즉, 양의 정수 1,2,3, ...) 숫자키를 가지는 요소만 고려한다는 점에 유의해야 한다. 따라서 앞의 예에서 #tA 값은 4이고 #tB 값은 3이다. 다른 예를 들어 보면


          print( #{ 11, 22, “abc", 44} ) -- 4가 찍힌다.

          print( #{ 11, 22, "abc", x=4} ) -- 3이 찍힌다.

          print( #{ [0]=11, 22, "abc", x=4} ) -- 2가 찍힌다.


  좀 더 정확히 얘기하면 루아에서 #의 정의는 '자연수키를 가지는 요소 중에서 [n]번 요소는 nil이 아니고 [n+1]요소가 nil일 때의 n값(들)' 이다. 테이블을 자연수키로만 얌전하게(...) 관리한 경우 #연산자로 그 크기를 정확하게 구할 수 있다.


  혼동의 여지가 있는 경우는 중간에 nil이 섞여 있을 때이다. 이 경우는 #연산자로 정확한 크기를 구할 수 없다.


          print( #{ 11, 22, nil ,44 } ) -- 4가 찍힌다.

          print( #{ 11, 22, nil, 44, 55, nil } ) -- 2가 찍힌다.


또한 for와 ipairs()함수를 조합하면 테이블의 자연수키 요소만을 고려하게 된다. 그런데 이 경우에도 nil이 나오면 거기서 멈춘다는 것에 주의해야 한다.


          local tE = {11, 22, x=33, 44, nil, 55}

          local cnt = 0

          for key, value in ipairs(tE) do

                    cnt = cnt + 1

                    print(key.." : ".. value) -- 1:11, 2:22, 3:44 까지만 찍힌다.

          end


위의 예에서 문자열키를 가지는 x=33는 건너뛰고, 44까지는 가는데 그 다음의 nil 때문에 55까지는 도달하지 못하게 된다. (이에 반해서 pairs() 함수는 nil을 제외한 숫자키와 문자열키를 가지는 요소 모두를 고려한다.)

  코로나로 코딩을 하다보면 다양한 객체들 (이미지, 타이머, 애니메이션 등등)을 테이블에 저장하여 배열로 관리하는 경우가 있을 것이다. 예를 들어서 다음과 같은 방식이다.


          local tTimer = {}

          local tSprite = {}

          ...

          tTimer[#tTimer+1] = timer.performWithDealy( 500, SomeFunc ) -- 테이블에 타이머레퍼런스 저장

          tSprite[#tSprite+1] = display.newSprite( someSheet, someSequence ) -- 테이블에 애니메이션 저장

          ...


이런 식으로 배열에 타이머나 애니메이션을 저장하면서 관리하다가 어떤 조건에 따라서 중간의 요소를 삭제하기도 할 것이다.


          ...

          timer.cancel( tTimer[some_Id] )

          tTimer[some_Id] = nil

          ...

          tSprite[ another_id ]:removeSelf()

          tSprite[ another_id ] = nil


이런 경우 위에서 언급한 내용을 고려해서 테이블의 크기를 구하거나 반복문으로 테이블에 저장된 요소들을 검사할 때 논리적인 오류가 발생하지 않도록 해야 한다. 테이블의 요소를 완전하게 제거하려면 table.remove() 함수를 사용해야 한다.


Posted by 살레시오

댓글을 달아 주세요

  루아에서 테이블은 마치 배열처럼 사용할 수 있는데 인덱스가 1로 부터 시작한다는 것이 특이하다.


          tA = {10,11, 20, 30, 40}

          print(tA[1]) -- 10 이 찍힌다

          print(tA[3]) -- 20 이 찍힌다


하지만 인덱스를 0으로부터 시작시킬 수도 있다.


          tB = {[0]=10,11, 20, 30, 40}

          print(tB[1]) -- 11 이 찍힌다

          print(tB[3]) -- 30 이 찍힌다


테이블이름 앞에 #을 붙이면 배열의 크기를 구할 수 있는데, 엄밀히 얘기하면 맨 마지막 자연수키를 반환하는 것 같다.


          print(#tA) -- 5가 찍힌다

          print(#tB) -- 4가 찍힌다


만약 tA의 한 요소를 삭제하기 위해서 nil로 지정하면, 예를 들어서


          tA[4]=nil

          print(tA[4]) -- nil 이 찍힌다

          print(#tA) -- 여전히 5가 찍힌다.


이때 오해하기 쉬운게 4번째 요소가 nil로 사라졌으니 tA의 크기는 4로 줄어야 되는 것 아니냐 하는 것인데 여전히 5이다. 4번째 요소는 nil로 바뀌었을뿐 여전히 자리를 차지하고 있다.

  완전히 삭제하려면 table.remove()를 써야한다.


          table.remove(tA, 4)

          print(tA[4]) -- 40이 찍힌다.

          print(#tA) -- 이제 4가 찍힌다.


즉, table.remove()함수를 사용하면 그 즉시로 배열의 인덱스값이 달라진다. 원래 인덱스가 5였던 것이 4로 바뀌는 것이다. 이 사실은 반복문 안에서 table.remove()함수를 사용할 때 반드시 고려해야 한다.


  프로그램을 작성하다보면 필요에 의해서 객체를 동적으로 생성한 후 (몬스터, 총알 등등) 배열에 집어넣게 된다. 그리고 어떤 조건에 맞으면 (화면에서 벗어났다던가) 그것을 삭제해야 되는데 그 조건 검사를 보통 for문으로 다음과 같이 하게 된다.


          for id=1, #tA do

                    ...

                    if condition1 == true then

                              ...

                              tA[id]:revmoveSelf()

                              tA[id] = nil

                    end

                    ...

          end


그냥 이렇게 하는 걸로는 충분하지 않은 이유는 배열의 크기는 그대로이기 때문에 새로운 객체가 생성될 때마다 배열이 계속 커지게 된다. 시간이 지날수록 조건 검사의 부담이 늘어날 것이다. 그래서 table.remove()를 다음과 같이 써야 한다.


          for id=1, #tA do

                    ...

                    if condition1 == true then

                              ...

                              tA[id]:removeSelf()

                              table.remove(tA, id)

                    end

                    ...

          end


그런데 이렇게 하면 모든 요소에 대해서 제대로 검사가 수행이 되지 않는데 그 이유는 table.remove()함수가 실행되면 그 즉시로 인덱스가 변하기 때문에 하나를 건너뛰게 되기 때문이다. 예를 들어 4번 요소가 조건이 맞아서 삭제되면 원래 5번이었던 것이 4번이 되고 그 다음 반복에서는 5번이(원래는 6번 이었던 것) 검사가 되기 때문이다.


  간단한 해법은 역순으로 검사를 하는 것이다.


          for id=#tA, 1, -1 do

                    ...

                    if condition1 == true then

                              ...

                              tA[id]:removeSelf() 

                              table.remove(tA, id)

                    end

                    ...

          end


이렇게 하면 table.remove()가 실행되어도 이후에 검색할 요소의 인덱스는 변하지 않으므로 모든 배열 요소에 대해서 조건검사가 수행이 되게 된다.


Posted by 살레시오

댓글을 달아 주세요