定義資料類型而非類別

更新於 發佈於 閱讀時間約 10 分鐘

寫上一篇文章時我意識到,類型,類別,型別這幾個詞在物件導向當道的現代變得有些模糊,常常會不小心當成是物件導向的類,但我指的其實是資料類型。在英文中,我常常這樣區分它們:物件導向的類是class,代表的是抽象的物件模型,而類型是type/data type,代表的是實際的資料結構。正如上一篇文章所說,在物件導向裡變數的類型基本上都是抽象的,或者說type在這裡也指抽象的物件模型。class跟type的區別很重要,例如rust的trait system來自Haskell的type class,wiki把它翻成類型類,這聽起來有夠奇怪,但如果帶入上面的解釋就能說得通:它是描述資料結構具有哪些抽象特性的模型。類型本身不應該用來描述抽象的概念,這應該是trait/interface的職責,而類型則應該關注於資料的組成。本篇文章將討論資料類型的概念。


資料類型描述的是資料的組成結構,例如範圍Range是由兩個浮點數組成的資料,在c++裡面可以定義成struct Range { float min; float max; }。在物件導向裡,min跟max就是類別的成員變數。但有些物件導向程式語言可以使用property getter/setter的方式定義成員變數,這種定義使得我們連存取成員變數都不能相信,例如ran.min = 1.0可能會呼叫其他方法並改寫其他屬性。這種方法的好處是可以為存取提供額外的功能,例如驗證合法性,或是為可推導出來的屬性提供直覺的存取方法,例如提供ran.width的存取方法。然而定義資料類型時不應該為了方便使用這種打破界線的方法,長遠來看這樣會使得語義變得更複雜。


除去這種語法後,資料類型的屬性就代表了真實的資料,若不考慮他們之間的限制,事實上就只是兩個湊在一起的獨立變數而已。與獨立變數不同的是,我們定義資料類型一般是為了把具有關聯意義的獨立變數組合起來,例如Range的min跟max代表的是範圍的上下界,並不是任意的兩個數字。有些程式語言有tuple類型,它能提供相同的功能,但仍然建議使用自己定義的類型,因為它能賦予資料意義。甚至當成員變數只有一個時也應該定義獨立的類型,例如弳度struct Radian { double value; }。類型的定義不只是把結構定義出來,還可以賦予資料意義,並與其他相同結構的類型做出區別。原本常見的做法都是使用已有的類型並在變數上註記它代表的意義,而使用自定義類型時我們可以把說明和相關的方法加註在這個類型的定義上。然而它的缺點是程式碼的重用性降低,如果我們自己定義了Quaternion而非使用已有的Vector4類型,那麼我們就不能直接利用Vector4相關的函式。


對於物件導向的程式語言,物件是藉由建構子建構的。建構子可以用來檢查合法性,例如前面定義的Range必須符合min<=max的約束,這可以在建構子裡進行檢查。然而一般建構子不能回傳任何值(或者說只能回傳建構的物件),若建構失敗就只能拋出例外,因此不應該在建構子裡使用太複雜的操作,例如開啟連線或是進行大型運算。建構子基本上就只是在物件一開始執行的函式,因此若是要處理更複雜的建構狀態,可以直接改用靜態方法實作建構方法。如果不管成員變數之間的約束,建構子就只是將所有參數設定給所有成員變數而已,對於rust就是如此。rust定義的struct自帶建構子與特殊的建構語法,若要檢查合法性或是處理更複雜的情況,只要另外實作關聯方法並透過封裝限制建構子的存取就行了。事實上,在函數式程式語言裡定義建構子就等價於定義資料類型(GADT),它們只是在解釋上有所差異而已。


再除去資料代表的意義後,struct定義的就真的只是資料結構而已了。不管是struct Range { double min; double max; } 或是 struct Vector2 { double x; double y; } 還是tuple<double, double>都是相同的,它們都只是兩個獨立的浮點數而已。這種由兩個類型組成的類型稱做product type,這個名字代表他有類似乘法的代數結構。例如u8有256個可能的值, 而tuple<u8,u8>有256*256個可能的值;tuple<u8,u8>可以看作是運算式u8*u8。乘法類型就只是多個獨立的變數,相反地,只要有多個獨立變數同時存在都可以組成乘法類型,例如函式的參數列表在概念上就是一種乘法類型,而函式在做的就是對這個資料結構作轉換與處理,也就是一種演算法。


前面提到開發者應該定義自己的類型,其中列舉就是已被廣泛使用的類型。列舉實際上就只是整數類型而已,通常用於描述不同的狀況。例如,比起回傳錯誤代碼,回傳列舉可以更明白地表達錯誤類型。但仍然有許多函式是藉由某些特殊值解釋狀況,例如compare回傳整數值並可根據其正負號判斷順序,但數值大小本身沒有意義;findIndex函式一般都以回傳-1代表找不到;如果一個函式回傳的是指標,當他回傳空指標時一般代表某種失敗。但如果有多種失敗狀態,在c++常常改用傳入的參考回傳結果,並以回傳值傳回執行狀態。在golang則是使用comma-ok pattern,以最後一個回傳值作為例外,如果它是nil就代表成功。這些方法雖然有用但並不好用。空指標一般會被當作正常的物件,編譯器並不會阻止你使用類別底下的方法,因此常常需要在使用前做null check;透過參考傳回的值的意義根據函式回傳的狀態有所不同,常常需要仔細看文檔才能理解要怎麼用。而代表正確的錯誤代碼往往是0,如果把它當作boolean就會出錯。golang的comma-ok雖然好用,但仍然避免不了誤用未指定回傳值的問題。


會有這些問題是因為缺少一個能同時表達狀態和資料的結構,不合法的狀態仍然能被表達出來,使得我們必須花費額外的專注力從上下文判斷是否合法。如果有辦法讓函式回傳錯誤代碼的同時,自動使計算結果變成不可取得,就能在編譯時期檢查出問題所在。在rust裡可以為列舉綁定變數:enum Result { Ok { value: f32; }, Error { message: String; } },藉此同時回傳狀態與資料,因此在回傳失敗訊息時是不可能取得計算結果的。這種透過列舉控制資料類型的方式稱作sum type。就像是列舉,這種類型的基本用法就是透過switch跳轉到不同的分支執行不同的程序,但它還會另外送你相關的資料。更關鍵的是這些資料只能透過這種方法取得,使得我們不可能搞混狀態與資料的關係。這種類型強調「狀態」不應該只是一個列舉,還應該包含相關的資料。狀態的差異會使程式進入不同的流程,每個分支再根據取得的相關資料做出不同的行為。因此可以說狀態是流程控制的開關(switch)。


sum type有時又稱作tagged union。c語言的union是將多個類型聯合起來的類型,它底下的變數可以是其中一個類型,但沒辦法知道實際是哪個。指標可以看作是空指標與非空指標的union,空指標什麼都不能做,因此當你試圖存取指標的內容時,如果它是空指標就會出錯。而tagged union則帶有標記說明實際類型,因此可以在執行時期確定變數的實際類型。這有點像抽象類別的繼承,父類別是所有子類別的聯集。我們也不知道變數的實際類型,但可透過 isinstanceof 等方法判斷。然而不同的是tagged union只允許有限且定義時包含的類型,但繼承基本上可以延伸出任意多的類別。物件導向常常建議開發者使用多型的方式達到行為的差異化,甚至建議比起使用if/switch不如使用多型(clean code principle)。例如要計算各種形狀的面積時,應該把函式實作成抽象方法,而非利用switch去判斷形狀再個別計算面積。他們的差別在於多型多了一個抽象的階層,當你看到程式碼裡使用了抽象方法getArea(),並不能馬上知道他是做什麼的(額⋯在更複雜的例子應該是這樣的),如果裡面有bug也沒辦法馬上知道。藉由switch的方法則會暴露實作細節,因此若是過程過於複雜,那麼就會使程式難以閱讀。然而我們應該基於抽象化與否區分何時使用多型何時使用判斷式,並不是像clean code所說應盡量使用多型,過多的抽象化也會消耗開發者的專注力。還有基於多型的流程控制一般只允許做特定任務,而基於判斷式則能完成任意操作。tagged union的switch statement其實就是一種特殊的函式,可以用它實作出任何物件導向的多型,當然前提是只能有特定的類別繼承自它。sum type沒辦法達到如物件導向的非特定子類別的多型,但就如前一篇文章所說,這種情況應該使用trait,因為這種情況必須使用抽象化。


跟product type一樣,除去定義類型時所賦予的意義,它代表的是類似於列舉的資料結構。它被稱作sum type是因為具有加法的代數結構。例如在rust裡有一種加法類型Result<A,B>,它就像是A+B。例如u8有256個可能的值, 而Result<u8,u8>有256+256個可能的值。加法類型就是多個不能同時存在的獨立變數,例如多個分支計算出來的結果就算類型不一樣,也可以藉由加法類型合併成同一個類型。如果同樣以演算法的角度來看加法類型,它本質上和流程控制是相對應的,加法類型的結構轉換就是流程控制上的跳轉。product type和sum type被命名成如此自然是因為它們之間的代數關係,例如Tuple<A,Result<B,C>>可以被轉換成Result<Tuple<A,B>,Tuple<A,C>>,就像是A*(B+C) = A*B+A*C。從流程控制上來理解就是:類型為A的變數獨立於兩個分支,其中兩個分支裡分別定義了B類型的變數和C類型的變數;這個狀況和兩個分支分別定義Tuple<A,B>的變數和Tuple<A,C>的變數一樣,因為在上一個狀況中A的變數可以同時被兩個分支存取。在理解了乘法類型與加法類型後,我們可以說:資料結構本質上就是演算法的橫切面,演算法就是資料結構的轉換。在這個意義上加法類型與乘法類型是同等重要的存在,然而直到現在加法類型仍然並非主流。在缺少語言級別的支援下,很難利用加法類型所提供的優勢,因此若是人們不去嘗試擁有加法類型的程式語言,是很難理解其必要性。

留言
avatar-img
留言分享你的想法!
avatar-img
have bear的沙龍
4會員
28內容數
這不是教你如何從物件導向到函數式編程的入門教程。我會深入探討物件導向與函數式編程的差異,並討論為什麼你應該使用函數式編程並徹底放棄物件導向。
have bear的沙龍的其他內容
2024/02/15
這個系列的文章主要專注於物件導向到函數式編程的差異與分析,並針對概念與機制上的不同進行比較。很多人說物件導向和函數式編程沒有哪個比較好的問題,只有哪個比較適合的問題,然而我並不這麼認為,我透過這一系列的文章從各個角度討論它們之間的優缺點就是為了闡述我的觀點。物件導向錯在沒有理論基礎,但它贏在熟悉性,
2024/02/15
這個系列的文章主要專注於物件導向到函數式編程的差異與分析,並針對概念與機制上的不同進行比較。很多人說物件導向和函數式編程沒有哪個比較好的問題,只有哪個比較適合的問題,然而我並不這麼認為,我透過這一系列的文章從各個角度討論它們之間的優缺點就是為了闡述我的觀點。物件導向錯在沒有理論基礎,但它贏在熟悉性,
2024/02/08
前一篇文章中所提的函數式的三個機制明確說明了它關注的規則與能力具體是什麼。然而這套對於函數式編程的定義主要基於特定的類型系統,作為一個編程範式來說過於狹隘(物件導向的定義也是這樣)。更廣義的,我認為函數式編程主要依循三個原則,它們可以應用於任何程式語言,就算沒有靜態類型系統的支援也可以。例如在不管類
2024/02/08
前一篇文章中所提的函數式的三個機制明確說明了它關注的規則與能力具體是什麼。然而這套對於函數式編程的定義主要基於特定的類型系統,作為一個編程範式來說過於狹隘(物件導向的定義也是這樣)。更廣義的,我認為函數式編程主要依循三個原則,它們可以應用於任何程式語言,就算沒有靜態類型系統的支援也可以。例如在不管類
2024/02/04
前面談了那麽多函數式編程與物件導向的差異,但我們還沒定義函數式編程。就像物件導向,函數式編程沒有明確的定義,每個人對於什麼是函數式編程都有不同的看法。在這裡我會總結前面的討論,給出我對於函數式編程的觀點。 物件導向注重封裝與延展性,因此一般基於三個機制:繼承、多型、封裝,它們代表了物件導向所重
2024/02/04
前面談了那麽多函數式編程與物件導向的差異,但我們還沒定義函數式編程。就像物件導向,函數式編程沒有明確的定義,每個人對於什麼是函數式編程都有不同的看法。在這裡我會總結前面的討論,給出我對於函數式編程的觀點。 物件導向注重封裝與延展性,因此一般基於三個機制:繼承、多型、封裝,它們代表了物件導向所重
看更多
你可能也想看
Thumbnail
孩子寫功課時瞇眼?小心近視!這款喜光全光譜TIONE⁺光健康智慧檯燈,獲眼科院長推薦,網路好評不斷!全光譜LED、180cm大照明範圍、5段亮度及色溫調整、350度萬向旋轉,讓孩子學習更舒適、保護眼睛!
Thumbnail
孩子寫功課時瞇眼?小心近視!這款喜光全光譜TIONE⁺光健康智慧檯燈,獲眼科院長推薦,網路好評不斷!全光譜LED、180cm大照明範圍、5段亮度及色溫調整、350度萬向旋轉,讓孩子學習更舒適、保護眼睛!
Thumbnail
創作者營運專員/經理(Operations Specialist/Manager)將負責對平台成長及收入至關重要的 Partnership 夥伴創作者開發及營運。你將發揮對知識與內容變現、影響力變現的精準判斷力,找到你心中的潛力新星或有聲量的中大型創作者加入 vocus。
Thumbnail
創作者營運專員/經理(Operations Specialist/Manager)將負責對平台成長及收入至關重要的 Partnership 夥伴創作者開發及營運。你將發揮對知識與內容變現、影響力變現的精準判斷力,找到你心中的潛力新星或有聲量的中大型創作者加入 vocus。
Thumbnail
第三章資料型態與函式重點整理,涵蓋純量型別、整數型別、浮點數型別、字元型別、布林值型別、組合型別 (Tuple、Array)、函式定義、型別提示、流程控制 (分支判斷、迴圈),並與其他程式語言如 Java, JavaScript, Python, TypeScript作比較。
Thumbnail
第三章資料型態與函式重點整理,涵蓋純量型別、整數型別、浮點數型別、字元型別、布林值型別、組合型別 (Tuple、Array)、函式定義、型別提示、流程控制 (分支判斷、迴圈),並與其他程式語言如 Java, JavaScript, Python, TypeScript作比較。
Thumbnail
本文介紹 Kotlin 中類別的定義方法與實際應用,以及類別的好處和優點。透過實例說明,讓讀者更了解如何在 Kotlin 中使用類別來實現相關功能。
Thumbnail
本文介紹 Kotlin 中類別的定義方法與實際應用,以及類別的好處和優點。透過實例說明,讓讀者更了解如何在 Kotlin 中使用類別來實現相關功能。
Thumbnail
【Kotlin 入門指南】系列文章目錄:https://bit.ly/3t8awwL Kotlin 線上課程教學影片:https://bit.ly/3qJ5a5Q 整數與浮點數 在 Kotlin 中,整數和浮點數可以進行基本的數學運算,如:加、減、乘和除法等。 資料類型宣告方式 方法一
Thumbnail
【Kotlin 入門指南】系列文章目錄:https://bit.ly/3t8awwL Kotlin 線上課程教學影片:https://bit.ly/3qJ5a5Q 整數與浮點數 在 Kotlin 中,整數和浮點數可以進行基本的數學運算,如:加、減、乘和除法等。 資料類型宣告方式 方法一
Thumbnail
資料型態-變數概念 上面這張圖片傳傳達了三個概念, 常值:可以是數值、浮點數、字串、布林等資料, 變數名稱:這邊也很好理解,就是好記得名稱,這邊使用中文是方便初學者入門, 盒子:代表在Python底層運作的狀況,Python創建變數時,會先在記憶體創建型態物件,這邊是數字型態,所以創建數字物件。
Thumbnail
資料型態-變數概念 上面這張圖片傳傳達了三個概念, 常值:可以是數值、浮點數、字串、布林等資料, 變數名稱:這邊也很好理解,就是好記得名稱,這邊使用中文是方便初學者入門, 盒子:代表在Python底層運作的狀況,Python創建變數時,會先在記憶體創建型態物件,這邊是數字型態,所以創建數字物件。
Thumbnail
👨‍💻簡介 Go 語言有各種資料型別,分為基本型別和複合型別。基本型別包括: 整數、浮點數、布林值、字串 複合型別包括: 陣列、片段、結構、函式、對映、通道、介面 等。 整數型別 整數型別有許多種,像是 int8、int16、int32、int64。我們可以依據實際需求選擇。
Thumbnail
👨‍💻簡介 Go 語言有各種資料型別,分為基本型別和複合型別。基本型別包括: 整數、浮點數、布林值、字串 複合型別包括: 陣列、片段、結構、函式、對映、通道、介面 等。 整數型別 整數型別有許多種,像是 int8、int16、int32、int64。我們可以依據實際需求選擇。
Thumbnail
這次分享的是常數、變數、宣告與初始化。 [常數]就是固定不變的數,如:PI=3.14 [變數]顧名思義就是會改變的數,如:y=2x (在數學中x確定後y才會確定,因此x為自變數,y為應變數,x、y都屬於變數) 一、常數   常數在定義的時候,一開始就必須指定好資料型別並且給予值,因為它在整個程式在執
Thumbnail
這次分享的是常數、變數、宣告與初始化。 [常數]就是固定不變的數,如:PI=3.14 [變數]顧名思義就是會改變的數,如:y=2x (在數學中x確定後y才會確定,因此x為自變數,y為應變數,x、y都屬於變數) 一、常數   常數在定義的時候,一開始就必須指定好資料型別並且給予值,因為它在整個程式在執
Thumbnail
這篇文章將會介紹函式(Function)及其回傳值(retrun)的定義及介紹。
Thumbnail
這篇文章將會介紹函式(Function)及其回傳值(retrun)的定義及介紹。
Thumbnail
變數(variable)、型別(type)、初始化(initialize)、宣告
Thumbnail
變數(variable)、型別(type)、初始化(initialize)、宣告
Thumbnail
接續上次的士兵類別,提到名字用了 String 變數。 String 是字串的意思,在 Kotlin 裡,常見變數可以分成幾個基本資料型別:數字(Number)、字串(String)、布林(Boolean)。 差別在於行為模式不同,以加法為例,數字執行數學課上的四則運算的加法,字串卻做了連接,布林則
Thumbnail
接續上次的士兵類別,提到名字用了 String 變數。 String 是字串的意思,在 Kotlin 裡,常見變數可以分成幾個基本資料型別:數字(Number)、字串(String)、布林(Boolean)。 差別在於行為模式不同,以加法為例,數字執行數學課上的四則運算的加法,字串卻做了連接,布林則
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News