为什么通道的方向变化不能兼容?

huangapple go评论106阅读模式
英文:

Why directional variations of channels of channels can't be compatible?

问题

我喜欢尽可能限制接口的编程方式,既可以避免错误的使用,又可以明确和自我记录。

因此,当用户需要单向使用时,我喜欢提供单向通道,但在内部当然会有双向通道的副本。

以下是要完成的任务:

var internal chan int
var external <-chan int
external = internal

但是现在我想向用户提供一个<-chan chan<- int类型(在函数返回值中),但以下代码无法工作:

var internal chan chan int
var external <-chan chan<- int
external = internal // 这个赋值会失败

我有两个问题:

  • 为什么这样做不起作用?
  • 那么,我可以声明一个<-chan chan<-类型的变量,但是...在实际意义上不能使用这样的类型吗?(因为尽管有单向通道,但据我所知,它们总是与双向通道一起用于编排,并且由于无法进行赋值,它们无法以这种方式使用)
英文:

I like to program providing interfaces as restrictively as possible, both to avoid bad usage as to be explicit and self-documented.

So, I like to provide directional channels when they are supposed to be used unidirectionally by the user, but of course, internally I have a bidirectional channel copy.

Assigment for the following works:

var internal chan int
var external &lt;-chan int
external = internal

But now I want to provide to the user a &lt;-chan chan&lt;- int type (in the return of a function), but the following won't work:

var internal chan chan int
var external &lt;-chan chan&lt;- int
external = internal // this fails

I have two questions:

  • Exactly why that doesn't work?
  • So, I can declare a variable of &lt;-chan chan&lt;- type, but... can't use such a type in any practical sense? (Because even though there're directional channels, they're AFAIK aways used in orchestration with bidirectional ones, and since assignment is not possible, they can't be used this way)

答案1

得分: 4

这不起作用的原因

规范对通道可分配性有如下说明:

> 当以下条件满足时,通道值 x 可以分配给类型为 T 的变量("x is assignable to T"):x 是双向通道值,T 是通道类型,x 的类型 VT 具有相同的元素类型,并且 VT 中至少有一个不是命名类型。

这正好反映了你所遇到的问题:

  • chan (chan int) 可以分配给 &lt;- chan (chan int)
  • chan (chan int) 不能分配给 &lt;- chan (chan&lt;- int)

原因是元素类型(在 chan 关键字之后的类型)不相等。

我可以声明它但不能使用它?

可以使用它,但不能按照你想要的方式。无法按照你的方式分配变量,但通过更正元素类型,你确实可以使用它:

var internal chan chan&lt;- int
var external &lt;-chan chan&lt;- int
external = internal

如果你只有 chan chan int 类型,你需要复制你的值(在 play 上的示例):

var internal chan chan int
var internalCopy chan chan&lt;- int

go func() { for e := range internal { internalCopy &lt;- e } }()

var external &lt;-chan chan&lt;- int
external = internalCopy
英文:

The reason why this does not work

The specification says this about channel assignability:

> A [channel] value x is assignable to a variable of type T ("x is assignable to T") [when] x is a bidirectional channel value, T is a channel type, x's type V and T have identical element types, and at least one of V or T is not a named type.

This reflects exactly what you're experiencing:

  • chan (chan int) to &lt;- chan (chan int) works
  • chan (chan int) to &lt;- chan (chan&lt;- int) does not

The reason for this is that the element types (the ones after the chan keyword)
are not equal.

I can declare it but not use it?

You can use it but not the way you want to. It is not possible to assign the variables
the way you do but by correcting the element types you can indeed use it:

var internal chan chan&lt;- int
var external &lt;-chan chan&lt;- int
external = internal

If you only have your chan chan int type you need to copy your values (Examples on play):

var internal chan chan int
var internalCopy chan chan&lt;- int

go func() { for e := range internal { internalCopy &lt;- e } }()

var external &lt;-chan chan&lt;- int
external = internalCopy

答案2

得分: 2

这是一个类似于在具有用户级泛型的语言中遇到的协变性和逆变性问题的案例。在使用Go的内部等效泛型类型(称为“复合类型”)时,您也可能会遇到这个问题。例如:

type A struct{}

// 所有的 `B` 都可以转换为 `A`
type B A

甚至是:

type A interface{}
// B 嵌入了 A
type B interface{ A }

var b B
// 这样做没有问题
var a A = A(b)

但考虑以下情况:

var bs []B
var as []A = ([]A)(bs)

在这里,编译失败并显示错误信息 cannot use bs (type []B) as type []A in assignment。尽管任何 B 都可以转换为等效的 A,但对于 []B[]A(或 chan Bchan A,或 map[string]Bmap[string]A,或 func(a A)func(b B) 或任何其他在其定义中使用 AB 的泛型类型),这并不成立。尽管类型是可转换的,但它们并不相同,而在Go中,这些“泛型”是这样工作的,根据规范:

> 每个类型 T 都有一个底层类型:如果 T 是预声明类型或类型字面量,则相应的底层类型是 T 本身。否则,T 的底层类型是 T 在其类型声明中引用的类型的底层类型。

type T1 string
type T2 T1
type T3 []T1
type T4 T3

> string、T1 和 T2 的底层类型是 string。[]T1、T3 和 T4 的底层类型是 []T1。

请注意,[]T1 的底层类型是 []T1,而不是 []string。这意味着 []T2 的底层类型将是 []T2,而不是 []string[]T1,这使得它们之间的转换是不可能的。

基本上,您正在尝试做类似于以下的事情:

var internal chan Type1
var external <-chan Type2
external = internal

由于类型系统认为 Type1Type2 是两种不同的类型,所以这将失败。

协变性和逆变性是非常困难的问题,正如维基百科文章的长度或在Java或C#中解开有界泛型层次所花费的时间所告诉您的那样。这是泛型实现如此困难并引发如此多争议的原因之一。

您可以通过在只读和读写通道之间的别名之间再深入一层来获得所需的行为,就像您在第一个示例中的 internal/external 通道上所做的那样:

package main

import "fmt"

// 这有您想要的正确签名
func ExportedFunction(c <-chan (chan<- int)) {
    // 向接收到的通道发送 1
    (<-c) <- 1
}

func main() {
    // 请注意,这是一个读/写通道的写入通道
    // 这样类型就正确了
    internal := make(chan (chan<- int))
    var external <-chan (chan<- int)
    // 这样做是可以的,因为通道中元素的类型是相同的
    external = internal

    // 这个通道是内部的,所以是读/写的
    internal2 := make(chan int)

    // 这通常是从外部调用的
    go ExportedFunction(external)

    fmt.Println("Sending channel...")
    // internal/external 的接收端看到的 internal2 的类型是只写通道
    internal <- internal2
    fmt.Println("We received:")
    fmt.Println(<-internal2)
}

与第一个示例基本相同,只是您必须在“读/写”与“只读(或写入)”别名之间再深入一层。

英文:

This is a case somewhat akin to the problems of covariance and contravariance encountered in languages with user-level generics. You can also encounter it in Go when using its internal equivalents of generic types (they're called 'composite types'). For instance:

type A struct{}

// All `B`s are convertible to `A`s
type B A

Or even:

type A interface{}
// B embeds A
type B interface{ A }

var b B
// This works without problem
var a A = A(b)

But consider the following case:

var bs []B
var as []A = ([]A)(bs)

Here, the compilation fails with error cannot use bs (type []B) as type []A in assignment. Although any B can be converted to the equivalent A, this does not hold true for []B and []A (or chan B and chan A, or map[string]B and map[string]A, or func(a A) and func(b B) or any other generic type using As and Bs in their definition). Although types are convertible to one another, they are not the same, and the way these generics work in Go is, from the spec:

> Each type T has an underlying type: If T is a predeclared type 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.

type T1 string
type T2 T1
type T3 []T1
type T4 T3

> The underlying type of string, T1, and T2 is string. The underlying type of []T1, T3, and T4 is []T1.

Note here that the underlying type of []T1 is []T1, not []string. This means that the underlying type of []T2 will be []T2, not []string or []T1, which makes conversion between them impossible.

Basically, you are trying to do something like:

var internal chan Type1
var external &lt;-chan Type2
external = internal

Which fails as Type1 and Type2 are two different types, as far as the type system is concerned.

Covariance and contravariance are very difficult problems, as the length of the wikipedia article or any time spent untangling layers of bounded generics in Java or C# will tell you. It is one of the reason generics are so difficult to implement and raise so many debate.

You can get the behaviour you want by going one level deeper on your aliases between read-only and read/write channels, exactly as you did with your internal/external channel on your first example:

package main

import &quot;fmt&quot;

// This has the correct signature you wanted
func ExportedFunction(c &lt;-chan (chan&lt;- int)) {
    // Sends 1 to the channel it receives
    (&lt;-c)&lt;- 1
}

func main() {
    // Note that this is a READ/WRITE channel of WRITE-ONLY channels
    // so that the types are correct
    internal := make(chan (chan&lt;- int))
    var external &lt;-chan (chan&lt;- int)
    // This works because the type of elements in the channel is the same
    external = internal

    // This channel is internal, so it is READ/WRITE
    internal2 := make(chan int)

    // This is typically called externally
    go ExportedFunction(external)

    fmt.Println(&quot;Sending channel...&quot;)
    // The type of internal makes the receiving end of internal/external
    // see a WRITE-ONLY channel
    internal &lt;- internal2
    fmt.Println(&quot;We received:&quot;)
    fmt.Println(&lt;-internal2)
}

The same thing on the playground.

Basically the same thing as your first example, except that you have to go one level deeper in the 'read/write' vs 'read(or write) only' aliases.

答案3

得分: 0

我怀疑这是不可能的,因为你需要同时转换外部和内部通道的类型。逐个转换是可行的。

我喜欢把这些发送/接收通道看作是限制函数中可以做的事情的一种好方法:你有一个 var c chan int,然后将它传递给一个 func f(ro <-chan int),现在在 f 中,你无法向 ro 发送数据。不需要显式类型转换。返回 c 也是一样:在 func g() <-chan int 中,你可以直接 return c。但无论如何,你必须约定通过通道传输的类型:可以是另一个双向通道、只发送或只接收的通道。一旦你将通道发送出去,你可以转换通道。

chan chan int 是一个整数通道的通道。你可以将其转换为只接收整数通道的通道 <-chan chan int,或者只发送整数通道的通道 chan<- chan int。无论如何,你发送或接收的始终是整数通道 chan int。在发送/接收之前/之后,可以将其转换为只发送/只接收,但不能将 chan chan int 转换为 chan chan<- int,就像将 chan chan int 转换为 chan chan int32 或者 chan string 一样。

英文:

I doubt that this is possible as you would have to convert the type of the outer and inner channel at the same time. One at a time works.

I like to think of these send/receive-only channels a a nice way to limit stuff you can do in a function: You have a var c chan int and you pass it to a func f(ro &lt;-chan int) and now in f you are save from sending to ro. No explicit type conversion needed. Same for returning c: You may just return c in func g() &lt;-chan int. But on any case you must agree what type to transfer via your your channel: This can be an other bidirectional channel, a send-only or a receive-only channel. Once you got that sent channel out you may convert the channel.

chan chan int is a channel of integer channels. You can convert this to a receive-only channel of integer channels &lt;-chan chan int or a send-only channel of integer channels chan&lt;- chan int. Anyway: What you send or receive is always a integer channel chan int. Each such can be converted to send/receive-only before/after sending/receiving but you cannot convert chan chan int to chan chan&lt;-int as this is like converting chan chan int to chan chan int32 or even chan string.

huangapple
  • 本文由 发表于 2013年12月11日 16:03:48
  • 转载请务必保留本文链接:https://go.coder-hub.com/20513661.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定