跳到內容
關於我 數位花園

筆記

4 篇文章擁有標籤:“筆記”

初探多執行緒實作模式

本篇是略讀「Multithreaded JavaScript」這本書第六章:「多執行緒實作模式」(Multithreaded Patterns),整理的一些筆記

本章節介紹了一些多執行緒常見的實作模式,有以下:

  1. 執行緒池(Thread Pool)
  2. 互斥鎖(Mutex)
  3. 環形緩衝(Ring Buffers)
  4. 演員模型(Actor Model)
  • 多執行緒應用程式很常使用的一個實作方式
  • 執行緒池是一個集合,裡面含有同質性(homogeneous)的 worker 執行緒,每個 worker 都可用來處理重負載、複雜運算的工作
  • Node.js 的 libuv 函式庫提供了執行緒池的功能,預設為四個執行緒,可以處理底層 I/O 的一些操作
  • 概念很類似分散式系統
  • 分為兩部分討論:執行緒池的大小(Pool Size)與委派策略(Dispatch Strategies)
  • 一般而言不會動態改變執行緒池的大小
  • 通常在作業系統中,處理器核心跟執行緒並沒有直接的關聯
  • 當執行緒的數量遠超過處理器核心數量時,效能反而會下降
  • 以 Node.js 的 libuv 函式庫為例,裡面包含三個執行緒,分別是:主執行緒、worker 執行緒、垃圾回收執行緒(Garbage Collection Thread)
Node.js
// browser
cores = navigator.hardwareConcurrency;
cores = require("os").cpus().length;
  • 別忘了還有主執行緒,所以是 n + 1
  • 依據不同的目的來決定執行緒的數量
    • 對於挖礦而言,99.9% 的工作在於 worker 上所執行繁重的運算,幾乎沒有 I/O,主執行緒也沒有特別的事情,因此可以開與核心數量相同的 worker 執行緒
    • 對串流影音或轉檔而言,則有大量的 CPU 運算及 I/O 操作,因此必須預留兩個核心給這兩個程序,剩下再分配給 worker 執行緒
  • 如果不確定要如何分配,那麼留一個核心給主執行緒是一個安全的作法

我們將高成本、繁重的運算收集整理起來,然後分配個 worker 執行緒去執行這些任務,這裡介紹三個常見的委派策略:

  • 按照順序指派工作,指派到最後一位,下一次就回到第一位 worker 身上
  • 可以確保每位 worker 都有事做
  • 可能造成每位 worker 的負擔不均
  • HAProxy 稱此委派策略為 roundrobin
  • 就如字面的意思,隨機選取一個 worker 來處理工作
  • 可能造成每位 worker 的負擔不均
  • 將新的任務指派給負載最低的 worker
  • 當有最低負載的 worker 有兩個的時候,隨機選取一個指派
  • HAProxy 稱此委派策略為 leastconn
  • Mutex 全名為 mutually exclusive lock.
  • 互斥鎖是一個存取共享資料的控制機制
  • 此機制確保共享資料在同一時間,只允許一項任務執行
  • 互斥鎖在有人存取共享資料的時候上鎖,並在結束後解鎖
  • 在上鎖及釋放鎖定之間,稱之為「臨界區段」(Critical Section)

串流資料與環形緩衝(Ring Buffers)

Section titled “串流資料與環形緩衝(Ring Buffers)”
  • 環形緩衝是「先進先出」(first-in-first-out, aka FIFO)佇列的典型實作,利用一組「配對指標」指向當下的記憶體位址
  • 當佇列的指標在陣列末位,下一步會移動到陣列首位,形成環形結構的概念
  • 在北美餐廳中常被使用的點餐圓盤(order wheel)則是類比世界中,同樣的概念實踐
  • head 指標:指向下一個寫入佇列的位置
  • tail 指標:指向下一個讀取佇列的位置
  • 佇列長度:我們想要建立多大的佇列,一個陣列長度,head 指標、tail 指標會在上面移動

借用一下書中的示意圖:

ring buffer

  • 當寫入佇列時,head 指標移動的下一個位置
  • 當讀取佇列時,tail 指標移動的下一個位置
  • 當指標位在佇列末位,下一次就會移到首位
  • 因為是環形,所以沒有頭尾之分,因此指標在哪個位置並不重要
  • tail 指標最多只會跟 head 指標在同一個位置,不能超過 head 指標
  • 緩衝區滿載的時候,若要再寫入佇列,則有兩種策略:
    • 覆蓋最舊的佇列:相較於未處理的舊資料,新資料比較重要
    • 拋出異常,且不寫入佇列:資料順序性很重要的時候
  • 必須正確讀出最舊佇列
  • 儲存在環形緩衝裡的每個元素不會做移動,只有加入與移除,適合實作「先進先出」(first-in-first-out, aka FIFO)佇列
  • 「非環形緩衝」若要實作 FIFO,則必須在處理完一個任務之後,移動佇列上所有的元素
  • 「非環形緩衝」較適合實作「後進先出」(last-in-first-out, aka LIFO)佇列
  • JavaScript 的堆疊(stack)就是 LIFO 的實作
  • 動態改變佇列的大小,意味著必須重新賦予記憶體,會影響效能。若要實作動態佇列,鍊表(linked list)則較為適合
  • 演員模型是實踐同步運算的一種程式設計模式
  • 一個 actor 代表一個執行程式碼的容器
  • 在 Erlang 程式語言中,actor 是一級公民,但在 JavaScript 中也可以模擬其實作
  • 每個 actor 皆具有三種功能:執行運算、建立新的 actor、actor 之間的相互傳遞訊息
  • 每個 actor 擁有自己的訊息佇列(message queue),以 FIFO 佇列順序處理每個任務
  • 因為 actor 都無法操作共享記憶體(shared memory),因此避免了多執行緒容易發生的情況:race condition 及 deadlock
  • actor 是單執行緒,一次執行一件事
  • 使用 actor 的系統必須能夠接受延遲或順序不一致的現象
  • 每個 actor 可以擁有一個位址,例如:tcp://127.0.0.1:1234/3 代表著在 1234 port 中,第三個 actor

借用一下書中的示意圖:

actor model

E2E 測試導向的開發流程

某天,我們從設計師那裡拿到了設計圖

於是我們開始切版,跟往常一樣,主要注重在畫面的樣式與互動上,此時並不會考慮到 E2E 測試

若開發期間,很幸運地拿到了 QA 的測試案例(或任何寫測試案例的人),我們是否可以在切版的當下,也將未來寫測試案例的情況也加入考慮?

此外,這裡提到的測試案例是指 E2E 測試(自動化測試),而非人工的整合測試

為了讓寫測試的過程能更順利,依據測試案例上的各種 action,例如:點擊某個 button、在 input 輸入文字,或是從某個 div 元素上獲取文字。在這些計畫做事的 DOM 元件上安插屬性(attribute)作為 querySelector 的查詢指標

若手上沒有測試案例又該如何呢?沒有測試案例,就無法準確知道哪些元素會被查詢,只能用推測的。此時腦中閃過一個想法:

如果制定一些規則,然後按照這個規則去添加屬性,是否就可以維持一定的覆蓋率?

因此我根據自己寫測試的經驗,嘗試制定了一些規則。我將會分成以下幾個部分說明:

  1. 元件取向
  2. 位置取向
  3. 狀態取向
  4. 無形資料取向
  5. 結構取向

以下所有程式,我會以 React 元件來解釋。實際上無論框架,最終都會回到 HTML 元素身上

插入 data-test-page 屬性於最外層的 HTML 元素:data-test-page='<頁面元件名稱>'

有了 data-test-page 屬性,我們可以很快地辨認出這個頁面元件的範圍(我更喜歡稱之為「邊界」)

註:屬性名稱全看個人喜好,這裡展示的是我自己覺得不錯的命名方式 🙃

例如我們有一個 home 元件:

function HomePage() {
return <div data-test-page="home">{/* ... */}</div>;
}

插入 data-test-comp 屬性於最外層的 HTML 元素:data-test-comp='<元件名稱>'

有了 data-test-comp 屬性,我們可以很快地辨認出這個基本元件的範圍

function Button() {
return <div data-test-comp="button">{/* ... */}</div>;
}

元件會在不同地方重複使用(這也是我們要抽成元件的初衷,DRY 原則),因此,data-test-comp 就會出現重複的情況,而且如果在相近的地方有多個重複的元件,就不太好快速辨識目標元件

比如說我們在一個頁面上,用了 5 個 button 元件,而測試案例的動作是:「選取其中一個 button,然後點擊它。」此時,我們必須先找出所有 button 的元素,然後在裡面尋找我們要做點擊的那個 button(比對 button 的文字;或是直接指定第幾個 button 元素 ⋯⋯ 等)

這個過程不困難,但我認為這個過程很煩,而且不斷重複在做這個查找的動作。於是我開始思考,是否有更精確、更快速的方式。如果有一個唯一值(或接近唯一的值,亦即很少重複),這個元素尋找的過程將會簡單許多

因此,像是這樣的情境,我們需要另一個表示功能的特徵1 屬性(這個特徵屬性最好是唯一值)

使用 data-test-feat 屬性,用來辨識這個元件在這裡所提供的功能(或目的)

我認為這個屬性比較偏向是非強制性的。倘若只有在元件上標示屬性,仍然可以拿到我們想要的元件。例如:在導覽列(navbar 元件)上的登入 button 及選單裡(menu 元件)的登入 button,分別包含在不同元件裡面。此外,過度使用可能會造成最後很多重複的屬性,唯一性的特徵消失了,精準打擊(aka 準確查詢)的初衷也會隨之化為泡影。因此,要在這之間取得平衡

所以,data-test-feat 屬性大概有兩種加入方式:

有時候,基於元件的樣式設計(有外層決定寬高、背景色 ⋯⋯ 等),我們會在元件的外面包一層容器元素

function HomePage() {
return (
<>
{/* ... */}
<div data-test-feat="confirmBtn">
<Button />
</div>
<div data-test-feat="cancelBtn">
<Button />
</div>
{/* ... */}
</>
);
}

上面的範例,可以很清楚地知道,在 home 頁面裡,有兩個 button:確認 button 及取消 button

如果在元件外面包一層容器元素不是你的菜,可以透過 props 的方式穿進去元件裡:

若沒有外層的容器元素,透過 props 傳進去是另一個選擇:

function HomePage() {
return (
<>
{/* ... */}
<Button dataTestFeat="confirmBtn" />
<Button dataTestFeat="cancelBtn" />
{/* ... */}
</>
);
}

在 button 元件裡:

function Button({ dataTestFeat, btnText, labelText }) {
return (
<div data-test-comp="button">
<label>
{labelText}
<button data-test-feat={dataTestFeat}>{btnText}</button>
</label>
</div>
);
}

有時候我們想知道某個元件的狀態,假如我們有一個 toggle button 元件:

toggle button

測試案例中,我們想要在點擊 toggle button 之後,確認它會確實從 on 狀態轉變成 off 的狀態

我們當然可以透過辨認 style 的變化得知它的狀態。如果是用 SASS 的話,或許可以很簡單地從 class 上辨認出(端看 class 怎麼設計了)。然而我最近比較常用的是 tailwindcss,此時就會變得不太直覺。以下是我的 toggle button 元件:

function ToggleButton() {
const [isOn, setIsOn] = useState(false);
const handleChange = () => setIsOn((prev) => !prev);
return (
<button
type="button"
className="rounded-full overflow-hidden relative w-16 h-7 shrink-0 text-white font-semibold text-sm uppercase ml-2.5 bg-neutral-500"
onClick={handleChange}
>
{/* here is what the difference by state */}
<div className={`absolute transition left-1.5 top-1/2 -translate-y-1/2 ${isOn && "translate-x-9"}`}>
<div className="rounded-full bg-white w-4 h-4 relative">
<div className="absolute right-full top-1/2 -translate-y-1/2 px-2 whitespace-nowrap">On</div>
<div className="absolute left-full top-1/2 -translate-y-1/2 px-2 whitespace-nowrap">Off</div>
</div>
</div>
</button>
);
}

這個元件裡,translate-x-9 是 on 與 off 狀態上差異的 class。我們仍然可以辨識,但會造成修改樣式時後容易抓不到元素的情況。Utility first 樣式庫的優點,在此時卻成了缺點

像這種情況,建議加入表示狀態的屬性:

function Button({ btnText, labelText }) {
const [isOn, setIsOn] = useState(false);
const handleChange = () => setIsOn((prev) => !prev);
return (
<div data-test-comp="button">
<label>
{labelText}
<button data-test-state={isOn} onClick={handleChange}>
{btnText}
</button>
</label>
</div>
);
}

另一個常見的情境是載入狀態(loading),假如有一個列表與搜尋列元件:

function SearchPage() {
// ...some state here
return (
<div data-test-page="searchPage">
<div data-test-comp="searchBar">
<input value={searchValue} data-test-feat="searchInput" />
<button onClick={handleSearch} data-test-feat="searchBtn">
Search
</button>
</div>
<div data-test-comp="list" data-test-loading={isLoading}>
{/* list data here */}
</div>
</div>
);
}

註:基於可讀性,我直接用 HTML 表示

點擊搜尋 button 之後,頁面就會開始 loading 打 API 拿資料,並在結束後,結束 loading 狀態,並更新資料至底下的列表

假如有一個 card 元件要顯示商品資訊,如在元素上存在 product id 的話,就能快速地找到目標商品。然而商品資訊卡片通常不會顯示 product id,此時就必須將這個值加到屬性當中:

function ProductCard({ productId, ...restProps }) {
return (
<div data-test-comp="productCard" data-test-product-id={productId}>
{/* product card content */}
</div>
);
}

至於命名原則,我認為只要夠語意化就行了

在一些特殊情境下,我們會需要用 div 元素去模擬其他元素

這裡舉個例子,用 div 元素去模擬 table 元素的結構。加入 data-test-el 屬性:data-test-el='<模擬的元素名稱>'

function Table() {
return (
<div data-test-el="table">
<div data-test-el="tbody">{/* ... */}</div>
</div>
);
}

或許會有人不太懂為何要這樣做,但我還是可以簡單說明一下

在開發的時候,遇到某些樣式在與 table 有關的元素上(也就是<table><thead><tbody>⋯⋯ 等),在 Safari 瀏覽器下的顯示會不如預期(Safari 的 bug ⋯⋯),因此必須用 div 元素重建整個 table 的結構,因此在加入屬性之後,可以很快地了解整個 table 的結構,藉此增加可讀性

誠如模擬元素,為了增加可讀性,我們也可以在其他地方加入屬性,可以更快地了解整個元件結構。例如一個 dialog 元件:

function Dialog() {
return (
<div data-test-comp="dialog">
<div data-test-el="dialogHeader">this is dialog header</div>
<div data-test-el="dialogBody">this is dialog body</div>
<div data-test-el="dialogFooter">this is dialog footer</div>
</div>
);
}

加入 data-test-el 屬性之後,可以有效率地一眼看出整個元件的結構(在這裡,dialog 分為 dialog header、dialog body、dialog footer 三個區塊)

開發時,加入屬性將會是一個漫長的過程。在開發的過程中,測試案例並非我們的優先考量。制定規則並遵守它,可以讓我們無腦地加入屬性,並將專注力放在實作樣式外觀、操作互動以及資料上。當開發結束後,撰寫測試案例將會更為順利,並感謝以前的自己

最後,說到底這些規則畢竟只是自己的想法,並非什麼業界標準那麼偉大的東西 😎

開發愉快

  1. Feature,我翻譯成「特徵」

用 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 做比較

初探 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 的靈活度也就顯而易見了