Lua 프로그래밍 - 반복자와 메타테이블, 객체지향 프로그래밍

반복자와 제네릭 for

반복자와 클로저

먼저 간단한 반복자를 구현해보겠습니다. 반복자는 내부 상태를 유지하는 클로저를 통해 구현됩니다.

-- 팩토리 함수: 테이블을 받아 반복자를 반환
local function factory(tbl)
    local index = 0
    return function()
        index = index + 1
        return tbl[index]
    end
end

data = {10, 20, 30}
iterator = factory(data) -- 팩토리 호출하여 반복자 생성

while true do
    local value = iterator() -- 반복자 호출
    if value == nil then
        break
    end
    print(value)
end

그러나 제네릭 for를 사용하면 더 간결하게 표현할 수 있습니다. 제네릭 for는 이러한 반복 작업을 위해 설계되었습니다.

data = {10, 20, 30}
-- 내부에서 반복 함수를 관리하므로 별도의 변수 불필요
for value in factory(data) do
    print(value)
end

다음은 파일에서 단어를 읽어 출력하는 예제입니다. 반복자 자체는 복잡하지만 사용은非常简单합니다.

function extractWords()
    local lineContent = io.read()
    local position = 1
    return function()
        while lineContent do
            local word, endPos = string.match(lineContent, "(%w+)()", position)
            if word then
                position = endPos
                return word
            else
                lineContent = io.read()
                position = 1
            end
        end
        return nil
    end
end

for word in extractWords() do
    print(word)
end

무상태 반복자

상태를 저장하지 않는 반복자는 여러 반복문에서 재사용할 수 있어 새로운 클로저 생성开销을 절약할 수 있습니다.

local function processFunc(tbl, idx)
    idx = idx + 1
    print("process idx = " .. idx)
    local element = tbl[idx]
    if element then
        return idx, element
    end
end

local function makeIterator(tbl)
    return processFunc, tbl, 0
end

local sample = {111, 222, nil, 555}
for key, val in makeIterator(sample) do
    print(key .. " " .. val)
end

출력:

process idx = 1
1 111
process idx = 2
2 222
process idx = 3

pairs 함수는 ipairs와 유사하지만, 기본 함수인 next를 반복 함수로 사용합니다.

local function iteratePairs(tbl)
    return next, tbl, nil
end

local sample = {111, 222, nil, 555}
for key, val in iteratePairs(sample) do
    print(key .. " " .. val)
end

출력:

1 111
2 222
4 555

테이블의 키를 알파벳순으로 정렬하여 출력하는 예제:

local functionNames = {
    ["luaH_set"] = 10,
    ["luaH_get"] = 24,
    ["luaH_present"] = 48,
}

local nameList = {}
for name in pairs(functionNames) do
    nameList[#nameList + 1] = name
end
table.sort(nameList)

for _, name in ipairs(nameList) do
    print(name)
end

출력:

luaH_get
luaH_present
luaH_set

반복자를 사용하여 키 순서로 정렬된 함수를 출력:

local functionNames = {
    ["luaH_set"] = 10,
    ["luaH_get"] = 24,
    ["luaH_present"] = 48,
}

function sortByKey(tbl, compareFunc)
    local keys = {}
    for key in pairs(tbl) do
        keys[#keys + 1] = key
    end
    table.sort(keys, compareFunc)
    
    local current = 0
    return function()
        current = current + 1
        return keys[current], tbl[keys[current]]
    end
end

for name, num in sortByKey(functionNames) do
    print(name, num)
end

출력:

luaH_get    24
luaH_present    48
luaH_set    10

메타테이블과 메타메서드

메타테이블의 메타메서드는 값이 특정 연산에直面했을 때의 동작을 정의합니다.

local Set = {}
local metaTable = {}

function Set.create(list)
    local instance = {}
    setmetatable(instance, metaTable)
    for _, v in ipairs(list) do
        instance[v] = true
    end
    return instance
end

function Set.union(a, b)
    if getmetatable(a) ~= metaTable or getmetatable(b) ~= metaTable then
        error("집합이 아닌 값을 연산하려고 합니다", 2)
    end
    local result = Set.create{}
    for k in pairs(a) do
        result[k] = true
    end
    for k in pairs(b) do
        result[k] = true
    end
    return result
end

function Set.intersection(a, b)
    local result = Set.create{}
    for k in pairs(a) do
        result[k] = b[k]
    end
    return result
end

function Set.toString(set)
    local elements = {}
    for e in pairs(set) do
        elements[#elements + 1] = tostring(e)
    end
    return "{" .. table.concat(elements, ", ") .. "}"
end

-- 관계 연산자 메타메서드 설정
metaTable.__le = function(a, b)
    for k in pairs(a) do
        if not b[k] then return false end
    end
    return true
end

metaTable.__lt = function(a, b)
    return a <= b and not(b <= a)
end

metaTable.__eq = function(a, b)
    return a <= b and b <= a
end

-- 산술 연산자 메타메서드 설정
metaTable.__add = Set.union
metaTable.__mul = Set.intersection
metaTable.__tostring = Set.toString

local set1 = Set.create{10, 20, 30, 50}
local set2 = Set.create{30, 1}

print(getmetatable(set1))
print(getmetatable(set2))
print(set1)
print(set1 + set2)
print(set1 * set2)

set1 = Set.create{2, 4}
set2 = Set.create{4, 10, 2}

print(set1 <= set2)
print(set1 < set2)
print(set1 >= set1)
print(set1 > set1)
print(set1 == set2 * set1)

metaTable.__metatable = "protected"
print(getmetatable(set1))

테이블 관련 메타메서드: __index와 __newindex

local prototype = {x = 0, y = 0, width = 100, height = 100}
local mt = {}

function createObject(params)
    setmetatable(params, mt)
    return params
end

mt.__index = function(_, key)
    return prototype[key]
end

mt.__newindex = function(_, key, value)
    return
end

obj = createObject{x = 10, y = 20}
print(obj.x)
print(obj.width)
mt.__index = prototype
print(obj.width)
obj.newField = 100
print(obj.newField)

기본값을 가진 테이블:

local uniqueKey = {}
local mt = {__index = function(t) return t[uniqueKey] end}

function setDefaultValue(tbl, default)
    tbl[uniqueKey] = default
    setmetatable(tbl, mt)
end

table1 = {x = 10, y = 20}
print(table1.z)
setDefaultValue(table1, 0)
print(table1.z)

table2 = {x = 10, y = 20}
setDefaultValue(table2, 1)
print(table2.z)
print(table1.z)

테이블 접근 추적:

function monitor(tbl)
    local proxy = {}
    local mt = {
        __index = function(_, k)
            print("*접근: " .. tostring(k))
            return tbl[k]
        end,
        __newindex = function(_, k, v)
            print("*수정: " .. tostring(k) .. " → " .. tostring(v))
            tbl[k] = v
        end,
        __pairs = function()
            return function(_, k)
                local nextKey, nextValue = next(tbl, k)
                if nextKey ~= nil then
                    print("*순회: " .. tostring(nextKey))
                end
                return nextKey, nextValue
            end
        end,
        __len = function() return #tbl end
    }
    setmetatable(proxy, mt)
    return proxy
end

t = {[2] = "hi"}
print(t[2])
t = monitor(t)
print(t[2])
t[2] = "hello"
print(t[2])

t = monitor({10, 20})
print(#t)
for k, v in pairs(t) do print(k, v) end

읽기 전용 테이블:

function readOnly(t)
    local proxy = {}
    local mt = {
        __index = t,
        __newindex = function(t, k, v)
            error("읽기 전용 테이블을 수정할 수 없습니다", 2)
        end
    }
    setmetatable(proxy, mt)
    return proxy
end

days = readOnly{
    "Sunday", "Monday", "Tuesday", "Wednesday",
    "Thursday", "Friday", "Saturday"
}

print(days[1])
days[2] = "Noday"

객체지향 프로그래밍

프로토타입 기반 언어의 접근법을 참고하여 Lua에서 클래스를 흉내낼 수 있습니다. 다른 객체의 프로토타입이 될 특별한 객체를 만들기만 하면 됩니다. 객체 A의 프로토타입을 B로 설정하려면:

setmetatable(A, {__index = B})

이제 A에서 찾을 수 없는 연산은 B에서 검색됩니다. B를 A의 클래스로 보는 것은 용어상의 변화일 뿐입니다.

클래스 사용 예제:

Account = {
    balance = 0,
    withdraw = function(self, amount)
        if amount > self.balance then
            error("잔액 부족")
        end
        self.balance = self.balance - amount
    end
}

function Account.deposit(self, amount)
    self.balance = self.balance + amount
end

function Account.new(self, obj)
    obj = obj or {}
    self.__index = self
    setmetatable(obj, self)
    return obj
end

a = Account:new{balance = 0}
a:deposit(300.00)
a:withdraw(100.00)
print(a.balance)

-- 상속
SpecialAccount = Account:new()
s = SpecialAccount:new{limit = 1000.00}
s:deposit(600.00)

function SpecialAccount.withdraw(self, amount)
    if amount - self.balance >= self:getLimit() then
        error("잔액 부족")
    end
    self.balance = self.balance - amount
end

function SpecialAccount.getLimit(self)
    return self.limit or 0
end

s:withdraw(800.00)
print(s.balance)

-- 다중 상속 구현
local function search(key, parentList)
    for i = 1, #parentList do
        local value = parentList[i][key]
        if value then return value end
    end
end

function createClass(...)
    local c = {}
    local parents = {...}
    
    setmetatable(c, {__index = function(t, k)
        return search(k, parents)
    end})
    
    setmetatable(c, {__index = function(t, k)
        local v = search(k, parents)
        t[k] = v
        return v
    end})
    
    c.__index = c
    function c:new(obj)
        obj = obj or {}
        setmetatable(obj, c)
        return obj
    end
    return c
end

Named = {}
function Named:getname()
    return self.name
end
function Named:setname(n)
    self.name = n
end

NA = createClass(Account, Named)
acc = NA:new{name = "Paul"}
print(acc:getname())

비공개성 구현:

function createAccount(initialBalance)
    local state = {balance = initialBalance}
    local withdraw = function(amount)
        state.balance = state.balance - amount
    end
    local deposit = function(amount)
        state.balance = state.balance + amount
    end
    local getBalance = function()
        return state.balance
    end
    return {
        withdraw = withdraw,
        deposit = deposit,
        getBalance = getBalance
    }
end

acc1 = createAccount(100.00)
acc1.withdraw(40.00)
print(acc1.getBalance())

function createAccount(initialBalance)
    local state = {
        balance = initialBalance,
        LIMIT = 10000.00,
    }
    local extra = function()
        if state.balance > state.LIMIT then
            return state.balance * 0.10
        else
            return 0
        end
    end
    local getBalance = function()
        return state.balance + extra()
    end
    return {
        getBalance = getBalance
    }
end

acc1 = createAccount(100000.00)
print(acc1.getBalance())

단일 메서드 객체를 사용한 반복자 구현:

function createObject(value)
    return function(action, v)
        if action == "get" then
            return value
        elseif action == "set" then
            value = v
        else
            error("잘못된 작업")
        end
    end
end

d = createObject(0)
print(d("get"))
d("set", 10)
print(d("get"))

이중 표현 방식을 이용한 비공개성:

local balance = {}
Account = {}

function Account.withdraw(self, v)
    balance[self] = balance[self] - v
end

function Account.deposit(self, v)
    balance[self] = balance[self] + v
end

function Account.balance(self)
    return balance[self]
end

function Account.new(self, obj)
    obj = obj or {}
    setmetatable(obj, self)
    self.__index = self
    balance[obj] = 0
    return obj
end

a = Account:new{}
a:deposit(100.00)
print(a:balance())

태그: Lua iterator metatable metamethod OOP

8월 8일 21:48에 게시됨