* 정의
__index는 테이블의 메타테이블 안에 정의할 수 있는 메타메소드이다. 메타테이블의 __index가 정의되어 있다면 테이블에서 정의되지 않은 키가 들어왔을 때, 바로 nil로 반환하지 않고 __index 메타메소드를 통해 반환하게 된다.
* 사용 이유
다음과 같은 기능을 활용하기 위해 사용된다.
- 테이블에서 정의되지 않은 키가 들어왔을 때 메타테이블에 존재하는 __index를 통해 메타테이블을 조회할 수 있다.
- 클래스의 상속을 구현할 수 있다.
* 동작 방식
- 테이블에 정의되지 않은 키를 추가로 찾아보는 과정
일반적으로 루아에서 테이블에 정의되지 않은 값에 접근 할 때 nil 값을 반환한다.
이렇게 정의되지 않은 값에 접근할 때 루아 인터프리터는 해당 테이블의 메타테이블의 __index 메타메소드를 확인한다.
- 만약 메타테이블이 존재하지 않거나 __index 메타메소드가 존재하지 않으면 nil을 반환한다.
- 메타테이블이 존재하고 __index 메타메소드에 정의되어 있으면 __index를 통해 해당 값을 반환하게 된다.
- __index 함수 버전 예시
이렇게 명시적으로 테이블의 메타테이블 안에 __index 메타메소드를 정의하여 기존 테이블에 존재하지 않은 특정 값들에 대해 nil이 아닌 다른 값들을 반환하도록 설정할 수 있다. 다음과 같이 사용할 수 있다.
local myTable = setmetatable({key1 = "value1"}, {
__index = function(myTable, key)
if key == "key2" then
return "metatablevalue"
end
-- 여기 들어온 이유가 myTable[key]가 nil이라서 들어온 것인데, 또 같은 조건이다. 무한 반복하게 되어 스택 오버플로가 발생한다.
--return myTable[key]
return nil
end
})
print(myTable.key1, myTable.key2, myTable.key3)
value1 metatablevalue nil
코드에서 보면 테이블에 key1밖에 존재하지 않지만 key2를 찾을 때 메타테이블을 선언할 때 정의한 내부 __index 메타메소드를 통해 nil이 아닌 "metatablevalue"을 반환하는 것을 볼 수 있다. 만약 __index 메타메소드가 정의되지 있지 않다면 nil을 반환했을 것이다. key3은 테이블과 __index를 통해서 찾을 수 없기 때문에 nil을 반환하게 된다.
- __index 단순화 버전 예시
__index를 함수가 아닌 테이블로 초기화할 수도 있다. 그러면 __index 테이블에서 키로 값을 찾게 된다.
메타테이블을 연결하고 __index 자체를 메타테이블로 지정하면 메타테이블을 그대로 상속받는 효과를 얻을 수 있다.
local table1 = {
value1 = 1,
value2 = 2,
value3 = 3,
print = function() print("print function in table1") end,
printValue = function(value) print("printValue function in table1 => ".. tostring(value)) end
}
-- 메타테이블로 사용할 때 해당 테이블에 특정 키가 없으면 이 테이블에서 찾아보겠다는 뜻이다.
table1.__index = table1
local table2 = setmetatable({value4 = 4, value5 = 5}, table1)
print(table2.value1)
table2.print()
table2.printValue(3)
1
print function in table1
printValue function in table1 => 3
위의 코드를 함수 버전으로 구현하면 다음과 같다.
local table1 = {
value1 = 1,
value2 = 2,
value3 = 3,
print = function() print("print function in table1") end,
printValue = function(value) print("printValue function in table1 => ".. tostring(value)) end
}
table1.__index = function(table, key)
local metatable = getmetatable(table)
--if not metatable then
-- return nil
--end
return metatable[key]
end
local table2 = setmetatable({value4 = 4, value5 = 5}, table1)
print(table2.value1)
table2.print()
table2.printValue(3)
1
print function in table1
printValue function in table1 => 3
- __index 함수 버전과 단순화 버전 호출 원리(추측)
두 버전을 적절하게 지원하는 이유는 myTable.key / myTable[key]로 접근하였을 때 특정 함수를 호출하여 적절하게 처리한다고 생각한다. 함수는 다음과 같이 구현되어 있을 가능성이 높다.(자료 서칭과 디버깅을 통해 작성된 코드입니다. 정확하지 않을 수 있습니다.)
local function GetValueByKey(table, key)
-- table[key]을 사용하면 다시 메타테이블의 __index를 통해서 찾기 때문에 실제로 원하는대로 동작하지는 않는다.
-- if table[key] then return table[key] end
-- 따라서 메타메소드를 우회할 수 있는 rawget함수를 사용한다.
local value = rawget(table, key)
if value then return table[key] end
-- 메타테이블에서 추가로 찾아본다.
local metatable = getmetatable(table)
if not metatable or not metatable.__index then return nil end
-- 타입에 따라서 적절하게 반환해준다.
local typeString = type(metatable.__index)
if typeString == "table" then
return metatable.__index[key]
end
-- __index값으로 table 이외에는 무조건 function을 넣어야 크래시가 발생하지 않는다.
return metatable.__index(table, key)
end
여기서 주의할 점은 table[key] 연산 자체가 내부적으로 메타메소드를 호출하고 있고, table[key] 무한 루프에 빠질 수 있기 때문에 메타메소드를 호출하지 않고 우회하는 함수인 rawget을 사용하여 해당 테이블에 범위에서만 키가 존재하는 지 검사 해야한다.
- __index 정의 시 주의 사항
- 여기서 주의해야할 점은 __index 내부로 들어오는 매개변수는 해당 테이블과 키값으로 들어온다는 점이다.
- __index 메타 메소드 내부에서는 다시 해당 테이블을 키값으로 접근하게 되면 재귀가 무한 반복되니 주의하여야 한다.
- 2번과 같은 논리가 테이블에도 적용된다. metatable.__index = table 으로 설정하면 문제가 생긴다.
__index 메타메소드가 호출된 이유는 myTable[key]가 nil이라서 들어온 것인데, __index 메타 메소드에서 myTable[key]를 접근하면 또 nil이 나오고 다시 __index 메타메소드를 호출하게 된다. 결국 무한 반복하게 되어 스택 오버플로가 발생한다.
'Lua in Roblox' 카테고리의 다른 글
[Lua / Roblox] 미니 배틀 로얄 게임 #5 : 게임 데이터 매니저 (0) | 2022.07.14 |
---|---|
[Lua] 클래스 구현(접근 제어, 상속, 리플렉션, 캐스팅 등 지원) (0) | 2022.07.13 |
[Lua] __newindex (0) | 2022.07.12 |
[Lua] metatable (0) | 2022.07.12 |
[Lua / Roblox] 미니 배틀 로얄 게임 #4 : 구조 정리, 개선된 점 (0) | 2022.07.11 |