iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 19
0
Software Development

30天 Lua重拾筆記系列 第 19

【30天Lua重拾筆記29】物件導向 之 HoloLive 炎上! 桐生可可&赤井心 禁足三周


本篇原預計為為第29天內容,原始標題為:「進階議題: 物件導向程式設計」。
如有發現自己穿越時空,或看不懂本文內容,屬於正常現象,請勿擔心。
您僅需要靜心等待,等待時間線補齊。


與ECMAScript相同,採用原形設計的物件導向。你可以與7天搞懂JS進階議題相互服用,可能會有意想不到的效果。

模擬封裝

Lua並沒有直接保護內部資料的方法,你可能會需要使用 閉包弱表metatable 等來達成條件。

但今天只討論封裝裡最簡單的概念--集成,也是物件的基礎:將相關的物件、方法關連到一個結構裡面。沒錯,就是 table,在本文中使用物件(object)一詞基本可視為同義。

Lua的table也確實很像是JS裡的Object,但可能更像是Map(一個ES6後出現的新類別)。
最基礎的資料結構概念是雜湊表。

建立物件[^1]

建立物件(object)就是建立表(table),非常簡單:

coco = {
  name = "桐生ココ",
  age = 3501,
  birth = {month = 6, day = 17},
  slogan = "good morning mother f**ker",
  say = function (self)
    print(self.name .. ": " .. self.slogan)
  end
}

coco:say() --> 桐生ココ: good morning mother f**ker

[^1]: 桐生ココ經典台詞取自: https://www.youtube.com/watch?v=EPz07MzczQE

工廠模式

但我們總不能每次都這樣直接建立物件,或許可以由一個加工工廠(函數)來處理?

function VTuber(options)
  local vtuber = {
    name = options.name or "",
    age = options.age or 0,
    birth = options.birth,
    slogan = options.slogan or "...",
    say = function (self)
      print(self.name .. ": " .. self.slogan)
    end
  }
  return vtuber
end

---------------------------
------- 建立實例 ----------
---------------------------

peko = VTuber {
  name = "兎田ぺこら",
  age = 111,
  birth = {month = 1, day = 12},
  slogan = "Peko↗ Peko↘ Peko↗ Peko↘ Peko↗ Peko↘~!?"
}

--------------------------

peko:say() --> 兎田ぺこら: Peko↗ Peko↘ Peko↗ Peko↘ Peko↗ Peko↘~!?

模擬繼承

已經看過兩個最簡單建立物件的方法,其中第二個可能有更高的可讀性與靈活性後,接著要來說說Lua的繼承要怎麼實現。來說說metatable裡可以設定的__index。這個值可以是另一個表或是一個函數,他會在表沒有實際欄位時,從其值尋找,也就是從其表或是函數尋找。

極類似Python裡的__getattr__

可以很間單的回傳default字串看看:

setmetatable(coco, {
               __index = function(key)
                 return "default"
               end
})

print(coco.abc) --> default
print(coco.name) --> 桐生ココ

現在可以利用此特性來模擬繼承:

VTuber = {
  say = function (self)
    print(self.name .. ": " .. self.slogan)
  end
}

VTuber_metatable = {
  __index = VTuber
}

----------------------

coco.say = nil
peko.say = nil

setmetatable(coco, VTuber_metatable)
setmetatable(peko, VTuber_metatable)

---------------------

coco:say()  --> 桐生ココ: good morning mother f**ker
peko:say()  --> 兎田ぺこら: Peko↗ Peko↘ Peko↗ Peko↘ Peko↗ Peko↘~!?

當然這樣看下來只是建立一個類別而已,所以接下來要嘗試將VTuber繼承YouTuber這個類別:

YouTuber = {
  name = "<<YouTuber Name>>",
  youtube_channel = "https://youtube.com/"
}

setmetatable(VTuber, {__index=YouTuber})

print(coco.name .. " youtuber channel: " .. coco.youtube_channel) --> 桐生ココ youtuber channel: https://youtube.com/

這樣,所有VTuber實例都繼承了YouTuberyoutube_channel預設值。當然也可以為不同實例使用不同的值:


coco.youtube_channel = "https://www.youtube.com/channel/UCS9uQI-jC3DE0L4IpXyvr6w"

print(coco.name .. " youtuber channel: " .. coco.youtube_channel) 
--> 桐生ココ youtuber channel: https://www.youtube.com/channel/UCS9uQI-jC3DE0L4IpXyvr6w

現在來看看細部內容:有兩個VTuber實例--cocopeko,這兩個實例的say方法,實際上並不是本身擁有,而已經移到了VTuber這個類別上。而這個類別繼承了YouTuber這個類別,以至於所有實例直接擁有了yotube_channel屬性。整體結構看起來會像是:

  • Youtuber
    • VTuber
      • coco
      • peko

※ 注意的是:__index並不直接在實例上,而是在metatable上。

是不是也有些和ES6的__proto有點像阿?

類別成員

唯讀屬性

我們已經讓實例共用部份屬性或方法了。接著,我們要來鎖定一些類別上的屬性:


CoverVTuber = {
  company = "OVER株式会社"
}

setmetatable(CoverVTuber, {
               __name = "class CoverVTuber",
               __index = VTuber,  -- CoverVTuber 繼承 VTuber
})

VTuber_metatable.__index = CoverVTuber  -- 改變現有實例繼承類別為CoverVTuber

VTuber_metatable.__newindex = function (self, key, new_value)
  local read_only<const> = { -- 標記唯讀屬性
    company = true
  }

  if not read_only[key] then --> 如我非唯讀,才跟新欄位
    rawset(self, key, new_value)
  end
end

------------

print(coco.company) --> OVER株式会社
coco.company = "Hololive"  --> company是唯讀,不會被改寫
print(coco.company) --> OVER株式会社
coco.a = "test"  --> 不受到唯獨限制
print(coco.a) --> test

__newindex只會有在是新的欄位時觸發,也就是已經有欄位的話,並不會受到唯讀的限制:

mano = {
  name = "魔乃アロエ",
  company = "OVER株式会社"
}


setmetatable(mano, VTuber_metatable)

print(mano.company) -->  OVER株式会社
mano.company = "No"
print(mano.company) --> No
mano.company = nil -- 移除欄位
print(mano.company) --> 類別值:OVER株式会社
mano.company = "No" --> 視為設定新值,受到唯讀限制
print(mano.company) --> OVER株式会社

因為原本設定新值的方式會受到__newindex影響,所以會需要使用更底層的方式指定值,也就是rawset。這並不是經常使用的方式,但這裡有必要這樣做。

Domain Specific Language(DSL) / 領域特定語言

Lua提供了設計DSL的能力,我們可以看看羅塞塔提供的思路:

class "foo" : inherits "bar"
{
 
}

【綜合示例】物件導向DSL設計與實現

既然我主要想回去參考ECMAScript的物件系統,那麼我也將關鍵字改成extends吧!

Class "foo" : extends "bar"
{
 
}
objects_metatable = {
  -- 保存物件使用的metatable。
  -- 同一類別的物件使用相同的metatable。
  -- key值是使用的類別;value是metatable。
  --------------------------
  -- class = metatable
}
setmetatable(objects_metatable, {__mode="k"})

top_class = {}  -- Class Type繼承的類別,擁有一些僅有Class Type可以使用的方法。
function top_class:extends(super_class)
  -- extends關鍵字的處理
  -- 僅有Class Type可以使用
  assert(objects_metatable[self], "Must a Class")
  assert(objects_metatable[_ENV[super_class]], "Super Class Must a Class")
  getmetatable(self).__index = _ENV[super_class] -- 繼承
  return self
end

-- method: new - to create new object
function top_class:new(options)
  local object = options -- WARN: note: 這裡使用複製可能會更好。
  for k, v in pairs(self) do
    object[k] = options[k] or self[k]
  end
  
  setmetatable(object, objects_metatable[self]) -- 註冊物件使用的metatable。
  return object
end

function Class(class_name)
  assert(type(class_name) == "string", "class name must a string")

  local class = {}
  _ENV[class_name] = class -- regist to global environment
  setmetatable(class, {
                 __name = "<"..class_name..">",
                 __index = top_class,  -- 繼承top_class
                 __call = function (self, options) -- 使class結構可以直接被呼叫,更新類別結構
                   for k, v in pairs(options) do
                     self[k] = v
                   end
                 end
  })


  objects_metatable[class] = {
    __index = class,  -- 使實例物件繼承類別(指定類別)
    __name = "<instance of " .. class_name .. ">",
  }

  return class
end

以上基本把Classextends等關鍵字的行為處理完畢,接著來建立實際類別看看:

Class "YouTuber" {
  name = "<<YouTuber Name>>",
  youtube_channel = "https://youtube.com/"
}

Class "VTuber" :extends "YouTuber" {
  name = "",
  age = 0,
  slogan = "...",
  say = function (self)
    print(self.name .. ": " .. self.slogan)
  end
}

Class "CoverVTuber" : extends "VTuber" {
  company = "OVER株式会社"
}

------------------

print(YouTuber)    --> <YouTuber>: 0x5622d8d7e290
print(VTuber)      --> <VTuber>: 0x5622d8d63c10
print(CoverVTuber) --> <CoverVTuber>: 0x5622d8d8eb30

貌似沒什麼問題呢❤️~
接著來建立實例使用看看吧!

coco = CoverVTuber:new {
  name = "桐生ココ",
  age = 3501,
  birth = {month = 6, day = 17},
  slogan = "good morning mother f**ker",
}

peko = CoverVTuber: new {
  name = "兎田ぺこら",
  age = 111,
  birth = {month = 1, day = 12},
  slogan = "Peko↗ Peko↘ Peko↗ Peko↘ Peko↗ Peko↘~!?"
}

hahama = CoverVTuber: new {
  name = "赤井はあと",
  age = 16,
  birth = {month = 8, day = 10},
  slogan = "哈恰瑪恰瑪",  -- https://www.youtube.com/watch?v=hvoBS-BzB3M
}

-----------

coco:say()  --> 桐生ココ: good morning mother f**ker
peko:say() --> 兎田ぺこら: Peko↗ Peko↘ Peko↗ Peko↘ Peko↗ Peko↘~!?
hahama:say() --> 赤井はあと: 哈恰瑪恰瑪

這樣實例的建立是否好理解多!

At Last

事件始末:

我不打算對此事有太深入的糾葛,但又認為沉默不是個好選擇。就簡單談談我對這件事情的一些個人看法吧!

對於Cover株式會社

我對於其作法覺得有問題也沒有問題。

沒有太大問題的部份是,如果懲處確實是合約裡禁止之事項,那麼不管是玩皮,還是公開後台數據,受到暫停直播的懲處,也是可以理解。

有問題的部份是其陰陽申明實在讓人看不懂這是什麼操作?這公關很像雙面人,反而會讓人更有不信任感吧!

雖然能理解,其旗下也有藝人生活於...大陸/中華人民共和國(不管你怎麼稱呼,反正地理位置就是那裡)。處理不當,也可能使其旗下其他藝人造成危險。但自己把政治議題扯到申明去...我是覺得不算很聰明(更何況還是陰陽申明)。如果真的是公開後台數據造成的問題的話,應該不用牽扯到政治吧?

處置方式:暫停直播,就目前看來也不是止血的好方法。

對於出征者

只想說...你出征對象應該是平台還是VTuber?是不是該先搞清楚阿?
不過YouTube聽說是不存在的網站...那裡面內容當然不會是服務給不存在的人使用吧?

對於文字獄

這我就真不知道怎麼說了......
就我自己事後去看的片段與了解... ...也就只因為在影片說出某些字,就被出征了?然後又有某些字不能用?

說個小故事:
我有長輩很討厭聽到「幹」這個字。但這個字不只是壞的阿!主幹、幹部,更有重要的意思。 不能說不代表不存在 ,而且很有可能會創造出其他說法。

每個人都有錯,但只有愚者才會執迷不悟。
-- 西塞羅

如果只是禁止他,而不正視他,那能進步的機會我想非常有限...
我想不透這樣只是禁止某些字,除了自己爽以外能得到什麼。一個字的使用也與上下文有關,只因為某種使用方式不好就禁止,文化會不會開倒車呢?

欸?怎麼好像也可用在Cover上?

當然,就技術角度上來看,這是最簡單處理的辦法。
只是就那些腦袋玻璃做的的人說,不知道聽到「泰... ...
...
...
...國」,是不是也馬上可以聽到玻璃碎的聲音?

對此我只能....

最後的最後

哈洽馬真洗腦阿~

喔對!最後祝各位中秋節快樂阿!


上一篇
【30天Lua重拾筆記18】基礎2: 應該知道的(總集+補充)
下一篇
【30天Lua重拾筆記19】基礎3: 陣列從1開始
系列文
30天 Lua重拾筆記36

尚未有邦友留言

立即登入留言