跳到內容
關於我 數位花園

部落格

[小說] 我只想好好說話

注意:內文有大量劇透,請自行斟酌

或許有時候

我們認為自己有悲慘的遭遇,認為別人過得幸福,又怎麼會理解

然而這不一定是真實

隱藏在美好外表下的,不一定也如此美好


本書的主角柏崎悠太,是剛就讀國中的男生

他入學第一天最大的煩惱,就是自我介紹

因為他有嚴重的口吃,只要跟別人說話,總是會口吃

就算在家裡,與家人對話也是一樣,這個問題一直困擾著他

他在學校刻意避開與其他人交流,以免被別人發現他有口吃

在校門口收到學校社團的傳單時,卻有一件事讓他在意

本來就不打算參加社團的他,無意間看到了廣播社的傳單:

十分歡迎對於說話感到棘手的人,社團活動會仔細教你發聲方法等。 只要透過練習,你一定也能夠好好發出完美又清晰的聲音!

這段話使他更積極、正向了一點,但也只維持了幾秒鐘

「還是不行吧!那是說給一般人聽的,而我連一句話都無法好好說出來,根本不是一般人」

於是,他在舒適圈的邊界之間,猶豫徘徊了一陣子


在故事的後半段,他從古部的自白中得知她的經歷

其實比悠太還慘

她不但被同學霸凌、失去好友、還被父母唾棄,甚至暴力相向

聽了這番經歷之後,悠太自責自己的不懂得珍惜

他的周遭,縱然仍有些人會嘲笑他,卻也有很多溫柔、體貼的人

想要理解他、出手幫助他

廣播社的社長,一位很和善的學長

身為考生的他,出場的機會並沒有很多

然而我認為卻是一個很重要的中介者角色

因為如果沒有他,悠太永遠不會知道姊姊在社團裡受到孤立

悠太也不會因此拿到古部手上的那本劇本,也不會知道古部想幫助他的心情

而且也不會知道椎名老師所做出非常不易察覺的改變

在我看來,立花所做的事,串連所有的角色

也讓主角悠太看到了這整個全貌

雖然他之後應該會因為把悠太姊姊的事情都抖出來,而被臭罵一頓吧


他跟悠太姊姊的關係,作者並沒有太多著墨

雖然我覺得他們之間的互動,會有一些有趣的故事

覺得有點可惜

坐隔壁的古部同學,是一名看來清純可人的女生

但相對於外表,她對任何人都很冷淡,沒有太多的表情,也不多話

總給人拒人於外的感覺(美少女就是這樣才有魅力啊

也是因為她的原因,悠太才半推半就地加入了廣播社

她始終對悠太的說話方式,沒有表現出任何特別的反應

有別於以往一般人會有的反應,例如:感到怪異、訕笑、皺起眉頭、或以尷尬的微笑表示同情

而她總是面無表情、耐心、平靜地聽他把話說完

我腦中浮現的,是不帶有任何的情緒、直視對方的表情

她拿了自己喜歡的動畫劇本,半強迫地邀請悠太一起做劇本的台詞練習

古部在最後的自白中,道出她過往不好的遭遇

她曾經歷過與悠太相同的痛苦,而且還遭遇身旁人們不友善地對待及霸凌

這其中還包括父母以及曾經的好友

因此,她認為唯有像悠太這樣同樣遭遇的人,才能夠了解她

也不惜被討厭的風險,強迫悠太陪她做劇本的台詞練習,希望能夠治好口吃的問題

她好幾次製造了與悠太獨處的機會,只是私心希望他不要跟其他人成為朋友

雖然不是「美少女看到男主角就一見鍾情、瘋狂追求」,那種輕小說才會出現的劇情

但是也有點病嬌的味道(抱歉歪樓了

正是因為被曾經唯一的好友背叛、同儕的霸凌,而對身邊的人們缺乏安全感與信任吧

班級的導師是一名隨性又古怪的老師

悠太去辦公室跟椎名老師領取社辦鑰匙的時候,瞄到老師的電腦螢幕上是跟工作無關的頁面

然而據立花學長的說法

椎名因為班上有特殊狀況的學生,所以也在教學方式上做了些的調整

她不再課堂上點學生回答問題,這也讓悠太鬆了一口氣

也因此她必須花更多的時間在備課

但可惜的是,椎名只能改變她自己負責的英文課,其他老師的教課時段,仍然是悠太的夢魘 ⋯⋯

在與悠太的通話過程中,也表示自己也是頭一次,遇到有口吃困擾的學生,頓時也不知道要怎麼做

她除了在教學方式上改變之外,也沒有特別做什麼了

我想你也不喜歡在眾人面前有特殊待遇吧?

或許就如同椎名所說,如果給悠太特殊待遇,說不定會引來霸凌之類的事情

這或許是老師沒有改變太多的原因之一

身為悠太的姊姊,也是身邊最關心他的人之一

她不時地關心、鼓勵他

雖然有點強勢、多管閒事(悠太的說法

但她曾經跟悠太一起在網路上查詢口吃的相關資料,希望能找到解決辦法

以悠太的視角來看,她是一個「過著幸福人生」的普通人

然而他不知道姊姊為了弟弟,在學校社團裡遭到嚴重的排擠

但她隱瞞著家人,表現出不讓人操心的孩子

理由是她「不希望把自己的問題帶回家」

由此可見她是一個十分堅強的孩子

在還未閱讀之前,我期待會有很多關於廣播社活動的描述

但是事實上卻沒有(笑

古部與悠太在廣播室裡,練習著劇本的對話

連放學後的校內廣播,也是輕描淡寫地帶過

很多時候,主要都是描述著練習時刻,悠太唸台詞時難受心情與心裡百般的掙扎

看了都不禁緊張起來,想要大喊:「夠了!可以不用再勉強自己了!」

古部很堅持要繼續做劇本的對話練習,試圖說服他相信這樣持續努力做,可以治好口吃

悠太不願相信,覺得在做沒有用的努力,於是抗拒練習

他們起了爭執,悠太無法接受古部如此強硬地堅持

在強烈的情緒下,他更無法說出自己的想法,於是他用了「筆談」的方式,道出自己的想法:

我已經不想再跟妳說話了

然後留下錯愕的古部同學,逃離了廣播室

看到筆談的內容,我感受到非常強烈的情緒,內心實在是百般交雜

我想當下看到這段話的古部,她應該很受傷吧

參賽的名額是兩人,原本是指派立花與古部

但是經過來一番波折,悠太鼓氣勇氣跟椎名老師說自己要參賽

所以最後由悠太與古部上場

至於比賽的結果,也算是 happy ending 啦

其實在故事的前期,我就隱約懷疑古部同學也曾有口吃的困擾

後來聽古部自己的自白之後,才確認我猜中了

至於為什麼會這麼猜測?

因為讓我聯想到另一個故事,那就是手塚治虫的一部很有名的漫畫「怪醫黑傑克」

我在看黑傑克漫畫的時候,有一段故事令我印象深刻

注意:以下有「怪醫黑傑克」劇透,請自行斟酌


故事的劇情如下:

有一位男孩,因小兒麻痺不便於行,平時非常努力地做復健

他看了一位醫生:本間丈太郎的書,紀錄他治療病患的故事

其中有一位患者,雖然不是小兒麻痺,但同樣也不良於行

書中記載著這位病患雖然有行走上的困難,仍然做了長途徒步旅行

男孩深受啟發,也效仿他,從廣島徒步走到大阪

途中遇到開車尾隨、臭名遠播的黑傑克醫生(因為時常收取高額醫療費用、還沒有醫師執照

男孩企圖要趕走黑傑克,覺得他心懷不軌

但黑傑克總是在艱困的路段給予有用的建議

男孩頓時恍然大悟,明白書中描述的病患,就是黑傑克本人

但是他疑惑問到:「但是你看起來很正常,不像是殘障者 ⋯⋯」

此時,黑傑克什麼也沒說,只是拉起他的褲管,秀出滿是縫合線的雙腿

幼時遭遇重大意外的黑傑克,身體支離破碎

他很努力在復健上,徒步旅行則是其中之一


古部同學就如同黑傑克一樣,而悠太則像是那個男孩

正因為經歷過同樣的痛苦,所以才能夠理解悠太的處境

也正因為自己克服了困難的障礙,才會不斷說服對方「一定可以治好的」

每個人的外表下,或許藏著不為人知的煩惱,表面上的幸福並非真實,有可能正好相反

悠太的姊姊不希望將自身的問題,帶給家人困擾

但並不代表她沒有任何煩惱與痛苦、過著幸福無憂的人生

大家都有不願讓人知道的事情或煩惱,但無人知曉不代表沒有

看來冷淡的人,也並非漠不關心

椎名老師沒有像悠太的姊姊有非常多積極地作為,相較之下,感覺是比較消極、散漫的類型(沒有貶義

我想要表達的是,有些人用自己的方式做出關心與貢獻

縱然當事人(主角)的感受可能微乎其微,但也不容忽視它

只是如果立花學長沒有告訴悠太的話,他大概永遠不會發現吧

即使沒親身經歷,也能夠有同理與諒解

在古部的自白中,曾說自己討厭所有人,這裡所有人指的是普通人,也就是那些沒有口吃困擾的人

這跟她的經歷有關,確實可以理解

她提到對和善的立花學長仍有一絲敵意,只因為他是普通人

還刻意支開他,讓她跟悠太可以在廣播社社辦獨處(這舉動莫名覺得可愛

但我希望她之後也能夠嘗試敞開心胸,接納那些善良的普通人

並非所有普通人都會對用嘲笑的態度去面對有困擾的人

像是立花學長、悠太的姊姊、椎名老師他們

雖然他們無法完全理解口吃的困擾,畢竟沒有真實經歷、只是個旁觀者

但不代表他們都是壞人,他們也願意去幫助人

無意間在圖書館找到這本書

當初不知道在哪裡看到這本書的廣告海報

頓時被標題及封面的繪圖深深吸引

poster

此圖來源:妞新聞

我原本預期想看到的是廣播社會從事的活動、期待有一些有趣的故事(也不是說本書的故事不有趣

然而卻跟想像的不太一樣

我很喜歡封面的繪圖,個人一種很安靜祥和的感覺(但故事的發展卻非如此

tw version book cover

查了一下,發現這是中文版才有的封面

日文原文版的封面長這樣:

jp version book cover

風格看起來陰沉許多,而且 ⋯⋯

沒有可愛的古部同學(抱歉又歪樓了

書中悠太的大部分台詞,都是結巴的狀態

雖然還沒有讀過日文版的內容,但是覺得譯者應該花了不少時間

也很敬佩譯者,台詞也充分傳達了

我很好奇,把結巴的台詞從日文翻譯成中文的過程,會是什麼樣的感受

前面提到黑傑克的故事,因為是很久很久以前看的

劇情的內容非常地模糊

只依稀記得大概而已

所以還特別去翻閱了一下漫畫,重新複習這段故事

結果是 ⋯⋯ 嗯,跟記憶中的劇情差異有點大 😂😂😂

果然記憶力不太可靠啊~(大概是年紀大了

用 JavaScript 偵測裝置的方位 - 直向及橫向

在 CSS 中,我們可以用 Media Queries 的方式,分別定義橫向與直向的樣式:

.box {
width: 300px;
height: 300px;
display: none;
}
@media (orientation: landscape) {
.landscapeBox {
display: block;
background: lightblue;
}
}
@media (orientation: portrait) {
.portraitBox {
display: block;
background: lightcoral;
}
}

但是,雖然可以隨著直、橫向定義不同的樣式

但卻無法讓元件知道現在的狀態,JS 並不知道目前的裝置方位為何

也就無法將這個值納入元件的 state 去做其他的判斷了

我原本打算使用 react-hook-screen-orientation 這個套件來判斷行動裝置的方位

於是我翻了一下這個套件的原始碼,發現它監聽的事件是 orientationchange,以下是部份原始碼:

window.addEventListener("orientationchange", updateOrientation);

於是,我查了一下 MDN 的文件,發現這個事件即將要捨棄了:

Deprecated: This feature is no longer recommended. Though some browsers might still support it, it may have already been removed from the relevant web standards, may be in the process of being dropped, or may only be kept for compatibility purposes. Avoid using it, and update existing code if possible; see the compatibility table at the bottom of this page to guide your decision. Be aware that this feature may cease to work at any time.

雖說要棄用,但目前這個事件在手機上還是可以正常作用

於是我又在原始碼發現了這一段:

const getOrientation = () => window.screen?.orientation?.type;

在 macOS Safari 的 console 裡輸入 window,發現 window.screen 底下沒有 orientation 這個屬性

所以使用套件,會抓到 undefined 也是很合理的

因此我推測所有使用 Webkit 的瀏覽器一樣沒有 orientation 屬性

測試了四個作業系統(Windows、macOS、iOS、Android)、三種瀏覽器(Chrome、Firefox、Safari)之後,得出以下結論:

ChromeFirefoxSafari
MacOSOOX
WindowsOONA
iOSXXX
AndroidOONA

Windows 與 Android 系統上沒有 Safari,所以就不在討論範圍內

果然不出所料,Webkit 系的瀏覽器,全部都拿不到 orientation 屬性

而 MDN 也列出 orientation 屬性對各瀏覽器的支援度,如下表:

browser compatibility

iOS 上的 Chrome、Firefox 應該也屬於 Safari on iOS 那一類

令人驚訝的是,IE11 居然有支援!

雖然行動裝置上也沒有 IE 可以測試,而 Windows 的 IE 整個打不開 CodeSandbox

所以就不實測了

至於 Safari 的結果就 ⋯⋯ 🤦‍♂️

不知道 Safari 什麼時候會支援啊 ⋯⋯

以下是測試結果中使用的裝置,其作業系統及各瀏覽器版本,有需要可以參考一下:

OSChromeFirefoxSafari
MacOSMonterey 12.3.1101.0.4951.64100.015.14
WindowsWin10 21H2100.0.4896.12787.0NA
iOS15.14.1101.0.4951.58100.115.14
Android11101.0.4951.6191.1.0NA

因此,就算 orientationchange 事件仍然可以使用

但只要有裝置上的 window.screen 物件沒有 orientation 屬性

就沒辦法用此方法來判斷裝置的方位

於是我查了一下看是否有其他替代方案,在 stack overflow 找到了一些解決方案

而用 window.matchMedia("(orientation: portrait)") 去判斷當前 window 的方位,似乎是可行的方案

並加上監聽 change 事件,即可在當 orientation 改變的時候,得到最新的方位

於是,我把它寫成一個 hook,以便之後方便使用:

import { useCallback, useEffect, useState } from "react";
const getPortraitFromMediaQuery = () => window.matchMedia("(orientation: portrait)");
function UseIsPortrait(): boolean {
const initialIsPortrait = getPortraitFromMediaQuery().matches;
const [isPortrait, setIsPortrait] = useState<boolean>(initialIsPortrait);
const updateOrientation = useCallback((event: MediaQueryListEvent) => {
console.log("event emitted!!");
if (event.matches) {
// Portrait mode
setIsPortrait(true);
} else {
// Landscape
setIsPortrait(false);
}
}, []);
useEffect(() => {
const portrait = getPortraitFromMediaQuery();
// start listening event
portrait.addEventListener("change", updateOrientation);
return () => portrait.removeEventListener("change", updateOrientation);
}, [updateOrientation]);
useEffect(() => {
console.log("isPortrait: ", isPortrait);
}, [isPortrait]);
return isPortrait;
}
export default UseIsPortrait;

若將 matchMedia 改寫成:window.matchMedia("(orientation: landscape)")

matches 屬性拿到的布林值就會是相反的,就看比較偏好用哪種方式判斷了

window.screen.orientation 確實是一個不錯的判斷方式

只可惜 WebKit 系的瀏覽器不支援,希望以後可以跟進(對 Safari 的成長速度頗為擔心就是了

window.matchMedia("(orientation: landscape)") 目前看來可以取代的方案

美中不足的是,它只能判斷「直」(portrait)與「橫」(landscape)

而無法像 orientation 屬性可以判斷主要橫向(landscape-primary)、次要橫向(landscape-secondary)、主要直向(portrait-primary)、次要橫向(portrait-secondary)四種不同的值(型別為 string

但其實基本的直、橫向判斷,就足以應付大部分的情況了

以上的程式碼全貌,可以參考以下的 CodeSandbox

可以試著在手機、平板等裝置上打開,看看不同瀏覽器的差異

最下面的有顏色的方塊是用純 CSS 的方式寫的,方便跟元件的 state 做比較

遞迴元件與巢狀列表

工作上遇到一個頁面,是要呈現一個巢狀的 list

每個 list 底下,還會有自己的屬於自己的 list

而每個 list item 都有一個 checkbox 在前面

HTML 會長得像以下結構:

<ul>
<li>
<input type="checkbox" />
<label>1</label>
<ul>
<li>
<input type="checkbox" />
<label>1-1</label>
</li>
<li>
<input type="checkbox" />
<label>1-2</label>
<ul>
<li>
<input type="checkbox" />
<label>1-2-1</label>
</li>
<li>
<input type="checkbox" />
<label>1-2-2</label>
</li>
{/* 繼續往下長.... */}
</ul>
</li>
</ul>
</li>
<li>
<input type="checkbox" />
<label>2</label>
</li>
</ul>

目前只有固定的三層,可以直接寫死三層的 list 結構

但這不是一個靈活的結構,因爲未來某一天若要添加第四層,就得在 HTML 再添加下一層的 <li>

我希望的是用 data-driven 的方式去做渲染

也就是 data 有幾層巢狀,HTML 的部分就會依據 data 長出幾層

巢狀的資料結構,我第一個想到的就是「遞迴」

某些場合我們會使用遞迴函式

函式可以遞迴,也就是在自己裡面呼叫自己,那元件是否也可行呢?

於是我查了一下關於 recursive component 的文章

發現 Vue 與 React 雖然寫法不太一樣,但都有遞迴元件的方式達成不斷巢狀渲染的目的

這裡就用 Vue 來示範

以下分三個方向來討論,分別是:

  • 資料面
  • 顯示面
  • 互動面

首先,先定義一下 data 的型別:

type Item = {
id: string;
isCheck: boolean;
children: Item[]; // <-- 這裡又是 type Item,不斷往下長⋯⋯
};
type List = Item[];

從 children 的型別可以看出,type Item 也是一個遞迴的結構

因此,我們的 data 的結構就會是 type List 的樣子:

var nestedList: List = [
{ id: "1", isCheck: true, children: [] },
{
id: "2",
children: [
{ id: "2-1", isCheck: true, children: [] },
{
id: "2-2",
isCheck: true,
children: [{ id: "2-2-1", isCheck: true, children: [] }],
},
],
isCheck: true,
},
];

我們在這裡只寫三層的 list 結構就好,三層若可以正常顯示,四層、五層、⋯⋯、n 層也不會有問題

接下來就是 HTML 的部分,我們定義一個元件,就叫做RecursiveList吧:

<template>
<div>
<ul>
<li v-bind:key="item.id" v-for="item in list">
<input type="checkbox" />
<label>{{ item.id }}</label>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "RecursiveList",
props: {
list: {
type: Array,
default: () => [],
},
},
};
</script>

這裡可以渲染第一層的 item 了,因此我們著手進行第二層以下

如果每個 <li> 有 children,就要再跑一次 v-for 去渲染

遞迴函式呼叫自己,就是爲了避免寫重複的邏輯、做重複的工作,遞迴元件也是同樣的道理

因此,我們在元件裡建立遞迴的結構:

<template>
<div>
<ul>
<li v-bind:key="item.id" v-for="item in list">
<input type="checkbox" v-bind:checked="item.isCheck" />
<label>{{ item.id }}</label>
{/* ⬇️ 這裡使用遞迴 ⬇️ */}
<RecursiveList v-bind:list="item.children" v-if="item.children" />
</li>
</ul>
</div>
</template>
<script>
// ...
</script>

從此,無論是第三層、第四層、……、第 n 層,都可以渲染出來了~

nested template

到這裡爲止,僅只是顯示方面,別忘了每一個 item 裡面還有一個 checkbox

接下來,我們要為這個 checkbox 增加互動行爲

我們做出的巢狀的 <ul><li> 結構之後,每個 item 的 checkbox 互動行爲又是如何呢?

這裡有兩個條件要符合:

  1. 當任一 checkbox 被 check/uncheck 時,該 checkbox 底下的所有 checkbox 都要被 check/uncheck
  2. 每個 checkbox 的下一層有任一 checkbox 為 check 狀態,它自身就要被 check

當 checkbox 點擊的當下,以這個 list item 的元件為主體(以下簡稱當事人),分為兩個方向:

  1. 向下的操作:改變所有子層(及子層的子層 ⋯⋯ 等) checkbox 的值
  2. 向上的檢查:確認父層是否要變動

首先,我們來做第一個條件的互動:

當事人若爲 check,底下所有子孫都要 check,也就是全選;反之,則全不選

所以我們新增一個 handleChildren 的方法

<template>
<div>
<ul>
<li v-bind:key="item.id" v-for="item in list">
<input type="checkbox" v-bind:checked="item.isCheck" v-on:change="handleChildren($event, item)" />
<label>{{ item.id }}</label>
<RecursiveList v-bind:list="item.children" v-if="item.children" />
</li>
</ul>
</div>
</template>
<script>
export default {
// ...
methods: {
handleChildren(event, item) {
const newValue = event.target.checked;
function changeAll(item, value) {
item.children.forEach((child) => {
child.isCheck = value;
changeAll(child, value);
});
}
changeAll(item, newValue);
item.isCheck = newValue; // 更新自身的值
},
},
};
</script>

handleChildren 方法裡面,changeAll 這個函式也是做遞迴呼叫

handleChildren 傳進來的 item 底下所有的 children 都掃過一遍

最後也別忘了更新自己本身的值

完成了向下的操作,接下來看向上的檢查

當事人觸發 change 事件後,若為 check,則父層不用做任何檢查

但若為 uncheck,他的父層(parent)要判定其子層,也就是當事人的平輩層(sibling)是否都沒有 check,若都沒有,就要 uncheck

這個父層若 uncheck 了,那又要執行一次這個父層的父層(aka 當事人的祖父層、grandparent)的檢查 ⋯⋯

也就是會一路往上每一層都做檢查,直到最上層

在當事人的視野裡,我們只從 props 接收到 list,也就是父層的 children 這個陣列

壓根不知道自己的父層是誰

於是,他必須通知自己的父層,也就是遞迴元件的上一層去做 children 的檢查

此時,Vue 的 $emit 就派上用場了

還記得前些年,前高雄市長韓國瑜的口號嗎?

沒錯,那就是「貨出去,人進來,高雄發大財」

來改編一下這句話:

event 出去,props 進來,前端發大財

woz-in-us-festival

利用 $emit 的特性,來通知上層:

你底下的其中一個子層 uncheck 了,請檢查自己該不該繼續 check

於是,新增一個 handleParent 方法

<template>
<div>
<ul>
<li v-bind:key="item.id" v-for="item in list">
<input
type="checkbox"
v-bind:checked="item.isCheck"
v-on:change="handleChildren($event, item), handleParent()"
/>
<label>{{ item.id }}</label>
<RecursiveList
v-bind:list="item.children"
v-if="item.children"
v-on:emit-parent="handleParent(item)"
/>
</li>
</ul>
</div>
</template>
<script>
export default {
// ...
methods: {
// ...
handleChildren() {
// ...
},
handleParent(item) {
this.$emit("emit-parent");
if (!item) return; // 沒有帶參數,表示是當事人,故不檢查
const childrenHasSomeChecked = item.children.some((item) => item.isCheck);
item.isCheck = childrenHasSomeChecked;
},
},
};
</script>

有別於一般 $emit 的寫法,這個 $emit 不帶任何 payload 出去,只是發送 emit-parent 事件

遞迴到上一層之後,emit-parent 事件觸發後執行的函式,依然是 handleParent 方法

此外,還從參數帶了父層的 item 進來,以便做 check 的檢查

至此就大功告成了


當事人父層的父層檢查、父層祖父層檢查,也都是同樣的邏輯

重複的工作,就交給遞迴去做了

emit-parent 事件會一路 $emit 上去

在最上層,也就是第一次出現 RecursiveList 的 App 元件裡面

handleEmitTop 方法去接 emit-parent 事件,並用 console.log 紀錄下來:

<template>
<div id="app">
<RecursiveList v-bind:list="nestedList" v-on:emit-parent="handleEmitTop($event)" />
</div>
</template>
<script>
import RecursiveList from "./components/RecursiveList.vue";
export default {
name: "App",
components: {
RecursiveList,
},
data() {
return {
nestedList: [
// ...
],
};
},
methods: {
handleEmitTop() {
console.log("do nothing");
},
},
};
</script>

果然會跟預想的一樣,每次的點擊,無論哪個 checkbox

每次都會一路 $emit 到最上層,然後印出 do nothing

但最上層沒有 checkbox 了,所以無需檢查,因此不用做任何事

上面的所有程式碼,請參考以下 CodeSandbox:

這次是因工作上的需要,而研究了關於遞迴的寫法

雖然這個不確定會巢狀幾層的 data 結構,會造成後端定義 model 的困擾而沒有採用

但也是一個不錯的嘗試

希望未來能有機會用到這種簡潔的寫法

Vue 因為框架的特性,直接對 data 的操作比較簡單,而無需在意是哪一層、哪一個 children

雖然過程中還挺手忙腳亂的,不知道哪一層是哪一層,然後誤打誤撞的寫出可以動的東西

it works

然而其實只需要專注在當事人身上就好了

話說回來,隨意修改 data 而不透過 props 去傳遞新的 data ,確實不是個好作法,但也確實能達到目的

若換作是 React,想必寫法會很不一樣

如果你好奇:文中那張照片、站在沙灘高舉雙手的人是誰?

他是蘋果電腦的共同創辦人:史蒂夫·沃茲尼克(Steve Wozniak)

那是 1982 年沃茲尼克所舉辦的音樂節 US Festival 現場

在他的自傳中提到自己夢想著籌辦音樂節

雖然音樂節圓滿舉辦,但不僅沒賺錢,反而虧損

可是他本人還是很開心,非常享受音樂節的每一刻

這張照片的肢體語言令我印象深刻

也充分表現出他的心情

就算沒有發大財,每天還是要過得開心

勞動節的放空提案

其實一直有勞動節的實驗計畫想要做

去年因為不小心有點小意外所以沒有做,所以今年就不能再錯過

雖然稱為計畫,也不是什麼非常不得了的事就是了

勞動節的本意是為了體恤勞工平常的辛勞,所以在這天讓身為勞工的大家可以休息一天

然而就廣義的角度而言

我們身為自己身體的 CEO,是不是也該讓自己的身體器官好好放一天假?

畢竟它們平常也沒有在週末休假啊

於是我開始有了「讓身體好好休息一整天」的初步想法

具體上來說要如何做呢?

大致的方向是這樣的

我制訂了 5/1 這天可以做的三件事:

  1. 吃飯
  2. 閱讀
  3. 睡覺

就這樣。

也是可以一整天不吃飯,但我沒嘗試過斷食一天,抱歉了腸胃,你們還是得工作

雖說是休息,總不能一整天都在發呆吧

於是我選擇了加入閱讀這一項,但僅限於紙本的書籍(而且是小說類,低耗腦力的類別)

若眼睛也要休息的話,大概只能睡覺了,抱歉了眼睛

至於睡覺 ⋯⋯ 稍微累的時候就去休息,在現代社會是多麼奢侈的一件事 🙃

基於生理構造的關係,大腦、心臟、肺大概也無法休息了,抱歉了三位

我只能在能力範圍內,執行放鬆的計畫

  • 任何 3C 產品、LCD 螢幕
  • 運動
  • 外出

因此,平時總擺在桌子中央的 Mac 就暫時收進櫃子裡了

手機作為通訊用及聽一點輕音樂,只有最低限度的使用

減少從螢幕接收的藍光,所以相關設備都先收起來

或許你會問:「為何運動不行?」

沒為什麼,我只是想說既然都要休息一天,那不如走節能路線

逛街、喝咖啡都挺耗費能量的,所以外出僅限吃飯

嗯,規則制訂完成

於是就依照上面的規則去執行了

悠哉地在早餐店吃著厚牛總匯三明治

感謝早餐店這天還上班,不然我就沒有早餐吃了

然後翻開圖書館借來的書

東野圭吾的「解憂雜貨店」

一直很想看的一本書

這本也不是什麼新書

然而一忙就忘記了

於是就過了這麼多年 ⋯⋯

雖進入了五月,但卻是一個涼爽的天氣

外面不時下著雨,雨聲也在耳邊迴響

雨聲真是療癒啊~

心裡不禁感嘆了起來

追隨著書中的文字,躍身小說的世界裡

很神奇的是,做的事情變少了,時間也有走得比較慢的錯覺

正好跟小說裡浪矢雜貨店的情節不謀而合

偶爾起身走動,回想了今日的計畫

嗯⋯⋯好像沒有什麼其他可做的事情

頓時驚覺:

習慣性在閒下來的時候找點事做,似乎成了現代人的行為模式

也就是說,時間上若有空白,就想要找個東西填進去

手機、平板、電腦、電視、網路都被去除之後,我們的生活還剩什麼呢?

我悠悠回想起那個沒有手機、電腦、網路不發達的年代,到底是怎麼生活的?

同樣的一天 24 小時,我們都做了些什麼活動?

我不禁思忖著

然而怎麼樣也回想不起來

只好再次回到小說裡

看書看累了,就爬上床休息

休息夠了就繼續看書

不知不覺,也來到了晚餐時間

心想著:吃個拉麵作為完美的 ending 吧!

窗外仍下著雨,如此奢侈的天氣

於是搭了捷運,去附近的一間拉麵店

感謝捷運的員工與司機們沒有休息,不然我就要走路了

走在路上,邊想著如果拉麵店今天公休,要吃什麼的備案

從不遠處望去,店面的落地窗透出搖曳昏黃的亮光

彷若像我招著手:「快進來坐啊~」

感謝拉麵店今天也很認真的營業,才有美味的拉麵

我不禁在心裡雙手合十

感謝款待,雖然我久久才來一次

回到家,伴隨著窗外不曾間斷的雨聲

繼續把手邊的書看完

以上就是這次的「什麼都不做勞動節計畫」

雖然字面上說是什麼都不做,但還是有做一些事就是了(笑

太過於認真的過每一天,最後變成忙得閒不下來

身心都沒有得到真正的休息

這大概已經是現代人的文明病了吧

這次的勞動節連假,因為台鐵的罷工停駛,新聞不斷在報導這件事

雖然我知道他們本來就是輪班制

但我其實有一個想法:

既然是勞動節,那就所有勞工、各行各業都休息一天如何?

雖然這種事如果發生了一定會引起大亂

可是,當我們放連假休息、出去玩的時候

如果百貨公司的員工沒有上班,我們如何逛街購物?

如果餐廳老闆不營業,我們也無法大啖美食

所以我才會對今天還在工作的人獻上由衷的感謝

此外,也再次感謝我的身體不辭辛勞的為我工作,雖然我不是對員工很好的 CEO(笑

而我又試著想像各行各業(包括便利商店)都休息一天的情況

整個台灣應該會街上空蕩蕩的,像是空城一樣吧

初探 C# 的 Array 與 List

C# 的 List 與 Array 有很多類似之處

在看 LINQ 介紹的時候,出現了兩個 LINQ 函式:ToList()ToArray()

看到這兩個詞之後我不禁好奇,List 與 Array 又有何不同呢?

C# 中的 Collections 資料型別有很多種,包含:

  • ArrayList
  • List
  • SortedList
  • Dictionary
  • Hashtable
  • Stack
  • Queue

而想討論的是 Array、List 與 ArrayList 這三種

Array 的定義如下:

An array is the data structure that stores a fixed number of literal values (elements) of the same data type. Array elements are stored contiguously in the memory.

由定義我們知道,C# 使用連續的記憶體空間存放 Array,因此在初始化的當下,就決定了這個 Array 的長度

我們可以替換 Array 裡面成員的值,但無法新增或是刪除裡面的成員

因此,若要增加陣列的長度,就要再建立一個更大長度的新陣列,然後將就原陣列的所有成員 copy 過去,再將原陣列刪除

宣告一個 Array,可以這樣寫:

int[] numbers;
string[] colors;

或在宣告的時候直接初始化:

int[] numbers = new int[3] { 1, 2, 3 };
string[] colors = new string[3] { "red", "green", "blue" };

若用 var 初始化陣列,則可以省去陣列長度,讓編譯器從成員去自行推斷陣列的長度:

var numbers = new int[] { 1, 2, 3 };
var colors = new string[] { "red", "green", "blue" };

更簡短的寫法:

int[] numbers = { 1, 2, 3 };
string[] colors = { "red", "green", "blue" };

在初始化的時候,必須指定 Array 的長度

此外,成員的數量也要與長度相同,不然就會報錯

ArrayList 的定義如下:

In C#, the ArrayList is a non-generic collection of objects whose size increases dynamically. It is the same as Array except that its size increases dynamically.

因為 ArrayList 在宣告的時候,並不用定義其內部成員的型別,意即每個成員可以是不一樣的型別

List 跟 ArrayList 都不使用連續的記憶體來儲存資料,因此它的長度是動態的,會隨著成員的數量而增減

宣告一個 ArrayList 有兩種方法:

ArrayList list = new ArrayList();
var list = new ArrayList();

可以用 Add() 方法,隨時加入新的成員至 ArrayList:

var list = new ArrayList();
list.Add(1);
list.Add("hello");
list.Add(false);
list.Add(null);

或用物件初始子(object initializer syntax)的寫法:

var list = new ArrayList()
{
1, "hello", false, null
};

由此可見,ArrayList 中的成員不一定是同型別的,這跟 JavaScript 的陣列很相似

ArrayList 提供一些方法,來操作內部的成員,剛才範例中的 Add() 就是其中之一

除此之外,還有 AddRange()Insert()Remove()Sort()IndexOf⋯⋯ 等

比較常見的屬性有:CapacityCount

Count 很好理解,也就是 ArrayList 的成員數,相當於 JavaScript 的 Array.prototype.length

Capacity 則代表這個 ArrayList 可以裝載多少成員,這其實跟記憶體的配置容量有關

List 的定義如下:

The List<T> is a collection of strongly typed objects that can be accessed by index and having methods for sorting, searching, and modifying list. It is the generic version of the ArrayList that comes under System.Collections.Generic namespace.

List 其實是泛型版本的 ArrayList,在 List 裡面的每個成員,其型別都要符合傳入的 T 泛用型別

List 來自於 System.Collections.Generic namespace,而 ArrayList 來自於 System.Collections

宣告、初始化、對成員的操作方法,基本上都跟 ArrayList 一樣

List 同 ArrayList 有 Count 這個屬性,表成員的數量

List 有些方法與 ArrayList 一樣,詳細的範例可以參考 TutorialsTeacher 網站

List 與 Array 將資料儲存至記憶體的方式不太一樣,在這篇文章分析了兩者的相異之處

以 List 而言,在建立一個 List 的時候,會先定義長度為 4 的陣列

當裡面的 item 數量大於 4 時,則再創建一個新的陣列,其長度為原本的兩倍(也就是 8),並將舊的陣列從記憶體中釋放(dereference, garbage collection 機制)

再下一次遇到 item 數量超越陣列長度的時候,就再做一次(長度增為 16)

可以從下面的範例印證:

可以看到,Capacity 確實是依照兩倍的方式增長

最後面,用 TrimExcess() 這個方法將 Capacity 多餘未使用的記憶體移除,達成 Count 跟 Capacity 相等。然後呼叫 Clear() 清空所有成員,Count 歸零,Capacity 則維持原狀

ArrayArrayListList
記憶體使用靜態且連續的記憶體使用動態且不連續的記憶體使用動態且不連續的記憶體
成員同型別資料異型別資料同型別資料
命名空間System.ArraySystem.CollectionsSystem.Collections.Generic
成員修改只能夠變動成員,無法增減可增減成員可增減成員

研究過 Array 及 List 之後,才知道最重要的差異就在於記憶體使用

然而,我在專案裡幾乎只有看到 List<T>,因為我們處理的資料大部分都是集合物件,也大量使用 LINQ 函式來處理資料

因此相較於 Array,List 的靈活度也就顯而易見了