2020年12月30日

[掘竅] Golang - 使用 slice of pointers 或 slice of structs

TL;DR;

如果這個 slice 或 slice 中的元素有可能會被操作修改,那麼就用 slice of pointers;如果這個 slice 單純只是拿來讀取,那麼就用 slice of structs;或者也可以無腦的使用 slice of pointers 以避免後續不小心而產生的 bug。

為什麼會看到 slice of pointers 的用法

在 Golang 中常會看到 slice 中存放的是 slice of pointers,使用 slice of pointers 或 slice of structs 有幾個很重要的差別。先來看看下面的程式碼。
假設我們先定義一個名為 Vertex 的 struct:
type Vertex struct {
	X int
	Y int
}
接者在建立 slice of struct Vertex:
  • 當我們把「slice 中的 struct」取出來的時候,實際上是把 slice[1] 複製一份到 point1 裡面,因此即使修改了 point1 內的 X 和 Y,在輸出 slice 時並不會看到改變
func main() {
	var slice []Vertex
	for i := 0; i < 4; i++ {
		slice = append(slice, Vertex{i * 10, i * 100})
	}

  // before change: [{X:0 Y:0} {X:10 Y:100} {X:20 Y:200} {X:30 Y:300}]
	fmt.Printf("before change: %+v \n", slice)


	point1 := slice[1] // 取出的是 slice of struct
	point1.X += 1
	point1.Y += 1

  // slice[1] 並沒有被修改: [{X:0 Y:0} {X:10 Y:100} {X:20 Y:200} {X:30 Y:300}]
	fmt.Printf("after change: %+v \n", slice)
}
  • 但若我們取出來的是「slice of pointer」,這時候才會參照回原本 slice 中的 struct,因此當我們修改 point1 內的 X 和 Y 時,會看到輸出的 slice 產生對應的改變
func main() {
	var slice []Vertex
	for i := 0; i < 4; i++ {
		slice = append(slice, Vertex{i * 10, i * 100})
	}

  // before change: [{X:0 Y:0} {X:10 Y:100} {X:20 Y:200} {X:30 Y:300}]
	fmt.Printf("before change: %+v \n", slice)

	point1 := &slice[1] // 差別在這:取出的是 slice of pointer
	point1.X += 1
	point1.Y += 1

  // slice[1] 有被修改:  [{X:0 Y:0} {X:11 Y:101} {X:20 Y:200} {X:30 Y:300}]
	fmt.Printf("after change: %+v \n", slice)
}
  • 除此之外,當 slice 因為長度改變時,會產生「記憶體重新配置(memory relocate)」,這時候的 slice 和原本一開始的 slice 已經是指稱到底層不同的 array,進而導致修改 point2 內的 X, Y 後,在最終輸出的 slice 並無法看到改變
func main() {
	var slice []Vertex
	for i := 0; i < 4; i++ {
		slice = append(slice, Vertex{i * 10, i * 100})
	}

	// before change: [{X:0 Y:0} {X:10 Y:100} {X:20 Y:200} {X:30 Y:300}]
	fmt.Printf("before change: %+v \n", slice)

	point1 := &slice[1]
	point2 := &slice[2]
	point1.X += 1
	point1.Y += 1

	// slice[1] 有被修改:[{X:0 Y:0} {X:11 Y:101} {X:20 Y:200} {X:30 Y:300}]
	fmt.Printf("after change: %+v \n", slice)

	// 改變 slice 的 len,使其超過原本的 cap,將導致 reallocate,
	// 這時候的 slice 和原本的 slice 已經指稱到不同的記憶體位置
	slice = append(slice, Vertex{4, 40})

	// 這時候對 point2 修改無法修改到原本 slice 中的 {X:2, Y:20}
	point2.X += 1
	point2.Y += 1

	// slice[2] 並沒有被修改: [{X:0 Y:0} {X:11 Y:101} {X:20 Y:200} {X:30 Y:300} {X:4 Y:40}]
	fmt.Printf("after reallocate: %+v \n", slice)
}

解決參照到不同 slice 的作法

要解決這樣的問題,有幾種不同的做法:

方法一:避免 slice 超過其 cap 導致重新 relocate 外

可以的話,在一開始定義 slice 時,就把 slice 的 length 設好,且避免 relocate:
func main() {
	// 方法一:先把 slice 的 len 設好,並避免 relocate
	slice := make([]Vertex, 8)
	for i := 0; i < 4; i++ {
		slice[i] = Vertex{i * 10, i * 100}
	}

  // ...
}

方法二:使用 slice of pointers

// 使用 2. slice of pointers
func main() {
	var slice []*Vertex  // 這裡使用 slice of pointers
	for i := 0; i < 4; i++ {
		slice = append(slice, &Vertex{i * 10, i * 100})
	}

	printAll("before change", slice) // [{X:0 Y:0} {X:10 Y:100} {X:20 Y:200} {X:30 Y:300}]
	point1 := slice[1]
	point2 := slice[2]
	point1.X += 1
	point1.Y += 1

	printAll("after change", slice) // [{X:0 Y:0} {X:11 Y:101} {X:20 Y:200} {X:30 Y:300}]

	// 改變 slice 的 len,使其超過原本的 cap,將導致 reallocate,
	// 這時候的 slice 和原本的 slice 已經指稱到不同的記憶體位置
	slice = append(slice, &Vertex{4, 40})

	point2.X += 1
	point2.Y += 1

  // 透過 slice 可以看到修改後的結果
	printAll("after reallocate", slice) // [{X:0 Y:0} {X:11 Y:101} {X:21 Y:201} {X:30 Y:300} {X:4 Y:40}]
}

func printAll(description string, slice []*Vertex) {
	sliceStruct := make([]Vertex, len(slice))
	for i, s := range slice {
		sliceStruct[i] = *s
	}
	fmt.Printf("%s: %+v \n", description, sliceStruct)
}

方法三:使用 index 而非使用 Pointer

func main() {
	var slice []Vertex
	for i := 0; i < 4; i++ {
		slice = append(slice, Vertex{i * 10, i * 100})
	}

	// before change: [{X:0 Y:0} {X:10 Y:100} {X:20 Y:200} {X:30 Y:300}]
	fmt.Printf("before change: %+v \n", slice)

	slice[1].X += 1
	slice[1].Y += 1

	// slice[1] 有被修改:[{X:0 Y:0} {X:11 Y:101} {X:20 Y:200} {X:30 Y:300}]
	fmt.Printf("after change: %+v \n", slice)

	// 改變 slice 的 len,使其超過原本的 cap,將導致 reallocate,
	// 這時候的 slice 和原本的 slice 已經指稱到不同的記憶體位置
	slice = append(slice, Vertex{4, 40})

	// 透過 index 的方式修改 slice
	slice[2].X += 1
	slice[2].Y += 1

	// slice[2] 有被修改:[{X:0 Y:0} {X:11 Y:101} {X:21 Y:201} {X:30 Y:300} {X:4 Y:40}]
	fmt.Printf("after reallocate: %+v \n", slice)
}

不一定要用 slice of pointers?

從上面的例子中可以看到,如果會需要修改到 slice 中的元素,要避免參照到錯誤的 slice 或 slice 中的元素,使用 slice of pointers 可以算是最簡單的方式,但這麼做對效能和記憶體並不見得是最好的!一般來說使用 slice of structs 的效能比 slice of pointers 好(參考:Potential dangers of storing structs rather than pointers)。
然而除了效能之外,可維護性也很重要,未來如果有需要修改元素內的資料時,是否有人會忘記把取出來的 slice 加上 & ,或者因為添加了 slice 中的元素而導致 slice relocate 而導致不必要的錯誤呢?
以我個人來說,如果這個 slice 或 slice 中的元素有可能會被操作修改,那麼就用 slice of pointers;如果這個 slice 單純只是拿來讀取,那麼就用 slice of structs。

參考

2020年12月20日

[Mobile] Android Samsung Note 10 vs. Apple iPhone mini 12 使用心得

我是一個對智慧型手機莫名有興趣的人,曾經在 hTC 打工時寫過幾篇開箱文、去年也寫過 Samsung S10+ 和 Note 10 的簡單比較,從智慧型手機出到現在,曾經用過 Micrsoft Lumia 620、LG G 系列、小米 Mix2S、小米 Mi 9、Google Pixel 3XL、Samsung S 系列,一直到現在用的 Samsung Note 10。
Samsung Note 10
雖然換過許多不同的裝置,手邊用 Mac 和 iPad,耳機使用的是 AirPods Pro,但就是從來都沒有實際用過 iPhone。從去年的 iPhone 11 到今年年初推出的 iPhone SE 2020 都吸引到我一波,然後都忍住了 XD,到了年底的 iPhone 12 mini 是壓倒駱駝的最後一根稻草,是時候來感受看看所謂蘋果生態系的時候了!
這篇文章就來分享一下當前使用 Note 10 和 iPhone 12 mini 的體驗心得。

前言

我覺得現在聊手機品牌,特別是聊到安卓(Android)和蘋果(Apple)的比較並不是一個令人太愉快的經驗,因為和他人討論這東西時,已經變成很像在聊「政治」,大家有自己很明確的立場,只要立場對了什麼都好,很難真的去考量功能本身的好壞,所以漸漸也覺得自己用得開心就好,不太想和別人實際去討論這些「功能」上的優劣,因為很多人常常也不願意理解你在說什麼,他只選擇相信自己想相信的。我想任何事物,只要到了 O 粉的程度,就已經是信仰層次,盲目是必然的現象,不論是政治或手機。

系統使用體驗

日常使用流暢性

以系統體驗來說,iPhone 的流暢性真的是非常優秀,過場動畫非常順暢,操作起來就是很開心。iPhone 總是能把每天需要用到的細節體驗優化到很好,以每天會做上 n 次的解鎖這個動作來說,整個動作一般來說就是行雲流水一般的順暢。用過 iPad 的 Touch ID,和 iPhone 的 Face ID,以我個人來說,即使在經常需要戴口罩的今天,Face ID 還是我認為非常優異的解鎖方式,雖然真的會稍不方便,但如果 Touch ID 和 FaceID 只能選一個的話,我會選 Face ID,對我來說能少一個動作實在是太重要了。
反觀 Samsung Note 10 在這一塊就顯得相當不足,雖然同時提供了指紋解鎖和臉部解鎖,但指紋解鎖的感受沒辦法達到過去實體指紋感測器那種「秒解」的爽度;臉部解鎖雖然還算堪用,但有時會發生已經臉部解鎖完成,我還傻傻在使用指紋解鎖的情形,因為臉部解鎖後,指紋的圖示並沒有消失。另外,一開始買的時候,進到解鎖畫面前,畫面會有稍微的卡頓的情況,這個部分在後續的更新已經好非常多,這些體驗上的細節雖然不影響使用,但總會覺得沒那麼流暢。

大眾運輸通勤最強 - Samsung Pay 悠遊卡

但除了解鎖之外,搭乘大眾運輸對我來說也是每天的例行事務,這時候 Samsung Note 10 的 Samsung Pay 悠遊卡發揮它極大的優勢,手機本身就是悠遊卡這點真的太強大了,甚至手機沒電時仍然可以使用,你不再需要每次到了捷運站去包包裡掏卡片。iPhone 雖然在國外也有整合交通卡的功能,但在台灣還沒辦法使用。這點也絕對不單單是 MegSafe 可以補足的。

App 使用流暢性差不多

以 App 使用流暢性來說,我覺得在 App 使用時,兩者使用的感覺並沒有差異非常大,所以如果擔心使用 App 時會有卡頓的情況,在我使用這兩隻時都不太有這樣的情況,不論是日常的 Facebook、Intagram、Mobile01 等等,兩隻都非常順暢。

單手操作

單手操作我覺得是 iPhone 的弱勢,用了 iPhone 基本上很難完全單手操作,即使我用的是 iPhone mini,偶爾還是會需要把右手大拇指橫跨過整個螢幕去點擊左上角按鍵鍵的情形。另外,雖然 iPhone 也有單手模式,但基本上就是把最上面的東西往下掉一半,方便你去點擊,點完一次後整個畫面又會彈回到全螢幕。突然理解了,為什麼 iPhone 的使用者似乎沒有很在意是否有單手操作的功能,因為不論是 iPhone 12 mini、iPhone 12、iPhone 12 Pro Max,經常性的都會需要雙手操作。
相對地,Note 10 或 Samsung 的手機雖然螢幕大了一圈,但 6.3" 的螢幕重量僅僅只有 168g,並再搭配 One Hand Operation+ 後,透過手勢就可以達到返回上一頁、鎖定螢幕、螢幕截圖、進入多工畫面等等,如果不夠用的話,和 iPhone 一樣可以透過手勢進入單手模式。Samsung 的單手模式是以等比例的方式縮小螢幕,可以自己調整縮放比例,而且不會點一次就整個彈回全螢幕,再加上 Google 鍵盤本身也有支援單手模式,因此當我在使用 Note 10 時,雖然螢幕很大,卻有更長時間是可以單手操作手機的。
Andorid 單手模式

拍照攝影

我平常蠻在意手機拍照的功能,因為我沒有相機,也不想出去玩時總是要背一台相機跑來跑去,因此對我來說,手機的拍照是重要的。這次雖然買的是 iPhone 12 mini,少了 2 倍鏡頭和 LiDAR,但光就一般拍照來說,應該還是可以簡單比較的。
我認為這兩隻手機在光線充足的情況下都能夠拍出非常優異的照片,但比較明顯的差異會發生在光線不足的情況,這個光線不足並不是指夜拍,而是一般常見的陰天、室內等環境。當光線不足的時候兩隻手機的成像則會有蠻明顯的差異。
在下面的照片中左側放的都是 Note 10(關閉 AI 辨識)、右側放的都是 iPhone 12 mini,並且都使用自動模式,沒去刻意調什麼參數。至於哪張比較「真實」、「自然」,哪張比較好看,每個人心中都有自己的評分標準,就不多說明了。

有一種真實,是 iPhone 覺得的真實

不論是 iPhone 的用戶或 Youtuber 最常自詡的就是 iPhone 的照片接近肉眼所近,最真實也最自然;而 Andorid 的手機就都是美顏、修很大、雖然好看但不自然。但真的是這樣嗎?
在夜間拍攝的時候,iPhone 預設的亮度變得非常高,第一眼會覺得非常亮眼,單若看廣告招牌上的文字就有些過曝。
iphone 12 mini vs Andriod Samsung Note 10
在光線比較不充足時,iPhone 12 mini 拍出來的照片較容易黃黃黑黑,對比度偏強的感覺:
iphone 12 mini vs Andriod Samsung Note 10
iphone 12 mini vs Andriod Samsung Note 10
iphone 12 mini vs Andriod Samsung Note 10
相對地,Samsung Note 10 則會比較偏白粉與柔和(可以留意照片中白色的部分):
iphone 12 mini vs Andriod Samsung Note 10

Samsung Note 10 光線不足時對比度不足、色彩不夠明確

這個情況有好有壞, Samsung Note 10 在光線不足(陰天或室內)則較容易因為對比不夠,而顯得顏色不那麼明確(偏白)。這時當場景本身就是暖色調時,iPhone 的成像就好很多:
iphone 12 mini vs Andriod Samsung Note 10
iphone 12 mini vs Andriod Samsung Note 10
iphone 12 mini vs Andriod Samsung Note 10
iphone 12 mini vs Andriod Samsung Note 10

iPhone 12 場景辨識功能

至於食物的部分,iPhone 12 mini 不知道是不是因為在相機的設定中多了場景辨識的緣故,我個人覺得這食物的顏色很強烈...,相較之下 Samsung Note 10 雖然第一眼看上去沒這麼討喜,但後續如果需要調整色彩應該會也比較容易:
iphone 12 mini vs Andriod Samsung Note 10
iphone 12 mini vs Andriod Samsung Note 10
除了食物之外,自然環境也同樣碰到類似的情況,iPhone 12 mini 的色彩雖然相當鮮豔,第一時間也覺得非常好看,但顏色還是相對較為飽和,對比度也較高,天空的效果也顯得更劇烈:
iphone 12 mini vs Andriod Samsung Note 10

結論

我認為這兩隻手機都是非常棒的手機,多數客觀的使用者也都承認 Apple 絕對有很多優異的功能,而三星的手機在拍照上也能有自然真實而好看的表現;往往令人感到難以理解或不舒服的是那種意識形態般的盲目擁護或盲目貶低。
以使用情境來說,在一般通勤的時候,我會想要使用 Samsung Note 10,不論是搭乘大眾運輸或在大眾運輸上滑手機,我覺得 Note 10 的方便性(悠遊卡、單手模式)和螢幕大小都更適合我。但到了公司若是中午外出去個便利商店、買個飲料等等這種會使用到行動支付的場景,iPhone 系統順暢的體驗會讓我覺得操作起來很舒適。
就我個人來說,iPhone 優異的流暢性適合給想要好好使用手機的使用者,它可能不是什麼地方都最好最強,但多數時候可以讓你在體驗上有 90 分以上的體驗,被照顧的舒舒服服;而三星或高階安卓的使用者則更適合喜歡「玩手機」的人,除了拍照的表現也相當優秀外,單手模式的客製化、主題的調整、Widget 的功能等等,有時還會有雖然兩個人都拿同一型號手機,都卻第一時間不知道怎麼使用對方手機的情況呢!
最後,附上兩隻手機拍的一些比較照片,在 Google 相簿 每張相片右上角有個 i,點擊之後即可檢視不同照片是用哪隻手機進行拍攝: