15. 不透明型態

一般來說,函數的返回值型態必須是確定的,即使將函數設計成泛型,呼叫者也會知道這個函數的具體返回型態。但不透明型態的目的就是讓返回值型態不具體,只能知道他符合某個協定,但是不知道具體的格式。

假設我們要設計一個提供各種幾何圖形的框架,以下述的程式碼為例,設計一個Rectangle的結構,內容不重要,先想像這個結構可以畫出一個矩形即可,並且讓這個結構符合Shape這個協定的規範。再設計一個Transformed結構,只要傳入一個符合Shape協定的圖形,這個結構就可以讓該圖形變形,想像一下,如果傳入一個矩形就會將這個矩形旋轉45度。最後再設計一個Union結構,如果傳入兩個符合Shape協定的圖形就會將這兩個圖形合併成一個。

protocol Shape { }

struct Rectangle: Shape { }

struct Transformed<T: Shape>: Shape {
    var shape: T
}

struct Union<T: Shape, U: Shape>: Shape {
    var a: T
    var b: U
}

接下來我們想要開發一套設計遊戲的框架,遊戲當然會有許多的圖形,我們要利用上述的幾何圖形框架來完成遊戲中的各種圖形,首先設計一個GameObject協定,只要符合這個協定的圖形都具有一個屬性shape,用來傳回該圖形的實體。

protocol GameObject {
    associatedtype T: Shape
    var shape: T { get }
}

現在我們要來設計真正的圖形了。假設這個遊戲需要許多的八角形,所以我們用一個矩形以及一個旋轉45度的矩形並且將他們合併在一起,就可以產生遊戲需要的八角形,程式碼如下:

struct EightPointedStar: GameObject {
    var shape: Union<Rectangle, Transformed<Rectangle>> {
        Union(a:Rectangle(), b:Transformed(shape: Rectangle()))
    }
}

先假設一下,到這邊為止,所有的程式碼都是我們寫好準備提供給別人使用的,也就是我們屬於第三方的框架開發者。現在有個遊戲開發團隊要使用這套框架來設計遊戲,於是他寫了一段程式碼來產生一個八角形,如下:

var eightStar = EightPointedStar().shape

當變數eightStar在宣告的時候就初始化,因此編譯器會自動推論eightStar的資料型態,但如果沒有要先初始化,這時資料型態就必須明確指定了。所以 eightStar的資料型態為何呢?在EightPointedStar結構中可以看到,屬性shape的資料型態為Union<Rectangle, Transformed<Rectangle>>,所以eightStar的資料型態自然也就是Union<Rectangle, Transformed<Rectangle>>。所以要明確的指出eightStar的資料型態時,程式碼如下:

var eightStar: Union<Rectangle, Transformed<Rectangle>>
eightStar = EightPointedStar().shape

資料型態使用不透明型態

但這樣子我們認為揭露太多訊息了。對遊戲開發團隊而言,他並不需要知道這個八角形的圖形是怎麼產生的,他只要知道他可以得到一個符合Shape協定的圖形即可。所以我們現在要把揭露的訊息隱藏掉,修改EightPointedStar這個結構。只改了一個地方,就是把Union<Rectangle, Transformed<Rectangle>>這一串不友善的程式碼換成some Shape。如下:

struct EightPointedStar: GameObject {
    var shape: some Shape {
        Union(a:Rectangle(), b:Transformed(shape: Rectangle()))
    }
}

現在遊戲開發團隊需要產生八角形圖案時,資料型態只要使用Shape就可以。

var eightStar: Shape
eightStar = EightPointedStar().shape

保留字some就是不透明型態,也可以看做是「反泛型」。因為如果是泛型形式,最終我們會很清楚的知道資料型態為何,但是some卻把他遮掉了,我們只知道傳回來的東西符合某個協定而已,其他什麼都不知道。當然程式碼也變的很簡潔。我們也不需要因為修改EightPointedStar結構中屬性shape的內容而要同步修改他的資料型態。

函數傳回值使用不透明型態

接下來我們再看另外一個需要使用不透明型態的場合。假設遊戲開發團隊寫了一個函數,可以傳回最受喜愛的圖形,如下:

func favoriteGameObject() -> EightPointedStar {
    EightPointedStar()
}

如果這個函數內容經常修改,例如新的最喜愛圖形是菱形,於是這個函數就要修改為如下的程式碼。會發現除了函數內容要更動外,還要跟著修改函數的傳回值型態。

struct Diamond: GameObject {
    var shape: some Shape {
        Transformed(shape: Rectangle())
    }
}

func favoriteGameObject() -> Diamond {
    Diamond()
}

每次只要改變最喜愛圖形時都要跟著修改函數傳回型態實在太麻煩了,而且對於該函數的呼叫者而言,傳回什麼型態並不重要,他只是要得到一個圖形而已,因此我們可以用some來解決這個問題,如下:

func favoriteGameObject() -> some GameObject {
    Diamond()
}

函數傳回泛型協定時必須使用不透明型態

除此之外,若函數傳回型態是協定,而協定使用了associatedtype而成為泛型協定時,該函數的傳回值型態一定要加上some。如下面這段程式碼,協定DataProtocol為泛型協定,因此函數someFunc()傳回值型態為該協定時,協定名稱前一定要加上some。

protocol DataProtocol {
    associatedtype T
    var data: T {get set}
}

struct DataStruct<T>: DataProtocol {
    var data: T
}

func someFunc() -> some DataProtocol {
    let tmp = DataStruct(data: 10)
    return tmp
}

最後,如果函數的傳回型態為不透明型態,且函數中不只一處有return,函數設計者必須確保所有return的型態都是一致的,否則會得到錯誤訊息,例如下述的程式碼中如果flag為true,DataProtocol協定的泛型符號會被指定為Int,如果flag為false,DataProtocol協定的泛型符號會被指定為String,這兩者造成回傳型態不一致,語法檢查器會傳回錯誤訊息。

func someFunc(_ flag: Bool) -> some DataProtocol {
    if flag {
        return DataStruct(data: 10)
    } else {
        return DataStruct(data: "hello")
    }
}

另外一個例子,下面這段程式碼也會得到同樣的錯誤訊息,因為Diamond()所產生的實體其型態為Diamond,而EightPointedStar()所產生的實體其型態為EightPointedStar,兩者並不一致。

func favoriteGameObject(_ flag: Bool) -> some GameObject {
    if flag {
        return Diamond()
    } else {
        return EightPointedStar()
    }
}

發表迴響