4

メソッドではなく属性のみを公開する Lua オブジェクトを作成するにはどうすればよいでしょうか? 例えば:

local obj = {
  attr1 = 1,
  attr2 = 2,
  print = function(...)
    print("obj print: ", ...)
  end,
}

プロデュース:

> for k,v in pairs(obj) do print(k, v) end
attr1   1
attr2   2
print   function: 0x7ffe1240a310

また、Lua で OOP のコロン構文を使用しないことは可能ですか? 継承もポリモーフィズムも必要ありません。カプセル化とプライバシーだけが必要です。

4

2 に答える 2

10

私は上記の質問から始め、うさぎの穴を追い求めた後、例の数が限られていること、さまざまなメタメソッド(つまり__ipairs__pairs__len

Lua は OOP を実行できますが、IMO OOP が規定されている方法は、言語とコミュニティに害を及ぼします (つまり、ポリモーフィズム、多重継承などをサポートするような方法で)。ほとんどの問題に対して Lua の OOP 機能のほとんどを使用する理由はほとんどありません。必ずしも分岐点があることを意味するわけではありません (たとえば、ポリモーフィズムをサポートするために、コロン構文を使用する必要があるとは言いません。文献で説明されている手法をクロージャベースの OOP メソッドに組み込むことができます)。

Lua で OOP を実行する方法がたくさんあることは理解していますが、オブジェクトの属性とオブジェクトのメソッド (例: obj.attr1vs obj:getAttr()vs obj.method()vs obj:method()) の構文が異なるのはイライラします。内部および外部と通信するための単一の統合 API が必要です。そのために、PiL 16.4 のプライバシーに関するセクションは素晴らしいスタートですが、この回答で修正したい不完全な例です。


次のコード例:

  • クラスの名前空間MyObject = {}をエミュレートし、オブジェクト コンストラクターをMyObject.new()
  • オブジェクトの内部動作のすべての詳細を非表示にして、オブジェクトのユーザーに純粋なテーブルのみが表示されるようにします (setmetatable()およびを参照__metatable) 。
  • クロージャを使用して情報を隠します ( Lua Pil 16.4およびオブジェクト ベンチマーク テストを参照)
  • オブジェクトの変更を防止します ( を参照__newindex)
  • メソッドのインターセプトを許可します (「 」を参照__index)
  • すべての機能と属性のリストを取得できます ( の「キー」属性を参照__index)
  • 通常の Lua テーブルのように見え、動作し、歩き、話す ( __pairs__len、を参照__ipairs)
  • 必要に応じて文字列のように見えます ( を参照__tostring)
  • で動作しますLua 5.2

new を構築するコードは次のとおりMyObjectです (これはスタンドアロン関数である可能性があり、テーブルに格納する必要はありません。一度作成された後に にMyObject結び付けられるものはまったくありません。これは、親しみやすさと慣例からのみ行われます)。 :objMyObject.new()

MyObject = {}
MyObject.new = function(name)
   local objectName = name

   -- A table of the attributes we want exposed
   local attrs = {
      attr1 = 123,
   }

   -- A table of the object's methods (note the comma on "end,")
   local methods = {
      method1 = function()
         print("\tmethod1")
      end,

      print = function(...)
         print("MyObject.print(): ", ...)
      end,

      -- Support the less than desirable colon syntax
      printOOP = function(self, ...)
         print("MyObject:printOOP(): ", ...)
      end,
   }

   -- Another style for adding methods to the object (I prefer the former
   -- because it's easier to copy/paste function()'s around)
   function methods.addAttr(k, v)
      attrs[k] = v
      print("\taddAttr: adding a new attr: " .. k .. "=\"" .. v .. "\"")
   end

   -- The metatable used to customize the behavior of the table returned by new()
   local mt = {
      -- Look up nonexistent keys in the attrs table. Create a special case for the 'keys' index
      __index = function(t, k)
         v = rawget(attrs, k)
         if v then
            print("INFO: Successfully found a value for key \"" .. k .. "\"")
            return v
         end
         -- 'keys' is a union of the methods and attrs
         if k == 'keys' then
            local ks = {}
            for k,v in next, attrs, nil do
               ks[k] = 'attr'
            end
            for k,v in next, methods, nil do
               ks[k] = 'func'
            end
            return ks
         else
            print("WARN: Looking up nonexistant key \"" .. k .. "\"")
         end
      end,

      __ipairs = function()
         local function iter(a, i)
            i = i + 1
            local v = a[i]
            if v then
               return i, v
            end
         end
         return iter, attrs, 0
      end,

      __len = function(t)
         local count = 0
         for _ in pairs(attrs) do count = count + 1 end
         return count
      end,

      __metatable = {},

      __newindex = function(t, k, v)
         if rawget(attrs, k) then
            print("INFO: Successfully set " .. k .. "=\"" .. v .. "\"")
            rawset(attrs, k, v)
         else
            print("ERROR: Ignoring new key/value pair " .. k .. "=\"" .. v .. "\"")
         end
      end,

      __pairs = function(t, k, v) return next, attrs, nil end,

      __tostring = function(t) return objectName .. "[" .. tostring(#t) .. "]" end,
   }
   setmetatable(methods, mt)
   return methods
end

そして今、使用法:

-- Create the object
local obj = MyObject.new("my object's name")

print("Iterating over all indexes in obj:")
for k,v in pairs(obj) do print('', k, v) end
print()

print("obj has a visibly empty metatable because of the empty __metatable:")
for k,v in pairs(getmetatable(obj)) do print('', k, v) end
print()

print("Accessing a valid attribute")
obj.print(obj.attr1)
obj.attr1 = 72
obj.print(obj.attr1)
print()

print("Accessing and setting unknown indexes:")
print(obj.asdf)
obj.qwer = 123
print(obj.qwer)
print()

print("Use the print and printOOP methods:")
obj.print("Length: " .. #obj)
obj:printOOP("Length: " .. #obj) -- Despite being a PITA, this nasty calling convention is still supported

print("Iterate over all 'keys':")
for k,v in pairs(obj.keys) do print('', k, v) end
print()

print("Number of attributes: " .. #obj)
obj.addAttr("goosfraba", "Satoshi Nakamoto")
print("Number of attributes: " .. #obj)
print()

print("Iterate over all keys a second time:")
for k,v in pairs(obj.keys) do print('', k, v) end
print()

obj.addAttr(1, "value 1 for ipairs to iterate over")
obj.addAttr(2, "value 2 for ipairs to iterate over")
obj.addAttr(3, "value 3 for ipairs to iterate over")
obj.print("ipairs:")
for k,v in ipairs(obj) do print(k, v) end

print("Number of attributes: " .. #obj)

print("The object as a string:", obj)

これにより、予想される-そしてフォーマットが不十分な-出力が生成されます。

Iterating over all indexes in obj:
    attr1   123

obj has a visibly empty metatable because of the empty __metatable:

Accessing a valid attribute
INFO: Successfully found a value for key "attr1"
MyObject.print():   123
INFO: Successfully set attr1="72"
INFO: Successfully found a value for key "attr1"
MyObject.print():   72

Accessing and setting unknown indexes:
WARN: Looking up nonexistant key "asdf"
nil
ERROR: Ignoring new key/value pair qwer="123"
WARN: Looking up nonexistant key "qwer"
nil

Use the print and printOOP methods:
MyObject.print():   Length: 1
MyObject.printOOP():        Length: 1
Iterate over all 'keys':
    addAttr func
    method1 func
    print   func
    attr1   attr
    printOOP        func

Number of attributes: 1
    addAttr: adding a new attr: goosfraba="Satoshi Nakamoto"
Number of attributes: 2

Iterate over all keys a second time:
    addAttr func
    method1 func
    print   func
    printOOP        func
    goosfraba       attr
    attr1   attr

    addAttr: adding a new attr: 1="value 1 for ipairs to iterate over"
    addAttr: adding a new attr: 2="value 2 for ipairs to iterate over"
    addAttr: adding a new attr: 3="value 3 for ipairs to iterate over"
MyObject.print():   ipairs:
1   value 1 for ipairs to iterate over
2   value 2 for ipairs to iterate over
3   value 3 for ipairs to iterate over
Number of attributes: 5
The object as a string: my object's name[5]

  • Lua をファサードとして埋め込んだり、API を文書化したりする場合、OOP + クロージャを使用すると非常に便利です。
  • Lua OOP は非常にクリーンでエレガントな場合もあります (これは主観的なものですが、このスタイルにはルールはありません.。属性またはメソッドにアクセスするには常に a を使用します)。
  • オブジェクトをテーブルとまったく同じように動作させることは、プログラムの状態をスクリプト化したり問い合わせたりするのに非常に役立ちます
  • サンドボックスで操作するときに非常に便利です

このスタイルは、オブジェクトごとにわずかに多くのメモリを消費しますが、ほとんどの状況では問題になりません。上記の例のコードはそうではありませんが、再利用のためにメタテーブルを除外することでこれに対処できます。

最終的な考え。Lua OOP は、文献のほとんどの例を無視すると、実際には非常に優れています。文献が悪いと言っているわけではありませんが (これは真実からかけ離れたものではありません!)、PiL やその他のオンライン リソースにある一連のサンプル例では、コロン構文のみを使用することになります (つまり、すべての関数は、 orへの参照を保持するself代わりに使用されます)。closureupvalueself

うまくいけば、これは便利でより完全な例です。


更新 (2013-10-08) : 上記で詳述したクロージャーベースの OOP スタイルには、注目すべき欠点が 1 つあります (スタイルにはオーバーヘッドの価値があると思いますが、余談です): 各インスタンスには独自のクロージャーが必要です。これは上記の lua バージョンでは明らかですが、C 側で処理する場合は少し問題になります。

ここから先は、C 側からの上記のクロージャ スタイルについて話していると仮定します。C 側の一般的なケースは、userdataviaオブジェクトを作成し、メタテーブルをvialua_newuserdata()にアタッチすることです。メタテーブル内のメソッドがユーザーデータの上位値を必要とすることに気付くまで、これは額面上は問題のようには見えません。userdatalua_setmetatable()

using FuncArray = std::vector<const ::luaL_Reg>;
static const FuncArray funcs = {
  { "__tostring", LI_MyType__tostring },
};

int LC_MyType_newInstance(lua_State* L) {
  auto userdata = static_cast<MyType*>(lua_newuserdata(L, sizeof(MyType)));
  new(userdata) MyType();

  // Create the metatable
  lua_createtable(L, 0, funcs.size());     // |userdata|table|
  lua_pushvalue(L, -2);                    // |userdata|table|userdata|
  luaL_setfuncs(L, funcs.data(), 1);       // |userdata|table|
  lua_setmetatable(L, -2);                 // |userdata|
  return 1;
}

int LI_MyType__tostring(lua_State* L) {
  // NOTE: Blindly assume that upvalue 1 is my userdata
  const auto n = lua_upvalueindex(1);
  lua_pushvalue(L, n);                     // |userdata|
  auto myTypeInst = static_cast<MyType*>(lua_touserdata(L, -1));
  lua_pushstring(L, myTypeInst->str());    // |userdata|string|
  return 1;                                // |userdata|string|
}

で作成されたテーブルがlua_createtable()、でメタテーブルを登録した場合と同じように、メタテーブル名に関連付けられていないことに注意してluaL_getmetatable()ください。これらの値はクロージャーの外では完全にアクセスできないため、これは 100% 大丈夫ですが、特定の の型luaL_getmetatable()を検索するために使用できないことを意味します。userdata同様に、これはluaL_checkudata()luaL_testudata()も立ち入り禁止であることを意味します。

肝心なのは、上位値 (userdata上記など) は関数呼び出し (たとえばLI_MyType__tostring) に関連付けられており、それ自体には関連付けられていないというuserdataことです。今のところ、インスタンス間でメタテーブルを共有できるように、上位値を値に関連付ける方法を知りません。


更新(2013-10-14) 以下に、登録されたメタテーブル ( luaL_newmetatable()) とlua_setuservalue()/lua_getuservalue()userdata「属性とメソッド」を使用する小さな例を含めます。また、過去に追い詰めなければならなかったバグ/ホットネスの原因となったランダムなコメントを追加します. また、__index.

namespace {

using FuncArray = std::vector<const ::luaL_Reg>;
static const std::string MYTYPE_INSTANCE_METAMETHODS{"goozfraba"}; // I use a UUID here
static const FuncArray MyType_Instnace_Metamethods = {
  { "__tostring", MyType_InstanceMethod__tostring },
  { "__index",    MyType_InstanceMethod__index },
  { nullptr,      nullptr }, // reserve space for __metatable
  { nullptr, nullptr } // sentinel
};

static const FuncArray MyType_Instnace_methods = {
  { "fooAttr", MyType_InstanceMethod_fooAttr },
  { "barMethod", MyType_InstanceMethod_barMethod },
  { nullptr, nullptr } // sentinel
};

// Must be kept alpha sorted
static const std::vector<const std::string> MyType_Instance___attrWhitelist = {
  "fooAttr",
};

static int MyType_ClassMethod_newInstance(lua_State* L) {
  // You can also use an empty allocation as a placeholder userdata object
  // (e.g. lua_newuserdata(L, 0);)
  auto userdata = static_cast<MyType*>(lua_newuserdata(L, sizeof(MyType)));
  new(userdata) MyType(); // Placement new() FTW

  // Use luaL_newmetatable() since all metamethods receive userdata as 1st arg
  if (luaL_newmetatable(L, MYTYPE_INSTANCE_METAMETHODS.c_str())) { // |userdata|metatable|
    luaL_setfuncs(L, MyType_Instnace_Metamethods.data(), 0); // |userdata|metatable|

    // Prevent examining the object: getmetatable(MyType.new()) == empty table
    lua_pushliteral(L, "__metatable");     // |userdata|metatable|literal|
    lua_createtable(L, 0, 0);              // |userdata|metatable|literal|table|
    lua_rawset(L, -3);                     // |userdata|metatable|
  }

  lua_setmetatable(L, -2);                 // |userdata|

  // Create the attribute/method table and populate with one upvalue, the userdata
  lua_createtable(L, 0, funcs.size());     // |userdata|table|
  lua_pushvalue(L, -2);                    // |userdata|table|userdata|
  luaL_setfuncs(L, funcs.data(), 1);       // |userdata|table|

  // Set an attribute that can only be accessed via object's fooAttr, stored in key "fooAttribute"
  lua_pushliteral(L, "foo's value is hidden in the attribute table"); // |userdata|table|literal|
  lua_setfield(L, -2, "fooAttribute");     // |userdata|table|

  // Make the attribute table the uservalue for the userdata
  lua_setuserdata(L, -2);                  // |userdata|
  return 1;
}

static int MyType_InstanceMethod__tostring(lua_State* L) {
  // Since we're using closures, we can assume userdata is the first value on the stack.
  // You can't make this assumption when using metatables, only closures.
  luaL_checkudata(L, 1, MYTYPE_INSTANCE_METAMETHODS.c_str()); // Test anyway
  auto myTypeInst = static_cast<MyType*>(lua_touserdata(L, 1));
  lua_pushstring(L, myTypeInst->str());    // |userdata|string|
  return 1;                                // |userdata|string|
}

static int MyType_InstanceMethod__index(lua_State* L) {
  lua_getuservalue(L, -2);        // |userdata|key|attrTable|
  lua_pushvalue(L, -2);           // |userdata|key|attrTable|key|
  lua_rawget(L, -2);              // |userdata|key|attrTable|value|
  if (lua_isnil(L, -1)) {         // |userdata|key|attrTable|value?|
    return 1;                     // |userdata|key|attrTable|nil|
  }

  // Call cfunctions when whitelisted, otherwise the caller has to call the
  // function.
  if (lua_type(L, -1) == LUA_TFUNCTION) {
    std::size_t keyLen = 0;
    const char* keyCp = ::lua_tolstring(L, -3, &keyLen);
    std::string key(keyCp, keyLen);

    if (std::binary_search(MyType_Instance___attrWhitelist.cbegin(),
                           MyType_Instance___attrWhitelist.cend(), key))
    {
      lua_call(L, 0, 1);
    }
  }

  return 1;
}

static int MyType_InstanceMethod_fooAttr(lua_State* L) {
  // Push the uservalue on to the stack from fooAttr's closure (upvalue 1)
  lua_pushvalue(L, lua_upvalueindex(1)); // |userdata|
  lua_getuservalue(L, -1);               // |userdata|attrTable|

  // I haven't benchmarked whether lua_pushliteral() + lua_rawget()
  // is faster than lua_getfield() - (two lua interpreter locks vs one lock + test for
  // metamethods).
  lua_pushliteral(L, "fooAttribute");    // |userdata|attrTable|literal|
  lua_rawget(L, -2);                     // |userdata|attrTable|value|

  return 1;
}

static int MyType_InstanceMethod_barMethod(lua_State* L) {
  // Push the uservalue on to the stack from barMethod's closure (upvalue 1)
  lua_pushvalue(L, lua_upvalueindex(1)); // |userdata|
  lua_getuservalue(L, -1);               // |userdata|attrTable|

  // Push a string to finish the example, not using userdata or attrTable this time
  lua_pushliteral(L, "bar() was called!"); // |userdata|attrTable|literal|

  return 1;
}

} // unnamed-namespace

lua スクリプト側は次のようになります。

t = MyType.new()
print(typue(t))    --> "userdata"
print(t.foo)       --> "foo's value is hidden in the attribute table"
print(t.bar)       --> "function: 0x7fb560c07df0"
print(t.bar())     --> "bar() was called!"
于 2013-08-11T21:55:47.223 に答える