我在阅读泛型时遇到了PECS(Producer extends和Consumer super的缩写)。

有人能向我解释一下如何使用PECS来解决extends和super之间的混淆吗?


当前回答

PECS“规则”仅确保以下内容合法:

消费者:什么?它可以合法地指代T制片人:什么?是的,它可以在法律上被T

列表中的典型配对<?扩展T>生产者,列表<?super T>使用者只是确保编译器可以强制执行标准的“is-A”继承关系规则。如果我们可以合法地这样做,那么说<T extends?>、<?extended T>(或者在Scala中更好,如您所见,它是[-T],[+T]。不幸的是,我们能做的最好的是<?super T>,<?extended T>。

当我第一次遇到这种情况并在脑海中分解时,机制是有道理的,但代码本身对我来说仍然很困惑——我一直在想“似乎边界不应该像那样颠倒”——尽管我对上面的内容很清楚——这只是为了保证遵守标准参考规则。

帮助我的是将普通作业作为类比。

考虑以下(非生产就绪)玩具代码:

// copies the elements of 'producer' into 'consumer'
static <T> void copy(List<? extends T> producer, List<? super T> consumer) {
   for(T t : producer)
       consumer.add(t);
}

用任务类比来说明这一点,对于消费者来说?通配符(未知类型)是引用-赋值的“左侧”-和<?超级T>确保了什么?T“is-A”?-T可以分配给它,因为?是T的超类型(或最多相同类型)。

对于制片人来说,他们的担忧是一样的,只是颠倒了:制片人的?通配符(未知类型)是指代-赋值的“右手边”-和<?扩展T>确保了什么?是“IS-A”T-它可以分配给T,因为?是T的子类型(或至少相同类型)。

其他回答

PECS“规则”仅确保以下内容合法:

消费者:什么?它可以合法地指代T制片人:什么?是的,它可以在法律上被T

列表中的典型配对<?扩展T>生产者,列表<?super T>使用者只是确保编译器可以强制执行标准的“is-A”继承关系规则。如果我们可以合法地这样做,那么说<T extends?>、<?extended T>(或者在Scala中更好,如您所见,它是[-T],[+T]。不幸的是,我们能做的最好的是<?super T>,<?extended T>。

当我第一次遇到这种情况并在脑海中分解时,机制是有道理的,但代码本身对我来说仍然很困惑——我一直在想“似乎边界不应该像那样颠倒”——尽管我对上面的内容很清楚——这只是为了保证遵守标准参考规则。

帮助我的是将普通作业作为类比。

考虑以下(非生产就绪)玩具代码:

// copies the elements of 'producer' into 'consumer'
static <T> void copy(List<? extends T> producer, List<? super T> consumer) {
   for(T t : producer)
       consumer.add(t);
}

用任务类比来说明这一点,对于消费者来说?通配符(未知类型)是引用-赋值的“左侧”-和<?超级T>确保了什么?T“is-A”?-T可以分配给它,因为?是T的超类型(或最多相同类型)。

对于制片人来说,他们的担忧是一样的,只是颠倒了:制片人的?通配符(未知类型)是指代-赋值的“右手边”-和<?扩展T>确保了什么?是“IS-A”T-它可以分配给T,因为?是T的子类型(或至少相同类型)。

tl;博士:“PECS”是从藏品的角度来看的。如果您只是从泛型集合中提取项,那么它是一个生产者,您应该使用extends;如果你只是在填充物品,那么它是一个消费者,你应该使用super。如果同时使用同一集合,则不应使用extends或super。


假设您有一个方法,它以一个集合作为参数,但您希望它比只接受集合<Thing>更灵活。

案例1:你想通过收集并处理每个项目。那么列表是一个生产者,因此您应该使用集合<?扩展Thing>。

理由是集合<?extendsThing>可以保存Thing的任何子类型,因此当您执行操作时,每个元素都将表现为Thing。(实际上不能向集合中添加任何内容(null除外)<?扩展Thing>,因为您无法在运行时知道集合持有Thing的哪个特定子类型。)

案例2:您想要向集合中添加内容。那么列表是消费者,因此您应该使用集合<?超级事物>。

这里的理由是,不同于集合<?扩展Thing>,Collection<?super Thing>可以始终保存Thing,无论实际参数化类型是什么。在这里,只要它允许添加Thing,您就不在乎列表中已经存在什么;这是什么?super Thing保证。

PECS(生产者延伸和消费者超级)

[协方差和反方差]

让我们来看看示例

public class A { }
//B is A
public class B extends A { }
//C is A
public class C extends A { }

泛型允许您以安全的方式动态处理类型

//ListA
List<A> listA = new ArrayList<A>();

//add
listA.add(new A());
listA.add(new B());
listA.add(new C());

//get
A a0 = listA.get(0);
A a1 = listA.get(1);
A a2 = listA.get(2);
//ListB
List<B> listB = new ArrayList<B>();

//add
listB.add(new B());

//get
B b0 = listB.get(0);

问题

由于Java的Collection是一种引用类型,因此我们面临以下问题:

问题#1

//not compiled
//danger of **adding** non-B objects using listA reference
listA = listB;

*Swift的泛型没有这样的问题,因为Collection是Value类型[About],因此创建了一个新的集合

问题#2

//not compiled
//danger of **getting** non-B objects using listB reference
listB = listA;

解决方案-通用通配符

通配符是引用类型功能,不能直接实例化

解决方案#1<? super A>又称下界,又称反方差,又称消费者保证它由A和所有超类操作,这就是为什么添加

List<? super A> listSuperA;
listSuperA = listA;
listSuperA = new ArrayList<Object>();

//add
listSuperA.add(new A());
listSuperA.add(new B());

//get
Object o0 = listSuperA.get(0);

解决方案#2

<? 扩展A>aka上界aka协方差aka生产者保证它由A和所有子类操作,这就是为什么它是安全的

List<? extends A> listExtendsA;
listExtendsA = listA;
listExtendsA = listB;

//get
A a0 = listExtendsA.get(0);

这是我认为extends与super最清晰、最简单的方式:

扩展用于读取super是用来写作的

我发现“PECS”是一种不明显的方式来思考谁是“生产者”,谁是“消费者”。“PECS”是从数据集合本身的角度定义的——如果对象正在被写入到集合中,则集合“消耗”(它消耗来自调用代码的对象),如果对象正在从集合中读取,则它“产生”(它向某些调用代码产生对象)。这与其他所有事物的命名方式相反。标准JavaAPI是从调用代码的角度命名的,而不是从集合本身命名的。例如,java.util.List的以集合为中心的视图应该有一个名为“receive()”的方法,而不是“add()”——毕竟,调用代码添加了元素,但列表本身接收了元素。

我认为从与集合交互的代码的角度来思考事情更直观、更自然、更一致——代码是“从集合中读取”还是“写入”集合?之后,向集合写入的任何代码都将是“生产者”,从集合读取的任何代码将是“消费者”。

协方差:接受子类型相反:接受超类型

协变类型是只读的,而逆变类型是只写的。