Що таке PECS (виробник розширює споживчий супер)?


729

Я натрапив на PECS (короткий для « Продюсер extendsта споживач»super ) під час читання генеричних матеріалів.

Чи може хтось пояснити мені, як використовувати PECS для усунення плутанини між extendsта super?


3
Дуже вдале пояснення з прикладом @ youtube.com/watch?v=34oiEq9nD0M&feature=youtu.be&t=1630, який пояснює superчастину, але дає уявлення про іншу.
lupchiazoem

Відповіді:


843

tl; dr: "PECS" - з точки зору колекції. Якщо ви лише витягуєте предмети із загальної колекції, це виробник, і вам слід користуватися extends; якщо ви тільки начиняєте речі, це споживач і вам слід користуватися super. Якщо ви обидва використовуєте одну і ту ж колекцію, ви не повинні використовувати ні те, extendsні super.


Припустимо, у вас є метод, який приймає за свій параметр набір речей, але ви хочете, щоб він був більш гнучким, ніж просто прийняття Collection<Thing>.

Випадок 1: Ви хочете пройти колекцію і зробити речі з кожним предметом.
Тоді цей список є виробником , тому вам слід скористатися Collection<? extends Thing>.

Аргументація полягає в тому, що a Collection<? extends Thing>може містити будь-який підтип Thing, і, таким чином, кожен елемент буде вести себе як Thingпри виконанні операції. (Ви насправді нічого не можете додати до Collection<? extends Thing>, оскільки ви не можете знати під час виконання, який саме підтип Thingколекції містить.)

Випадок 2: Ви хочете додати речі до колекції.
Тоді цей список є споживачем , тож вам слід скористатися Collection<? super Thing>.

Міркування тут полягає в тому, що, на відміну від цього Collection<? extends Thing>, Collection<? super Thing>завжди можна виконати Thingнезалежно від того, який фактично параметризований тип. Тут вам не байдуже, що вже є у списку, доки це дозволить Thingдодати а; ось що ? super Thingгарантує.


142
Я завжди намагаюся думати про це так: Виробнику дозволяється виробляти щось більш конкретне, отже, поширюється , споживачеві дозволяється приймати щось більш загальне, отже, супер .
Фейермурмель

10
Ще один спосіб запам’ятати відмінність виробника / споживача - продумати підпис методу. Якщо у вас є метод doSomethingWithList(List list), ви використовуєте список, і тому вам знадобиться коваріація / розширення (або інваріантний список). З іншого боку , якщо ваш метод List doSomethingProvidingList, то ви виробляти цей список і потребуватиме в контраваріаціі / супер (або інваріантне List).
Раман

3
@MichaelMyers: Чому ми не можемо просто використовувати параметризований тип для обох цих випадків? Чи є якась конкретна перевага використання тут макетів чи це просто засіб для поліпшення читабельності, подібний, скажімо, використання посилань на constпараметри методу в C ++ для позначення того, що метод не змінює аргументи?
Chatterjee

7
@Raman, я думаю, ти це просто переплутав. У doSthWithList (ви можете мати Список <? Super Thing>), оскільки ви є споживачем, ви можете використовувати super (пам'ятайте, CS). Однак це Список <? розширює Thing> getList (), оскільки вам дозволяється повернути щось більш конкретне при виробництві (PE).
masterxilo

4
@AZ_ Я поділяюся вашими почуттями. Якщо метод дійсно отримує () зі списку, метод вважатиметься Споживачем <T>, а список вважається постачальником; але правило PECS є "з точки зору списку", тому вимагається "розширення". Це повинен бути GEPS: отримати розширення; поставити супер.
Treefish Zhang

561

Принципи, які стоять за цим в інформатиці, називаються

  • Коваріації: ? extends MyClass,
  • Суперечність: ? super MyClassі
  • Інваріантність / недисперсія: MyClass

На малюнку нижче слід пояснити концепцію. З люб'язності: Андрій Тюкін

Коваріація проти противаріантності


143
Всім привіт. Я Андрій Тюкін, я просто хотів підтвердити, що anoopelias & DaoWen зв’язалися зі мною і отримали мій дозвіл на використання ескізу, це ліцензія під (CC) -BY-SA. Thx @ Anoop за надання йому другого життя -відмінність сайту ... Можливо, я повинен написати більш детальну відповідь, яка чітко показує, як цей ескіз застосовується до Яви ...
Андрій Тюкін

3
Це одне з найпростіших і найясніших пояснень коваріації та противагу, яке я коли-небудь знаходив!
cs4r

@Andrey Tyukin Привіт, я також хочу використовувати це зображення. Як я можу зв’язатися з вами?
слоуч

Якщо у вас є запитання щодо цієї ілюстрації, ми можемо обговорити їх у чаті: chat.stackoverflow.com/rooms/145734/…
Андрій Тюкін


48

PECS (виробник extendsта споживач super)

мнемонічний → Дістати та покласти принцип.

Цей принцип говорить, що:

  • Використовуйте підстановку з розширенням, коли ви отримуєте лише значення зі структури.
  • Використовуйте супер підстановку, коли ви лише вводите значення в структуру.
  • І не використовуйте підстановку, коли ви обидва отримаєте і покладете.

Приклад на Java:

class Super {

    Object testCoVariance(){ return null;} //Covariance of return types in the subtype.
    void testContraVariance(Object parameter){} // Contravariance of method arguments in the subtype.
}

class Sub extends Super {

    @Override
    String testCoVariance(){ return null;} //compiles successfully i.e. return type is don't care(String is subtype of Object) 
    @Override
    void testContraVariance(String parameter){} //doesn't support even though String is subtype of Object

}

Принцип заміщення Ліскова: якщо S є підтипом T, то об'єкти типу T можуть бути замінені об'єктами типу S.

У системі типів мови програмування діє правило набору тексту

  • коваріантний, якщо він зберігає впорядкування типів (≤), який упорядковує типи від більш конкретних до більш загальних;
  • противаріантний, якщо він скасовує це впорядкування;
  • інваріантний або неваріантний, якщо не застосовується жодне з них.

Коваріація та противаріантність

  • Типи даних (джерела) лише для читання можуть бути коваріантними ;
  • Типи даних лише для запису (раковини) можуть бути протилежними .
  • Змінні типи даних, які діють як джерела, так і раковини, повинні бути інваріантними .

Для ілюстрації цього загального явища розглянемо тип масиву. Для типу Animal ми можемо зробити тип Animal []

  • коваріант : Кішка [] - тварина [];
  • противаріант : тварина [] - кішка [];
  • інваріант : тварина [] не кішка [], а кіт [] не тварина [].

Приклади Java:

Object name= new String("prem"); //works
List<Number> numbers = new ArrayList<Integer>();//gets compile time error

Integer[] myInts = {1,2,3,4};
Number[] myNumber = myInts;
myNumber[0] = 3.14; //attempt of heap pollution i.e. at runtime gets java.lang.ArrayStoreException: java.lang.Double(we can fool compiler but not run-time)

List<String> list=new ArrayList<>();
list.add("prem");
List<Object> listObject=list; //Type mismatch: cannot convert from List<String> to List<Object> at Compiletime  

більше прикладів

обмежений (тобто, спрямований кудись) підстановочний код : Є три різні аромати символів:

  • Індикація / Недисперсія: ?або ? extends Object- Без обмеженого підстановки. Це означає сім'ю всіх типів. Використовуйте, коли ви отримуєте і кладете.
  • Ко-дисперсія: ? extends T(сімейство всіх типів, що є підтипами T) - макіяж із верхньою межею . Tце верхня -Велика клас в ієрархії успадкування. Використовуйте extendsпідстановку, коли ви отримуєте лише значення зі структури.
  • Контра-дисперсія: ? super T(сімейство всіх типів, що є супертипами T) - макіяж із нижньою межею . Tє найнижчим класом в ієрархії спадкування. Використовуйте superпідстановку, коли ви лише вводите значення в структуру.

Примітка: підстановка ?означає нуль або один раз , являє собою невідомий тип. Підстановочний знак може використовуватися як тип параметра, ніколи не використовується як аргумент типу для виклику загального методу, створення екземпляра загального класу. (Тобто, коли використовується wildcard, що посилання не використовується в іншому місці програми, як ми використовуємо T)

введіть тут опис зображення

class Shape { void draw() {}}

class Circle extends Shape {void draw() {}}

class Square extends Shape {void draw() {}}

class Rectangle extends Shape {void draw() {}}

public class Test {
 /*
   * Example for an upper bound wildcard (Get values i.e Producer `extends`)
   * 
   * */  

    public void testCoVariance(List<? extends Shape> list) {
        list.add(new Shape()); // Error:  is not applicable for the arguments (Shape) i.e. inheritance is not supporting
        list.add(new Circle()); // Error:  is not applicable for the arguments (Circle) i.e. inheritance is not supporting
        list.add(new Square()); // Error:  is not applicable for the arguments (Square) i.e. inheritance is not supporting
        list.add(new Rectangle()); // Error:  is not applicable for the arguments (Rectangle) i.e. inheritance is not supporting
        Shape shape= list.get(0);//compiles so list act as produces only

        /*You can't add a Shape,Circle,Square,Rectangle to a List<? extends Shape> 
         * You can get an object and know that it will be an Shape
         */         
    }
      /* 
* Example for  a lower bound wildcard (Put values i.e Consumer`super`)
* */
    public void testContraVariance(List<? super Shape> list) {
        list.add(new Shape());//compiles i.e. inheritance is supporting
        list.add(new Circle());//compiles i.e. inheritance is  supporting
        list.add(new Square());//compiles i.e. inheritance is supporting
        list.add(new Rectangle());//compiles i.e. inheritance is supporting
        Shape shape= list.get(0); // Error: Type mismatch, so list acts only as consumer
        Object object= list.get(0); // gets an object, but we don't know what kind of Object it is.

        /*You can add a Shape,Circle,Square,Rectangle to a List<? super Shape> 
        * You can't get an Shape(but can get Object) and don't know what kind of Shape it is.
        */  
    }
}

дженерики та приклади


Гей, я просто хотів дізнатися, що ви мали на увазі під час останньої сентенції: "Якщо ви вважаєте, що моя аналогія неправильна, будь ласка, оновіть". Ви маєте на увазі, якщо це етично неправильно (що є суб'єктивним) або якщо воно неправильне в контексті програмування (що об'єктивно: ні, це не неправильно)? Я хотів би замінити його більш нейтральним прикладом, який є загальноприйнятим, незалежно від культурних норм та етичних переконань; Якщо з вами це нормально.
Нейрон

нарешті я міг це отримати. Приємне пояснення.
Олег Куц

2
@Premraj, In-variance/Non-variance: ? or ? extends Object - Unbounded Wildcard. It stands for the family of all types. Use when you both get and put.я не можу додати елемент до списку <?> Або списку <? розширює Object>, тому я не розумію, чому це може бути Use when you both get and put.
LiuWenbin_NO.

1
@LiuWenbin_NO. - Ця частина відповіді вводить в оману. ?- "необмежений підмітний знак" - відповідає точно протилежному інваріантності. Будь ласка, зверніться до наступної документації: docs.oracle.com/javase/tutorial/java/generics/… де зазначено: У випадку, коли коду потрібно отримати доступ до змінної як змінної "in", так і "out", зробіть не використовувати підстановку. (Вони використовують "in" та "out" як синоніми "get" та "put"). За винятком nullви не можете додати до колекції, параметризовані на ?.
мишачі

29
public class Test {

    public class A {}

    public class B extends A {}

    public class C extends B {}

    public void testCoVariance(List<? extends B> myBlist) {
        B b = new B();
        C c = new C();
        myBlist.add(b); // does not compile
        myBlist.add(c); // does not compile
        A a = myBlist.get(0); 
    }

    public void testContraVariance(List<? super B> myBlist) {
        B b = new B();
        C c = new C();
        myBlist.add(b);
        myBlist.add(c);
        A a = myBlist.get(0); // does not compile
    }
}

Отже, "? Extends B" слід інтерпретувати як "? B extends". Це щось, що B поширюється так, що включає всі суперкласи B до Об'єкта, виключаючи B. Дякуємо за код!
Саурабх Патіл

3
@SaurabhPatil Ні, ? extends Bозначає B і все, що продовжує B.
Asgs

24

Як я пояснюю в моїй обороні на інше питання, PECS мнемонічне пристрій , створене Джош Блох , щоб допомогти згадати P roducer extends, C onsumer super.

Це означає, що коли параметризований тип, який передається методу, буде створювати екземпляри T(вони будуть витягнуті з нього певним чином), ? extends Tслід використовувати, оскільки будь-який примірник підкласу Tтакож є а T.

Коли параметризований тип, що передається методу, буде споживати екземпляри T(вони будуть передані йому, щоб зробити щось), ? super Tслід використовувати, тому що екземпляр Tюридично може бути переданий будь-якому методу, який приймає деякий супертип T. Наприклад, A Comparator<Number>може бути використаний на Collection<Integer>. ? extends Tне буде працювати, тому що a Comparator<Integer>не міг працювати на a Collection<Number>.

Зверніть увагу , що , як правило , ви повинні бути тільки з допомогою ? extends Tі ? super Tдля параметрів деякого методу. Методи повинні просто використовувати Tяк параметр типу для загального типу повернення.


1
Чи дотримується цей принцип лише для колекцій? Це має сенс, коли людина намагається співвіднести це зі списком. Якщо ви думаєте про підпис сорту (Список <T>, Порівняльник <? Супер T>) --->, то тут Компаратор використовує супер, це означає, що це споживач у контексті PECS. Якщо ви дивитесь на реалізацію, наприклад: public int порівняння (Person a, Person b) {return a.age <b.age? -1: a.age == b.age? 0: 1; } Я відчуваю, що Людина нічого не споживає, але призводить до віку. Це мене бентежить. Чи є недолік у моїх міркуваннях або PECS має право лише для колекцій?
Фатіх Арслан

23

Коротше кажучи, три простих правила запам'ятати PECS:

  1. Використовуйте <? extends T>підстановку, якщо вам потрібно отримати об'єкт типу Tз колекції.
  2. Використовуйте <? super T>підстановку, якщо вам потрібно помістити об'єкти типу Tв колекцію.
  3. Якщо вам потрібно задовольнити обидві речі, добре, не використовуйте жодні символи. Так просто.

10

припустимо цю ієрархію:

class Creature{}// X
class Animal extends Creature{}// Y
class Fish extends Animal{}// Z
class Shark extends Fish{}// A
class HammerSkark extends Shark{}// B
class DeadHammerShark extends HammerSkark{}// C

Давайте уточнимо, що ПЕ - виробник розширює:

List<? extends Shark> sharks = new ArrayList<>();

Чому ви не можете додати до цього списку об’єкти, що розширюють "Shark"? подібно до:

sharks.add(new HammerShark());//will result in compilation error

Оскільки у вас є список, який може бути типу A, B або C під час виконання , ви не можете додати до нього жодного об'єкта типу A, B або C, оскільки ви можете отримати комбінацію, яка заборонена в java.
На практиці компілятор дійсно може побачити, що ви додаєте B:

sharks.add(new HammerShark());

... але це не може сказати, чи під час виконання ваш B буде підтипом або супертипом типу списку. Під час виконання тип списку може бути будь-якого типу A, B, C. Отже, ви не можете додати, наприклад, HammerSkark (супер тип) до списку DeadHammerShark.

* Ви скажете: "Гаразд, але чому я не можу додати в нього HammerSkark, оскільки це найменший тип?". Відповідь: Це найменший ви знаєте. Але HammerSkark теж може поширити хтось інший, і ви опинитесь в тому ж сценарії.

Давайте уточнимо CS - Consumer Super:

У цій же ієрархії ми можемо спробувати це:

List<? super Shark> sharks = new ArrayList<>();

Що і чому ви можете додати до цього списку?

sharks.add(new Shark());
sharks.add(new DeadHammerShark());
sharks.add(new HammerSkark());

Ви можете додати вищевказані типи об’єктів, тому що все, що нижче акули (A, B, C), завжди буде підтипом чого-небудь вище акули (X, Y, Z). Легко зрозуміти.

Ви не можете додавати типи вище Shark, оскільки під час виконання тип доданого об'єкта може бути вищим за ієрархією, ніж оголошений тип списку (X, Y, Z). Це заборонено.

Але чому ви не можете прочитати з цього списку? (Я маю на увазі, що ви можете отримати елемент з нього, але ви не можете призначити його іншому, крім Об'єкта o):

Object o;
o = sharks.get(2);// only assignment that works

Animal s;
s = sharks.get(2);//doen't work

Під час виконання типом списку може бути будь-який тип вище A: X, Y, Z, ... Компілятор може скласти вашу заяву про призначення (що здається правильним), але під час виконання тип s (Animal) може бути нижчим у ієрархія, ніж оголошений тип списку (який може бути Creature або вище). Це заборонено.

Підсумовуючи

Ми використовуємо <? super T>для додавання об'єктів типів, рівних або нижче, Tдо List. Ми не можемо читати з нього.
Ми використовуємо <? extends T>для читання об'єктів типів, рівних чи нижчих Tзі списку. Ми не можемо додати елемент до нього.


9

(додаючи відповідь, тому що ніколи не вистачає прикладів з підказками Generics)

       // Source 
       List<Integer> intList = Arrays.asList(1,2,3);
       List<Double> doubleList = Arrays.asList(2.78,3.14);
       List<Number> numList = Arrays.asList(1,2,2.78,3.14,5);

       // Destination
       List<Integer> intList2 = new ArrayList<>();
       List<Double> doublesList2 = new ArrayList<>();
       List<Number> numList2 = new ArrayList<>();

        // Works
        copyElements1(intList,intList2);         // from int to int
        copyElements1(doubleList,doublesList2);  // from double to double


     static <T> void copyElements1(Collection<T> src, Collection<T> dest) {
        for(T n : src){
            dest.add(n);
         }
      }


     // Let's try to copy intList to its supertype
     copyElements1(intList,numList2); // error, method signature just says "T"
                                      // and here the compiler is given 
                                      // two types: Integer and Number, 
                                      // so which one shall it be?

     // PECS to the rescue!
     copyElements2(intList,numList2);  // possible



    // copy Integer (? extends T) to its supertype (Number is super of Integer)
    private static <T> void copyElements2(Collection<? extends T> src, 
                                          Collection<? super T> dest) {
        for(T n : src){
            dest.add(n);
        }
    }

4

Це найясніший і найпростіший спосіб для мене думати про розширення і супер:

  • extendsдля читання

  • superпризначений для написання

Я вважаю, що "PECS" є неочевидним способом думати про речі щодо того, хто є "виробником", а хто "споживачем". "PECS" визначається з точки зору самого збору даних - збірник "споживає", якщо до нього записуються об'єкти (він споживає об'єкти з кодів виклику), і "виробляє", якщо з нього читаються об'єкти (це виробляє об'єкти до якогось викликового коду). Це суперечить тому, як все інше названо. Стандартні API Java називаються з точки зору викликового коду, а не самої колекції. Наприклад, вигляд java.util.List, орієнтований на колекцію, повинен мати метод з назвою "prima ()" замість "add ()" - адже,елемент, але сам список отримує елемент.

Я думаю, що інтуїтивніше, природніше та послідовніше думати про речі з точки зору коду, який взаємодіє із колекцією - чи код "читає" чи "записує" до колекції? Після цього будь-який запис коду до колекції буде "виробником", а будь-яке зчитування коду з колекції буде "споживачем".


Я зіткнувся з тим же психічним зіткненням і буду схильний погодитися хіба що PECS не указ іменування коду і кордон типу самі є встановленої на деклараціях колекції. Крім того, що стосується імен, у вас часто є назви для створення / споживання колекцій, таких як srcі dst. Отже, ви маєте справу з кодом і контейнерами одночасно, і я в кінцевому підсумку роздумував над цим - "споживаючий код" споживає з контейнера, що виробляє, а "код коду" виробляє для споживача.
мишачі

4

"Правило" PECS просто гарантує, що наступне є законним:

  • Споживач: що б там ?не було, це може юридично посилатися T
  • Виробник: що б там ?не було, це може юридично посилатися T

Типове сполучення за принципами List<? extends T> producer, List<? super T> consumerпросто гарантує, що компілятор може виконувати стандартні правила відносин успадкування "IS-A". Якби ми могли це зробити на законних підставах, це може бути простіше сказати <T extends ?>, <? extends T>(а ще краще в Scala, як ви бачите вище, так [-T], [+T]. На жаль, найкраще, що ми можемо зробити - це) <? super T>, <? extends 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);
}

Ілюструючи це з точки зору аналогії призначення, для consumerв ?підстановки (невідомий тип) посилання - «ліва сторона» на поступки - і <? super T>гарантує , що все ?це, T«IS-A» ?- що Tможе бути покладено на нього, тому що ?це супер тип (або щонайбільше того ж типу) як T.

Для producerконцерну це те ж саме , це просто перевернуте: producer«s ?підстановлювальний (невідомий типу) є референтом -" права рука сторона "поступки - і <? extends T>гарантує , що все ?це,? " IS-A " T- що він може бути призначений на аT , тому що ?це підтип (або принаймні того ж типу) як T.


2

Запам'ятай це:

Споживач їсть вечерю (супер); Продюсер розширює завод своїх батьків


1

Використовуючи приклад із реального життя (з деякими спрощеннями):

  1. Уявіть вантажний поїзд із вантажними вагонами як аналогію до переліку.
  2. Ви можете покласти вантаж у вантажний вагон, якщо вантаж має той самий або менший розмір, ніж вантажний вагон =<? super FreightCarSize>
  3. Ви можете вивантажити вантаж з вантажного вагона, якщо у вашому складі достатньо місця (більше, ніж розмір вантажу) =<? extends DepotSize>

1

Коваріація : прийняти підтипи
Контраваріантність : прийняти супертипи

Коваріантні типи є лише для читання, тоді як противаріантні - лише для запису.


0

Розглянемо приклад

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

Generics дозволяє вам безпечно працювати з типами динамічно

//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 є еталонним типом, у нас виникають наступні проблеми:

Завдання №1

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

* У загальних даних Swift немає такої проблеми, оскільки колекція є Value type[About], тому створюється нова колекція

Завдання №2

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

Рішення - Generic Wildcards

Уайлдкард є характеристикою еталонного типу, і його неможливо встановити безпосередньо

Рішення №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

<? extends A>aka верхня межа aka коваріація ака виробники гарантує, що вона функціонує за допомогою A та всіх підкласів, тому її безпечно отримувати та передавати

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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.