Object Properties

lua-users home
wiki

Some object-orientated languages (like [C#]) support properties, which appear like public data fields on objects but are really syntactic sugar for accessor functions (getters and setters). In Lua it might look like this:
obj.field = 123  -- equivalent to obj:set_field(123)
x = obj.field    -- equivalent to x = obj:get_field()
Here is one way to implement such a thing in Lua:

-- Make proxy object with property support.
-- Notes:
--   If key is found in <getters> (or <setters>), then
--     corresponding function is used, else lookup turns to the
--     <class> metatable (or first to <priv> if <is_expose_private> is true).
--   Given a proxy object <self>, <priv> can be obtained with
--     getmetatable(self).priv .
-- @param class - metatable acting as the object class.
-- @param priv - table containing private data for object.
-- @param getters - table of getter functions
--                  with keys as property names. (default is nil)
-- @param setters - table of setter functions,
--                  with keys as property names. (default is nil)
-- @param is_expose_private - Boolean whether to expose <priv> through proxy.
--                  (default is nil/false)
-- @version 3 - 20060921 (D.Manura)
local function make_proxy(class, priv, getters, setters, is_expose_private)
  setmetatable(priv, class)  -- fallback priv lookups to class
  local fallback = is_expose_private and priv or class
  local index = getters and
    function(self, key)
      -- read from getter, else from fallback
      local func = getters[key]
      if func then return func(self) else return fallback[key] end
    end
    or fallback  -- default to fast property reads through table
  local newindex = setters and
    function(self, key, value)
      -- write to setter, else to proxy
      local func = setters[key]
      if func then func(self, value)
      else rawset(self, key, value) end
    end
    or fallback  -- default to fast property writes through table
  local proxy_mt = {         -- create metatable for proxy object
    __newindex = newindex,
    __index = index,
    priv = priv
  }
  local self = setmetatable({}, proxy_mt)  -- create proxy object
  return self
end

Here's some tests of that

-- Test Suite

-- test: typical usage

local Apple = {}
Apple.__index = Apple
function Apple:drop()
  return self.color .. " apple dropped"
end
local Apple_attribute_setters = {
  color = function(self, color)
    local priv = getmetatable(self).priv
    assert(color == "red" or color == "green")
    priv.color = string.upper(color)
  end
}
function Apple:new()
  local priv = {color = "RED"} -- private attributes in instance
  local self = make_proxy(Apple, priv, nil, Apple_attribute_setters, true)
  return self
end


local a = Apple:new()
assert("RED" == a.color)
a:drop()         -- "RED apple dropped"

a.color = "green"
assert("GREEN apple dropped" == a:drop())
a.color = "red"
assert("RED apple dropped" == a:drop())

a.weight = 123   -- new field
assert(123 == a.weight)

-- fails as expected (invalid color)
local is_ok = pcall(function() a.color = "blue" end)
assert(not is_ok)


-- test: simple
local T = {}
T.__index = T
local T_setters = {
  a = function(self, value)
    local priv = getmetatable(self).priv
    priv.a = value * 2
  end
}
local T_getters = {
  b = function(self, value)
    local priv = getmetatable(self).priv
    return priv.a + 1
  end
}
function T:hello()
  return 123
end
function T:new() return make_proxy(T, {a=5}, T_getters, T_setters) end
local t = T:new()
assert(123 == t:hello())
assert(nil == t.hello2)
assert(nil == t.a)
assert(6 == t.b)
t.a = 10
assert(nil == t.a)
assert(21 == t.b)

-- test: is_expose_private = true
local t = make_proxy(T, {a=5}, T_getters, T_setters, true)
assert(5 == t.a)
assert(6 == t.b)

print("done")

Variations of this are possible, and this might not be optimal (--RiciLake). You may have different design constraints. One suggestion was possibly to memoize the lookup Apple_attribute_funcs[key] or abstract away the actual rawset out of the setter functions.

-- DavidManura

Here is another way of doing this, shown first in lua, then again in C:

-- Rewrite in lua of array example from http://www.lua.org/pil/28.4.html
-- that implements both array and OO access.
array = {
  new = function(self, size)
      local o = {
        _size = size,
        _array = {},
      }
      for i = 1, o._size do
        o._array[i] = 0
      end
      setmetatable(o, self)
      return o
    end,

  size = function(self)
      return self._size
    end,

  get = function(self, i)
      -- should do bounds checking on array
      return self._array[tonumber(i)]
    end,

  set = function(self, i, v)
      -- should do bounds checking on array
      self._array[tonumber(i)] = tonumber(v)
    end,

  __index = function(self, key)
      return getmetatable(self)[key] or self:get(key)
    end,

  __newindex = function(self, i, v)
      self:set(i, v)
    end,
}

In C, this is:

/*
Rewrite in C of array example from http://www.lua.org/pil/28.4.html that
implements both array and OO access.

Lacks bounds checking, its not pertinent to this example.
*/
#include "lauxlib.h"
#include "lua.h"

#include <assert.h>
#include <stdint.h>
#include <string.h>

#define ARRAY_REGID  "22d3fa81-aef3-4335-be43-6ff037daf78e"
#define ARRAY_CLASS  "array"

struct array {
	lua_Integer size;
	lua_Number data[1];
};

typedef struct array* array;

static array array_check(lua_State* L, int index) 
{
	void* userdata = luaL_checkudata(L,index,ARRAY_REGID);
	assert(userdata);
	return  userdata;
}
int array_new(lua_State* L) 
{
	// Ignoring [1], the "array" global table.
	int size = luaL_checkinteger(L, 2);
	array self = (array) lua_newuserdata(L,sizeof(*self) + (size-1) * sizeof(self->data));

	self->size = size;
	for(size = 0; size < self->size; size++)
		self->data[size] = 0;

	luaL_getmetatable(L, ARRAY_REGID);
	lua_setmetatable(L, -2);
	return 1;
}
int array_size(lua_State* L) 
{
	array self = array_check(L, 1);
	lua_pushinteger(L, self->size);
	return 1;
}
int array_get(lua_State* L) 
{
	array self = array_check(L, 1);
	lua_Integer i = luaL_checkinteger(L, 2);
	// TODO bounds checking on i
	lua_pushnumber(L, self->data[i-1]);
	return 1;
}
int array_set(lua_State* L) 
{
	array self = array_check(L, 1);
	lua_Integer i = luaL_checkinteger(L, 2);
	lua_Number v = luaL_checknumber(L, 3);
	// TODO bounds checking on i
	self->data[i-1] = v;
	return 0;
}
int array_index(lua_State* L) 
{
	const char* key = luaL_checkstring(L, 2);

	lua_getmetatable(L, 1);
	lua_getfield(L, -1, key);

   	// Either key is name of a method in the metatable
	if(!lua_isnil(L, -1))
		return 1;

	// ... or its a field access, so recall as self.get(self, value).
	lua_settop(L, 2);

	return array_get(L);
}
static const struct luaL_reg array_class_methods[] = {
	{ "new",            array_new },
	{ NULL, NULL }
};
static const struct luaL_reg array_instance_methods[] = {
	{ "get",             array_get },
	{ "set",             array_set },
	{ "size",            array_size },
	{ "__index",         array_index },
	{ "__newindex",      array_set },
	{ NULL, NULL }
};

int array_open(lua_State* L) 
{
	luaL_newmetatable(L, ARRAY_REGID);
	luaL_openlib(L, NULL, array_instance_methods, 0);
	luaL_openlib(L, ARRAY_CLASS, array_class_methods, 0);
	return 1;
}

For both implementations, array can be used as:

o = array:new(3)

print(o:size())

o[1] = 1
o[2] = 2
o[3] = 3

print(o:get(2))
o:set(3, -1)
print(o[3])

-- see also GeneralizedPairsAndIpairs to allow "pairs" and "ipairs" to work with this.

See Also


RecentChanges · preferences
edit · history
Last edited March 14, 2009 4:48 pm GMT (diff)