原文地址:https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md
摘要
我們建議擴(kuò)展Go語(yǔ)言,以便為類型和函數(shù)添加可選的類型參數(shù)。類型參數(shù)可能受接口類型的約束膀斋。我們還建議擴(kuò)展用作類型約束的接口類型,允許列出可能分配給它們的類型集湘换。在許多情況下,可以支持通過(guò)統(tǒng)一算法進(jìn)行類型推斷统阿,從而可以從函數(shù)調(diào)用中省略類型實(shí)參彩倚。該設(shè)計(jì)與Go 1 完全向后兼容。
如何閱讀本文
這篇文章很長(zhǎng)扶平。下面是一些有關(guān)如何閱讀的指南帆离。
- 我們從一個(gè)高層次的概述開始,非常簡(jiǎn)要地描述這些概念蜻直。
- 然后盯质,我們從頭開始解釋整個(gè)設(shè)計(jì)袁串,并通過(guò)簡(jiǎn)單的示例介紹我們需要的細(xì)節(jié)概而。
- 完整描述設(shè)計(jì)之后呼巷,我們將討論實(shí)現(xiàn),設(shè)計(jì)中的一些問(wèn)題以及與其他泛型方法的比較赎瑰。
- 然后王悍,我們提供有關(guān)如何在實(shí)踐中使用此設(shè)計(jì)的幾個(gè)完整示例。
- 在示例之后餐曼,附錄中討論了一些次要細(xì)節(jié)压储。
高度概括
本節(jié)非常簡(jiǎn)短地解釋了設(shè)計(jì)草案建議的更改。本部分適用于已經(jīng)熟悉泛型如何在Go語(yǔ)言中工作的人們源譬。這些概念將在后續(xù)各節(jié)中詳細(xì)說(shuō)明集惋。
- 函數(shù)可以使用關(guān)鍵字
type
引入的其他類型參數(shù)列表:func F(type T)(p T) { ... }
。 - 這些類型參數(shù)可以作為常規(guī)參數(shù)在函數(shù)體內(nèi)使用踩娘。
- 類型也可以具有類型參數(shù)列表:
type M(type T) []T
刮刑。 - 每個(gè)類型參數(shù)可以具有可選的類型約束:
func F(type T Constraint)(p T) { ... }
- 類型約束是接口類型。
- 用作類型約束的接口類型可以擁有預(yù)先聲明的類型的列表养渴。只有其基礎(chǔ)類型是這些類型之一的類型才能實(shí)現(xiàn)該接口雷绢。
- 使用泛型函數(shù)或類型需要傳遞類型實(shí)參。
- 在常見情況下理卑,可以通過(guò)類型推斷省略類型實(shí)參翘紊。
- 如果類型參數(shù)具有類型約束,則其類型實(shí)參必須實(shí)現(xiàn)此接口藐唠。
- 泛型函數(shù)只能使用類型約束所允許的操作帆疟。
在以下各節(jié)中,我們將詳細(xì)介紹每種更改宇立。你也可以跳過(guò)這些直接查看示例踪宠。
背景
這個(gè)版本的設(shè)計(jì)草案與2019年7月31日提出的版本有很多相似之處,但協(xié)議已被刪除并由接口類型代替泄伪。
有許多要求為Go添加泛型支持的請(qǐng)求殴蓬。在issue和在線文檔中已經(jīng)進(jìn)行了廣泛的討論。
有幾種添加類型參數(shù)的建議蟋滴,可以通過(guò)上面的鏈接找到染厅。這里提出的許多想法以前都曾出現(xiàn)過(guò)。此處描述的主要新特性是語(yǔ)法和對(duì)作為約束的接口類型的仔細(xì)校準(zhǔn)津函。
此設(shè)計(jì)草案建議擴(kuò)展Go語(yǔ)言肖粮,添加一種參數(shù)多態(tài)的形式,其中類型參數(shù)不受聲明的子類型關(guān)系(如在某些面向?qū)ο蟮恼Z(yǔ)言中)的約束尔苦,但受顯式定義的約束結(jié)構(gòu)的約束涩馆。
此設(shè)計(jì)不支持模板編程或任何其他形式的編譯時(shí)處理行施。
由于術(shù)語(yǔ)“泛型”在Go社區(qū)中得到了廣泛使用,因此我們將在下文中將其用作表示具有類型參數(shù)的函數(shù)或類型的簡(jiǎn)寫魂那。不要將本設(shè)計(jì)中使用的泛型(generic)一詞與其他語(yǔ)言(例如C ++蛾号,C#,Java或Rust)中的同一術(shù)語(yǔ)混淆涯雅;它們有相似之處鲜结,但不相同。
設(shè)計(jì)
我們將基于簡(jiǎn)單的示例分階段描述完整的設(shè)計(jì)活逆。
類型參數(shù)
泛型代碼是使用稍后將指定的類型編寫的代碼丁鹉。未指定的類型稱為類型參數(shù)鸟赫。運(yùn)行泛型代碼時(shí),type形參將設(shè)置為type實(shí)參。
這是一個(gè)打印出切片的每個(gè)元素的函數(shù)匈庭,其中切片的元素類型(此處稱為T
)是未知的剩失。這是我們要允許以支持泛型編程的函數(shù)類型的一個(gè)簡(jiǎn)單例子勘究。(稍后我們還將討論泛型類型)渗鬼。
// Print prints the elements of a slice.
// It should be possible to call this with any slice value.
func Print(s []T) { // Just an example, not the suggested syntax.
for _, v := range s {
fmt.Println(v)
}
}
使用這種方法,首先要做出的決定是:如何聲明T
類型參數(shù)迷殿?在Go之類的語(yǔ)言中儿礼,我們希望每個(gè)標(biāo)識(shí)符都以某種方式聲明。
在這里庆寺,我們進(jìn)行的設(shè)計(jì)決策是:類型參數(shù)與普通的非類型函數(shù)參數(shù)相似蚊夫,因此應(yīng)與其他參數(shù)一起列出。但是懦尝,類型參數(shù)與非類型參數(shù)不同知纷,因此,盡管它們出現(xiàn)在參數(shù)列表中陵霉,但我們還是要加以區(qū)分琅轧。因此我們做出以下設(shè)計(jì)決策:我們定義一個(gè)附加的,可選的參數(shù)列表踊挠,描述類型參數(shù)乍桂。該參數(shù)列表出現(xiàn)在常規(guī)參數(shù)之前。它以關(guān)鍵字type
開頭效床,并列出類型參數(shù)睹酌。
// Print prints the elements of any slice.
// Print has a type parameter T, and has a single (non-type)
// parameter s which is a slice of that type parameter.
func Print(type T)(s []T) {
// same as above
}
這表示在函數(shù)Print
中,標(biāo)識(shí)符T
是一個(gè)類型參數(shù)剩檀,一種當(dāng)前未知的類型憋沿,但在調(diào)用該函數(shù)時(shí)將是已知的。如上所述沪猴,當(dāng)描述普通的非類型參數(shù)時(shí)辐啄,類型參數(shù)可以用作類型采章。它也可以在函數(shù)體內(nèi)使用。
由于Print
具有類型參數(shù)壶辜,因此任何調(diào)用都Print
必須提供類型實(shí)參悯舟。稍后,我們將看到通常如何通過(guò)使用函數(shù)參數(shù)類型推斷來(lái)使用非類型參數(shù)推導(dǎo)出該類型參數(shù)∈扛矗現(xiàn)在图谷,我們將顯式傳遞type參數(shù)翩活。類型實(shí)參的傳遞與聲明類型形參的傳遞非常相似:作為單獨(dú)的參數(shù)列表阱洪。在調(diào)用時(shí)不使用type
關(guān)鍵字。
// Call Print with a []int.
// Print has a type parameter T, and we want to pass a []int,
// so we pass a type argument of int by writing Print(int).
// The function Print(int) expects a []int as an argument.
Print(int)([]int{1, 2, 3})
// This will print:
// 1
// 2
// 3
約束條件
讓我們的例子稍微復(fù)雜一些菠镇。讓我們將其轉(zhuǎn)換為一個(gè)函數(shù)冗荸,該函數(shù)可以將任意類型的切片轉(zhuǎn)換為[]string。
// This function is INVALID.
func Stringify(type T)(s []T) (ret []string) {
for _, v := range s {
ret = append(ret, v.String()) // INVALID
}
return ret
}
乍一看似乎不錯(cuò)利耍,但在此示例v為type T蚌本,而我們對(duì)T一無(wú)所知。特別是隘梨,我們不知道T有String方法程癌。因此,v.String()的調(diào)用無(wú)效轴猎。
當(dāng)然嵌莉,在支持泛型編程的其他語(yǔ)言中也會(huì)出現(xiàn)相同的問(wèn)題。例如捻脖,在C ++中锐峭,泛型函數(shù)(用C ++術(shù)語(yǔ)來(lái)說(shuō)是函數(shù)模板)可以對(duì)泛型類型的值調(diào)用任何方法。也就是說(shuō)可婶,在C ++方法中沿癞,調(diào)用v.String()
就可以了。如果使用沒(méi)有String方法的類型實(shí)參調(diào)用該函數(shù)矛渴,則在編譯v.String時(shí)將會(huì)報(bào)錯(cuò)椎扬。這些錯(cuò)誤可能很長(zhǎng),因?yàn)樵阱e(cuò)誤發(fā)生之前可能有幾層函數(shù)調(diào)用具温,為了了解出了什么問(wèn)題必須報(bào)告所有這些層的異常蚕涤。
對(duì)于Go語(yǔ)言,C ++方法將是一個(gè)糟糕的選擇桂躏。原因之一是語(yǔ)言的風(fēng)格钻趋。在Go語(yǔ)言中,我們不會(huì)進(jìn)行名稱的引用剂习,例如在這種情況下蛮位,是希望String存在较沪。Go會(huì)將所有看到的名稱解析為其聲明。
另一個(gè)原因是Go旨在支持大規(guī)模編程失仁。我們必須考慮以下情況:泛型函數(shù)定義(Stringify)和對(duì)泛型函數(shù)的調(diào)用(未顯示尸曼,但可能在其他軟件包中)相距甚遠(yuǎn)。通常萄焦,所有泛型代碼都希望類型實(shí)參滿足某些要求控轿。我們將這些要求稱為約束(其他語(yǔ)言也有類似的想法,稱為類型界限或特征界限或其他的概念)拂封。在這種情況下茬射,約束非常明顯:類型必須具有String() string
方法。在其他情況下冒签,它可能不那么明顯在抛。
我們不想從Stringify
的任何使用中得出約束(在這種情況下,調(diào)用String方法)萧恕。如果這樣做刚梭,對(duì)Stringify
的微小更改可能會(huì)更改約束。這意味著較小的更改可能導(dǎo)致調(diào)用該函數(shù)的代碼變更而意外中斷票唆。Stringify
故意更改其約束并強(qiáng)迫用戶進(jìn)行更改也是一種方法朴读。我們要避免的是意外更改Stringify
約束。
這意味著約束必須對(duì)調(diào)用者傳遞的類型實(shí)參和泛型函數(shù)中的代碼都設(shè)置限制走趋。調(diào)用者只能傳遞滿足約束的類型參數(shù)衅金。泛型函數(shù)只能以約束所允許的方式使用這些值。我們認(rèn)為這是一條重要規(guī)則吆视,適用于在Go中定義泛型編程的任何嘗試:泛型代碼只能使用已知其類型參數(shù)實(shí)現(xiàn)的操作典挑。
允許任何類型的操作
在進(jìn)一步討論約束之前,讓我們簡(jiǎn)要地指出在沒(méi)有約束的情況下會(huì)發(fā)生什么啦吧。如果泛型函數(shù)沒(méi)有為類型參數(shù)指定約束(如上述Print
方法那樣)您觉,則該參數(shù)允許使用任何類型參數(shù)。泛型函數(shù)與該類型參數(shù)的值一起使用的操作只能是允許用于任何類型的值的那些操作授滓。在上面的示例中琳水,Print
函數(shù)聲明了一個(gè)變量v
,類型為類型參數(shù)T
般堆,并將該變量傳遞給函數(shù)在孝。
允許任何類型的操作是:
- 聲明那些類型的變量
- 將相同類型的其他值分配給這些變量
- 將這些變量傳遞給函數(shù)或從函數(shù)返回它們
- 獲取那些變量的地址
- 將這些類型的值轉(zhuǎn)換或賦值給類型
interface{}
- 將類型
T
的值轉(zhuǎn)換為類型T
(允許但無(wú)用) - 使用類型斷言將接口值轉(zhuǎn)換為類型
- 在類型switch中將類型用作case
- 定義和使用這些類型的復(fù)合類型,例如該類型的切片
- 將類型傳遞給一些內(nèi)置函數(shù)淮摔,例如
new
盡管目前預(yù)計(jì)不會(huì)進(jìn)行任何修改私沮,但將來(lái)的語(yǔ)言更改可能還會(huì)添加其他此類操作。
定義約束
Go已經(jīng)具有與我們需要的約束接近的構(gòu)造:接口類型(interface)和橙。接口類型是一組方法仔燕≡於猓可以分配給接口類型的變量的是那些實(shí)現(xiàn)相同方法的類型的值。只能使用接口類型執(zhí)行的操作是調(diào)用方法晰搀。
使用類型參數(shù)調(diào)用泛型函數(shù)類似于分配給接口類型的變量:類型參數(shù)必須實(shí)現(xiàn)類型參數(shù)的約束五辽。編寫泛型函數(shù)就像使用接口類型的值一樣:泛型代碼只能使用約束所允許的操作(或任何其他類型所允許的操作)。
在這種設(shè)計(jì)中外恕,約束只是接口類型杆逗。實(shí)現(xiàn)約束只是實(shí)現(xiàn)接口類型。(稍后鳞疲,我們將看到如何為方法定義約束)罪郊。
對(duì)于Stringify
示例,我們需要一個(gè)接口類型建丧,該接口類型的String
方法不帶任何參數(shù)并返回type的值string
排龄。
// Stringer is a type constraint that requires the type argument to have
// a String method and permits the generic function to call String.
// The String method should return a string representation of the value.
type Stringer interface {
String() string
}
(此討論無(wú)關(guān)緊要,但這定義了與標(biāo)準(zhǔn)庫(kù)的fmt.Stringer
類型相同的接口翎朱,實(shí)際代碼可能只使用fmt.Stringer
。)
使用約束
對(duì)于泛型函數(shù)尺铣,可以將約束視為類型實(shí)參的類型:元類型拴曲。因此,盡管不需要泛型函數(shù)使用約束凛忿,但泛型函數(shù)在使用時(shí)會(huì)在類型參數(shù)列表中作為類型參數(shù)的元類型列出澈灼。
// Stringify calls the String method on each element of s,
// and returns the results.
func Stringify(type T Stringer)(s []T) (ret []string) {
for _, v := range s {
ret = append(ret, v.String())
}
return ret
}
在這種情況下,單個(gè)類型參數(shù)T
后跟適用的約束店溢。在這個(gè)例子里就是Stringer
叁熔。
多種類型參數(shù)
盡管該Stringify
示例僅使用單個(gè)類型參數(shù),但函數(shù)可能具有多個(gè)類型參數(shù)床牧。
// Print2 has two type parameters and two non-type parameters.
func Print2(type T1, T2)(s1 []T1, s2 []T2) { ... }
比較一下
// Print2Same has one type parameter and two non-type parameters.
func Print2Same(type T)(s1 []T, s2 []T) { ... }
在Print2
中 s1
和s2
可以是不同類型的切片荣回。Print2Same
中 s1和s2必須是相同元素類型的切片。
每個(gè)類型參數(shù)可能都有其自己的約束戈咳。
// Stringer is a type constraint that requires a String method.
// The String method should return a string representation of the value.
type Stringer interface {
String() string
}
// Plusser is a type constraint that requires a Plus method.
// The Plus method is expected to add the argument to an internal
// string and return the result.
type Plusser interface {
Plus(string) string
}
// ConcatTo takes a slice of elements with a String method and a slice
// of elements with a Plus method. The slices should have the same
// number of elements. This will convert each element of s to a string,
// pass it to the Plus method of the corresponding element of p,
// and return a slice of the resulting strings.
func ConcatTo(type S Stringer, P Plusser)(s []S, p []P) []string {
r := make([]string, len(s))
for i, v := range s {
r[i] = p[i].Plus(v.String())
}
return r
}
如果為任何類型參數(shù)指定了約束心软,則每個(gè)類型參數(shù)都必須具有約束。如果某些類型參數(shù)需要約束而有些則不需要著蛙,則那些不需要的需要有一個(gè)interface{}
約束删铃。
// StrAndPrint takes a slice of labels, which can be any type,
// and a slice of values, which must have a String method,
// converts the values to strings, and prints the labelled strings.
func StrAndPrint(type L interface{}, T Stringer)(labels []L, vals []T) {
// Stringify was defined above. It returns a []string.
for i, s := range Stringify(vals) {
fmt.Println(labels[i], s)
}
}
單個(gè)約束可用于多個(gè)類型參數(shù),就像單個(gè)類型可用于多個(gè)非類型函數(shù)參數(shù)一樣踏堡。約束分別應(yīng)用于每個(gè)類型參數(shù)猎唁。
// Stringify2 converts two slices of different types to strings,
// and returns the concatenation of all the strings.
func Stringify2(type T1, T2 Stringer)(s1 []T1, s2 []T2) string {
r := ""
for _, v1 := range s1 {
r += v1.String()
}
for _, v2 := range s2 {
r += v2.String()
}
return r
}
泛型類型
我們不僅需要泛型函數(shù):我們還需要泛型類型。我們建議將類型擴(kuò)展為可接受類型參數(shù)顷蟆。
// Vector is a name for a slice of any element type.
type Vector(type T) []T
類型的參數(shù)就像函數(shù)的類型參數(shù)一樣诫隅。
在類型定義內(nèi)缎患,可以像其他類型一樣使用。
要使用泛型類型阎肝,必須提供類型參數(shù)挤渔。這看起來(lái)像一個(gè)函數(shù)調(diào)用,只是在這種情況下該函數(shù)實(shí)際上是一個(gè)類型风题。這稱為實(shí)例化判导。當(dāng)通過(guò)為類型參數(shù)提供類型實(shí)參來(lái)實(shí)例化類型時(shí),我們會(huì)生成一個(gè)類型沛硅,其中類型定義中對(duì)類型參數(shù)的每次使用都將被相應(yīng)的類型實(shí)參替換眼刃。
// v is a Vector of int values.
//
// This is similar to pretending that "Vector(int)" is a valid identifier,
// and writing
// type "Vector(int)" []int
// var v "Vector(int)"
// All uses of Vector(int) will refer to the same "Vector(int)" type.
//
var v Vector(int)
泛型類型可以具有方法。方法的接收類型必須聲明與接收類型的定義中聲明的數(shù)量相同的類型參數(shù)摇肌。聲明它們時(shí)沒(méi)有type
關(guān)鍵字或任何約束抛蚤。
// Push adds a value to the end of a vector.
func (v *Vector(T)) Push(x T) { *v = append(*v, x) }
方法聲明中列出的類型參數(shù)不必與類型聲明中的類型參數(shù)具有相同的名稱。特別是如果方法未使用它們悠瞬,則可以使用_
酣衷。
如果一個(gè)類型可以引用自身,則泛型類型也可以引用自身肯适,但是當(dāng)這樣做時(shí)变秦,類型實(shí)參必須是按相同順序列出的類型形參。此限制可防止類型實(shí)例化的無(wú)限遞歸框舔。
// List is a linked list of values of type T.
type List(type T) struct {
next *List(T) // this reference to List(T) is OK
val T
}
// This type is INVALID.
type P(type T1, T2) struct {
F *P(T2, T1) // INVALID; must be (T1, T2)
}
此限制適用于直接和間接引用蹦玫。
// ListHead is the head of a linked list.
type ListHead(type T) struct {
head *ListElement(T)
}
// ListElement is an element in a linked list with a head.
// Each element points back to the head.
type ListElement(type T) struct {
next *ListElement(T)
val T
// Using ListHead(T) here is OK.
// ListHead(T) refers to ListElement(T) refers to ListHead(T).
// Using ListHead(int) would not be OK, as ListHead(T)
// would have an indirect reference to ListHead(int).
head *ListHead(T)
}
(注意:在了解人們?nèi)绾尉帉懘a的情況下,可以放寬此規(guī)則刘绣,以允許某些情況下使用不同的類型參數(shù)樱溉。)
泛型類型的類型參數(shù)可能具有約束。
// StringableVector is a slice of some type, where the type
// must have a String method.
type StringableVector(type T Stringer) []T
func (s StringableVector(T)) String() string {
var sb strings.Builder
for i, v := range s {
if i > 0 {
sb.WriteString(", ")
}
// It's OK to call v.String here because v is of type T
// and T's constraint is Stringer.
sb.WriteString(v.String())
}
return sb.String()
}
不支持泛型方法
盡管泛型類型的方法可以使用該類型的參數(shù)纬凤,但是方法本身沒(méi)有附加的類型參數(shù)福贞。
在后面的[issues]章節(jié)對(duì)此進(jìn)行了更多討論。
操作符
如我們所見移斩,我們將接口類型用作約束肚医。接口類型提供了一組方法,僅此而已向瓷。這意味著到目前為止肠套,我們已經(jīng)看到,泛型函數(shù)可以對(duì)類型參數(shù)的值執(zhí)行的唯一操作(對(duì)任何類型都允許的操作除外)是調(diào)用方法猖任。
但是你稚,對(duì)于我們要表達(dá)的所有內(nèi)容,僅靠方法調(diào)用還不夠〉罄担考慮下面這個(gè)簡(jiǎn)單的函數(shù)搁痛,該函數(shù)返回值切片的最小元素,其中該切片被假定為非空宇弛。
// This function is INVALID.
func Smallest(type T)(s []T) T {
r := s[0] // panic if slice is empty
for _, v := range s[1:] {
if v < r { // INVALID
r = v
}
}
return r
}
任何合理的泛型實(shí)現(xiàn)都會(huì)讓你編寫一個(gè)類似的函數(shù)鸡典。問(wèn)題是表達(dá)式v < r
。這假定了T
支持<
運(yùn)算符枪芒,但T
沒(méi)有約束彻况。在沒(méi)有約束的情況下,該函數(shù)Smallest
只能使用可用于所有類型的操作舅踪,但是并不是所有的Go類型都支持操作<
纽甘。不幸的是,由于<
這不是一種方法抽碌,因此沒(méi)有明顯的方法來(lái)為<
編寫允許的約束(接口類型)悍赢。
我們需要寫一種僅接受支持<
類型的約束的方法。對(duì)此货徙,我們注意到左权,除了稍后將要討論的兩個(gè)例外,語(yǔ)言定義的所有算術(shù)破婆,比較和邏輯運(yùn)算符都只能與該語(yǔ)言預(yù)先聲明的類型或已定義的類型一起使用涮总。其基礎(chǔ)類型是那些預(yù)先聲明的類型之一。也就是說(shuō)祷舀,運(yùn)算符<
只能與預(yù)先聲明的類型(例如int
或float64
)一起使用,或者只能將其基礎(chǔ)類型為這些類型之一的已定義類型使用烹笔。Go不允許<
與復(fù)合類型或任意定義的類型一起使用裳扯。
這意味著我們不必嘗試為<
編寫約束,而是可以采用另一種方法:與其說(shuō)約束應(yīng)支持哪些運(yùn)算符谤职,不如說(shuō)約束應(yīng)接受哪些(底層)類型饰豺。
約束中的類型列表
用作約束的接口類型可以列出可以用作類型參數(shù)的顯式類型。這是通過(guò)使用type
關(guān)鍵字后跟逗號(hào)分隔的類型列表來(lái)完成的允蜈。
例如:
// SignedInteger is a type constraint that permits any
// signed integer type.
type SignedInteger interface {
type int, int8, int16, int32, int64
}
SignedInteger
限制規(guī)定類型參數(shù)必須是所列出的類型之一冤吨。更準(zhǔn)確地說(shuō),類型實(shí)參的基礎(chǔ)類型必須與類型列表中類型之一的基礎(chǔ)類型相同饶套。這意味著SignedInteger
將接受列出的整數(shù)類型漩蟆,并且還將接受定義為這些類型之一的任何類型。
當(dāng)泛型函數(shù)使用具有這些約束的類型參數(shù)時(shí)妓蛮,它可以使用所有列出的類型允許的任何操作怠李。這可以像操作<
,range
,<-
捺癞,等等夷蚊。如果可以使用約束中列出的每種類型成功編譯函數(shù),則允許該操作髓介。
一個(gè)約束可能只有一個(gè)類型列表惕鼓。
對(duì)于前面顯示的Smallest
示例,我們可以使用如下約束:
package constraints
// Ordered is a type constraint that matches any ordered type.
// An ordered type is one that supports the <, <=, >, and >= operators.
type Ordered interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64,
string
}
在實(shí)踐中唐础,很可能會(huì)定義此約束并將其導(dǎo)出到新的標(biāo)準(zhǔn)庫(kù)包constraints
中箱歧,以便函數(shù)和類型定義可以使用它。
給定該約束彻犁,我們就可以編寫此函數(shù)叫胁,下面這個(gè)函數(shù)現(xiàn)在有效:
// Smallest returns the smallest element in a slice.
// It panics if the slice is empty.
func Smallest(type T constraints.Ordered)(s []T) T {
r := s[0] // panics if slice is empty
for _, v := range s[1:] {
if v < r {
r = v
}
}
return r
}
約束中的可比較類型
前面我們提到,有兩個(gè)例外的規(guī)則汞幢,即運(yùn)算符只能與語(yǔ)言預(yù)先聲明的類型一起使用驼鹅。==
和!=
是例外,結(jié)構(gòu)森篷,數(shù)組和接口類型均允許進(jìn)行比較操作输钩。如果我們希望能夠編寫一個(gè)接受任何可比較類型的約束,知道這一點(diǎn)就很有用了仲智。
為此买乃,我們引入了一個(gè)新的預(yù)先聲明的類型約束:comparable
。具有comparable
約束的類型參數(shù)接受任何可比較類型作為實(shí)參钓辆。它允許使用==
剪验,!=
與該類型參數(shù)的值進(jìn)行比較。
例如前联,可以使用任何可比較的類型實(shí)例化下面這個(gè)函數(shù):
// Index returns the index of x in s, or -1 if not found.
func Index(type T comparable)(s []T, x T) int {
for i, v := range s {
// v and x are type T, which has the comparable
// constraint, so we can use == here.
if v == x {
return i
}
}
return -1
}
與所有約束一樣功戚,由于comparable
是接口類型,因此可以將其嵌入用作約束的另一種接口類型中:
// ComparableHasher is a type constraint that matches all
// comparable types with a Hash method.
type ComparableHasher interface {
comparable
Hash() uintptr
}
約束ComparableHasher
可以通過(guò)任何可比較類型實(shí)現(xiàn)似嗤,也可以使用一種Hash() uintptr
方法來(lái)實(shí)現(xiàn)啸臀。ComparableHasher
用作約束的泛型函數(shù)可以比較該類型的值并可以調(diào)用該Hash
方法。
接口類型中的類型列表
具有類型列表的接口類型只能用作對(duì)類型參數(shù)的約束烁落。它們可能不能用作普通接口類型乘粒。預(yù)先聲明的接口類型comparable
也是如此。
在將來(lái)的語(yǔ)言版本中可能會(huì)取消此限制伤塌。具有類型列表的接口類型可用作求和類型的一種形式灯萍,盡管它可以具有value nil
〈缑眨可能需要一些替代語(yǔ)法來(lái)匹配相同類型而不是基礎(chǔ)類型竟稳。也許會(huì)是type ==
属桦。不過(guò)目前這是不允許的。
函數(shù)參數(shù)類型推斷
在許多情況下他爸,當(dāng)調(diào)用帶有類型參數(shù)的函數(shù)時(shí)聂宾,我們可以使用interface
來(lái)避免顯式寫出類型參數(shù)。
回顧之前的Print
方法:
Print(int)([]int{1, 2, 3})
類型參數(shù)int
中的函數(shù)調(diào)用可以從非類型參數(shù)的類型來(lái)推斷诊笤。
僅當(dāng)所有函數(shù)的類型參數(shù)都用于函數(shù)(非類型)輸入?yún)?shù)的類型時(shí)系谐,才可以這樣做。如果有一些類型參數(shù)僅用于函數(shù)的結(jié)果參數(shù)類型讨跟,或者僅在函數(shù)體中使用纪他,我們的算法無(wú)法推斷函數(shù)的類型參數(shù),因?yàn)闆](méi)有值可用來(lái)推斷類型晾匠。
當(dāng)可以推斷函數(shù)的類型參數(shù)時(shí)茶袒,語(yǔ)言便可以使用統(tǒng)一類型。在調(diào)用方凉馆,我們有實(shí)際(非類型)參數(shù)的類型列表薪寓,在Print
例中為[]int
。在函數(shù)這邊是類型函數(shù)的無(wú)類型參數(shù)澜共,這對(duì)于Print
是[]T
向叉。在列表中,我們丟棄函數(shù)側(cè)不使用的類型參數(shù)的各個(gè)參數(shù)嗦董。然后母谎,我們必須統(tǒng)一其余的參數(shù)類型。
類型統(tǒng)一是一種two-pass算法京革。在第一遍中奇唤,我們忽略了調(diào)用方的無(wú)類型化常量及其在函數(shù)定義中的對(duì)應(yīng)類型。
我們?cè)诹斜碇斜容^相應(yīng)的類型匹摇。它們的結(jié)構(gòu)必須相同冻记,除非函數(shù)一側(cè)的類型參數(shù)與出現(xiàn)在調(diào)用者一側(cè)的類型參數(shù)相匹配。如果同一個(gè)類型參數(shù)在函數(shù)側(cè)出現(xiàn)多次来惧,它將與調(diào)用方側(cè)的多個(gè)參數(shù)類型匹配。這些調(diào)用者類型必須相同演顾,否則類型統(tǒng)一失敗我們將報(bào)告錯(cuò)誤供搀。
第一次通過(guò)后,我們?cè)谡{(diào)用方檢查所有未定義類型的常量钠至。如果沒(méi)有無(wú)類型的常量葛虐,或者相應(yīng)函數(shù)類型中的類型參數(shù)與其他輸入類型匹配,則類型統(tǒng)一完成棉钧。
否則屿脐,對(duì)于第二遍,對(duì)于尚未設(shè)置相應(yīng)函數(shù)類型的任何未類型化的常量,我們將以默認(rèn)的方式確定未類型常量的默認(rèn)類型的诵。然后我們?cè)俅芜\(yùn)行類型統(tǒng)一算法万栅,這次沒(méi)有任何未類型化的常量。
在這個(gè)例子中
s1 := []int{1, 2, 3}
Print(s1)
我們比較[]int
和[]T
西疤,T
匹配int
烦粒。單一類型參數(shù)T
是int
,因此我們推斷對(duì)Print
的調(diào)用實(shí)際上是對(duì)Print(int)
的調(diào)用代赁。
對(duì)于更復(fù)雜的示例扰她,請(qǐng)考慮
// Map calls the function f on every element of the slice s,
// returning a new slice of the results.
func Map(type F, T)(s []F, f func(F) T) []T {
r := make([]T, len(s))
for i, v := range s {
r[i] = f(v)
}
return r
}
這兩個(gè)類型參數(shù)F
和T
都用于輸入?yún)?shù),因此可以進(jìn)行類型推斷芭碍。在調(diào)用中
strs := Map([]int{1, 2, 3}, strconv.Itoa)
我們將[]int
與[]F
統(tǒng)一徒役,F
匹配int
。我們統(tǒng)一strconv.Itoa
的類型窖壕,即func(int) string
與func(F) T
忧勿,F
與int
以及T
與string
。類型參數(shù)F
匹配兩次艇拍,兩次都匹配int
狐蜕。匹配成功,因此Map
的調(diào)用是調(diào)用Map(int, string)
卸夕。
要查看有效的無(wú)類型常量規(guī)則层释,請(qǐng)考慮:
// NewPair returns a pair of values of the same type.
func NewPair(type F)(f1, f2 F) *Pair(F) { ... }
在上面的調(diào)用中,NewPair(1, 2)
兩個(gè)參數(shù)都是未類型化的常量快集,因此在第一遍中都將忽略贡羔。第一次通過(guò)后就沒(méi)有什么可以統(tǒng)一的了,此時(shí)我們還有兩個(gè)未類型化的常量个初。兩者均設(shè)置為其默認(rèn)類型int
乖寒。類型統(tǒng)一的第二次運(yùn)行用int
匹配F
,所以最終調(diào)用NewPair(int)(1, 2)
院溺。
在NewPair(1, int64(2))
調(diào)用中楣嘁,第一個(gè)參數(shù)是無(wú)類型的常量,因此我們?cè)诘谝槐橹袑⑵浜雎哉湟荨H缓笪覀儗?code>int64與F
統(tǒng)一逐虚。此時(shí)颈抚,未類型化常量相對(duì)應(yīng)的類型參數(shù)已完全確定钙蒙,因此最終調(diào)用為NewPair(int64)(1, int64(2))
。
在NewPair(1, 2.5)
調(diào)用中实辑,兩個(gè)參數(shù)都是未類型化的常量漱病,因此我們繼續(xù)進(jìn)行第二遍买雾。這次我們將第一個(gè)常數(shù)設(shè)置為int
把曼,將第二個(gè)常數(shù)設(shè)置為float64
。然后漓穿,我們嘗試F
與int
和float64
統(tǒng)一嗤军,統(tǒng)一失敗,因此我們報(bào)告編譯錯(cuò)誤器净。
注意型雳,類型推斷是在不考慮約束的情況下完成的。首先山害,我們使用類型推斷來(lái)確定要用于該函數(shù)的類型參數(shù)纠俭,然后,如果成功浪慌,則檢查這些類型參數(shù)是否實(shí)現(xiàn)了約束(如果有)冤荆。
請(qǐng)注意,在成功進(jìn)行類型推斷之后权纤,對(duì)于任何函數(shù)調(diào)用钓简,編譯器仍必須檢查是否參數(shù)分配是否合理。
(注意:類型推斷是一種便利功能汹想。盡管我們認(rèn)為這是一項(xiàng)重要功能外邓,但它并未為設(shè)計(jì)添加任何功能,只是為使用提供了便利古掏∷鸹埃可以從初始實(shí)現(xiàn)中將其省略,并查看是否被需要槽唾,也就是此功能不需要其他語(yǔ)法丧枪,并且可以生成更具可讀性的代碼。)
使用約束中引用自己的類型
對(duì)于泛型函數(shù)來(lái)說(shuō)庞萍,要求類型自變量及其方法本身就是類型的方法可能是有用的拧烦。例如這在比較方法中自然會(huì)出現(xiàn)。(請(qǐng)注意钝计,我們?cè)谶@里討論的是方法恋博,而不是運(yùn)算符。)假設(shè)我們要編寫一個(gè)Index
方法私恬,該方法使用一種Equal
方法來(lái)檢查它是否找到了所需的值交播。我們想這樣寫:
// Index returns the index of e in s, or -1 if not found.
func Index(type T Equaler)(s []T, e T) int {
for i, v := range s {
if e.Equal(v) {
return i
}
}
return -1
}
為了編寫Equaler
約束,我們必須編寫一個(gè)可以引用傳入的類型實(shí)參的約束践付。我們無(wú)法直接實(shí)現(xiàn),但是我們可以做的是編寫一個(gè)使用類型形參的接口類型缺厉。
// Equaler is a type constraint for types with an Equal method.
type Equaler(type T) interface {
Equal(T) bool
}
為此永高,Index
將類型參數(shù)T
傳遞給Equaler
隧土。規(guī)則是如果類型約束具有單個(gè)類型參數(shù),并且在沒(méi)有顯式類型參數(shù)的情況下用于函數(shù)的類型參數(shù)列表命爬,則這個(gè)類型參數(shù)是要約束的類型參數(shù)曹傀。換句話說(shuō),在上面的Index
定義中饲宛,約束Equaler
被視為Equaler(T)
皆愉。
此版本的Index
可以與此處定義的equalInt
類型一起使用:
// equalInt is a version of int that implements Equaler.
type equalInt int
// The Equal method lets equalInt implement the Equaler constraint.
func (a equalInt) Equal(b equalInt) bool { return a == b }
// indexEqualInts returns the index of e in s, or -1 if not found.
func indexEqualInt(s []equalInt, e equalInt) int {
return Index(equalInt)(s, e)
}
在此示例中,當(dāng)傳遞equalInt
給Index
時(shí)艇抠,我們檢查是否equalInt
實(shí)現(xiàn)了Equaler
約束幕庐。由于Equaler
有一個(gè)類型參數(shù),我們傳遞Index
的類型參數(shù)到Equaler
,equalInt
作為類型實(shí)參家淤。這樣异剥,該約束Equaler(equalInt)
可以由任何類型的Equal(equalInt) bool
的方法滿足。equalInt
類型具有一個(gè)Equal
接受類型參數(shù)的方法equalInt
絮重,因此一切都滿足要求冤寿,并且編譯成功。
類型參數(shù)的相互引用
在單個(gè)類型參數(shù)列表中青伤,約束可以引用任何其他的類型參數(shù)督怜,即使是稍后在同一列表中聲明的參數(shù)也是如此。(類型參數(shù)的范圍從參數(shù)列表的type
關(guān)鍵字開始狠角,并擴(kuò)展到封閉函數(shù)或類型聲明的末尾号杠。)
例如,考慮一個(gè)關(guān)于圖的類包擎厢,其中包含可用于圖的通用算法究流。該算法使用兩種類型的,Node
和Edge
动遭。Node有一種方法Edges() []Edge
芬探。Edge
有一種方法Nodes() (Node, Node)
。圖可以表示為[]Node
厘惦。
這種簡(jiǎn)單的表示足以實(shí)現(xiàn)圖算法偷仿,例如找到最短路徑。
package graph
// NodeConstraint is the type constraint for graph nodes:
// they must have an Edges method that returns the Edge's
// that connect to this Node.
type NodeConstraint(type Edge) interface {
Edges() []Edge
}
// EdgeConstraint is the type constraint for graph edges:
// they must have a Nodes method that returns the two Nodes
// that this edge connects.
type EdgeConstraint(type Node) interface {
Nodes() (from, to Node)
}
// Graph is a graph composed of nodes and edges.
type Graph(type Node NodeConstraint(Edge), Edge EdgeConstraint(Node)) struct { ... }
// New returns a new graph given a list of nodes.
func New(
type Node NodeConstraint(Edge), Edge EdgeConstraint(Node)) (
nodes []Node) *Graph(Node, Edge) {
...
}
// ShortestPath returns the shortest path between two nodes,
// as a list of edges.
func (g *Graph(Node, Edge)) ShortestPath(from, to Node) []Edge { ... }
這里有很多類型參數(shù)和實(shí)例化宵蕉。在Graph
中Node
的約束酝静,被傳遞給類型約束NodeConstraint
的Edge
是Graph
的第二個(gè)類型參數(shù)。NodeConstraint
用類型參數(shù)實(shí)例化Edge
羡玛,因此我們看到Node
必須有一個(gè)返回Edges
的切片的方法Edge
别智,這就是我們想要的。這個(gè)約束同樣適用Edge
稼稿,對(duì)于New
函數(shù)對(duì)是重復(fù)相同的類型參數(shù)和約束薄榛。我們并不是說(shuō)這很簡(jiǎn)單讳窟,而是我們認(rèn)為這是可能的。
值得注意的是敞恋,乍一看丽啡,這看起來(lái)像是接口類型的典型用法,Node
和Edge
是具有特定方法的非接口類型硬猫。為了使用graph.Graph
补箍,用于Node
和Edge
的類型實(shí)參必須定義遵循確定模式的方法,但實(shí)際上不必使用接口類型啸蜜。特別是坑雅,這些方法不返回接口類型。
例如盔性,在其他軟件包中考慮以下類型定義:
// Vertex is a node in a graph.
type Vertex struct { ... }
// Edges returns the edges connected to v.
func (v *Vertex) Edges() []*FromTo { ... }
// FromTo is an edge in a graph.
type FromTo struct { ... }
// Nodes returns the nodes that ft connects.
func (ft *FromTo) Nodes() (*Vertex, *Vertex) { ... }
此處沒(méi)有接口類型霞丧,但是我們可以使用類型實(shí)參*Vertex
和*FromTo
實(shí)例化graph.Graph
。
var g = graph.New(*Vertex, *FromTo)([]*Vertex{ ... })
*Vertex
和*FromTo
不是接口類型冕香,但是當(dāng)一起使用時(shí)蛹尝,它們定義實(shí)現(xiàn)graph.Graph
約束的方法。請(qǐng)注意悉尾,我們不能將Vertex
或FromTo
傳遞給graph.New
突那,因?yàn)?code>Vertex和FromTo
沒(méi)有實(shí)現(xiàn)約束。在指針類型*Vertex
和*FromTo
定義了Edges
和Nodes
的方法; 類型Vertex
构眯,FromTo
沒(méi)有任何方法愕难。
當(dāng)使用泛型接口類型作為約束時(shí),我們首先使用類型參數(shù)列表中提供的類型實(shí)參實(shí)例化該類型惫霸,然后將對(duì)應(yīng)的類型實(shí)參與實(shí)例化的約束進(jìn)行比較猫缭。在此示例中,to graph.New
的類型實(shí)參Node
具有一個(gè)約束NodeConstraint(Edge)
壹店。當(dāng)我們使用Node
類型實(shí)參*Vertex
和Edge
類型實(shí)參*FromTo
進(jìn)行graph.New
調(diào)用時(shí)猜丹,為了檢查對(duì)Node
的約束,編譯器必須使用類型實(shí)參*FromTo
實(shí)例化NodeConstraint
硅卢。這就產(chǎn)生了一個(gè)實(shí)例化的約束射窒,在這種情況下,該約束是擁有Edges() []*FromTo
的Node
将塑,并且編譯器將驗(yàn)證*Vertex
是否滿足約束脉顿。
雖然Node
和Edge
沒(méi)有使用接口類型進(jìn)行實(shí)例化,如果你喜歡shi使用接口類型也可以点寥。
type NodeInterface interface { Edges() []EdgeInterface }
type EdgeInterface interface { Nodes() (NodeInterface, NodeInterface) }
我們可以使用類型NodeInterface
和EdgeInterface
實(shí)例化graph.Graph
艾疟,因?yàn)樗鼈儗?shí)現(xiàn)了類型約束。沒(méi)有太多理由以這種方式實(shí)例化類型,但這是允許的汉柒。
類型參數(shù)引用其他類型參數(shù)的能力說(shuō)明了一個(gè)重要的觀點(diǎn):任何嘗試向Go添加泛型的要求都應(yīng)該是可以實(shí)例化具有多個(gè)類型參數(shù)的泛型代碼误褪,這些類型參數(shù)以相互引用,編譯器可以檢查碾褂。
指針?lè)椒?/h3>
在某些情況下,僅當(dāng)類型實(shí)參A
具有在指針類型*A
上定義的方法時(shí)历葛,泛型函數(shù)才能按預(yù)期工作正塌。在編寫期望修改調(diào)用值的方法的泛型函數(shù)時(shí)會(huì)遇到這種情況。
考慮下面函數(shù)的示例恤溶,該函數(shù)期望一種類型T
乓诽,該類型具有一種基于字符串初始化值的Set(string)
方法。
// Setter is a type constraint that requires that the type
// implement a Set method that sets the value from a string.
type Setter interface {
Set(string)
}
// FromStrings takes a slice of strings and returns a slice of T,
// calling the Set method to set each returned value.
//
// Note that because T is only used for a result parameter,
// type inference does not work when calling this function.
// The type argument must be passed explicitly at the call site.
//
// This example compiles but is unlikely to work as desired.
func FromStrings(type T Setter)(s []string) []T {
result := make([]T, len(s))
for i, v := range s {
result[i].Set(v)
}
return result
}
現(xiàn)在咒程,讓我們看看其他程序包中的一些代碼(此示例無(wú)效)鸠天。
// Settable is a integer type that can be set from a string.
type Settable int
// Set sets the value of *p from a string.
func (p *Settable) Set(s string) {
i, _ := strconv.Atoi(s) // real code should not ignore the error
*p = Settable(i)
}
func F() {
// INVALID
nums := FromStrings(Settable)([]string{"1", "2"})
// Here we want nums to be []Settable{1, 2}.
...
}
我們希望是用FromStrings
來(lái)獲取[]Settable
的切片。不幸的是帐姻,該示例無(wú)效稠集,無(wú)法編譯。
問(wèn)題是FromStrings
需要具有Set(string)
方法的類型饥瓷。函數(shù)F
是試圖通過(guò)Settable
實(shí)例化FromStrings
剥纷,但Settable
沒(méi)有Set
方法。有Set
方法的類型是*Settable
呢铆。
因此晦鞋,讓我們使用*Settable
改寫F
。
func F() {
// Compiles but does not work as desired.
// This will panic at run time when calling the Set method.
nums := FromStrings(*Settable)([]string{"1", "2"})
...
}
這可以成功編譯棺克,但不幸的是它將在運(yùn)行時(shí)拋出異常悠垛。問(wèn)題是FromStrings
創(chuàng)建了[]T
切片。當(dāng)使用*Settable
實(shí)例化時(shí)娜谊,表示類型為 []*Settable
确买。當(dāng)FromStrings
調(diào)用result[i].Set(v)
時(shí),會(huì)將存儲(chǔ)在result[i]
中的指針傳遞給Set
方法因俐。那個(gè)指針是nil
拇惋。Settable.Set
方法將由nil
接收方調(diào)用,并導(dǎo)致空指針異常抹剩。
我們需要的是寫一種FromStrings
方法撑帖,使得它可以將類型Settable
作為參數(shù)但可以調(diào)用指針?lè)椒ā?qiáng)調(diào)一遍澳眷,我們不能使用Settable
因?yàn)樗鼪](méi)有Set
方法胡嘿,我們也不能使用*Settable
因?yàn)槟菢游覀兙筒荒軇?chuàng)建Settable
切片。
一種可行的方法是使用兩個(gè)不同的類型參數(shù):Settable
和*Settable
钳踊。
package from
// Setter2 is a type constraint that requires that the type
// implement a Set method that sets the value from a string,
// and also requires that the type be a pointer to its type parameter.
type Setter2(type B) interface {
Set(string)
type *B
}
// FromStrings2 takes a slice of strings and returns a slice of T,
// calling the Set method to set each returned value.
//
// We use two different type parameters so that we can return
// a slice of type T but call methods on *T.
// The Setter2 constraint ensures that PT is a pointer to T.
func FromStrings2(type T interface{}, PT Setter2(T))(s []string) []T {
result := make([]T, len(s))
for i, v := range s {
// The type of &result[i] is *T which is in the type list
// of Setter2, so we can convert it to PT.
p := PT(&result[i])
// PT has a Set method.
p.Set(v)
}
return result
}
我們可以這樣調(diào)用FromStrings2
:
func F2() {
// FromStrings2 takes two type parameters.
// The second parameter must be a pointer to the first.
// Settable is as above.
nums := FromStrings2(Settable, *Settable)([]string{"1", "2"})
// Now nums is []Settable{1, 2}.
...
}
這種方法可以正常工作衷敌,但是很尷尬勿侯。它通過(guò)強(qiáng)制傳遞兩個(gè)類型參數(shù)解決FromStrings2
。第二個(gè)類型參數(shù)必須是第一個(gè)類型參數(shù)的指針缴罗。這是一個(gè)復(fù)雜的要求助琐,因?yàn)榭雌饋?lái)應(yīng)該是一個(gè)相當(dāng)簡(jiǎn)單的情況。
另一種方法是傳遞函數(shù)而不是調(diào)用方法面氓。
// FromStrings3 takes a slice of strings and returns a slice of T,
// calling the set function to set each returned value.
func FromStrings3(type T)(s []string, set func(*T, string)) []T {
results := make([]T, len(s))
for i, v := range s {
set(&results[i], v)
}
return results
}
我們可以像下面這樣調(diào)用Strings3
:
func F3() {
// FromStrings3 takes a function to set the value.
// Settable is as above.
nums := FromStrings3(Settable)([]string{"1", "2"},
func(p *Settable, s string) { p.Set(s) })
// Now nums is []Settable{1, 2}.
}
這種方法也可以按預(yù)期工作兵钮,但也很尷尬。調(diào)用者必須傳遞一個(gè)函數(shù)才能調(diào)用Set
方法舌界。這是我們?cè)谑褂梅盒蜁r(shí)希望避免的樣板代碼掘譬。
盡管這些方法很尷尬,但它們確實(shí)有效呻拌。就是說(shuō)葱轩,我們建議另一個(gè)解決此類問(wèn)題的功能:一種表示對(duì)類型參數(shù)的指針(而不是對(duì)類型參數(shù)本身)的約束的方法。完成此操作的方法是將type參數(shù)寫為指針類型:(type *T Constraint)藐握。
在類型參數(shù)列表中寫*T
而不是T
會(huì)改變兩件事靴拱。假設(shè)調(diào)用的類型參數(shù)為A
,并且約束為Constraint
(可以使用此語(yǔ)法而沒(méi)有約束趾娃,但是沒(méi)有理由這樣做)缭嫡。
更改的第一件事Constraint
是應(yīng)用于*A
而不是A
曹仗。即*A
必須實(shí)現(xiàn)Constraint
慨菱。A
可以實(shí)現(xiàn)Constraint
垃它,但是要求是*A
實(shí)現(xiàn)它瓤狐。請(qǐng)注意梆造,如果Constraint
有任何方法待德,則意味著A
一定不能是指針類型:如果A
是指針類型丁眼,則*A
是指向指針的指針锨亏,并且此類永遠(yuǎn)不會(huì)有任何方法炕泳。
更改的第二件事是纵诞,在函數(shù)體內(nèi)的任何方法Constraint
都被視為指針?lè)椒āV荒茉趖ype的*T
值或type的可尋址值上調(diào)用它們T
培遵。
// FromStrings takes a slice of strings and returns a slice of T,
// calling the Set method to set each returned value.
//
// We write *T, meaning that given a type argument A,
// the pointer type *A must implement Setter.
//
// Note that because T is only used for a result parameter,
// type inference does not work when calling this function.
// The type argument must be passed explicitly at the call site.
func FromStrings(type *T Setter)(s []string) []T {
result := make([]T, len(s))
for i, v := range s {
// result[i] is an addressable value of type T,
// so it's OK to call Set.
result[i].Set(v)
}
return result
}
同樣浙芙,使用*T
意味著給定類型參數(shù)A
,該類型*A
必須實(shí)現(xiàn)約束Setter
籽腕。在這種情況下嗡呼,Set
必須位于*A
中。在內(nèi)部FromStrings
皇耗,使用 *T
表示Set
只能在T
的可尋址值上調(diào)用南窗。
我們現(xiàn)在可以這樣使用
func F() {
// With the rewritten FromStrings, this is now OK.
// *Settable implements Setter.
nums := from.Strings(Settable)([]string{"1", "2"})
// Here nums is []Settable{1, 2}.
...
}
要明確的是,使用type *T Setter
并不意味著Set
方法只能是指針?lè)椒ā?code>Set仍然可能是一種普通方法。這樣就可以了万伤,因?yàn)樗兄祩鬟f方法也都在指針類型的方法集中窒悔。在此示例中,只有Set
可以將其編寫為值方法才有意義敌买,在包含指針字段的結(jié)構(gòu)上定義方法時(shí)可能就是這種情況简珠。
使用泛型類型作為匿名函數(shù)參數(shù)類型
當(dāng)將實(shí)例化類型解析為匿名函數(shù)參數(shù)類型時(shí),存在解析歧義虹钮。
var f func(x(T))
在此示例中北救,我們不知道該函數(shù)是否具有實(shí)例化類型的單個(gè)匿名參數(shù)x(T)
,或者這是否是(T)
類型的命名參數(shù)x
芜抒。
我們希望這表示前者:實(shí)例化類型的匿名參數(shù)x(T)
。實(shí)際上托启,這當(dāng)前語(yǔ)言并不向后兼容宅倒,這意味著后者。但是屯耸,當(dāng)前gofmt
程序會(huì)重寫func(x(T))
為func(x T)
拐迁,因此func(x(T))
在普通的Go代碼中非常不尋常。
因此疗绣,我們建議更改語(yǔ)言线召。這可能會(huì)破壞一些現(xiàn)有程序,但解決方法是簡(jiǎn)單地運(yùn)行g(shù)ofmt多矮。這可能會(huì)改變編寫沒(méi)有使用gofmt的func(x(T))
的程序的含義缓淹,而是選擇引入x
與帶有括號(hào)類型的函數(shù)參數(shù)同名的泛型。我們認(rèn)為塔逃,此類程序?qū)O為罕見讯壶。
盡管如此,這仍然是一種風(fēng)險(xiǎn)湾盗,如果風(fēng)險(xiǎn)似乎太大伏蚊,我們可以避免進(jìn)行此更改。
類型參數(shù)的值不被“裝箱”(boxed)
在Go的當(dāng)前實(shí)現(xiàn)中格粪,接口值始終包含指針躏吊。將非指針值放在接口變量中會(huì)導(dǎo)致該值被裝箱。這意味著實(shí)際值存儲(chǔ)在堆或堆棧上的其他位置帐萎,接口值保存指向該位置的指針比伏。
在這種設(shè)計(jì)中,泛型的值不會(huì)裝箱吓肋。例如凳怨,讓我們回顧一下前面的示例from.Strings
。當(dāng)用Settable
實(shí)例化時(shí),它返回[]Settable
肤舞。例如紫新,我們可以寫
// Settable is an integer type that can be set from a string.
type Settable int
// Set sets the value of *p from a string.
func (p *Settable) Set(s string) (err error) {
// same as above
}
func F() {
// The type of nums is []Settable.
nums, err := from.Strings(Settable)([]string{"1", "2"})
if err != nil { ... }
// Settable can be converted directly to int.
// This will set first to 1.
first := int(nums[0])
...
}
當(dāng)我們用Settable
類型調(diào)用from.Strings
時(shí),我們得到一個(gè)[]Settable
(和一個(gè)錯(cuò)誤)李剖。該切片的元素將是Settable
值芒率,也就是說(shuō),它們將是整數(shù)篙顺。即使它們是由泛型函數(shù)創(chuàng)建和設(shè)置的偶芍,也不會(huì)被裝箱。
同樣德玫,當(dāng)實(shí)例化泛型類型時(shí)匪蟀,它將具有預(yù)期的類型作為組件。
type Pair(type F1, F2) struct {
first F1
second F2
}
實(shí)例化該字段時(shí)宰僧,這些字段將不會(huì)被裝箱材彪,并且不會(huì)發(fā)生額外的內(nèi)存分配。類型Pair(int, string)
可以轉(zhuǎn)換為struct { first int; second string }
琴儿。
有關(guān)類型列表的更多信息
現(xiàn)在讓我們回顧類型列表段化,看一些相對(duì)的次要細(xì)節(jié)。這些不是額外規(guī)則或概念造成,而是類型列表工作方式的結(jié)果显熏。
約束中的類型列表和方法
約束可以同時(shí)使用類型列表和方法。
// StringableSignedInteger is a type constraint that matches any
// type that is both 1) defined as a signed integer type;
// 2) has a String method.
type StringableSignedInteger interface {
type int, int8, int16, int32, int64
String() string
}
此約束允許其基礎(chǔ)類型是列出的類型之一的任何類型晒屎,只要它也具有String() string
方法喘蟆。值得注意的是,雖然StringableSignedInteger
約束明確列出int
夷磕,類型int
不會(huì)本身允許作為類型參數(shù)履肃,因?yàn)?code>int沒(méi)有一個(gè)String
方法。允許的類型參數(shù)的示例是MyInt
坐桩,定義如下:
// MyInt is a stringable int.
type MyInt int
// The String method returns a string representation of mi.
func (mi MyInt) String() string {
return fmt.Sprintf("MyInt(%d)", mi)
}
約束中的復(fù)合類型
約束中的類型可以是字面上的類型尺棋。
type byteseq interface {
type string, []byte
}
遵循通常的規(guī)則:此約束的type參數(shù)可以是string
或[]byte
或定義為這些類型之一的類型;具有此約束的泛型函數(shù)可以使用string
和允許的任何操作[]byte
绵跷。
byteseq
約束允許編寫可用于string
或[]byte
類型的通用函數(shù)膘螟。
// Join concatenates the elements of its first argument to create a
// single value. sep is placed between elements in the result.
// Join works for string and []byte types.
func Join(type T byteseq)(a []T, sep T) (ret T) {
if len(a) == 0 {
// Use the result parameter as a zero value;
// see discussion of zero value in the Issues section.
return ret
}
if len(a) == 1 {
// We know that a[0] is either a string or a []byte.
// We can append either a string or a []byte to a []byte,
// producing a []byte. We can convert that []byte to
// either a []byte (a no-op conversion) or a string.
return T(append([]byte(nil), a[0]...))
}
// We can call len on sep because we can call len
// on both string and []byte.
n := len(sep) * (len(a) - 1)
for _, v := range a {
// Another case where we call len on string or []byte.
n += len(v)
}
b := make([]byte, n)
// We can call copy to a []byte with an argument of
// either string or []byte.
bp := copy(b, a[0])
for _, s := range a[1:] {
bp += copy(b[bp:], sep)
bp += copy(b[bp:], s)
}
// As above, we can convert b to either []byte or string.
return T(b)
}
類型列表中的類型參數(shù)
約束中的類型可以引用約束的類型參數(shù)。在此示例中碾局,泛型函數(shù)Map
采用兩個(gè)類型參數(shù)荆残。要求第一類型參數(shù)具有作為第二類型參數(shù)的切片的基礎(chǔ)類型。第二個(gè)slice參數(shù)沒(méi)有限制净当。
// SliceConstraint is a type constraint that matches a slice of
// the type parameter.
type SliceConstraint(type T) interface {
type []T
}
// Map takes a slice of some element type and a transformation function,
// and returns a slice of the function applied to each element.
// Map returns a slice that is the same type as its slice argument,
// even if that is a defined type.
func Map(type S SliceConstraint(E), E interface{})(s S, f func(E) E) S {
r := make(S, len(s))
for i, v := range s {
r[i] = f(v)
}
return r
}
// MySlice is a simple defined type.
type MySlice []int
// DoubleMySlice takes a value of type MySlice and returns a new
// MySlice value with each element doubled in value.
func DoubleMySlice(s MySlice) MySlice {
v := Map(MySlice, int)(s, func(e int) int { return 2 * e })
// Here v has type MySlice, not type []int.
return v
}
類型轉(zhuǎn)換
在具有兩個(gè)類型參數(shù)的函數(shù)From
和To
内斯,如果接受的From
約束可以被轉(zhuǎn)換類型To
的約束蕴潦,類型的值From
可以被轉(zhuǎn)換成類型的值To
。如果任何一個(gè)類型參數(shù)不接受類型俘闯,則不允許類型轉(zhuǎn)換潭苞。
通用規(guī)則的結(jié)果,通用規(guī)則可以使用類型列表中列出的所有類型允許的任何操作真朗。
例如:
type integer interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr
}
func Convert(type To, From integer)(from From) To {
to := To(from)
if From(to) != from {
panic("conversion out of range")
}
return to
}
Convert
允許進(jìn)行類型轉(zhuǎn)換此疹,因?yàn)镚o允許將每個(gè)整數(shù)類型都轉(zhuǎn)換為其他整數(shù)類型。
無(wú)類型常量
某些函數(shù)可以使用無(wú)類型的常量遮婶。如果類型參數(shù)約束所接受的每種類型都允許使用無(wú)類型常量蝗碎。
與類型轉(zhuǎn)換一樣,可以使用類型列表中列出的所有類型所允許的任何操作旗扑。
type integer interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr
}
func Add10(type T integer)(s []T) {
for i, v := range s {
s[i] = v + 10 // OK: 10 can convert to any integer type
}
}
// This function is INVALID.
func Add1024(type T integer)(s []T) {
for i, v := range s {
s[i] = v + 1024 // INVALID: 1024 not permitted by int8/uint8
}
}
類型列表中復(fù)合類型的注釋
尚不清楚我們是否完全理解類型列表中復(fù)合類型的使用蹦骑。例如
type structField interface {
type struct { a int; x int },
struct { b int; x float64 },
struct { c int; x uint64 }
}
func IncrementX(type T structField)(p *T) {
v := p.x
v++
p.x = v
}
這種對(duì)IncrementX
的類型參數(shù)的約束使得每個(gè)有效的類型參數(shù)都是具有某些數(shù)字類型字段x
的結(jié)構(gòu)。因此臀防,很容易說(shuō)這IncrementX
是有效的功能脊串。這意味著的類型v
是基于類型參數(shù)的類型,隱式約束為interface { type int, float64, uint64 }
清钥。這可能會(huì)變得相當(dāng)復(fù)雜,并且這里可能有一些我們不了解的細(xì)節(jié)放闺。
初始實(shí)現(xiàn)可能根本不支持類型列表中的復(fù)合類型祟昭,盡管這會(huì)使前面顯示的Join
示例無(wú)效。
嵌入約束中的類型列表
當(dāng)一個(gè)約束嵌入另一個(gè)約束時(shí)怖侦,最終約束的類型列表就是所涉及的所有類型列表的交集。如果存在多個(gè)嵌入式類型,則交集保留的類型參數(shù)必須滿足所有嵌入式類型要求的屬性坊夫。
// Addable is types that support the + operator.
type Addable interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64, complex64, complex128,
string
}
// Byteseq is a byte sequence: either string or []byte.
type Byteseq interface {
type string, []byte
}
// AddableByteseq is a byte sequence that supports +.
// This is every type is that is both Addable and Byteseq.
// In other words, just the type string.
type AddableByteseq interface {
Addable
Byteseq
}
類型列表的一般說(shuō)明
在約束中顯式列出類型似乎很尷尬碌秸,但是這樣很清楚,在調(diào)用上允許使用哪些類型參數(shù)艳悔,以及泛型函數(shù)允許使用哪些操作急凰。
如果語(yǔ)言后來(lái)更改為支持運(yùn)算符方法(目前尚無(wú)此類計(jì)劃),則約束將像處理其他方法一樣處理它們猜年。
預(yù)聲明類型總是會(huì)有數(shù)量限制抡锈,以及這些類型支持的數(shù)量有限的運(yùn)算符。將來(lái)的語(yǔ)言更改不會(huì)從根本上改變這些事實(shí)乔外,因此此方法將繼續(xù)有用床三。
這種方法不會(huì)嘗試處理所有可能的運(yùn)算符。尚不清楚它是否適用于復(fù)合類型杨幼。期望這些將在泛型函數(shù)和類型聲明中使用復(fù)合類型來(lái)處理聂渊,而不是要求復(fù)合類型作為類型參數(shù)。例如四瘫,我們希望要索引到切片T
的函數(shù)在slice元素類型上進(jìn)行參數(shù)化汉嗽,并使用type的參數(shù)或變量[]T
。
如上面的示例
DoubleMySlice所示莲组,這種方法使聲明接受并返回復(fù)合類型并想要返回與其參數(shù)類型相同的結(jié)果類型的泛型函數(shù)變得笨拙诊胞。定義的復(fù)合類型并不常見,但確實(shí)會(huì)出現(xiàn)锹杈。這種尷尬是這種方法的缺點(diǎn)撵孤。
反射
我們不建議以任何方式更改反射包。當(dāng)實(shí)例化類型或函數(shù)時(shí)竭望,所有類型參數(shù)將變?yōu)槠胀ǖ姆欠盒皖愋托奥搿?shí)例化類型String
的reflect.Type
值的方法將返回在括號(hào)中帶有類型參數(shù)的名稱。例如咬清,List(int)
闭专。
非泛型代碼不可能在不實(shí)例化的情況下引用泛型代碼,因此旧烧,沒(méi)有實(shí)例化的泛型類型或函數(shù)沒(méi)有反射信息影钉。
實(shí)踐
Russ Cox 的著作指出,泛型需要在慢速的程序員掘剪,慢速的編譯器或慢速的執(zhí)行時(shí)間之間進(jìn)行選擇平委。
我們認(rèn)為該設(shè)計(jì)允許不同的實(shí)現(xiàn)選擇《崴可以為每組類型實(shí)參分別編譯代碼廉赔,或者可以像對(duì)每個(gè)類型實(shí)參以類似于方法調(diào)用的接口類型進(jìn)行處理一樣進(jìn)行編譯,或者可以將兩者組合在一起匾鸥。
換句話說(shuō)蜡塌,這種設(shè)計(jì)放棄了讓程序員編程更慢,并絕對(duì)在慢速的編譯器(分別編譯每個(gè)類型的參數(shù)集)或慢速的執(zhí)行時(shí)間(對(duì)類型參數(shù)的值的每個(gè)操作使用方法調(diào)用)之間進(jìn)行決定勿负。 )馏艾。
總結(jié)
盡管本文檔冗長(zhǎng)且過(guò)于細(xì)節(jié),但實(shí)際設(shè)計(jì)僅涉及幾個(gè)要點(diǎn)奴愉。
- 函數(shù)和類型可以具有類型參數(shù)攒至,該類型參數(shù)是使用可選約束(接口類型)定義的。
- 約束描述了類型參數(shù)所需的方法和允許的類型躁劣。
- 約束描述了類型參數(shù)允許的方法和操作迫吐。
- 當(dāng)使用類型參數(shù)調(diào)用函數(shù)時(shí),類型推斷通常會(huì)允許省略類型參數(shù)账忘。
此設(shè)計(jì)完全向后兼容志膀,除了一個(gè)關(guān)于func F(x(T))
的修改建議熙宇。
我們相信,這種設(shè)計(jì)可以滿足人們對(duì)Go中泛型編程的需求溉浙,而不會(huì)使該語(yǔ)言變得不必要的復(fù)雜烫止。
在沒(méi)有多年實(shí)踐經(jīng)驗(yàn)的基礎(chǔ)上,我們無(wú)法真正了解這些對(duì)語(yǔ)言的影響戳稽。這里有一些猜測(cè)馆蠕。
復(fù)雜
Go的優(yōu)點(diǎn)之一就是它的簡(jiǎn)單性。顯然惊奇,這種設(shè)計(jì)使語(yǔ)言更加復(fù)雜互躬。
我們認(rèn)為,對(duì)于閱讀良好書面通用代碼而不是編寫書面代碼的人們來(lái)說(shuō)颂郎,增加的復(fù)雜性很小吼渡。人們自然必須學(xué)習(xí)用于聲明類型參數(shù)的新語(yǔ)法。這種新的語(yǔ)法以及對(duì)接口中類型列表的新支持是該設(shè)計(jì)中唯一的新語(yǔ)法構(gòu)造乓序。如下面的示例所示寺酪,泛型函數(shù)中的代碼讀取方式與普通的Go代碼類似。從[]int
到[]T
是一個(gè)簡(jiǎn)單的轉(zhuǎn)變替劈。類型參數(shù)約束可以有效地用作描述類型的文檔寄雀。
我們期望大多數(shù)軟件包不會(huì)定義泛型類型或函數(shù),但是許多軟件包可能會(huì)使用在其他地方定義的泛型類型或函數(shù)陨献。在通常情況下咙俩,泛型函數(shù)的工作原理與非泛型函數(shù)完全相同:你只需調(diào)用它們即可。類型推斷意味著你不必顯式寫出類型參數(shù)湿故。類型推斷規(guī)則的設(shè)計(jì)是毫不奇怪的:正確推斷出類型實(shí)參,或者調(diào)用失敗并需要顯式類型變量膜蛔。類型推斷使用類型標(biāo)識(shí)坛猪,沒(méi)有嘗試解析兩個(gè)相似但不相同的類型,這消除了相當(dāng)大的復(fù)雜性皂股。
使用泛型類型的程序包將必須傳遞顯式類型參數(shù)墅茉。語(yǔ)法很熟悉。唯一的變化是將參數(shù)傳遞給類型呜呐,而不是僅傳遞給函數(shù)就斤。
總的來(lái)說(shuō),我們?cè)噲D避免設(shè)計(jì)上的一些大動(dòng)作蘑辑。只有時(shí)間才能證明我們的設(shè)計(jì)是否成功洋机。
普及推廣
我們希望將一些新軟件包添加到標(biāo)準(zhǔn)庫(kù)中。一個(gè)新的slices
包將類似于現(xiàn)有的byte和string包洋魂,可在任何元素類型的切片上運(yùn)行绷旗。新 maps
和chans
包將提供簡(jiǎn)單的算法喜鼓,這些算法當(dāng)前已針對(duì)每種元素類型進(jìn)行了重復(fù)實(shí)現(xiàn)。set
可以添加一個(gè)程序包衔肢。
新constraints
程序包將提供標(biāo)準(zhǔn)約束庄岖,例如允許所有整數(shù)類型或所有數(shù)字類型的約束。
像包container/list
和container/ring
角骤,和類型隅忿,如sync.Map
和sync/atomic.Value
,將被更新為編譯時(shí)類型安全的邦尊,或者使用新的名稱或軟件包的更新版本背桐。
該math
軟件包將得到擴(kuò)展,為所有數(shù)值類型(例如流行Min
和Max
函數(shù))提供一組簡(jiǎn)單的標(biāo)準(zhǔn)算法胳赌。
我們可能會(huì)在sort
包裝中添加泛型變量牢撼。
可能會(huì)開發(fā)新的特殊用途的編譯時(shí)類型安全的容器類型。
我們不希望像C ++ STL迭代器類型這樣的方法被廣泛使用疑苫。在Go中熏版,這種想法更自然地使用接口類型表達(dá)。用C ++術(shù)語(yǔ)來(lái)說(shuō)捍掺,將接口類型用于迭代器可能帶有抽象損失撼短,因?yàn)檫\(yùn)行時(shí)效率將低于實(shí)際上內(nèi)聯(lián)所有代碼的C ++方法。
隨著我們獲得更多的容器類型挺勿,我們可能會(huì)開發(fā)一個(gè)標(biāo)準(zhǔn)Iterator
接口曲横。這可能反過(guò)來(lái)導(dǎo)致施加壓力,要求修改語(yǔ)言以添加一些Iterator
與with range
子句一起使用的機(jī)制不瓶。不過(guò)禾嫉,這是非常投機(jī)的。
效率
人們希望通過(guò)泛型代碼提高哪種效率尚不清楚蚊丐。
可以使用基于接口的方法來(lái)編譯泛型函數(shù)而不是泛型類型熙参。這樣可以優(yōu)化編譯時(shí)間,因?yàn)樵摵瘮?shù)僅編譯一次,但是會(huì)花費(fèi)一些運(yùn)行時(shí)間。
對(duì)于每個(gè)類型參數(shù)集洞辣,泛型類型可能會(huì)多次編譯押逼。顯然,這將帶來(lái)編譯時(shí)間成本,但不會(huì)帶來(lái)任何運(yùn)行時(shí)間成本。編譯器還可以選擇使用類似于接口類型的方法來(lái)實(shí)現(xiàn)泛型類型,使用專用方法訪問(wèn)依賴于類型參數(shù)的每個(gè)元素锐涯。
只有總結(jié)經(jīng)驗(yàn)才能知道人們對(duì)該領(lǐng)域的期望。
遺漏
我們認(rèn)為該設(shè)計(jì)涵蓋了泛型編程的基本要求填物。但是秀仲,還有許多不支持的構(gòu)造。
- 無(wú)法編寫與特定類型參數(shù)一起使用的泛型函數(shù)的多個(gè)版本壶笼。
- 沒(méi)有辦法編寫在編譯時(shí)執(zhí)行代碼以生成要在運(yùn)行時(shí)執(zhí)行的代碼神僵。
- 沒(méi)有更高級(jí)別的抽象。除了調(diào)用或?qū)嵗瘮?shù)外覆劈,沒(méi)有其他方法可以討論帶有類型參數(shù)的函數(shù)保礼。除了實(shí)例化泛型之外,沒(méi)有其他方法可以討論责语。
- 沒(méi)有泛型類型的描述炮障。為了在泛型函數(shù)中使用運(yùn)算符,約束列出了特定的類型坤候,而不是描述類型必須具有的特征胁赢。這很容易理解白筹,但有時(shí)可能會(huì)受到限制。
- 參數(shù)不支持協(xié)變或逆變(Contravariance or Covariance)。
- 你可以編寫一個(gè)編譯時(shí)類型安全的通用容器,但只能使用普通方法而不是使用
c[k]
語(yǔ)法來(lái)訪問(wèn)它。 - 除了使用輔助函數(shù)或包裝器類型外土辩,沒(méi)有其他方法指定一些類型參數(shù)启涯。
- 不支持可變參數(shù)類型參數(shù),只能通過(guò)函數(shù)接受不同數(shù)量的類型參數(shù)和常規(guī)參數(shù)。
- 沒(méi)有適配器。約束無(wú)法定義可用于支持尚未實(shí)現(xiàn)約束的類型參數(shù)的適配器,例如,根據(jù)
==
方法定義Equal
運(yùn)算符,反之亦然雅任。 - 沒(méi)有對(duì)非類型值(例如常量)進(jìn)行參數(shù)化禽车。這對(duì)于數(shù)組來(lái)說(shuō)最明顯逸月,有時(shí)可能很方便編寫
type Matrix(type n int) [n][n]float64
。為容器類型指定有效值有時(shí)也很有用,例如元素的默認(rèn)值。
Issues
此設(shè)計(jì)存在一些問(wèn)題,值得更詳細(xì)的討論窒朋。與整體設(shè)計(jì)相比欺劳,我們認(rèn)為這些問(wèn)題相對(duì)較小,但是仍然值得我們進(jìn)行完整的思考和討論。
零值
該設(shè)計(jì)沒(méi)有簡(jiǎn)單的表達(dá)式來(lái)表示類型參數(shù)的零值。例如递惋,考慮使用指針的可選值的以下實(shí)現(xiàn):
type Optional(type T) struct {
p *T
}
func (o Optional(T)) Val() T {
if o.p != nil {
return *o.p
}
var zero T
return zero
}
在o.p == nil
的情況下,我們想要返回T
的零值萍虽,但是我們無(wú)法編寫該值睛廊。可能寫為return nil
會(huì)很好,但是如果T
是int
财异,那是行不通的。在那種情況下一疯,我們將不得不寫return 0
十偶。而且狱窘,當(dāng)然躲胳,沒(méi)有辦法編寫約束來(lái)支持return nil
或 return 0
。
一些解決方法是:
-
var zero T
如上使用俊扭,它可以與現(xiàn)有設(shè)計(jì)一起使用溺蕉,但需要另外聲明伶丐。 - 使用
*new(T)
,這很神秘疯特,但可以與現(xiàn)有設(shè)計(jì)一起使用哗魂。 - 僅針對(duì)結(jié)果,將結(jié)果參數(shù)命名為
_
漓雅,并使用裸return
語(yǔ)句返回零值录别。 - 擴(kuò)展設(shè)計(jì)以允許使用
nil
任何匹配通用類型的零值(但請(qǐng)參見問(wèn)題22729)。 - 擴(kuò)展設(shè)計(jì)以允許使用
T{}
邻吞,其中T
是類型參數(shù)组题,以指示類型的零值。 - 如問(wèn)題19642中所建議抱冷,更改語(yǔ)言以允許
_
在分配(包括return
或函數(shù)調(diào)用)的右側(cè)使用往踢。 - 如問(wèn)題21182中所建議,更改語(yǔ)言以允許
return ...
返回結(jié)果類型的零值徘层。
我們認(rèn)為在決定確定哪種方法之前峻呕,還需要更多有關(guān)此設(shè)計(jì)的經(jīng)驗(yàn)。
很多惱人的括號(hào)
如果無(wú)法推斷出類型實(shí)參趣效,則使用類型實(shí)參調(diào)用函數(shù)需要附加的類型實(shí)參列表瘦癌。如果函數(shù)返回一個(gè)函數(shù),并且我們調(diào)用它跷敬,則會(huì)得到更多的括號(hào)讯私。
F(int, float64)(x, y)(s)
我們嘗試了其他語(yǔ)法,例如使用冒號(hào)將類型參數(shù)與常規(guī)參數(shù)分開西傀。在我們看來(lái)斤寇,當(dāng)前的設(shè)計(jì)是最好的,但可能會(huì)有更好的選擇拥褂。
定義復(fù)合類型
正如上面所討論的娘锁,當(dāng)一個(gè)額外的類型參數(shù)是必需的,其基礎(chǔ)類型是復(fù)合類型饺鹃,作為結(jié)果返回相同的是定義的類型莫秆。
例如间雀,此函數(shù)將在切片上映射一個(gè)函數(shù)。
// Map applies f to each element of s, returning a new slice
// holding the results.
func Map(type T)(s []T, f func(T) T) []T {
r := make([]T, len(s))
for i, v := range s {
r[i] = f(v)
}
return r
}
但是镊屎,在調(diào)用已定義類型時(shí)惹挟,它將返回該類型的元素類型的切片,而不是已定義類型本身缝驳。
// MySlice is a defined type.
type MySlice []int
// DoubleMySlice returns a new MySlice whose elements are twice
// that of the corresponding elements of s.
func DoubleMySlice(s MySlice) MySlice {
s2 := Map(s, func(e int) int { return 2 * e })
// Here s2 is type []int, not type MySlice.
return MySlice(s2)
}
正如上面所討論的连锯,這可以通過(guò)使用一個(gè)額外的類型參數(shù)Map
避免,并且使用描述切片和元件類型之間的關(guān)系所需的約束條件用狱。這種方式可行运怖,但很尷尬。
識(shí)別匹配的預(yù)聲明類型
該設(shè)計(jì)沒(méi)有提供任何方法來(lái)測(cè)試與類型實(shí)參匹配的基礎(chǔ)類型齿拂。代碼可以通過(guò)轉(zhuǎn)換為空接口類型并使用類型斷言或類型開關(guān)的某種笨拙方法來(lái)測(cè)試實(shí)際的類型參數(shù)。這可以讓代碼測(cè)試實(shí)際的與基礎(chǔ)類型不同的類型參數(shù)肴敛。
下面是一個(gè)顯示差異的示例署海。
type Float interface {
type float32, float64
}
func NewtonSqrt(type T Float)(v T) T {
var iterations int
switch (interface{})(v).(type) {
case float32:
iterations = 4
case float64:
iterations = 5
default:
panic(fmt.Sprintf("unexpected type %T", v))
}
// Code omitted.
}
type MyFloat float32
var G = NewtonSqrt(MyFloat(64))
在初始化G
時(shí),該代碼就會(huì)拋異常医男,因?yàn)樵?code>NewtonSqrt中的v
是MyFloat
砸狞,而不是float32
或float64
。該函數(shù)實(shí)際要測(cè)試的不是的類型v
镀梭,而是v
約束中匹配的類型刀森。
處理此問(wèn)題的一種方法是允許在類型T
上進(jìn)行類型切換,條件是該類型T
將始終與約束中定義的類型匹配报账。僅當(dāng)約束條件列出顯式類型時(shí)才允許這種類型的切換研底,并且僅允許約束條件中列出的類型作為情況使用。
無(wú)法表達(dá)可轉(zhuǎn)換性
該設(shè)計(jì)無(wú)法表達(dá)兩個(gè)不同類型參數(shù)之間的可轉(zhuǎn)換性透罢。例如榜晦,無(wú)法編寫此函數(shù):
// Copy copies values from src to dst, converting them as they go.
// It returns the number of items copied, which is the minimum of
// the lengths of dst and src.
// This implementation is INVALID.
func Copy(type T1, T2)(dst []T1, src []T2) int {
for i, x := range src {
if i > len(dst) {
return i
}
dst[i] = T1(x) // INVALID
}
return len(src)
}
從類型T2
到類型的轉(zhuǎn)換T1
是無(wú)效的,因?yàn)槿魏我环N類型都沒(méi)有允許轉(zhuǎn)換的約束羽圃。更糟糕的是乾胶,通常沒(méi)有辦法編寫這樣的約束。在T1
和T2
都可能需要某種類型列表的特殊情況下朽寞,可以像前面討論的使用類型列表進(jìn)行類型轉(zhuǎn)換時(shí)所述那樣編寫此函數(shù)识窿。但是例如,T1
是接口類型脑融,T2
實(shí)現(xiàn)了該接口的類型喻频,這類約束無(wú)法編寫。
值得注意的是肘迎,如果T1
是接口類型半抱,則可以使用對(duì)空接口類型的轉(zhuǎn)換和類型斷言來(lái)編寫泥耀,但這當(dāng)然不是編譯時(shí)類型安全的。
// Copy copies values from src to dst, converting them as they go.
// It returns the number of items copied, which is the minimum of
// the lengths of dst and src.
func Copy(type T1, T2)(dst []T1, src []T2) int {
for i, x := range src {
if i > len(dst) {
return i
}
dst[i] = (interface{})(x).(T1)
}
return len(src)
}
沒(méi)有參數(shù)化方法
本設(shè)計(jì)草案不允許方法聲明特定于該方法的類型參數(shù)躲履。接收器可能具有類型參數(shù)鸽心,但是該方法不能添加任何類型參數(shù)。
在Go中史简,方法的主要作用之一是允許類型實(shí)現(xiàn)接口乃秀。尚不清楚是否有可能允許參數(shù)化方法實(shí)現(xiàn)接口。例如圆兵,考慮以下代碼跺讯。此代碼使用了多個(gè)程序包使問(wèn)題更清楚。
package p1
// S is a type with a parameterized method Identity.
type S struct{}
// Identity is a simple identity method that works for any type.
func (S) Identity(type T)(v T) T { return v }
package p2
// HasIdentity is an interface that matches any type with a
// parameterized Identity method.
type HasIdentity interface {
Identity(type T)(T) T
}
package p3
import "p2"
// CheckIdentity checks the Identity method if it exists.
// Note that although this function calls a parameterized method,
// this function is not itself parameterized.
func CheckIdentity(v interface{}) {
if vi, ok := v.(p2.HasIdentity); ok {
if got := vi.Identity(int)(0); got != 0 {
panic(got)
}
}
}
package p4
import (
"p1"
"p3"
)
// CheckSIdentity passes an S value to CheckIdentity.
func CheckSIdentity() {
p3.CheckIdentity(p1.S{})
}
在此示例中殉农,我們有一個(gè)帶有參數(shù)化方法的S
類型刀脏,也有一個(gè)帶有參數(shù)化方法的HasIdentity
。S
實(shí)現(xiàn)了HasIdentity
超凳。因此愈污,函數(shù)p3.CheckIdentity
可以使用int
參數(shù)調(diào)用vi.Identity
,在此示例中為參數(shù)S.Identity(int)
轮傍。但是包p3對(duì)類型p1.S
一無(wú)所知暂雹。程序中可能沒(méi)有其他地方調(diào)用S.Identity
。我們需要實(shí)例化S.Identity(int)
创夜,但是如何做到杭跪?
我們可以在鏈接器時(shí)期實(shí)例化它,但是在一般情況下驰吓,它要求鏈接器遍歷程序的完整調(diào)用圖以確定可能傳遞給CheckIdentity
的類型集涧尿。甚至在涉及類型反射的一般情況下,這種遍歷也是不夠的檬贰,因?yàn)榉瓷淇赡軙?huì)根據(jù)用戶輸入的字符串查找方法现斋。因此,通常在鏈接器中實(shí)例化參數(shù)化方法可能需要為每個(gè)可能的類型實(shí)參實(shí)例化每個(gè)參數(shù)化方法偎蘸,這是不可行的庄蹋。
或者,我們可以在它的運(yùn)行時(shí)間實(shí)例化迷雪。通常限书,這意味著使用某種JIT,或編譯代碼以使用某種基于反射的方法章咧。每種方法的實(shí)施都非常復(fù)雜倦西,并且在運(yùn)行時(shí)會(huì)很慢。
或者赁严,我們可以確定參數(shù)化的方法實(shí)際上不實(shí)現(xiàn)接口扰柠,但是我們?yōu)槭裁葱枰椒ň筒磺宄朔垲怼H绻覀兒雎越涌冢瑒t任何參數(shù)化方法都可以實(shí)現(xiàn)為參數(shù)化函數(shù)卤档。
因此蝙泼,盡管乍看之下參數(shù)化方法似乎很有用,但我們必須確定它們的含義以及如何實(shí)現(xiàn)劝枣。
廢棄的想法
這種設(shè)計(jì)并不完美汤踏,隨著我們積累經(jīng)驗(yàn),它將進(jìn)一步完善舔腾。也就是說(shuō)溪胶,我們已經(jīng)詳細(xì)考慮了許多想法。本節(jié)列出了其中的一些想法稳诚,希望這將有助于減少重復(fù)的討論哗脖。這些想法以常見問(wèn)題解答的形式列出。
協(xié)議怎么樣了扳还?
較早的泛型設(shè)計(jì)草案使用新語(yǔ)言構(gòu)造實(shí)現(xiàn)了約束才避,稱之為協(xié)議(contracts)。類型列表僅出現(xiàn)在協(xié)議中普办,而不出現(xiàn)在接口類型上工扎。但是徘钥,許多人很難理解協(xié)議和接口類型之間的區(qū)別衔蹲。事實(shí)證明,協(xié)議可以表示為一組相應(yīng)的接口呈础。因此舆驶,沒(méi)有協(xié)議并不會(huì)減弱表達(dá)能力。我們決定簡(jiǎn)化為僅使用接口類型的方法而钞。
為什么不使用方法而是類型列表沙廉?
類型列表很奇怪。 為什么不為所有運(yùn)算符編寫方法臼节?
可以允許使用運(yùn)算符作為方法名稱撬陵,從而導(dǎo)致諸如+(T) T
的方法。不幸的是网缝,這還不夠巨税。我們需要某種機(jī)制來(lái)描述與任何整數(shù)類型匹配的類型,以便進(jìn)行不限于單個(gè)int類型的操作(例如粉臊,移位<<(integer) T
和索引編制[](integer) T
)草添。對(duì)于諸如之類的操作,我們需要一個(gè)無(wú)類型的布爾類型比如==(T) untyped bool
扼仲。我們需要為諸如轉(zhuǎn)換之類的操作引入新的符號(hào)远寸,或者表達(dá)一個(gè)可能在一種類型上變化的范圍抄淑,這可能需要一些新的語(yǔ)法。我們需要某種機(jī)制來(lái)描述無(wú)類型常量的有效值驰后。我們將不得不考慮是否支持< (T) bool
肆资,泛型函數(shù)也可以使用它<=
,并且類似地倡怎,是否支持+(T) T
表示函數(shù)也可以使用++
迅耘。使這種方法可行也許是可行的,但并非一帆風(fēng)順监署。此設(shè)計(jì)中使用的方法似乎更簡(jiǎn)單颤专,并且僅依賴于一個(gè)新的語(yǔ)法構(gòu)造(類型列表)和一個(gè)新的名稱(comparable
)。
為什么不將類型參數(shù)封裝成包钠乏?
我們對(duì)此進(jìn)行了廣泛的調(diào)查栖秕。當(dāng)你要編寫一個(gè)list
程序包,并且希望該程序包包含一個(gè)Transform
函數(shù)將List
一個(gè)元素類型轉(zhuǎn)換為另一個(gè)List
元素類型時(shí)晓避,這將成為問(wèn)題簇捍。包的一個(gè)實(shí)例化中的函數(shù)返回需要同一包的不同實(shí)例化的類型是非常尷尬的。
它還使包邊界與類型定義混淆俏拱。沒(méi)有特別的理由認(rèn)為泛型類型的使用會(huì)整齊地分解為包暑塑。有時(shí)他們會(huì),有時(shí)他們不會(huì)锅必。
為什么不使用的語(yǔ)法F<T>
類似于C ++和Java事格?
在解析函數(shù)(例如)v := F<T>
,看到的<
是不確定的搞隐,不知道我們看到的是類型實(shí)例化還是使用<
運(yùn)算符的表達(dá)式驹愚。解決這個(gè)問(wèn)題需要有效的無(wú)限制的預(yù)解析。我們希望努力保持Go解析器的效率劣纲。
為什么不使用語(yǔ)法F[T]
逢捺?
解析類型聲明時(shí)type A [T] int
,這是一個(gè)定義為泛型類型int
還是帶有T
元素的數(shù)組類型癞季,這是不確定的劫瞳。但是,這可以通過(guò)type A [type T] int
泛型來(lái)解決绷柒。
解析聲明func f(A[T]int)
(如類型的單個(gè)參數(shù)[T]int
)和func f(A[T], int)
(兩個(gè)參數(shù)志于,一個(gè)類型A[T]
和一個(gè)類型int
)的解析聲明表明,需要進(jìn)行一些額外的預(yù)解析辉巡。這是可解決的恨憎,但增加了解析的復(fù)雜性。
該語(yǔ)言通常在逗號(hào)分隔的列表中允許尾隨逗號(hào),因此對(duì)于A[T,]
如果A
是泛型憔恳,則應(yīng)允許使用瓤荔,但對(duì)于索引表達(dá)式通常不允許。但是钥组,解析器無(wú)法知道A
是泛型類型還是slice输硝,array或map類型的值,因此只有在類型檢查完成后才能報(bào)告此解析錯(cuò)誤程梦。同樣点把,可解決但復(fù)雜。
更普遍的認(rèn)知是屿附,我們覺(jué)得方括號(hào)在頁(yè)面上有點(diǎn)煩郎逃,括號(hào)更像Go。隨著我們獲得更多經(jīng)驗(yàn)挺份,我們未來(lái)會(huì)重新評(píng)估該決定褒翰。
為什么不使用F?T?
图柏?
我們考慮過(guò)了耻涛,但是我們無(wú)法要求使用非ASCII。
為什么不在內(nèi)置包中定義約束班缎?
不用寫出類型列表各聘,而使用像* constraints.Arithmetic
和constraints.Comparable
*這樣的名稱揣非。
列出所有可能的類型組合非常冗長(zhǎng)。它還引入了一組新名稱躲因,不僅是代碼的作者早敬,讀者也必須記住。此設(shè)計(jì)的驅(qū)動(dòng)目標(biāo)之一是引入盡可能少的新名稱毛仪。在此設(shè)計(jì)中搁嗓,我們僅引入一個(gè)新的預(yù)聲明的名稱芯勘。
我們希望箱靴,如果人們發(fā)現(xiàn)這樣的名稱有用,我們可以引入一個(gè)constraints
包荷愕,該包以約束的形式定義有用的名稱衡怀,這些約束可以由其他類型和函數(shù)使用并嵌入其他約束中。這將在標(biāo)準(zhǔn)庫(kù)中定義安疗,同時(shí)使程序員可以靈活地在適當(dāng)?shù)牡胤绞褂闷渌愋偷慕M合抛杨。
為什么不允許在類型為類型參數(shù)的值上使用類型聲明?
在此設(shè)計(jì)的早期版本中荐类,我們?cè)试S對(duì)類型為類型參數(shù)或基于類型參數(shù)的變量使用類型聲明和類型判斷怖现。我們刪除了此功能,因?yàn)榭梢詫⑷魏晤愋偷闹缔D(zhuǎn)換為空接口類型,然后在其上使用類型斷言或類型判斷屈嗤。同樣潘拨,有時(shí)會(huì)混淆,具有類型列表的約束中類型斷言饶号,還是類型切換將使用實(shí)際的類型實(shí)參而不是類型實(shí)參的基礎(chǔ)類型铁追。
與Java比較
關(guān)于Java泛型的大多數(shù)抱怨都圍繞類型擦除。此設(shè)計(jì)沒(méi)有類型擦除茫船。泛型類型的反射信息將包括完整的編譯時(shí)類型信息琅束。
在Java類型中,通配符(List<? extends Number>
算谈,List<? super Number>
)實(shí)現(xiàn)協(xié)變和逆變涩禀。Go缺少這些概念,這使得泛型類型更加簡(jiǎn)單然眼。
與C ++的比較
C ++模板不對(duì)類型參數(shù)施加任何約束(除非采用了概念上的建議)埋泵。這意味著更改模板代碼可能會(huì)意外破壞某處的實(shí)例。這也意味著僅在實(shí)例化時(shí)報(bào)告異常罪治,并且可能嵌套得很深并且難以理解丽声。這種設(shè)計(jì)通過(guò)明確的要求約束避免了這些問(wèn)題。
C ++支持模板元編程觉义,可以理解為為在編譯時(shí)使用與非模板C ++完全不同的語(yǔ)法進(jìn)行的普通編程雁社。我們的設(shè)計(jì)沒(méi)有類似功能,這樣可以降低大量的復(fù)雜性晒骇。
C ++使用兩階段名稱查找霉撵,其中一些名稱是在模板定義的上下文中查找的,而某些名稱是在模板實(shí)例化的上下文中查找的洪囤。在這種設(shè)計(jì)中徒坡,所有名稱都在編寫時(shí)進(jìn)行查找。
實(shí)際上瘤缩,所有C ++編譯器都在實(shí)例化模板時(shí)編譯每個(gè)模板喇完。這會(huì)減慢編譯時(shí)間。這種設(shè)計(jì)為如何處理泛型函數(shù)的編譯提供了靈活性剥啤。
與Rust比較
我們的設(shè)計(jì)中描述的泛型類似于Rust中的泛型锦溪。
一個(gè)區(qū)別是,在Rust中府怯,必須明確定義特征綁定和類型之間的關(guān)聯(lián)刻诊。用Go術(shù)語(yǔ),這意味著我們將不得不在某個(gè)地方聲明類型是否滿足約束牺丙。正如Go類型可以在沒(méi)有顯式聲明的情況下滿足Go接口一樣则涯,在本設(shè)計(jì)中,Go類型的參數(shù)可以在沒(méi)有顯式聲明的情況下滿足約束。
在這種設(shè)計(jì)使用類型列表的地方粟判,Rust標(biāo)準(zhǔn)庫(kù)為比較之類的操作定義了標(biāo)準(zhǔn)特征肖揣。這些標(biāo)準(zhǔn)特征由Rust的原始類型自動(dòng)實(shí)現(xiàn),也可以由用戶定義的類型實(shí)現(xiàn)浮入。Rust提供了相當(dāng)廣泛的特征列表龙优,至少包含34個(gè),涵蓋了所有運(yùn)算符事秀。
Rust支持方法上的類型參數(shù)彤断,而本設(shè)計(jì)不支持。
例子
以下各節(jié)是如何使用此設(shè)計(jì)的示例易迹。這旨在解決由于Go缺乏泛型而出現(xiàn)的特殊場(chǎng)景宰衙。
Map/Reduce/Filter
這是一個(gè)如何為切片編寫map, reduce, 和 filter 功能的示例。這些功能希望能夠提供在Lisp睹欲,Python供炼,Java等中的類似的功能。
// Package slices implements various slice algorithms.
package slices
// Map turns a []T1 to a []T2 using a mapping function.
// This function has two type parameters, T1 and T2.
// There are no constraints on the type parameters,
// so this works with slices of any type.
func Map(type T1, T2)(s []T1, f func(T1) T2) []T2 {
r := make([]T2, len(s))
for i, v := range s {
r[i] = f(v)
}
return r
}
// Reduce reduces a []T1 to a single value using a reduction function.
func Reduce(type T1, T2)(s []T1, initializer T2, f func(T2, T1) T2) T2 {
r := initializer
for _, v := range s {
r = f(r, v)
}
return r
}
// Filter filters values from a slice using a filter function.
// It returns a new slice with only the elements of s
// for which f returned true.
func Filter(type T)(s []T, f func(T) bool) []T {
var r []T
for _, v := range s {
if f(v) {
r = append(r, v)
}
}
return r
}
這是這些函數(shù)的一些調(diào)用示例窘疮。根據(jù)非類型參數(shù)的類型推斷類型參數(shù)袋哼。
s := []int{1, 2, 3}
floats := slices.Map(s, func(i int) float64 { return float64(i) })
// Now floats is []float64{1.0, 2.0, 3.0}.
sum := slices.Reduce(s, 0, func(i, j int) int { return i + j })
// Now sum is 6.
evens := slices.Filter(s, func(i int) bool { return i%2 == 0 })
// Now evens is []int{2}.
Map keys
這是獲取任何map鍵值的方法。
// Package maps provides general functions that work for all map types.
package maps
// Keys returns the keys of the map m in a slice.
// The keys will be returned in an unpredictable order.
// This function has two type parameters, K and V.
// Map keys must be comparable, so key has the predeclared
// constraint comparable. Map values can be any type;
// the empty interface type imposes no constraints.
func Keys(type K comparable, V interface{})(m map[K]V) []K {
r := make([]K, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}
在典型使用中闸衫,將推斷出映射key和val類型涛贯。
k := maps.Keys(map[int]int{1:2, 2:4})
// Now k is either []int{1, 2} or []int{2, 1}.
Sets
許多人要求擴(kuò)展或簡(jiǎn)化Go的內(nèi)置map類型以支持集合類型。下面的set類型是基于類型安全的實(shí)現(xiàn)蔚出,盡管它使用的是方法而不是類似的運(yùn)算符[]弟翘。
// Package set implements sets of any comparable type.
package set
// Set is a set of values.
type Set(type T comparable) map[T]struct{}
// Make returns a set of some element type.
func Make(type T comparable)() Set(T) {
return make(Set(T))
}
// Add adds v to the set s.
// If v is already in s this has no effect.
func (s Set(T)) Add(v T) {
s[v] = struct{}{}
}
// Delete removes v from the set s.
// If v is not in s this has no effect.
func (s Set(T)) Delete(v T) {
delete(s, v)
}
// Contains reports whether v is in s.
func (s Set(T)) Contains(v T) bool {
_, ok := s[v]
return ok
}
// Len reports the number of elements in s.
func (s Set(T)) Len() int {
return len(s)
}
// Iterate invokes f on each element of s.
// It's OK for f to call the Delete method.
func (s Set(T)) Iterate(f func(T)) {
for v := range s {
f(v)
}
}
示例:
// Create a set of ints.
// We pass (int) as a type argument.
// Then we write () because Make does not take any non-type arguments.
// We have to pass an explicit type argument to Make.
// Type inference doesn't work because the type argument
// to Make is only used for a result parameter type.
s := set.Make(int)()
// Add the value 1 to the set s.
s.Add(1)
// Check that s does not contain the value 2.
if s.Contains(2) { panic("unexpected 2") }
本示例說(shuō)明如何使用此設(shè)計(jì)為現(xiàn)有API提供編譯時(shí)類型安全的包裝器。
sort
在引入sort.Slice
之前骄酗,一個(gè)普遍的抱怨是需要使用樣板定義才能使用sort.Sort
稀余。通過(guò)這種設(shè)計(jì),我們可以如下所示添加到sort包中趋翻,:
// Ordered is a type constraint that matches all ordered types.
// (An ordered type is one that supports the < <= >= > operators.)
// In practice this type constraint would likely be defined in
// a standard library package.
type Ordered interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64,
string
}
// orderedSlice is an internal type that implements sort.Interface.
// The Less method uses the < operator. The Ordered type constraint
// ensures that T has a < operator.
type orderedSlice(type T Ordered) []T
func (s orderedSlice(T)) Len() int { return len(s) }
func (s orderedSlice(T)) Less(i, j int) bool { return s[i] < s[j] }
func (s orderedSlice(T)) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// OrderedSlice sorts the slice s in ascending order.
// The elements of s must be ordered using the < operator.
func OrderedSlice(type T Ordered)(s []T) {
// Convert s to the type orderedSlice(T).
// As s is []T, and orderedSlice(T) is defined as []T,
// this conversion is permitted.
// orderedSlice(T) implements sort.Interface,
// so can pass the result to sort.Sort.
// The elements will be sorted using the < operator.
sort.Sort(orderedSlice(T)(s))
}
現(xiàn)在我們可以寫:
s1 := []int32{3, 5, 2}
sort.OrderedSlice(s1)
// Now s1 is []int32{2, 3, 5}
s2 := []string{"a", "c", "b"})
sort.OrderedSlice(s2)
// Now s2 is []string{"a", "b", "c"}
同樣睛琳,我們可以添加一個(gè)使用比較函數(shù)進(jìn)行排序的函數(shù),類似于sort.Slice
但該函數(shù)偏向獲取值而不是切片索引嘿歌。
// sliceFn is an internal type that implements sort.Interface.
// The Less method calls the cmp field.
type sliceFn(type T) struct {
s []T
cmp func(T, T) bool
}
func (s sliceFn(T)) Len() int { return len(s.s) }
func (s sliceFn(T)) Less(i, j int) bool { return s.cmp(s.s[i], s.s[j]) }
func (s sliceFn(T)) Swap(i, j int) { s.s[i], s.s[j] = s.s[j], s.s[i] }
// SliceFn sorts the slice s according to the function cmp.
func SliceFn(type T)(s []T, cmp func(T, T) bool) {
Sort(sliceFn(E){s, cmp})
}
調(diào)用此示例如下:
var s []*Person
// ...
sort.SliceFn(s, func(p1, p2 *Person) bool { return p1.Name < p2.Name })
Channels
從未編寫過(guò)許多簡(jiǎn)單的泛型通道函數(shù)掸掏,因?yàn)樗鼈儽仨毷褂梅瓷鋪?lái)編寫茁影,并且調(diào)用者必須鍵入斷言結(jié)果宙帝。通過(guò)這種設(shè)計(jì),它們變得易于編寫募闲。
// Package chans implements various channel algorithms.
package chans
import "runtime"
// Ranger provides a convenient way to exit a goroutine sending values
// when the receiver stops reading them.
//
// Ranger returns a Sender and a Receiver. The Receiver provides a
// Next method to retrieve values. The Sender provides a Send method
// to send values and a Close method to stop sending values. The Next
// method indicates when the Sender has been closed, and the Send
// method indicates when the Receiver has been freed.
func Ranger(type T)() (*Sender(T), *Receiver(T)) {
c := make(chan T)
d := make(chan bool)
s := &Sender(T){values: c, done: d}
r := &Receiver(T){values: c, done: d}
// The finalizer on the receiver will tell the sender
// if the receiver stops listening.
runtime.SetFinalizer(r, r.finalize)
return s, r
}
// A Sender is used to send values to a Receiver.
type Sender(type T) struct {
values chan<- T
done <-chan bool
}
// Send sends a value to the receiver. It reports whether any more
// values may be sent; if it returns false the value was not sent.
func (s *Sender(T)) Send(v T) bool {
select {
case s.values <- v:
return true
case <-s.done:
// The receiver has stopped listening.
return false
}
}
// Close tells the receiver that no more values will arrive.
// After Close is called, the Sender may no longer be used.
func (s *Sender(T)) Close() {
close(s.values)
}
// A Receiver receives values from a Sender.
type Receiver(type T) struct {
values <-chan T
done chan<- bool
}
// Next returns the next value from the channel. The bool result
// reports whether the value is valid. If the value is not valid, the
// Sender has been closed and no more values will be received.
func (r *Receiver(T)) Next() (T, bool) {
v, ok := <-r.values
return v, ok
}
// finalize is a finalizer for the receiver.
// It tells the sender that the receiver has stopped listening.
func (r *Receiver(T)) finalize() {
close(r.done)
}
下一節(jié)將提供使用此功能的示例步脓。
容器
Go中對(duì)泛型的常見要求之一是能夠編寫編譯時(shí)類型安全的容器。這種設(shè)計(jì)使為現(xiàn)有容器編寫編譯時(shí)類型安全的包裝器變得容易。我們不會(huì)為此寫一個(gè)例子靴患。這種設(shè)計(jì)還使編寫不使用裝箱的編譯時(shí)類型安全的容器變得容易仍侥。
這是實(shí)現(xiàn)為二叉樹的有序映射的示例。它如何工作的細(xì)節(jié)不是太重要鸳君。要點(diǎn)是下面兩點(diǎn):
- 該代碼以Go風(fēng)格編寫农渊,并在需要時(shí)使用鍵和值類型。
- 鍵和值直接存儲(chǔ)在樹的節(jié)點(diǎn)中或颊,不使用指針砸紊,也不作為接口值裝箱。
// Package orderedmap provides an ordered map, implemented as a binary tree.
package orderedmap
import "chans"
// Map is an ordered map.
type Map(type K, V) struct {
root *node(K, V)
compare func(K, K) int
}
// node is the type of a node in the binary tree.
type node(type K, V) struct {
k K
v V
left, right *node(K, V)
}
// New returns a new map.
// Since the type parameter V is only used for the result,
// type inference does not work, and calls to New must always
// pass explicit type arguments.
func New(type K, V)(compare func(K, K) int) *Map(K, V) {
return &Map(K, V){compare: compare}
}
// find looks up k in the map, and returns either a pointer
// to the node holding k, or a pointer to the location where
// such a node would go.
func (m *Map(K, V)) find(k K) **node(K, V) {
pn := &m.root
for *pn != nil {
switch cmp := m.compare(k, (*pn).k); {
case cmp < 0:
pn = &(*pn).left
case cmp > 0:
pn = &(*pn).right
default:
return pn
}
}
return pn
}
// Insert inserts a new key/value into the map.
// If the key is already present, the value is replaced.
// Reports whether this is a new key.
func (m *Map(K, V)) Insert(k K, v V) bool {
pn := m.find(k)
if *pn != nil {
(*pn).v = v
return false
}
*pn = &node(K, V){k: k, v: v}
return true
}
// Find returns the value associated with a key, or zero if not present.
// The bool result reports whether the key was found.
func (m *Map(K, V)) Find(k K) (V, bool) {
pn := m.find(k)
if *pn == nil {
var zero V // see the discussion of zero values, above
return zero, false
}
return (*pn).v, true
}
// keyValue is a pair of key and value used when iterating.
type keyValue(type K, V) struct {
k K
v V
}
// InOrder returns an iterator that does an in-order traversal of the map.
func (m *Map(K, V)) InOrder() *Iterator(K, V) {
type kv = keyValue(K, V) // convenient shorthand
sender, receiver := chans.Ranger(kv)()
var f func(*node(K, V)) bool
f = func(n *node(K, V)) bool {
if n == nil {
return true
}
// Stop sending values if sender.Send returns false,
// meaning that nothing is listening at the receiver end.
return f(n.left) &&
sender.Send(kv{n.k, n.v}) &&
f(n.right)
}
go func() {
f(m.root)
sender.Close()
}()
return &Iterator{receiver}
}
// Iterator is used to iterate over the map.
type Iterator(type K, V) struct {
r *chans.Receiver(keyValue(K, V))
}
// Next returns the next key and value pair. The bool result reports
// whether the values are valid. If the values are not valid, we have
// reached the end.
func (it *Iterator(K, V)) Next() (K, V, bool) {
kv, ok := it.r.Next()
return kv.k, kv.v, ok
}
這個(gè)包的使用效果如下:
import "container/orderedmap"
// Set m to an ordered map from string to string,
// using strings.Compare as the comparison function.
var m = orderedmap.New(string, string)(strings.Compare)
// Add adds the pair a, b to m.
func Add(a, b string) {
m.Insert(a, b)
}
append
存在預(yù)先聲明的append
功能來(lái)替換樣板囱挑,否則需要增加切片醉顽。在append
添加之前,bytes包中有一個(gè)Add
函數(shù):
// Add appends the contents of t to the end of s and returns the result.
// If s has enough capacity, it is extended in place; otherwise a
// new array is allocated and returned.
func Add(s, t []byte) []byte
Add
將兩個(gè)[]byte
值附加在一起平挑,返回一個(gè)新的切片游添。這對(duì)于[]byte
很好,但是如果你有其他類型的切片通熄,則必須編寫基本相同的代碼以附加更多值唆涝。如果那時(shí)可以使用這種設(shè)計(jì),也許我們不會(huì)增加append
唇辨。相反石抡,我們可以這樣寫:
// Package slices implements various slice algorithms.
package slices
// Append appends the contents of t to the end of s and returns the result.
// If s has enough capacity, it is extended in place; otherwise a
// new array is allocated and returned.
func Append(type T)(s []T, t ...T) []T {
lens := len(s)
tot := lens + len(t)
if tot < 0 {
panic("Append: cap out of range")
}
if tot > cap(s) {
news := make([]T, tot, tot + tot/2)
copy(news, s)
s = news
}
s = s[:tot]
copy(s[lens:], t)
return s
}
該示例使用了預(yù)先聲明的copy
函數(shù),但是沒(méi)關(guān)系助泽,我們也可以編寫該函數(shù):
// Copy copies values from t to s, stopping when either slice is
// full, returning the number of values copied.
func Copy(type T)(s, t []T) int {
i := 0
for ; i < len(s) && i < len(t); i++ {
s[i] = t[i]
}
return i
}
這些功能可以按如下使用:
s := slices.Append([]int{1, 2, 3}, 4, 5, 6)
// Now s is []int{1, 2, 3, 4, 5, 6}.
slices.Copy(s[3:], []int{7, 8, 9})
// Now s is []int{1, 2, 3, 7, 8, 9}
這段代碼并未實(shí)現(xiàn)將string
追加或復(fù)制到[]byte
的特殊情況啰扛,因此它的效率不如預(yù)定義函數(shù)的實(shí)現(xiàn)那樣高效。該示例仍然表明嗡贺,使用此設(shè)計(jì)將允許append
和copy
使用泛型的方式編寫一次隐解,而無(wú)需任何其他特殊語(yǔ)言功能。
Metrics
在Go體驗(yàn)報(bào)告 中Sameer Ajmani描述了一種Metrics實(shí)現(xiàn)诫睬。每個(gè)指標(biāo)都有一個(gè)值和一個(gè)或多個(gè)字段煞茫。字段具有不同的類型。定義指標(biāo)需要指定字段的類型摄凡,并使用Add方法創(chuàng)建一個(gè)值朋其。Add方法將字段類型作為參數(shù)惯驼,并記錄該字段集的實(shí)例。C ++實(shí)現(xiàn)使用可變參數(shù)模板。Java實(shí)現(xiàn)包括類型名稱中的字段數(shù)体斩。C ++和Java實(shí)現(xiàn)都提供了編譯時(shí)類型安全的Add方法汇鞭。
下面是如何使用此設(shè)計(jì)在Go中實(shí)現(xiàn)編譯時(shí)類型安全的類似的Add方法婆廊。因?yàn)椴恢С挚勺償?shù)量的類型參數(shù),所以對(duì)于像Java中的不同數(shù)量的參數(shù)其弊,我們必須使用不同的名稱。此實(shí)現(xiàn)僅適用于可比較的類型膀斋。更復(fù)雜的實(shí)現(xiàn)可以接受比較函數(shù)以使用任意類型梭伐。
// Package metrics provides a general mechanism for accumulating
// metrics of different values.
package metrics
import "sync"
// Metric1 accumulates metrics of a single value.
type Metric1(type T comparable) struct {
mu sync.Mutex
m map[T]int
}
// Add adds an instance of a value.
func (m *Metric1(T)) Add(v T) {
m.mu.Lock()
defer m.mu.Unlock()
if m.m == nil {
m.m = make(map[T]int)
}
m.m[v]++
}
// key2 is an internal type used by Metric2.
type key2(type T1, T2 comparable) struct {
f1 T1
f2 T2
}
// Metric2 accumulates metrics of pairs of values.
type Metric2(type T1, T2 comparable) struct {
mu sync.Mutex
m map[key2(T1, T2)]int
}
// Add adds an instance of a value pair.
func (m *Metric2(T1, T2)) Add(v1 T1, v2 T2) {
m.mu.Lock()
defer m.mu.Unlock()
if m.m == nil {
m.m = make(map[key2(T1, T2)]int)
}
m.m[key2(T1, T2){v1, v2}]++
}
// key3 is an internal type used by Metric3.
type key3(type T1, T2, T3 comparable) struct {
f1 T1
f2 T2
f3 T3
}
// Metric3 accumulates metrics of triples of values.
type Metric3(type T1, T2, T3 comparable) struct {
mu sync.Mutex
m map[key3(T1, T2, T3)]int
}
// Add adds an instance of a value triplet.
func (m *Metric3(T1, T2, T3)) Add(v1 T1, v2 T2, v3 T3) {
m.mu.Lock()
defer m.mu.Unlock()
if m.m == nil {
m.m = make(map[key3(T1, T2, T3)]int)
}
m.m[key3(T1, T2, T3){v1, v2, v3}]++
}
// Repeat for the maximum number of permitted arguments.
使用此程序包如下所示:
import "metrics"
var m = metrics.Metric2(string, int){}
func F(s string, i int) {
m.Add(s, i) // this call is type checked at compile time
}
由于缺少對(duì)可變參數(shù)類型參數(shù)的支持,此實(shí)現(xiàn)具有一定的重復(fù)性仰担。但是糊识,使用該軟件包很容易,而且類型安全摔蓝。
列表轉(zhuǎn)換
雖然切片是高效且易于使用的技掏,但在某些情況下,使用鏈表更合適项鬼。此示例主要顯示將一種類型的鏈表轉(zhuǎn)換為另一種類型哑梳,作為使用同一泛型類型的不同實(shí)例的示例。
// Package list provides a linked list of any type.
package list
// List is a linked list.
type List(type T) struct {
head, tail *element(T)
}
// An element is an entry in a linked list.
type element(type T) struct {
next *element(T)
val T
}
// Push pushes an element to the end of the list.
func (lst *List(T)) Push(v T) {
if lst.tail == nil {
lst.head = &element(T){val: v}
lst.tail = lst.head
} else {
lst.tail.next = &element(T){val: v }
lst.tail = lst.tail.next
}
}
// Iterator ranges over a list.
type Iterator(type T) struct {
next **element(T)
}
// Range returns an Iterator starting at the head of the list.
func (lst *List(T)) Range() *Iterator(T) {
return Iterator(T){next: &lst.head}
}
// Next advances the iterator.
// It reports whether there are more elements.
func (it *Iterator(T)) Next() bool {
if *it.next == nil {
return false
}
it.next = &(*it.next).next
return true
}
// Val returns the value of the current element.
// The bool result reports whether the value is valid.
func (it *Iterator(T)) Val() (T, bool) {
if *it.next == nil {
var zero T
return zero, false
}
return (*it.next).val, true
}
// Transform runs a transform function on a list returning a new list.
func Transform(type T1, T2)(lst *List(T1), f func(T1) T2) *List(T2) {
ret := &List(T2){}
it := lst.Range()
for {
if v, ok := it.Val(); ok {
ret.Push(f(v))
}
if !it.Next() {
break
}
}
return ret
}
點(diǎn)積
通用點(diǎn)積實(shí)現(xiàn)绘盟,適用于任何數(shù)字類型的切片鸠真。
// Numeric is a constraint that matches any numeric type.
// It would likely be in a constraints package in the standard library.
type Numeric interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64,
complex64, complex128
}
// DotProduct returns the dot product of two slices.
// This panics if the two slices are not the same length.
func DotProduct(type T Numeric)(s1, s2 []T) T {
if len(s1) != len(s2) {
panic("DotProduct: slices of unequal length")
}
var r T
for i := range s1 {
r += s1[i] * s2[i]
}
return r
}
(注意:泛型實(shí)現(xiàn)方法可能會(huì)影響DotProduct
是否使用FMA,從而影響使用浮點(diǎn)類型時(shí)的確切結(jié)果龄毡。目前尚不清楚這是什么問(wèn)題吠卷,或者是否有任何方法可以解決。)
絕對(duì)差
通過(guò)使用Abs
方法來(lái)計(jì)算兩個(gè)數(shù)值之間的絕對(duì)差沦零。這使用了上一個(gè)示例中定義的Numeric
相同約束祭隔。
此示例使用了比用于計(jì)算絕對(duì)差的簡(jiǎn)單情況更多的組件。旨在說(shuō)明如何將算法的通用部分分解為使用方法的代碼路操,其中方法的確切定義可以根據(jù)所使用的類型的種類而變化疾渴。
// NumericAbs matches numeric types with an Abs method.
type NumericAbs(type T) interface {
Numeric
Abs() T
}
// AbsDifference computes the absolute value of the difference of
// a and b, where the absolute value is determined by the Abs method.
func AbsDifference(type T NumericAbs)(a, b T) T {
d := a - b
return d.Abs()
}
我們可以定義Abs
適合于不同數(shù)字類型的方法。
// OrderedNumeric matches numeric types that support the < operator.
type OrderedNumeric interface {
type int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64
}
// Complex matches the two complex types, which do not have a < operator.
type Complex interface {
type complex64, complex128
}
// OrderedAbs is a helper type that defines an Abs method for
// ordered numeric types.
type OrderedAbs(type T OrderedNumeric) T
func (a OrderedAbs(T)) Abs() OrderedAbs(T) {
if a < 0 {
return -a
}
return a
}
// ComplexAbs is a helper type that defines an Abs method for
// complex types.
type ComplexAbs(type T Complex) T
func (a ComplexAbs(T)) Abs() ComplexAbs(T) {
d := math.Hypot(float64(real(a)), float64(imag(a)))
return ComplexAbs(T)(complex(d, 0))
}
然后屯仗,我們可以通過(guò)與我們剛剛定義的類型之間進(jìn)行轉(zhuǎn)換來(lái)定義為調(diào)用者完成工作的函數(shù)搞坝。
// OrderedAbsDifference returns the absolute value of the difference
// between a and b, where a and b are of an ordered type.
func OrderedAbsDifference(type T OrderedNumeric)(a, b T) T {
return T(AbsDifference(OrderedAbs(T)(a), OrderedAbs(T)(b)))
}
// ComplexAbsDifference returns the absolute value of the difference
// between a and b, where a and b are of a complex type.
func ComplexAbsDifference(type T Complex)(a, b T) T {
return T(AbsDifference(ComplexAbs(T)(a), ComplexAbs(T)(b)))
}
值得注意的是,此設(shè)計(jì)的功能不足以編寫如下代碼:
// This function is INVALID.
func GeneralAbsDifference(type T Numeric)(a, b T) T {
switch (interface{})(a).(type) {
case int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64:
return OrderedAbsDifference(a, b) // INVALID
case complex64, complex128:
return ComplexAbsDifference(a, b) // INVALID
}
}
OrderedAbsDifference
和ComplexAbsDifference
的調(diào)用是無(wú)效的魁袜,因?yàn)椴皇撬袑?shí)現(xiàn)了Numeric
約束的類型可以實(shí)現(xiàn)OrderedNumeric
或Complex
約束桩撮。盡管類型切換意味著該代碼在概念上將在運(yùn)行時(shí)運(yùn)行,但是不支持在編譯時(shí)解析此代碼峰弹。這是表達(dá)上述遺漏之一的另一種方式:此設(shè)計(jì)未提供定制化店量。
附錄
本附錄涵蓋了設(shè)計(jì)的各種細(xì)節(jié),這些細(xì)節(jié)似乎沒(méi)有足夠的重要性在前面的部分中進(jìn)行介紹鞠呈。
泛型別名
type別名可以引用泛型融师,但是type別名可能沒(méi)有自己的參數(shù)。之所以存在此限制粟按,是因?yàn)樯胁磺宄绾问褂镁哂屑s束的類型參數(shù)來(lái)處理類型別名诬滩。
type VectorAlias = Vector
在這種情況下霹粥,類型別名的使用將必須提供適合于被別名化的通用類型的類型參數(shù)灭将。
var v VectorAlias(int)
類型別名也可以指代實(shí)例化的類型疼鸟。
type VectorInt = Vector(int)
實(shí)例化函數(shù)
Go通常允許在不傳遞任何參數(shù)的情況下引用函數(shù),從而產(chǎn)生函數(shù)類型的值庙曙。你不能使用具有類型參數(shù)的函數(shù)來(lái)執(zhí)行此操作空镜;必須在編譯時(shí)知道所有類型的參數(shù)。就是說(shuō)捌朴,你可以通過(guò)傳遞類型實(shí)參來(lái)實(shí)例化該函數(shù)吴攒,但不必調(diào)用實(shí)例化。這將產(chǎn)生一個(gè)沒(méi)有類型參數(shù)的函數(shù)值砂蔽。
// PrintInts is type func([]int).
var PrintInts = Print(int)
嵌入式類型參數(shù)
當(dāng)泛型類型是結(jié)構(gòu)體洼怔,并且類型參數(shù)作為字段嵌入在結(jié)構(gòu)體中時(shí),該字段的名稱就是類型參數(shù)的名稱左驾。
// A Lockable is a value that may be safely simultaneously accessed
// from multiple goroutines via the Get and Set methods.
type Lockable(type T) struct {
T
mu sync.Mutex
}
// Get returns the value stored in a Lockable.
func (l *Lockable(T)) Get() T {
l.mu.Lock()
defer l.mu.Unlock()
return l.T
}
// Set sets the value in a Lockable.
func (l *Lockable(T)) Set(v T) {
l.mu.Lock()
defer l.mu.Unlock()
l.T = v
}
內(nèi)聯(lián)約束
正如我們?cè)?code>interface{}用作類型約束的示例中所看到的镣隶,約束沒(méi)有必要使用命名接口類型。類型參數(shù)列表可以使用接口類型诡右。
// Stringify calls the String method on each element of s,
// and returns the results.
func Stringify(type T interface { String() string })(s []T) (ret []string) {
for _, v := range s {
ret = append(ret, v.String())
}
return ret
}
復(fù)合類型推斷
我們現(xiàn)在不建議使用此功能安岂,但可以考慮該語(yǔ)言的未來(lái)版本。
我們也可以考慮為泛型類型復(fù)合支持類型推斷帆吻。
type Pair(type T) struct { f1, f2 T }
var V = Pair{1, 2} // inferred as Pair(int){1, 2}
目前尚不清楚這在實(shí)際代碼中出現(xiàn)的頻率域那。
泛型函數(shù)參數(shù)的類型推斷
我們現(xiàn)在不建議使用此功能,但可以考慮使用更高版本猜煮。
在下面的例子中次员,考慮在FindClose
中調(diào)用Find
。類型推斷可以確定類型參數(shù)Find
是T4
王带,從我們知道的最后一個(gè)參數(shù)的類型必須是func(T4, T4) bool
翠肘,從我們可以推斷出類型參數(shù)IsClose
也必須T4
。但是辫秧,前面介紹的類型推斷算法無(wú)法做到這一點(diǎn)束倍,因此我們必須顯式編寫IsClose(T4)
。
乍一看這似乎很深?yuàn)W盟戏,在將泛型函數(shù)傳遞給泛型Map和Filter函數(shù)時(shí)會(huì)出現(xiàn)這種情況绪妹。
// Differ has a Diff method that returns how different a value is.
type Differ(type T1) interface {
Diff(T1) int
}
// IsClose returns whether a and b are close together, based on Diff.
func IsClose(type T2 Differ)(a, b T2) bool {
return a.Diff(b) < 2
}
// Find returns the index of the first element in s that matches e,
// based on the cmp function. It returns -1 if no element matches.
func Find(type T3)(s []T3, e T3, cmp func(a, b T3) bool) int {
for i, v := range s {
if cmp(v, e) {
return i
}
}
return -1
}
// FindClose returns the index of the first element in s that is
// close to e, based on IsClose.
func FindClose(type T4 Differ)(s []T4, e T4) int {
// With the current type inference algorithm we have to
// explicitly write IsClose(T4) here, although it
// is the only type argument we could possibly use.
return Find(s, e, IsClose(T4))
}
類型參數(shù)的反射
盡管我們不建議更改反射包,但將來(lái)可能要考慮的一種可能性添加兩個(gè)新方法,reflect.Type:NumTypeArgument() int
將類型實(shí)參的數(shù)量返回給類型柿究,TypeArgument(i) Type
返回第i個(gè)類型實(shí)參邮旷。NumTypeArgument
將為實(shí)例化的泛型類型返回非零值∮可以為reflect.Value
定義類似的方法婶肩,對(duì)于NumTypeArgument
實(shí)例化的泛型函數(shù)办陷,該方法將返回非零值÷杉撸可能會(huì)有某種程序會(huì)關(guān)心此信息民镜。
在類型字面量中實(shí)例化類型
在類型字面量的末尾實(shí)例化類型時(shí),存在解析歧義险毁。
x1 := []T(v1)
x2 := []T(v2){}
在此示例中制圈,第一種情況是將類型v1
轉(zhuǎn)換為type []T
。第二種情況是類型的復(fù)合字面量[]T(v2)
畔况,其中T
是我們使用type參數(shù)實(shí)例化的泛型類型v2
鲸鹦。歧義是在我們看到開放括號(hào)的那一刻:解析器不知道它是在看到類型轉(zhuǎn)換還是類似復(fù)合字面量的東西。
為了避免這種歧義跷跪,我們需要在類型文字的末尾加上類型實(shí)例化馋嗜。要編寫作為類型實(shí)例化切片的類型字面量,必須編寫[](T(v1))
吵瞻。如果沒(méi)有這些括號(hào)葛菇,[]T(x)
則將解析為([]T)(x)
,而不是[](T(x))
听皿。這僅適用于以類型名稱結(jié)尾的slice熟呛,array,map尉姨,chan和func類型字面量庵朝。
嵌入實(shí)例化的接口類型
將實(shí)例化的接口類型嵌入另一個(gè)接口類型時(shí),存在解析歧義又厉。
type I1(type T) interface {
M(T)
}
type I2 interface {
I1(int)
}
在此示例中九府,我們不知道interface I2
是否具有一個(gè)名為I1
,還是要嘗試將實(shí)例化類型I1(int)
嵌入到I2
中覆致。
為了向后兼容侄旬,我們將其視為前一種情況:I2
具有名為I1
的方法。
為了嵌入實(shí)例化的接口類型煌妈,我們要求使用額外的括號(hào)儡羔。
type I2 interface {
(I1(int))
}
當(dāng)前語(yǔ)法不允許這樣做,這將放寬現(xiàn)有規(guī)則璧诵。
將實(shí)例化類型嵌入結(jié)構(gòu)中也是如此汰蜘。
type S1 struct {
T(int) // field named T of type int
}
type S2 struct {
(T(int)) // embedded field of type T(int)
}