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)