16 編譯、執(zhí)行和錯(cuò)誤

16.1 編譯

此前,我們已經(jīng)介紹過函數(shù) dofile,它是運(yùn)行 Lua 代碼的主要方式之一。實(shí)際上,函數(shù) dofile 是一個(gè)輔助函數(shù),函數(shù) loadfile 才完成了真正的核心工作。與函數(shù) dofile 類似,函數(shù) loadfile 也是從文件中加載 Lua 代碼段,但它不會(huì)運(yùn)行代碼,而只是編譯代碼,然后將編譯后的代碼段作為一個(gè)函數(shù)返回。此外,與函數(shù) dofile 不同,函數(shù) loadfile 只返回錯(cuò)誤碼而不拋出異常??梢哉J(rèn)為,函數(shù) dofile 就是:

---@return function
---@param filename stirng
function dofile(filename)
    local f = assert(loadfile(filename))
    return f()
end

請(qǐng)注意,如果函數(shù) loadfile 執(zhí)行失敗,那么函數(shù) assert 會(huì)引發(fā)一個(gè)錯(cuò)誤。
對(duì)于簡單的需求而言,由于函數(shù) dofile 在一次調(diào)用中就做完了所有工作,所以該函數(shù)非常易用。不過,函數(shù) loadfile 更靈活。在發(fā)生錯(cuò)誤的情況中,函數(shù) loadfile 會(huì)返回 nil 及錯(cuò)誤信息,以允許我們按自定義的方式來處理錯(cuò)誤。此外,如果需要多次運(yùn)行同一個(gè)文件,那么只需調(diào)用一次 loadfile 函數(shù)以后再多次調(diào)用它的返回結(jié)果即可。由于只編譯一次文件,因此這種方式的開銷要比多次調(diào)用dofile 小得多(編譯在某種程度上相比其他操作開銷更大)。
函數(shù) load 與文件 loadfile 類似,不同之處在于該函數(shù)從一個(gè)字符串或函數(shù)中讀取代碼段,而不是從文件中讀取。例如,考慮如下代碼:

f = load("i = i + 1")

在這句代碼執(zhí)行后,變量 f 就會(huì)變成一個(gè)被調(diào)用時(shí)執(zhí)行 i = i + 1 的函數(shù):

i = 0
f()
print(i)
f()
print(i)

盡管函數(shù) load 的功能很強(qiáng)大,但還是應(yīng)該謹(jǐn)慎地使用。相對(duì)于其他可選的函數(shù)而言,該函數(shù)的開銷較大并且可能會(huì)引起詭異的問題。請(qǐng)先確定當(dāng)下已經(jīng)找不到更簡單的解決方式后再使用該函數(shù)。
如果要編寫一個(gè)用后即棄的 dostring 函數(shù)(例如加載并運(yùn)行一段代碼),那么我們可以直接調(diào)用函數(shù) load 的返回值:

load(s)()

不過,如果代碼中有語法錯(cuò)誤,函數(shù) load 就會(huì)返回 nil 和形如 “試圖調(diào)用一個(gè) nil 值” 的錯(cuò)誤信息。為了更清楚地展示錯(cuò)誤信息,最好使用函數(shù) assert:

assert(load(s))()

通常,用函數(shù) load 來加載字符串常量是沒有意義的。例如,如下得到兩行代碼基本等價(jià):

f = load("i = i + 1")
f  = function () i = i + 1 end

但是,由于第 2 行代碼會(huì)與其外層的函數(shù)一起被編譯,所以其執(zhí)行速度要快得多。與之對(duì)比,第一段代碼在調(diào)用函數(shù) load 時(shí)會(huì)進(jìn)行一次獨(dú)立的編譯。
由于函數(shù) load 在編譯時(shí)不涉及詞法定界,所以上述示例的兩段代碼可能并不完全等價(jià)。為了清晰的展示它們之間的區(qū)別,讓我們稍微修改一下上面的例子:

i = 32
local i = 0
f = load("i = i + 1; print(i)")
g = function()
    i = i + 1
    print(i)
end
f()    -->    33
g()    -->    1

函數(shù) g 像我們所預(yù)期地那樣操作局部變量 i,但函數(shù) f 操作的卻是全局變量 i,這是由于函數(shù) load 總是在全局環(huán)境中編譯代碼段。
函數(shù) load 最典型的用法是執(zhí)行外部代碼(即那些來自程序本身之外的代碼段)或動(dòng)態(tài)生成的代碼。例如,我們可能想運(yùn)行用戶定義的函數(shù),由用戶輸入函數(shù)的代碼后調(diào)用函數(shù) load 對(duì)其求值。請(qǐng)注意,函數(shù) load 期望的輸入時(shí)一段程序,也就是一系列的語句。如果需要對(duì)表達(dá)式求值,那么可以在表達(dá)式前添加 return,這樣才能構(gòu)成一條返回指定表達(dá)式值得語句。例如:

print("enter your expression:")
io.flush()    --由于我使用的是idea 所以需要用這個(gè)語句來刷新流 否則在控制臺(tái)看不到上一句 print 函數(shù)的內(nèi)容
local line = io.read()
local func = assert(load("return " .. line))
print("the value of your expression is " .. func())

由于函數(shù) load 所返回的函數(shù)就是一個(gè)普通函數(shù),因此可以返回對(duì)其進(jìn)行調(diào)用:

print("enter function to be plotted (with variable 'x'): ")
io.flush()

local line = io.read()
local f = assert(load("return " .. line))

for i = 1, 20 do
    x = i
    print(string.rep("*", f()))
end

我們也可以使用讀取函數(shù)作為函數(shù) load 的第一個(gè)參數(shù)。讀取函數(shù)可以分幾次返回一段程序,函數(shù) load 會(huì)不斷地調(diào)用讀取函數(shù)直到讀取函數(shù)返回 nil(表示程序段結(jié)束)。作為示例,以下的調(diào)用與函數(shù) loadfile 等價(jià):

f = load(io.lines(filename, "*L"))

正如我們?cè)诘?7 章所看到的,調(diào)用 io.lines(filename, "*L") 返回一個(gè)函數(shù),這個(gè)函數(shù)每次被調(diào)用時(shí)就從指定文件返回一行。因此,函數(shù) load 會(huì)一行一行地從文件中讀出一段程序。以下的版本與之相似但效率稍高:

f = load(io.lines(filename, 1024))

這里,函數(shù) io.lines 返回的迭代器會(huì)以 1024 字節(jié)為塊讀取源文件。
Lua 語言將所有獨(dú)立的代碼當(dāng)做匿名可變長參數(shù)的函數(shù)體。例如,load("a = 1") 的返回值與以下表達(dá)式等價(jià):

function(...) a = 1 end

像其他任何函數(shù)一樣,代碼段中可以聲明局部變量:

f = load("local a = 10; print(a + 20)")
f()

使用這個(gè)特性,可以在不使用全局變量 x 的情況下重寫之前運(yùn)行用戶定義函數(shù)的示例:

print("enter function to be plotted (with variable 'x'): ")
io.flush()

local line = io.read()
local f = assert(load("local x = ...; return " .. line))
for i = 1, 20 do
    print(string.rep("*", f(i)))
end

在上述代碼中,在代碼段開頭增加了 "local x = ..." 來將 x 聲明為局部變量。之后使用參數(shù) i 調(diào)用函數(shù) f,參數(shù) i 就是可變長參數(shù)的表達(dá)式的值 (...)。
函數(shù) load 和函數(shù) loadfile 從來不引發(fā)錯(cuò)誤。當(dāng)有錯(cuò)誤發(fā)生時(shí),它們會(huì)返回 nil 及錯(cuò)誤信息:

print(load("i i"))    -->    nil    [string "i i"]:1: syntax error near 'i'

此外,這些函數(shù)沒有任何副作用,它們既不改變或創(chuàng)建變量,也不向文件寫入等。這些函數(shù)只是將程序段編譯為一種中間形式,然后將結(jié)果作為匿名函數(shù)返回。一種常見的誤解是認(rèn)為加載一段程序也就是定義了函數(shù),但實(shí)際上在 Lua 語言中函數(shù)定義是運(yùn)行時(shí)而不是在編譯時(shí)發(fā)生的一種賦值操作。例如,假設(shè)有一個(gè)文件 foo.lua:

--- foo.lua
---@return void
---@param x any
function foo(x)
    print(x)
end

當(dāng)執(zhí)行:

f = loadfile("foo.lua")

時(shí),編譯 foo 的命令并沒有定義 foo,只有運(yùn)行代碼才會(huì)定義它:

f = loadfile("foo.lua")
print(foo)    -->    nil
f()
foo("ok")    -->    ok

這種行為可能看上去有些奇怪,但如果不使用語法糖對(duì)其進(jìn)行重寫則看上去會(huì)清晰很多:

--- foo.lua
---@return void
---@param x any
foo = function(x)
    print(x)
end

如果線上產(chǎn)品級(jí)別的程序需要執(zhí)行外部代碼,那么應(yīng)該處理加載程序段時(shí)報(bào)告的所有錯(cuò)誤。此外,為了避免不愉快的副作用發(fā)生,可能還應(yīng)該在一個(gè)受保護(hù)的環(huán)境中執(zhí)行這些代碼我們會(huì)在第 22 章中討論相關(guān)的細(xì)節(jié)。


16.2 預(yù)編譯的代碼

生成預(yù)編譯文件(也被稱為二進(jìn)制文件)的最簡單方式是,使用標(biāo)準(zhǔn)發(fā)行版中附帶的 luac 程序。例如,下列命令會(huì)創(chuàng)建文件 prog.lua 的預(yù)編譯版本 prog.lc:

$ luac -o prog.lc prog.lua

Lua 解析器會(huì)像執(zhí)行普通 Lua 代碼一樣執(zhí)行這個(gè)新文件,完成與原來代碼完全一致的動(dòng)作:

lua prog.lc

幾乎在 Lua 語言中所有能夠使用源碼的地方都可以使用預(yù)編譯代碼。特別地,函數(shù) loadfile 和函數(shù) load 都可以接受預(yù)編譯代碼。
我們可以直接在 Lua 語言中實(shí)現(xiàn)一個(gè)最簡單的 luac:

p = loadfile(arg[1])
f = io.open(arg[2], "wb")
f:write(string.dump(p))
f:close()

這里的關(guān)鍵函數(shù)是 string.dump,該函數(shù)的入?yún)⑹且粋€(gè) Lua 函數(shù),返回值是傳入函數(shù)對(duì)應(yīng)的字符串形式的預(yù)編譯代碼(已被正確地格式化,可由 Lua 語言直接加載)。
luac 程序提供了一些有意思的選項(xiàng)。特別地,選項(xiàng) -l 會(huì)列出編譯器為指定代碼段生成的操作碼(opcode)。例如,示例 16.1 展示了函數(shù) luac 針對(duì)如下只有一行內(nèi)容的文件在帶有 -l 選項(xiàng)時(shí)的輸出:

a = x + y - z

示例16.1 luac -l 的輸出示例

main <test.lua:0,0> (7 instructions, 28 bytes at 00710520)
0+ params, 2 slots, 0 upvalues, 0 locals, 4 constants, 0 functions
        1       [1]     GETGLOBAL       0 -2    ; x
        2       [1]     GETGLOBAL       1 -3    ; y
        3       [1]     ADD             0 0 1
        4       [1]     GETGLOBAL       1 -4    ; z
        5       [1]     SUB             0 0 1
        6       [1]     SETGLOBAL       0 -1    ; a
        7       [1]     RETURN          0 1

預(yù)編譯形式的代碼不一定比源代碼更小,但是卻加載得更快。預(yù)編譯形式的代碼的另一個(gè)好處是,可以避免由于意外而修改源碼。然而,與源代碼不同,蓄意損壞或構(gòu)造的二進(jìn)制代碼可能會(huì)讓 Lua 解析器崩潰或甚至執(zhí)行用戶提供的機(jī)器碼。當(dāng)運(yùn)行一般的代碼時(shí)通常無須擔(dān)心,但應(yīng)該避免運(yùn)行以預(yù)編譯形式給出的非受信代碼。這種需求,函數(shù) load 正好有一個(gè)選項(xiàng)可以適用。
除了必須的第 1 個(gè)參數(shù)外,函數(shù) load 還有 3 個(gè)可選參數(shù)。第 2 個(gè)參數(shù)是程序段的名稱,只在錯(cuò)誤信息中心被用到。第 4 個(gè)參數(shù)是環(huán)境,我們會(huì)在第 22章中對(duì)其進(jìn)行討論。第 3 個(gè)參數(shù)正是我們所關(guān)心的,它控制了允許加載的代碼段的類型。如果該參數(shù)存在,則只能是如下的字符串:字符串 "t" 允許加載文本類型的代碼段,字符串 "b" 只允許加載二進(jìn)制類型的代碼段,字符串 "bt" 允許同時(shí)加載上述兩種類型的代碼段(默認(rèn))。


16.3 錯(cuò)誤

Lua 語言會(huì)在遇到非預(yù)期的情況時(shí)引發(fā)錯(cuò)誤。例如,當(dāng)試圖將兩個(gè)非數(shù)值類型的值相加,對(duì)不是函數(shù)的值進(jìn)行調(diào)用,對(duì)不是表類型的值進(jìn)行索引等。我們可以顯式地調(diào)用函數(shù) error 并傳入一個(gè)錯(cuò)誤信息作為參數(shù)來引發(fā)一個(gè)錯(cuò)誤。通常,這個(gè)函數(shù)就是在代碼中提示出錯(cuò)的合理方式:

print("enter a number")
io.flush()
n = io.read("n")
if not n then
    error("invalid input")
end

由于“針對(duì)某些情況調(diào)用函數(shù) error”這樣的代碼結(jié)構(gòu)太常見了,所以 Lua 語言提供了一個(gè)內(nèi)建的函數(shù) assert 來完成這類工作:

print("enter a number")
io.flush()
n = assert(io.read("*n"), "invalid input")

函數(shù) assert 檢查其第一個(gè)參數(shù)是否為真,如果該參數(shù)為真則返回該參數(shù);如果該參數(shù)為假則引發(fā)一個(gè)錯(cuò)誤。該函數(shù)的第 2 個(gè)參數(shù)是一個(gè)可選的錯(cuò)誤信息。不過,要注意函數(shù) assert 只是一個(gè)普通函數(shù),所以 Lua 語言總是在調(diào)用該函數(shù)前先對(duì)參數(shù)進(jìn)行求值。如果編寫形如:

n = io.read()
assert(tonumber(n), "invalid input: " .. n .. " is not a number")

的代碼,那么即使 n 是一個(gè)數(shù)值類型,Lua 語言也總是會(huì)進(jìn)行字符串連接。在這種情況下使用顯式的測試可能更加明智。
當(dāng)一個(gè)函數(shù)發(fā)現(xiàn)某種意外的情況發(fā)生時(shí),在進(jìn)行異常處理時(shí)可以采取兩種基本方式:一種是返回錯(cuò)誤代碼(nil 或者 false),另一種是通過調(diào)用函數(shù) error 引發(fā)一個(gè)錯(cuò)誤。如何在這兩種方式之間選擇并沒有固定的規(guī)則,但筆者通常遵循如下的指導(dǎo)原則:容易避免的異常應(yīng)該引發(fā)錯(cuò)誤,否則應(yīng)該返回錯(cuò)誤碼
以函數(shù) math.sin 為例,當(dāng)調(diào)用時(shí)參數(shù)傳入了一個(gè)表該如何反應(yīng)呢?如果要檢查錯(cuò)誤,那么久不得不編寫如下代碼:

local res = math.sin(x)
if not res then
    --error-handling code
end 

當(dāng)然,也可以在調(diào)用函數(shù)前輕松地檢查出這種異常:

if not tonumber(x) then
    --error-handling code
end 

通常,我們既不會(huì)檢查參數(shù)也不會(huì)檢查函數(shù) math.sin 的返回值:如果 sin 的參數(shù)不是一個(gè)數(shù)值,那么就意味著我們的程序可能出現(xiàn)了問題。此時(shí),處理異常最簡單也是最實(shí)用的做法就是停止運(yùn)行,然后輸出一條錯(cuò)誤信息。
另一方面,讓我們?cè)倏紤]一下用于打開文件的函數(shù) io.open。如果要打開的文件不存在,那么該函數(shù)應(yīng)該有怎樣的行為呢?在這種情況下,沒有什么簡單的方法可以在調(diào)用函數(shù)前檢測到這種異常。在很多系統(tǒng)中,判斷一個(gè)文件是否存在的唯一方法就是試著去打開這個(gè)文件,。因此如果由于外部原因(比如文件不存在或權(quán)限不足)導(dǎo)致 io.open 無法打開一個(gè)文件,那么它應(yīng)該返回 false 及一條錯(cuò)誤信息。通過這種方式,我們就有機(jī)會(huì)采取恰當(dāng)?shù)姆绞絹硖幚懋惓G闆r,例如要求用戶提供另一個(gè)文件名:

local file, msg
repeat
    print("enter a file name")
    io.flush()
    local name = io.read()
    if not name then
        return
    end
    file, msg = io.open(name, "r")
    if not file then
        print(msg)
    end
until file

如果不想處理這些情況,但又想安全地運(yùn)行程序,那么只需要使用 assert:

file = assert(io.open(name, "r"))

這是 Lua 語言中的一種典型技巧:如果函數(shù) io.open 執(zhí)行失敗,assert 就引發(fā)一個(gè)錯(cuò)誤。請(qǐng)讀者注意,錯(cuò)誤信息(函數(shù)的第 2 個(gè)返回值)是如何變成 assert 的第 2 個(gè)參數(shù)的。


16.4 錯(cuò)誤處理和異常

對(duì)于大多數(shù)應(yīng)用而言,我們無須在 Lua 代碼中做任何錯(cuò)誤處理,應(yīng)用程序本身會(huì)負(fù)責(zé)處理這類問題。所有 Lua 語言的行為都是由應(yīng)用程序的一次調(diào)用而觸發(fā)的,這類調(diào)用通常是要求 Lua 語言執(zhí)行一段代碼。如果執(zhí)行中發(fā)生了錯(cuò)誤,那么調(diào)用會(huì)返回一個(gè)錯(cuò)誤代碼,以便應(yīng)用程序采取適當(dāng)?shù)男袨閬硖幚礤e(cuò)誤。當(dāng)獨(dú)立解釋器中發(fā)生錯(cuò)誤時(shí),主循環(huán)會(huì)打印錯(cuò)誤信息,然后繼續(xù)顯示提示符,并等待執(zhí)行指定的命令。
不過,如果要在 Lua 代碼中處理錯(cuò)誤,那么就應(yīng)該使用函數(shù) pcall 來封裝代碼。
假設(shè)要執(zhí)行一段 Lua 代碼并捕獲執(zhí)行中發(fā)生的所有錯(cuò)誤,那么首先需要將這段代碼封裝到一個(gè)函數(shù)中,這個(gè)函數(shù)通常是一個(gè)匿名函數(shù)。之后,通過 pcall 來調(diào)用這個(gè)函數(shù):

local ok, msg = pcall(function()
    -- some code
    if (unexpected_condition) then
        error()
    end

    -- some code

    print(a[i])     -- 潛在錯(cuò)誤: 'a' 可能不是一個(gè)表

    -- some code
end
)

if ok then      -- 執(zhí)行被保護(hù)的代碼時(shí)沒有發(fā)生錯(cuò)誤
    -- regular code
else        -- 執(zhí)行被保護(hù)的代碼時(shí)有錯(cuò)誤發(fā)生,進(jìn)行恰當(dāng)?shù)奶幚?    -- error-handling code
end

函數(shù) pcall 會(huì)以一種保護(hù)模式來調(diào)用它的第一個(gè)參數(shù),以便捕獲該函數(shù)執(zhí)行中的錯(cuò)誤。無論是否有錯(cuò)誤發(fā)生,函數(shù) pcall 都不會(huì)引發(fā)錯(cuò)誤。如果沒有錯(cuò)誤發(fā)生,那么 pcall 返回 true 及被調(diào)用函數(shù)(作為 pcall 的第 1 個(gè)參數(shù)傳入)的所有返回值;否則,返回 false 及錯(cuò)誤信息。
使用 “錯(cuò)誤信息” 的命名方式可能會(huì)讓人誤解錯(cuò)誤信息必須使用一個(gè)字符串,因此稱之為 錯(cuò)誤對(duì)象 可能更好,這主要是因?yàn)楹瘮?shù) pcall 能夠返回傳遞給 error 的任意 Lua 語言類型的值。

local status, err = pcall(function()
    error({ code = 121 })
end)
print(err.code)

這些機(jī)制為我們提供了在 Lua 語言中進(jìn)行異常處理的全部。我們可以通過 error 來拋出異常,然后用函數(shù) pcall 來捕獲異常,而錯(cuò)誤信息則用來標(biāo)識(shí)錯(cuò)誤的類型。


16.5 錯(cuò)誤信息和?;厮?/h2>

雖然能夠使用任何類型的值作為錯(cuò)誤對(duì)象,但錯(cuò)誤對(duì)象通常是一個(gè)描述出錯(cuò)內(nèi)容的字符串。當(dāng)遇到內(nèi)部錯(cuò)誤(比如嘗試對(duì)一個(gè)非表類型的值進(jìn)行索引操作)出現(xiàn)時(shí),Lua 語言負(fù)責(zé)產(chǎn)生錯(cuò)誤對(duì)象(這種情況下錯(cuò)誤對(duì)象永遠(yuǎn)是字符串;而在其他情況下,錯(cuò)誤對(duì)象就是傳遞給函數(shù) error 的值。)如果錯(cuò)誤對(duì)象是一個(gè)字符串,那么 Lua 語言還會(huì)嘗試把一些有關(guān)錯(cuò)誤發(fā)生位置的信息附上:

local status, err = pcall(function() error("my error") end)
print(error)

位置信息中給出了出錯(cuò)代碼段的名稱和行號(hào)。
函數(shù) error 還有第 2 個(gè)可選參數(shù) level,用于指出向函數(shù)調(diào)用層次中的哪層函數(shù)報(bào)告錯(cuò)誤,以說明誰該為錯(cuò)誤負(fù)責(zé)。例如,假設(shè)編寫一個(gè)用來檢查其自身是否被正常調(diào)用了的函數(shù):

function foo(str)
    if(type(str) ~= "string") then
        error("string expected")
    end
    -- regular code
end

如果調(diào)用時(shí)被傳遞了錯(cuò)誤參數(shù):

foo({x = 1})

由于是函數(shù) foo 調(diào)用的 error,所以 Lua 語言會(huì)認(rèn)為是函數(shù) foo 發(fā)生了錯(cuò)誤。然而,真正的肇事者其實(shí)是函數(shù) foo 的調(diào)用者。為了糾正這個(gè)問題,我們需要告訴 error 函數(shù)錯(cuò)誤實(shí)際發(fā)生在函數(shù)調(diào)用層次的第 2 層中(第 1 層是 foo 函數(shù)自己):

function foo(str)
    if(type(str) ~= "string") then
        error("string expected", 2)
    end
    -- regular code
end

通常,除了發(fā)生錯(cuò)誤的位置以外,我們還希望錯(cuò)誤發(fā)生時(shí)得到更多調(diào)試信息。至少,我們希望得到具有發(fā)生錯(cuò)誤時(shí)完整函數(shù)調(diào)用棧的棧回溯。當(dāng)函數(shù) pcall 返回錯(cuò)誤信息時(shí),部分的調(diào)用已經(jīng)被破壞了。因此,如果希望得到一個(gè)有意義的?;厮?,那么就必須在函數(shù) pcall 返回前先將調(diào)用棧構(gòu)建好。為了完成這個(gè)需求,Lua 語言提供了函數(shù) xpcall。該函數(shù)與函數(shù) pcall 類似,但它的第 2 個(gè)參數(shù)是一個(gè)消息處理函數(shù)。當(dāng)發(fā)生錯(cuò)誤時(shí),Lua 會(huì)在調(diào)用棧展開前調(diào)用這個(gè)消息處理函數(shù),以便消息處理函數(shù)能夠使用調(diào)試庫來獲取有關(guān)錯(cuò)誤的更多信息。兩個(gè)常用的消息處理函數(shù)是 debug.debug 和 debug.traceback,前者為用戶提供一個(gè) Lua 提示符來讓用戶檢查錯(cuò)誤發(fā)生的原因;后者則使用調(diào)用棧來構(gòu)造詳細(xì)的錯(cuò)誤信息,Lua 語言的獨(dú)立解釋器就是使用這個(gè)函數(shù)來構(gòu)造錯(cuò)誤信息的。


練習(xí)題我個(gè)人感覺實(shí)用性比較低,決定跳過它

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容