루아(Lua)언어 기초 문법

 1. 루아(Lua) 개요

 2. 식별자(identifier)

 3. 숫자형 데이터

 4. 산술연산자

 5. 부울형 데이터와 조건연산자

 6. 문자열과 '..' 연산자

 7. 루아의 nil

 8. 조건분기문 if ~ then ~ end

 9. 중첩된 조건문을 위한 elseif와 else 명령

10. 논리연산자 and, or, not

11.


2장 루아의 제어문


2.1 루아의 if ~ elseif ~ else ~ end 제어문

2.2 루아의 for ~ do ~ end 반복문

2.3 루아의 while ~ do ~ end 반복문


3장 루아의 함수


3.1 함수의 정의

3.2 지역변수/전역변수


4장 루아의 테이블


4.1 테이블 정의

4.2 

4.2


'주제별 글목록' 카테고리의 다른 글

전기전자기초실험  (2) 2016.08.28
C언어 기초  (0) 2016.01.28
자바스크립트(JS) 강좌글 모음  (0) 2016.01.23
라즈베리 파이 (raspberry pi) 기초 및 응용  (0) 2015.12.23
C# 강좌 모음  (0) 2015.08.01
Posted by 살레시오
,

  프로그램의 덩치가 조금씩 커지면 특정한 기능은 외부파일(모듈)로 따로 분리시켜야 여러모로 효율적이다. 코로나(루아)에서 외부 모듈은 다음과 같이 사용한다. 먼저 새로운 화일을 생성해서 다음과 같이 작성한다.

┌─────────────────────────────
    local M = {}
    M.a = 10
    M.t = {x=0, y=20}
    function M.Fa()
        print("Fa() called.")
    end
    return M
└─────────────────────────────

혹은 위와 완전히 동일하지만 다음과 같이 작성할 수도 있다.

┌─────────────────────────────
    local M = {
    a = 10,
    t = {x=0, y=20},
    Fa = function()
        print("Fa() called.")
    end,
}
    return M
└─────────────────────────────

이 화일에서 하는 일은 테이블 M을 생성한 후 변수 a, t, Fa를 채워서 반환하는 것이다.
이것을 살펴보면 입력변수가 없는 일반적인 함수의 내부와 동일한 구조이고 위아래에function() ... end가 없는 함수의 본체와 모양이 같다는 것을 알 수 있다.
이제 이것을 "lib1.lua" 화일로 저장한 후(화일 이름은 각자 다를 것이다) 현재 화일 내에서 (예를 들어서 main.lua)

-------------------------------------------------------------------------
local libA = require "lib1"
-------------------------------------------------------------------------


과 같이 읽어들이면 "lib1.lua" 화일에서 반환된 테이블 M이 libA변수에 올라온다. 이제 libA변수로 "lib1.lua"모듈의 변수 a, t, Fa등을 다음과 같이 읽고 쓸 수 있다.

-------------------------------------------------------------------------
local b = libA.a -- 변수 읽기
libA.t.x = 100 -- 변수 쓰기
libA.Fa() -- 함수 호출
-------------------------------------------------------------------------

이렇게 기본적인 사용법은 굉장히 간단한데 한 가지 오해할 수 있는 사항이 있다. 예를 들어서 main.lua 가 다음과 같이 작성되어 있다고 하자.

┌─────────────────────────────
    local libA = require "lib1"
    print("libA.t.x="..libA.t.x)
    libA.t.x=100

    local libB = require "lib1"
    print("libB.t.x="..libB.t.x)
└─────────────────────────────

이것의 두 번째 print()문의 출력이 무엇일지 짐작해 보자. 필자는 처음에는 당연히 0일 줄 알았는데 이것은 새로운 테이블 M을 생성해서 libB에 할당하는 것으로 오해하기 쉽기 때문이다. 그런데 실제 실행결과는 100이 찍힌다. 즉 libA의 변경결과가 libB에 반영이 되는 것이다.
이것으로 짐작할 수 있을텐데 lua에서는 같은 외부모듈을 다시 읽어들일 때는 이 전에(맨 처음에) 메모리에 올라온 내용을 참조한다. 즉 libB는 새로 생성되지 않고 이전에 올라온 libA와 완전히 같은 곳을 참조하고 있다. 이후로 몇 번을 반복해서 불러오거나 다른 화일에서 같은 모듈을 읽어 올 때도 맨 처음에 생성된 테이블을 참조한다.

심지어 libA를 삭제해도 모듈은 여전히 메모리에 남아 있다는 것도 유의해야 한다.

┌─────────────────────────────
    local libA = require "lib1"
    print("libA.t.x="..libA.t.x)
    libA.t.x=100

    libA = nil
    collectgarbage("collect") -- libA 완전히 삭제
    local libB = require "lib1"
    print("libB.t.x="..libB.t.x) -- 여전히 출력은 100이다.
└─────────────────────────────

이것을 이용하면 프로젝트 전반에서 공통적으로 참조해야하는 변수를 글로벌 변수로 사용하지 않고 하나의 외부 모듈에 모아놓고 관리할 수도 있다.


Posted by 살레시오
,

 루아(Lua)는 쉽고 간결하고 문법 체계를 가진 프로그래밍 언어로서 중소규모의 앱을 개발하는데 사용될 목적으로 개발되었다. 그만큼 초보자가 익히기 쉬운 장점을 가지고 있으며 익히기 쉽다는 파이썬과 비교해서도 문법이 더 간결하지만 반대 급부로 기능의 제한이 있다. 대표적인 것이 클래스를 구현하는 표준 문법이 없다.


www.lua.org 에서 다운로드할 수 있음


 루아는 성능이 좋고 용량이 작아서 많은 게임의 내장 스크립트 언어로 사용되어 왔으며 대표적인 예로 블리자드사의 World of Warcrafe 에서 사용된 것으로도 유명하다. 루아 전체를 구현한 소스가 ANSI C 코드로 6천 줄 이하이고 기본 엔진의 용량이 100KB이하라고 한다.

 필자도 처음으로 루아라는 프로그램 언어를 접한 계기가 Corona SDK 라는 플랫폼으로 스마트폰 프로그래밍을 공부하면서이다. Corona SDK라는 플랫폼은 현재 무료로 사용할 수 있으며 자신만의 앱을 만들어 동일한 소스로 아이폰과 안드로이드폰의 앱을 동시에 빌드하여 업로드 할 수 있다.(현재는 윈도우즈 같은 데스크탑 프로그램으로 import할 수 있다.) Corona SDK에서 사용하는 언어가 루아이므로 네이티브 언어를 사용해서 개발하는 것과는 비교할 수 없이 난이도가 낮아진다는 큰 장점을 가진다.


www.coronalabs.com


필자의 경우도 루아라는 언어에 대한 기초 지식이 전혀 없었음에도 불구하고 이삼일 정도 이 루아의 문법과 Corona SDK 라이브러리 용법을 익힌 후에 갤럭시노트에서 우주선이 터치하는 대로 움직이는 프로그램을 작성할 수 있었다. 그 당시에는 신선한 충격이었다. 안드로이드 앱은 자바(java), 아이폰은 objective-c 혹은 swift 라고 알고 있었는데 이런 개발 도구가 있다니하고 놀랬던 기억이 있다. 취미나 호기심에서 앱을 (특히 게임앱을) 만들어보고 싶다면 가장 난이도가 낮은 앱인벤터(app inventor)나 루아를 사용하는 코로나SDK, 또는 Gideros가 개발 도구로 적당하다고 생각한다.



Posted by 살레시오
,

  루아(코로나)의 테이블의 요소(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 살레시오
,

  잘 알려져 있다시피 클래스(class)는 객체지향 프로그램에서 핵심적인 역할을 하고 있고 루아에서는 이 기능을 베이스 수준에서 지원하지는 않지만 어느 정도 흉내는 낼 수 있다. 사실 객체지향에서 중요한 특성으로 캡슐화, 상속, 다형성 세 가지 정도가 언급이 되지만 소규모 프로젝트에서는 캡슐화 정도만 어느 정도 구현되어도 코딩과 수정 그리고 디버깅이 상당히 용이해진다고 개인적으로 생각한다. 글을 쓰고 있는 본인도 깊이 있는 지식은 없으므로 여기에서는 초보자들이 간단하게 쓸 수 있는 정도로만 설명하고자 한다.


  

  객체지향이나 클래스의 개념이 생소하다면 일단은 ‘특정한 임무에 관련된 변수들과 그 변수들을 핸들링하는 관련 함수의 집합’ 정도로 이해해도 될 것 같다. 예를 들어서 ‘좌표점과 그것에 관련된 계산’이라는 임무에 대해서


   (1) x좌표

   (2) y좌표

   (3) 한 좌표의 원점으로부터의 거리를 구하는 함수

   (4) 두 점의 거리를 구하는 함수


정도를 구현한다고 하자. 보통 (1),(2)번을 멤버변수라고 하고 (3)(4)번은 멤버함수라고 한다. 이것들을 전체를 하나의 이름으로 묶은 것을 클래스라고 한다.


코로나에서 이것을 외부 모듈로 구현한다면 먼저 다음과 같은 형태를 생각해 볼 수 있다. (외부모듈에 대한 기본적인 것은 이전 포스트를 참조)


┌─────────────────────────────


     local Sqrt = math.sqrt

     local M={}

     

     function M.New(x,y)

          local pt = {x=x or 0, y = y or 0} -- 먼저 멤버변수를 테이블로 새로 생성


          function pt:GetLength() -- 첫 번째 멤버함수를 pt안에서 생성

               return Sqrt(self.x*self.x + self.y*self.y)

          end


          function pt:DistTo(pt2) -- 두 번째 멤버함수를 pt 안에서 생성

               local dx = self.x - pt2.x

               local dy = self.y - pt2.y

               return Sqrt(dx*dx + dy*dy)

          end


          return pt -- 생성된 테이블(인스턴스)를 반환한다.


     end


     return M


└─────────────────────────────


  이 예제에서는 M.New() 함수 안에서 새로운 테이블을 생성한 후 이 안에서 변수와 함수를 다 정의하여 반환하는 식으로 처리했다. 이것을 예를 들어서 ‘point.lua’라고 저장했다면 다른 파일(예를 들어서 main.lua)에서 다음과 같이 불러서 쓸 수 있다.


┌── "main.lua" ───────────────────────────


     local CPoint = require "point" -- 외부모듈을 읽어들인다.


     local pt1 = CPoint.New(10,20) -- 첫 번째 인스턴스 생성

     local pt2 = CPoint.New(30,40) -- 두 번째 인스턴스 생성


     print("length of pt1:".. pt1:GetLength() ) -- 길이 22.36이 찍힘

     print("length of pt2:".. pt2:GetLength() ) -- 길이 50이 찍힘


     print("distance:".. pt1:DistTo(pt2) ) -- 두 점의 거리 28.28이 찍힌다


└─────────────────────────────


이제 CPoint.New()함수를 호출해서 새로운 점좌표를 얼마든지 생성할 수 있으며 보통 이렇게 생성되는 객체를 인스턴스(instance)라고 부른다. 그리고 이렇게 생성된 인스턴스를 통해서 관련 함수를 호출할 수 있다. (print 함수 안의 명령들)


  그런데 이 point1.lua 의 단점은 인스턴스를 생성할 때 마다 그 인스턴스 안에 함수의 본체도 같이 구현된다는 것이다. 예를 들어 100개를 생성하면 함수 본체도 각각 100개가 존재한다. 멤버함수의 개수나 덩치가 커진다면 이것은 실행이나 메모리 관점에서 굉장히 비효율적이다. 그래서 다음과 같이 멤버함수는 외부로 빼는 방식을 생각해 볼 수 있다.


┌─────────────────────────────

          local Sqrt = math.sqrt

          

          local function GetLength(tbl) -- 함수 본체를 외부에 정의

                    return Sqrt(tbl.x*tbl.x + tbl.y*tbl.y)

          end


          local function DistTo(pt1, pt2) -- 함수 본체를 외부에 정의

                    local dx = pt1.x - pt2.x

                    local dy = pt1.y - pt2.y

                    return Sqrt(dx*dx + dy*dy)

          end


          local M={}


          function M.New(x,y)

                    local pt = {x=x or 0, y = y or 0}


                    function pt:GetLength() -- 본체로 리다이렉션시킨다

                              return GetLength(self)

                    end


                    function pt:DistTo(pt2) -- 본체로 리다이렉션시킨다

                              return DistTo(self, pt2)

                    end

                    

                    return pt

          end

          return M

└─────────────────────────────


이제 함수 본체는 (인스턴스 개수와 상관없이) 외부에 하나만 존재하며 인스턴스 안에는 단지 본체로 리다이렉션 시켜주는 조그만 함수가 있을 뿐이다. 앞의 경우보다는 훨씬 효율적이지만 여전히 (작은 크기지만) 함수가 인스턴스 내부에 존재하고 본체로 재호출한다는 점에서 비효율적이다.


  좀 더 루아스럽고 우아하게(...) 개선하려면 이전 포스트에서 설명한 메타테이블의 __index 를 사용하면 된다.


┌─────────────────────────────

          local Sqrt = math.sqrt

          

          local mtIndex = {}


          function mtIndex:GetLength()

                    return Sqrt(self.x*self.x + self.y*self.y)

          end


          function mtIndex:DistTo(pt2)

                    local dx = self.x - pt2.x

                    local dy = self.y - pt2.y

                    return Sqrt(dx*dx + dy*dy)

          end


          local M={}


          function M.New(x,y)

                    local pt = {x=x or 0, y=y or 0} -- 멤버변수를 생성

                   return setmetatable(pt, {__index = mtIndex}) -- 멤버 함수를 메타테이블로 첨부한 후 반환

          end

          

          return M

└─────────────────────────────


이 방법이 코딩의 간결성이나 실행의 효율성에서 앞에서 소개한 방법들 보다 좀 더 앞선다고 할 수 있다.


Posted by 살레시오
,

  루아에서는 테이블에 메타테이블(metatable)이라는 것을 붙일 수 있다. 이 메타테이블에 미리 정해진 필드가 채워져 있다면 이것에 의해서 메타테이블이 붙어 있는 원래 테이블의 동작(특성)을 바꿀 수 있다.

  어떤 테이블에 메타테이블을 붙이는 것은 setmetatable()이라는 함수를 사용한다.



          setmetatable(tA, mtA) -- 테이블 tA에 메타테이블 mtA를 첨가

          tB = setmetatable({}, mtA) -- 빈 테이블에 메타테이블을 mtA를 첨가한 것을 tB에 반환


메타테이블에는 정해진 문자열 키값을 갖는 테이블을 필드로 가져야 되는데 이 미리 정해진 문자열 키값들은 __index, __newindex, __call, __tostring, __add 등등이 있다.

  이 중에서 __index 에 대해서만 간단히 설명하면 다음과 같다. 만약 테이블 tA 의 키값으로 요소들을 접근한다고 할 때(tA.nA, tA[1], tA[“FuncA”] 등등) 그 키값이 tA에 없을 때에는 nil 을 반환할 것이다. 하지만 tA에 메타테이블이 연결되어 있다면 그 연결된 메타테이블의 __index 테이블에 등록된 필드를 추가로 검사한다. 예를 들어서



          local tA={x=10}

          print(tA.y) -- "y"라는 키값이 없으므로 nil 이 찍힌다.

          local mt = { __index = { y = 20 } }

          setmetatable(tA, mt)
          print( tA.y ) -- 메타테이블에 있는 20이 찍힌다.



메타테이블의 __index 내부에는 함수도 물론 정의할 수 있다.



          local mtIndex = {}

          function mtIndex:sum()
                    return self.x + self.y — self는 메타테이블이 붙은 원래 테이블

          end

          local tA={x=10, y=20}
          setmetatable( tA, { __index=mtIndex } )

          print( tA:sum() ) -- 30이 찍힌다


          local tB = setmetatable( {x=30, y=40}, {__index = mtIndex} )
          print( tB:sum() ) -- 70이 찍힌다



위의 예제는 간단하지만 이것을 이해했다면 루아(코로나)에서 객체지향을 간단하게나마 구현하는데 응용할 수 있다.


Posted by 살레시오
,

  루아는 C언어와 다르게 true, false 값만을 가지는 부울형이 있다. C언어에서는 내부적으로 0값을 거짓(false)으로 취급하지만


     ‘루아는 nil 과 false 만을 거짓으로 간주한다


는 점을 유의해야 한다. 정수 0도 루아에서는 진리값이 true이다. C언어에 익숙했다면 참 헷갈리기 쉬운 부분이다.


이 사실을 염두에 두고 루아의 논리연산자를 살펴보자. 루아의 논리연산자는 and, or, not 세 가지가 있다. 각각의 동작을 정리하면 다음과 같다.


  • not A - A가 거짓이면 (즉 nil 혹은 false이면) true, 아니면 false 반환
  • A and B — A가 거짓이면 (즉 nil 혹은 false이면) A를 바로 반환, A가 참이면 B를 반환
  • A or B — A가 참이면 (즉 nil도 false도 아니면) A를 바로 반환, A가 거짓이면 B를 반환


A and B 연산에서 A가 거짓이면 B는 아예 보지도 않고(계산도 안하고) 바로 A를 반환하며, 반대로 A or B 연산에서는 A가 참이면 B는 아예 보지도 않고 바로 A를 반환한다. 이것을 조금만 생각해 보면 not은 항상 부울값(true, false)을 반환하는데 반해서 and와 or는 그 반환값이 부울값이 아닐 수도 있다는 것을 알 수 있다. 몇 가지 예를 들어보면


local a = 1 and 0 — a에는 0값이 이 저장됨

local b = nil and 1 — b에는 nil 이 저장됨

local c = 1 or 0 — c에는 1값이 이 저장됨

local d = nil or 0 — d에는 0 이 저장됨


이것을 응용하면 예를 들어서 함수의 입력인자가 nil인지 아닌지를 따져서 내부변수를 다르게 초기화할 때 용이하다.


---------------------------------------------------

local function Func( xR, yR )

     local x, y

     if xR==nil then

          x = 0

     else

          x = xR

     end

     if yR==nil then

          y = 0

     else

          y = yR

     end

---------------------------------------------------


위와 같은 긴 코드를 다음과 같이 간단하게 한 줄로 줄일 수 있다.


---------------------------------------------------

local function Func( xR, yR )

     local x, y = xR or 0, yR or 0

---------------------------------------------------


또 다른 예로 어떤 테이블이 nil인지 아닌지를 따져서 그 내부 요소를 참조하고자 할 때도 유용하다. 예를 들면


---------------------------------------------------

local x = tLoc.x

---------------------------------------------------


의 경우 만약 tLoc 테이블 자체가 nil 인 경우에는 변수 x에 nil 이 저장되는 것으로 오해하기 쉬운데 실제로는 런타임 에러가 발생하고 프로그램이 멈추게 된다. 개인적으로 코로나로 코딩하면서 초기에 많이 접한 것이 이런 종류의 런타임 에러이다. (반면 tLoc 테이블 자체는 nil이 아닌데 x요소가 없는 경우에는 x에 nil 이 저장되고 런타임 에러는 발생하지 않는다.)

이러한 런타임 에러를 없애고 테이블 자체가 nil일 경우를 처리하려면 예를 들어서 다음과 같이 해야 할 것이다.


---------------------------------------------------

local x

if tLoc == nil then

     x = nil

else

     x = tLoc.x

end

---------------------------------------------------


위와 같은 코드를 and연산자를 이용하면 다음과 같이 한 줄로 바꿀 수 있다.


---------------------------------------------------

     local x = tLoc and tLoc.x

---------------------------------------------------


이와 같이 and 와 or 연산의 특성을 이용하면 프로그램을 간결하게 작성할 수 있다.


Posted by 살레시오
,

루아의 변수는 다음과 같이 8가지 데이터타입을 갖는다.


nil

부울값

숫자

문자열

테이블

함수

----------

코루틴

유저데이터


이 중에서 코루틴과 유저데이터는 코로나SDK와 별로 관련이 없으므로 제외하고 나머지 여섯 개 중 참조를 갖는 것은 함수와 테이블이다. 문자열은 참조가 아니라는 것에 유의해야 한다. 참조란 실제 데이터가 저장된 곳을 가리키는 주소이다. (사실 참조도 주소'값'이므로 루아는 다 값이라고 주장하기도 하지만 혼동의 여지가 있으므로 여기서는 참조와 값을 구분하도록 하겠다.)

따라서 만약 어떤 테이블을 함수의 입력파라메터로 넘길 때 참조가 넘어가므로 그것을 받아서 조작하면 원래의 테이블값도 바뀌게 된다.


┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓

    local function Func1(a, tA)

        a = 20

        tA.x = 20

    end


    local function Func2(tA)

        tA = {x=30}

    end


    local x, tX = 10 , {x=10}

    Func1(x, tX) -- x는 값이, tX는 참조가 넘어간다.

    print(x, tX.x) -- 10, 20 이 찍힌다.

    Func2(tX)

    print(tX.x) --20이 찍힌다.

┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛


위의 예에서 Func1()을 호출할 때 로컬 변수 x는 값이 넘어가고 테이블 tX는 참조가 넘어가므로 함수 호출 후 테이블의 필드만 값이 변경되었다. 반면에 Func2()내에서는 tA가 tX의 참조를 받기는 하지만 새로운 테이블의 참조로 초기화되었다. 따라서 이경우 tA는 넘어온 tX와는 전혀 별개의 것이 되어서 tX에는 아무런 영향을 끼치지 못한다. 그래서 맨 마지막 print()에서 20이 찍히는 것이다.


이와 마찬가지로 함수의 리턴값이 테이블이나 함수일 경우도 참조를 반환한다. 다음 예는 처음에는 조금 이해하기 힘들 수 있으나 한 번 살펴보도록 하겠다.


┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓

    local function Func1(xR)

        local x = xR or 0 -- 내부변수 x를 넘어온 값으로 초기화

        local y = 0

        local Func = function() print("x :"..x);end -- 여기서 x가 사용되었다.

        return y, Func -- y는 '값'을, Func는 '참조'를 반환

    end


    local y1, Func2 = Func1(10)

    local y2, Func3 = Func1(20)

    Func3() -- 20 이 찍힌다.

    Func2() -- 10 이 찍힌다.

┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛


함수 Func1()은 그 안에서 내부변수 y의 '값'과 내부변수 Func의 '참조'를 반환한다. 기본적으로 어떤 함수의 지역변수는 함수가 종료되면 사라진다는 것은 알고 있을 것이다. 변수 y는 함수가 종료되면 더 이상 참조되는 곳이 없으므로 GC의 타겟이 되어 사라질 것이다. 하지만 내부함수 Func는 그 참조가 호출된 곳의 Func2 변수를 통해서 계속 사용되므로 사라지지 않고 살아남게 된다. 그럼 내부변수 x는 어떨까? x변수는 Func안의 print()문에서 참조를 하고 있으므로 Func가 살아있는 한 x도 계속 살아있게 된다.


또 한가지 주의할 것은 Func2 와 Func3에서 참조하는 변수 x는 서로 별개의 것이라는 점이다. 따라서 처음 Func3()호출에는 20이 찍히고 맨 마지막 Func2() 함수를 호출하면 10이 찍힌다. 서로 '다른' 변수 x를 참조하고 있기 때문이다.


이것을 이해했다면 클래스를 구현할 때 외부에서는 접근할 수 없고 내부에서만 사용할 수 있는 private 변수/함수 를 구현하는데 응용할 수 있다.

Posted by 살레시오
,