简体   繁体   中英

What's the meaning of the new token ~ in Go (approximation elements)?

Go introduces the new token ~ .

~T means the set of all types with underlying type T

However, I could not understand it, ask someone to help explain.

The following is an example.

type Ordered interface {
      Integer | Float | ~string
}

In the generics proposal, the ~ tilde token is used to define approximation constraint elements. The generics proposal does a fine job of explaining what it is without going into language-lawery details:

Listing a single type is useless by itself. For constraint satisfaction, we want to be able to say not just int, but “any type whose underlying type is int”. [...] If a program uses type MyString string , the program can use the < operator with values of type MyString . It should be possible to instantiate [a function] with the type MyString .

If you want a formal reference, the tip version of the language specifications has placed the definition of underlying types in its own section :

Each type T has an underlying type: If T is one of the predeclared boolean, numeric, or string types, or a type literal, the corresponding underlying type is T itself. Otherwise, T's underlying type is the underlying type of the type to which T refers in its type declaration .

This covers the very common cases of type literals and other composite types with bound identifiers, or types you define over predeclared identifiers, which is the case mentioned in the generics proposal:

// underlying type = struct literal -> itself -> struct { n int }
type Foo struct {
    n int
}

// underlying type = slice literal -> itself -> []byte
type ByteSlice []byte

// underlying type = predeclared -> itself -> int8
type MyInt8 int8

// underlying type = predeclared -> itself -> string
type MyString string

The practical implication is that an interface constraint whose type set has only exact elements will not allow your own defined types:

// hypothetical constraint without approximation elements
type ExactSigned interface {
    int | int8 | int16 | int32 | int64
}

// CANNOT instantiate with MyInt8
func echoExact[T ExactSigned](t T) T { return t }

// constraints.Signed uses approximation elements e.g. ~int8
// CAN instantiate with MyInt8
func echo[T constraints.Signed](t T) T { return t }

As with other constraint elements, you can use the approximation elements in unions, as in constraints.Signed or in anonymous constraints with or without syntactic sugar. Notably the syntactic sugar with only one approx element is valid:

// anonymous constraint
func echoFixedSize[T interface { ~int8 | ~int32 | ~int64 }](t T) T { 
    return t 
}

// anonymous constraint with syntactic sugar
func echoFixedSizeSugar[T ~int8 | ~int32 | ~int64](t T) T { 
    return t 
}

// anonymous constraint with syntactic sugar and one elem
func echoFixedSizeSugarOne[T ~int8](t T) T { 
    return t 
}

As anticipated above, common use case for approximation elements is with composite types (slices, structs, etc.) that need to have methods. In that case you must bind the identifier:

// must bind identifier in order to declare methods
type ByteSeq []byte

func (b ByteSeq) DoSomething() {}

and now the approximation element is handy to allow instantiation with ByteSeq :

// ByteSeq not allowed, or must convert func argument first
func foobar[T interface { []byte }](t T) { /* ... */ }


// ByteSeq allowed
func bazquux[T interface { ~[]byte }](t T) { /* ... */ }

func main() {
    b := []byte{0x00, 0x01}
    seq := ByteSeq{0x02, 0x03}

    foobar(b)           // ok
    foobar(seq)         // compiler error
    foobar([]byte(seq)) // ok, allows inference
    foobar[[]byte](seq) // ok, explicit instantiation, then can assign seq to argument type []byte

    bazquux(b)          // ok
    bazquux(seq)        // ok
}

NOTE : you can not use the approximation token with a type parameter:

// INVALID!
type AnyApprox[T any] interface {
    ~T
}

There is not just the new token, but the new syntax for interfaces. You can declare an interface with type constraints in addition to method constraints.

To satisfy an interface, the type must satisfy both the method constraints and the type constraints.

From the docs :

An interface representing all types with underlying type int which implement the String method.

 interface { ~int String() string }

For a type to have an "underlying type" of int , that means the type takes the following form:

type SomeType int

And to satisfy the method constraint, there must be declared a method with the specified signature:

func (v SomeType) String() string {
  return fmt.Sprintf("%d", v)
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM