go 學習筆記之go是否是面嚮對象語言是否支持面對對象編程?

  面向對象編程風格深受廣大開發者喜歡,尤爲是以 C++, Java 爲典型表明的編程語言大行其道,十分流行!
  
  有意思的是這兩中語言幾乎毫無心外都來源於 C 語言,卻不一樣於 C 的面向過程編程,這種面向對象的編程風格給開發者帶來了極大的便利性,解放了勞動,鬆耦合,高內聚也成爲設計的標準,從而讓咱們可以更加愉快地複製粘貼,作代碼的搬運工,不少第三方工具開箱即用,語義明確,職責清晰,這都是面向對象編程的好處!
  
  Go 語言也是來源於 C 語言,不知道你是否也會好奇 Go 語言是否支持面向對象這種編程風格呢?
  
  準確的說,Go 既支持面向對象編程又不是面嚮對象語言!
  
  是也不是,難道像是薛定諤的貓同樣具備不肯定性?
  
  其實這個答案是官方的回答,並非我我的憑空杜撰而來的,如需瞭解詳情可參考 Is Go an object-oriented language?
  
  go-oop-about-schrodinger-cat.png
  
  爲何這麼說呢?
  
  Go 支持封裝,卻不支持繼承和多態,因此嚴格按照面向對象規範來講, Go 語言不是面向對象的編程語言.
  
  可是,Go 提供的接口是一種很是簡單上手且更加通用的方式,雖然和其餘主流的編程語言表現形式上略有不一樣,甚至不能實現多態,但 Go 的接口不只僅適用於結構體,也能夠適用於任何數據類型,這無疑是很靈活的!
  
  爭議性比較大的當屬繼承,因爲沒有任何關鍵字支持繼承特性,所以是找不到繼承的痕跡.雖然的確存在着某些方式能夠將類型嵌入到其餘類型中以實現子類化,但那卻不是真正的繼承.
  
  因此說,Go 既支持面向對象的編程風格又不徹底是面向對象的編程語言.
  
  若是換個角度看問題的話,正是因爲沒有繼承特性使得Go 相對於面向對象編程語言更加輕量化,不妨想想繼承的特性,子類和父類的關係,單繼承仍是多繼承,訪問控制權限等問題吧!
  
  go-oop-about-go-lovely.png
  
  若是按照面向對象的編程規範,實現封裝特性的那部分應該是類和對象,但這種概念與實現語言的關鍵字class 是密不可分的,然而 Go 並無 class 關鍵字而是 C 語言家族的 struct 關鍵字,因此叫作類或對象也不是十分貼切,因此下面的講解過程仍是採用結構體吧!
  
  如何定義結構體
  
  stuct 關鍵字聲明結構體,屬性之間回車換行.
  
  好比下面示例中定義了動態數組結構體,接下來的示例中都會以動態數組結構體做爲演示對象.
  
  type MyDynamicArray struct {
  
  ptr *[]int
  
  len int
  
  cap int
  
  }
  
  Go 語言中定義對象的多屬性時使用直接換行方式而不是分號來分隔?爲何和其餘主流的編程語言不呢?
  
  對於習慣分號結尾的開發者可能一時並不習慣 Go 的這種語法,因而決定探索一下 Go 的編程規範!
  
  go-oop-about-redundant-semicolon.png
  
  若是手動添加分號的話,編輯器則會提示分號重複,因此猜測是多是Go編譯器已經自動添加了分號,並將分號做爲語句聲明的分隔符,手動添加分號後,Go 無論不顧仍是添加了分號,因而就有了上述的報錯.
  
  這樣作有什麼好處呢?
  
  本身添加分號和編譯器無條件添加分號結果不都是同樣的嗎,更況且其餘主流的編程語言都是手動添加分號的啊!
  
  存在多個屬性時直接換行而不是添加分號做爲分隔符,對於從未接觸過編程語言的小白來講,可能會省事兒,可是對於已有編程經驗的開發者來講,卻須要特別記住不能添加分號,這一點確實有些鬧騰!
  
  若是多個屬性所有寫在一行時,沒有換行符我看你還怎麼區分,此時用逗號分隔仍是用分號分隔呢?
  
  go-oop-about-semicolon-or-new-line.png
  
  首先空格確定是不能分隔多個屬性的,所以嘗試分號或者逗號是否能夠.
  
  根據提示說須要分號或者新的換行符,而換行符是標準形式,因此接下來試一下分號能不能分隔?
  
  go-oop-about-semicolon-in-one-line.png
  
  編輯器此時沒有報錯或警告信息,所以在一行上多個屬性之間應該用分號分隔,也就是說 Go 編譯器識別多個屬性仍然是同其餘主流的編程語言同樣,使用分號分隔,而開發者卻不能用!
  
  go-oop-about-semicolon-fire-by-official.jpeg
  
  相似於上述的規則記憶很簡單,驗證也比較容易,難點在於理解爲何?
  
  Go 爲何會這麼設計?或者說如何理解這種設計思路所表明的語義?
  
  Go 做爲一門新的編程語言,不只體如今具體的語法差別上,更重要的是編程思想的特殊性.
  
  正如面向對象中的接口概念同樣,設計者只須要定義抽象的行爲並不用關心行爲的具體實現.
  
  若是咱們也採用這種思路來理解不一樣的編程語言,那麼就能透過現象看本質了,不然真的很容易陷入語法細節上,進而可能忽略了背後的核心思想.
  
  其實關於結構體的多屬性分隔符問題上,實際上不論採用什麼做爲分隔符都行,哪怕就是一個逗號,句號都行,只要能讓編譯器識別到這是不一樣的屬性就行.
  
  因爲大多數主流的編程語言通常採用分號做爲分隔符,開發者須要手動編寫分隔號以供編譯器識別,而 Go 語言卻不這麼認爲,算了吧,直接換行,我同樣能夠識別出來(儘管底層 Go 編譯器進行編譯時仍然是採用分號表示換行的)!
  
  go-oop-about-semicolon-ninja.png
  
  添加或者不添加分號,對於開發者而言,僅僅是一種分隔多個屬性的標誌而已,若是能不添加就能實現,那爲何還要添加呢?
  
  go-oop-about-semicolon-ok.png
  
  是什麼,爲何和怎麼樣是三個基本問題,若是是簡單學習瞭解的話,學會是什麼和怎麼樣就已經足夠了,可是這樣一來學着學着不免會陷入各自爲政的場面,也就是說各個編程語言之間沒有任何關係,每一種語言都是獨立存在的?!
  
  世界語言千千萬,編程語言也很多,學了新語言卻沒有利用舊語言,那學習新語言時和純小白有何差別?
  
  學到是學會了,惋惜卻對舊語言沒什麼幫助並無加深舊語言的理解,只是單純的學習一種全新的語言罷了.
  
  語言是演變創造出來的,不是空中樓閣,是創建在已有體系下逐漸發展演變而來,任何新語言都能或多或少找到舊語言的影子.
  
  因此何不嘗試一下,弄清楚新語言設計的初衷和以及設計時所面臨的問題,而後再看該語言是如何解決問題的,解決的過程稱之爲實現細節,我想這種方式應該是一種比較好的學習方式吧!
  
  雖然沒法身處語言設計時環境,也不必定明白語言設計時所面臨的挑戰,但先問嘗試着問一下爲何,不這麼設計行不行諸如此類的問題,應該是一種不錯的開端.
  
  因此接下來的文章都會採用語義性分析的角度,嘗試理解 Go語言背後的設計初衷,同時以大量的輔助性的測試驗證猜測,再也不是簡單的知識羅列整理過程,固然必要的知識概括仍是很重要的,這一點天然也不會放棄.
  
  go-oop-about-go-cheer.png
  
  如今動態數組已經定義完畢,也就是做爲設計者的工做暫時告一段落,那做爲使用者,如何使用咱們的動態數組呢?
  
  按照面向對象的說法,由類創造出對象的過程叫作實例化,然而咱們已經知道 Go 並非徹底的面嚮對象語言,所以爲了儘量避免用面向對象的專業術語去稱呼 Go 的實現細節,咱們暫時能夠將其理解爲結構體類型和結構體變量的關係,之後隨着學習的深刻,可能會對這部分有更加深入的認識.
  
  func TestMyDynamicArray(t *testing.T){
  
  var arr MyDynamicArray
  
  // {<nil> 0 0}
  
  t.Log(arr)
  
  }
  
  上述寫法並無特殊強調過,徹底是用前幾篇文章中已經介紹過的語法規則實現的,var arr MyDynamicArray 表示聲明類型爲 MyDynamicArray 的變量 arr ,此時直接打印該變量的值,獲得的是 {<nil> 0 0}.
  
  後兩個值都是 0,天然很好理解,由於在講解 Go 語言中的變量時咱們就已經介紹過,Go 的變量類型默認初始化都有相應的零值,int 類型的 len cap 屬性天然就是 0,而 ptr *[]int 是指向數組的指針,因此是 nil.
  
  等等,有點不對勁,這裏有個設計錯誤,明明叫作動態數組結果內部倒是切片,這算怎麼回事?
  
  先修正這個錯誤再說,因而可知,一時粗心影響多麼惡劣以致於語義都變了,容我先改正過來!
  
  go-oop-about-myDynamicArray-array-size-with-cap.png
  
  咱們知道要使用數組必須指定數組的初始化長度,第一感受是使用 cap 表示的容量來初始化 *[cap]int 數組,然而並不能夠,編輯器提示說必須使用整型數字.
  
  雖然 cap 是 int 類型的變量,但內部數組 [cap]int 並不能識別這種方式,多是由於這兩個變量時一塊聲明的,cap 和 [cap]int 都是變量,沒法分配.
  
  那若是指定初始化長度應該指定多少呢,若是是 0 的話,語義上正確但和實際使用狀況不符合,由於這樣一來內部數組根據就沒辦法插入了!
  
  go-oop-about-myDynamicArray-array-size-with-zero.png
  
  因此數組的初始化長度不能爲零,這樣解決了沒法操做數組的問題,但語義上又不正確了,所以這種狀況下須要維護兩個變量 len 和 cap 的值來確保語義和邏輯正確,其中 len 表示真正的數組個數,cap 表示內部數組實際分配的長度,因爲這兩個變量相當重要,不該該被調用者隨意修改,最多隻能查看變量的值,因此必須提供一種機制保護變量的值.
  
  接下來,咱們嘗試用函數封裝的思路來完成這種需求,代碼實現以下:
  
  type MyDynamicArray struct {
  
  ptr *[10]int
  
  len int
  
  cap int
  
  }
  
  func TestMyDynamicArray(t *testing.T){
  
  var myDynamicArray MyDynamicArray
  
  t.Log(myDynamicArray)
  
  myDynamicArray.len = 0
  
  myDynamicArray.cap = 10
  
  var arr [10]int
  
  myDynamicArray.ptr = &arr
  
  t.Log(myDynamicArray)
  
  t.Log(*myDynamicArray.ptr)
  
  }
  
  var myDynamicArray MyDynamicArray 聲明結構體變量後並設置告終構體的基本屬性,而後操做了內部數組,實現了數組的訪問修改.
  
  go-oop-about-myDynamicArray-array-size-with-init.png
  
  然而,咱們犯了一個典型的錯誤,調用者不該該關注實現細節,這不是一個封裝該乾的事!
  
  具體實現細節應該由設計者完成,將有關數據封裝成一個總體對外提供相應的接口,這樣調用者才能安全方便地調用.
  
  第一步,先將與內部數組相關的兩個變量進行封裝,對外僅提供訪問接口不提供設置接口,防止調用者隨意修改.
  
  很顯然這部分應該是函數來實現,因而乎有了下面的改造過程.
  
  go-oop-about-myDynamicArray-method-inner.png
  
  很遺憾,編輯器直接報錯: 必須是類型名稱或是指向類型名稱的指針.
  
  函數不能夠放在結構體內,這一點卻是像極了 C 家族,可是 Java 這種衍生家族會以爲難以想象,無論怎麼說,這意味着結構體只能定義結構而不能定義行爲!
  
  那咱們就把函數移動到結構體外部吧,但是咱們定義的函數名叫作 len,而系統也有 len 函數,此時可否正常運行呢?讓咱們拭目以待,眼見爲實.
  
  go-oop-about-myDynamicArray-method-len.png
  
  除了函數自己報錯外,函數內部的 len 也報錯了,是由於此時的函數和結構體還沒有創建起任何聯繫,怎麼可能訪問到 len 屬性呢,不報錯纔怪呢!
  
  解決這個問題很簡單,直接將結構體的指針傳遞給 len 函數不就行了,這樣一來函數內部就能夠訪問到結構體的屬性了.
  
  go-oop-about-myDynamicArray-method-len-with-args.png
  
  從設計的角度上來說,確實解決了函數定義的問題,可是使用者調用函數時的使用方法看起來和麪向對象的寫法有些不同.
  
  func TestMyDynamicArray(t *testing.T) {
  
  var myDynamicArray MyDynamicArray
  
  t.Log(myDynamicArray)
  
  myDynamicArray.len = 0
  
  myDynamicArray.cap = 10
  
  var arr [www.jintianxuesha.com ]int
  
  myDynamicArray.ptr = &arr
  
  t.Log(myDynamicArray)
  
  t.Log(*myDynamicArray.ptr)
  
  (*myDynamicArray.ptr)[0] = 1
  
  t.Log(*myDynamicArray.ptr)
  
  t.Log(len(&myDynamicArray))
  
  }
  
  面向對象的方法中通常都是經過點操做符 . 訪問屬性或方法的,而咱們實現的屬性訪問是 . 但方法倒是典型的函數調用形式?這看起來明顯不像是方法嘛!
  
  爲了讓普通函數看起來像是面向對象中的方法,Go 作了下面的改變,經過將當前結構體的變量聲明移動到函數名前面,從而實現相似於面嚮對象語言中的 this 或 self 的效果.
  
  func len(myArr *MyDynamicArray) int {
  
  return myArr.len
  
  }
  
  go-oop-about-myDynamicArray-method-len-ahead-name.png
  
  此時方法名和參數返回值又報錯了,根據提示說函數名和字段名不能相同?
  
  真的又是一件神奇的事情,難不成 Go 沒法區分函數和字段?這就不得而知了.
  
  那咱們只好修改函數名,改爲面向對象中喜聞樂見的方法命名規則,以下:
  
  func (myArr *MyDynamicArray) GetLen() int {
  
  return myArr.len
  
  }
  
  簡單說一下 Go 的訪問性規則,大寫字母開頭的表示公開的 public 權限,小寫字母開頭表示私有的 private 權限,Go 只有這兩類權限,都是針對包 package 而言,之後會再細說,如今先這麼理解就好了.
  
  按照實驗獲得的方法規則,繼續完善其餘方法,補充 GetCap 和 IsEmpty 等方法.
  
  如今咱們已經解決了私有變量的訪問性問題,對於初始化的邏輯尚未處理,通常來講,初始化邏輯能夠放到構造函數中執行,那 Go 是否支持構造函數呢,以及怎麼才能觸發構造函數?
  
  go-oop-about-myDynamicArray-constuct.png
  
  嘗試按照其餘主流的編程語言中構造函數的寫法來編寫 Go 的構造函數 , 沒想到 Go 編譯器直接報錯了,提示從新定義了 MyDynamicArray 類型,以致於影響了其他部分!
  
  若是修改方法名稱的話,理論上能夠解決報錯問題,可是這並非構造函數的樣子了,難不成 Go 不支持構造函數嗎?
  
  此時,面向對象形式的構造函數轉變成自定義函數實現的構造函數,更加準確的說法,這是一種相似於工廠模式實現的構造函數方式.
  
  func NewMyDynamicArray() *MyDynamicArray {
  
  var myDynamicArray MyDynamicArray
  
  return &myDynamicArray
  
  }
  
  難道 Go 語言真的不支持構造函數?
  
  至因而否支持構造函數或者說應該如何支持構造函數,真相不得而知,隨着學習的深刻,相信之後必定會有明確的答案,這裏簡單表達一下我的見解.
  
  首先咱們知道 Go 的結構體中只能定義數據,而結構體的方法確定是在結構體外定義的,爲了符合面向對象的使用習慣,也就是經過實例對象的點操做符來訪問方法,Go 的方法只能是函數的變體,即普通函數中關於指向結構體變量的聲明部分轉移到函數名前面來實現方法,這種由函數轉變成爲方法的模式也符合 Go 一向的命名規則: 向來是按照人的思惟習慣命名,先有輸入再有輸出等邏輯.
  
  結構體的方法從語法和語義的兩個維度上支持了面向對象規範,那麼構造函數想要實現面向對象應該如何作呢?
  
  構造函數正如其名應該是函數,而不是方法,方法由指向自身的參數,這一點構造函數不該該有,不然都有實例對象了還構造毛線啊?
  
  既然構造函數是普通函數,那麼按照面向對象的命名習慣,方法名應該是結構體名,然而真的操做了,編輯器直接就報錯了,因此這不符合面向對象的命名習慣!
  
  如此一來,構造函數的名稱可能並非結構體類型的名稱,有多是其餘特殊的名稱,最好這個名稱可以見名知義且具有實例化對象時自動調用的能力.
  
  固然這個名稱依賴於 Go 的設計者如何命名,這裏靠猜想是很難猜對的,不然我就是設計者了啊!
  
  除此以外,還有另一種可能,那就是 Go 並無構造函數,想要實現構造函數的邏輯只能另闢蹊徑.
  
  這麼說有沒有什麼靠譜的依據呢?
  
  我想大概是有的,構造函數雖然提供了自動初始化能力,可是若是真的在構造函數中加入複雜的初始化邏輯,無疑會增大之後出錯的排查難度並給使用者帶來必定的閱讀障礙,因此說必定程度上,構造函數頗有可能被濫用了!
  
  那是否就意味着不須要構造函數了呢?
  
  也不能這麼說,構造函數除了基本的變量初始化以及簡單的邏輯外,在實際編程中仍是有必定用途的,爲了不濫用而直接禁用,多少有點飲鴆止渴的感受吧?
  
  所以,我的的見解是應該能夠保留構造函數這種初始化邏輯,也能夠換一種思路去實現,或者乾脆直接放棄構造函數轉而由編譯器自動實現構造函數,正如編譯器能夠自動添加多字段之間的分號那樣.
  
  若是開發者真的有構造函數的需求,經過工廠模式或者單例模式等手段老是能夠定製結構體初始化的邏輯,因此放棄也何嘗不可!
  
  最後,以上這些純屬我的猜測,目前並不知道 Go 是否存在構造函數,有了解的人,還請明確告訴我答案,我的傾向於不存在構造函數,最多隻提供相似於構造函數初始化的邏輯!
  
  如今,咱們已經封裝告終構體的數據,定義告終構體的方法以及實現告終構體的工廠函數.那麼接下來讓咱們繼續完善動態數組,實現數組的基本操做.
  
  func NewMyDynamicArray() *MyDynamicArray {
  
  var myDynamicArray MyDynamicArray
  
  myDynamicArray.len = 0
  
  myDynamicArray.cap = 10
  
  var arr [10]int
  
  myDynamicArray.ptr = &arr
  
  return &myDynamicArray
  
  }
  
  func TestMyDynamicArray(t *testing.T) {
  
  myDynamicArray := NewMyDynamicArray()
  
  t.Log(myDynamicArray)
  
  }
  
  首先將測試用例中的邏輯提取到工廠函數中,默認無參的工廠函數初始化的內部數組長度爲 10 ,後續再考慮調用者指定以及實現動態數組等功能,暫時先實現最基本的功能.
  
  初始化的內部數組均是零值,所以須要首先提供給外界可以添加的接口,實現以下:
  
  func (myArr *MyDynamicArray) Add(index, value int) {
  
  if myArr.len =www.fengshen157.com= myArr.cap {
  
  return
  
  }
  
  if index < 0 || index > myArr.len {
  
  return
  
  }
  
  for i := myArr.len - 1; i >= index; i-- {
  
  (*myArr.ptr)[www.chaoyul.com] = (*myArr.ptr)[i]
  
  }
  
  (*myArr.ptr)[index] = value
  
  myArr.len++
  
  }
  
  因爲默認的初始化工廠函數暫時是固定長度的數組,所以新增元素實際上是操做固定長度的數組,不過這並不妨礙後續實現動態數組部分.
  
  爲了操做方便,再提供插入頭部和插入尾部兩種接口,能夠基於動態數組實現比較高級的數據結構.
  
  func (myArr *MyDynamicArray) AddLast(value int) {
  
  myArr.Add(myArr.len, value)
  
  }
  
  func (myArr *MyDynamicArray) AddFirst(value int) {
  
  myArr.Add(0, value)
  
  }
  
  爲了方便測試動態數組的算法是否正確,所以提供打印方法查看數組結構.
  
  go-oop-about-myDynamicArray-print.png
  
  因而可知,打印方法顯示的數據結構和真實的結構體數據是同樣的,接下來咱們就比較有信心繼續封裝動態數組了!
  
  func (myArr *MyDynamicArray) Set(index, value int) {
  
  if index < 0 || index >= myArr.len {
  
  return
  
  }
  
  (*myArr.ptr)[www.yacuangpt.com] = value
  
  }
  
  func (myArr *MyDynamicArray) Get(index int) int {
  
  if index < 0 || index >= myArr.len {
  
  return -1
  
  }
  
  return (*myArr.ptr)[index]
  
  }
  
  這兩個接口更加簡單,更新數組指定索引的元素以及根據索引查詢數組的值.
  
  接下來讓咱們開始測試一下動態數組的所有接口吧!
  
  go-oop-about-myDynamicArray-test.png
  
  動態數組暫時告一段落,不知道你是否好奇爲何以動態數組爲例講解面向對象?
  
  其實主要是爲了驗證上一篇文章中的猜測,也就是切片和數組的究竟是什麼關係?
  
  我以爲切片的底層是數組,只不過語法層面提供了支持以致於看不出數組的影子,仙子阿既然學習了面向對象,那麼就用面向對象的方式實現下切片的功能,雖然沒法模擬語法級別的實現,可是功能特性徹底是能夠模仿的啊!
  
  下面仍是梳理總結一下本文的只要知識點吧,也就是封裝的實現.
  
  如何封裝結構體
  
  之因此稱之爲結構體是由於 Go 的關鍵字是 struct 而不是 class,也是面向對象編程風格中惟一支持的特性,繼承和多態都不支持,到時候另開文章細說.
  
  結構體是對數據進行封裝所使用的手段,結構體內只能定義數據而不能定義方法,這些數據有時候被稱爲字段,有時候叫作屬性或者乾脆叫作變量,至於什麼叫法不是特別重要,如何命名和所處的環境語義有關.
  
  type MyDynamicArray struct {
  
  ptr *[www.yongshenyuL.com]int
  
  len int
  
  cap int
  
  }
  
  這種結構體內就有三個變量,變量之間直接換行進行分隔而不是分號並換行的形式,剛開始以爲有些怪,不過編輯器通常都很智能,假如習慣性添加了分號,會提示你進行刪除,因此語法細節上沒必要在乎.
  
  結構體內不支持編寫函數,僅支持數據結構,這樣就意味着數據和行爲是分開的,二者之間的關聯是比較弱的.
  
  func (myArr *MyDynamicArray) IsEmpty(www.51kunlunyule.com) bool {
  
  return myArr.len =www.zhuyngyule.cn= 0
  
  }
  
  這種方式的函數和普通函數略有不一樣,將包含結構體變量的參數提早到函數名前面,語義上也比較明確,表示的是結構體的函數,爲了和普通函數有所區別,這種函數稱之爲方法.
  
  其實,單純地就實現功能上看,方法和函數並無什麼不一樣,無外乎調用者的使用方式不同罷了!
  
  func IsEmpty(myArr *MyDynamicArray) bool {
  
  return myArr.len == 0
  
  }
  
  之因此是這種設計方式,一方面體現了函數的重要性,畢竟是 Go 語言中的一等公民嘛!
  
  另外一方面是爲了實現面向對象的語法習慣,不論屬性仍是方法,通通用點 . 操做符進行調用.
  
  官方的文檔中將這種結構體參數稱之爲接收者,由於數據和行爲是弱關聯的,這裏的接收者充當的就是關聯數據的做用,接收者顧名思義就是接受數據的人,那發送數據的人又是誰呢?
  
  不言而喻,發送者應該是調用者傳遞的結構體實例對象,結構體變量將數據結構發送給接收者方法,從而數據和行爲聯繫在一塊兒了.
  
  func TestMyDynamicArray(www.51dfyLgw.com *testing.T) {
  
  myDynamicArray := NewMyDynamicArray()
  
  fmt.Println(myDynamicArray.IsEmpty())
  
  }
  
  好了,以上就是面向對象初體驗中的所有部分,僅僅是這麼一小部分卻耗費我整整三天的時間了,想說換種思惟不簡單,寫好一篇文章也不容易啊!
  
  下篇文章中將繼續介紹面向對象的封裝特性,講解更多幹貨,若是以爲本文對你有所幫助,歡迎轉發評論,感受你的閱讀!算法

相關文章
相關標籤/搜索