Коли ви хочете дві посилання на один і той же об’єкт?


20

В Java конкретно, але, ймовірно, і в інших мовах: коли було б корисно мати два посилання на один і той же об’єкт?

Приклад:

Dog a = new Dog();
Dob b = a;

Чи є ситуація, коли це було б корисно? Чому це було б кращим рішенням для використання, aколи ви хочете взаємодіяти з об'єктом, представленим a?


Це суть за схемою Flyweight .

@MichaelT Ви могли б детальніше розробитись?
Басинатор

7
Якщо aі b завжди посилаються на одне і те ж Dog, то в цьому немає жодного сенсу. Якщо вони іноді можуть, то це дуже корисно.
Гейб

Відповіді:


45

Наприклад, якщо ви хочете мати один і той же об'єкт у двох окремих списках :

Dog myDog = new Dog();
List dogsWithRabies = new ArrayList();
List dogsThatCanPlayPiano = new ArrayList();

dogsWithRabies.add(myDog);
dogsThatCanPlayPiano.add(myDog);
// Now each List has a reference to the same dog

Ще одне використання - коли у вас один і той же об’єкт, який грає кілька ролей :

Person p = new Person("Bruce Wayne");
Person batman = p;
Person ceoOfWayneIndustries = p;

4
Мені дуже шкода, але я вважаю, що для вашого прикладу Брюса Уейна є недоліки дизайну, позиція Бетмена та Цео повинна бути роллю для вашої людини.
Сільвіу Бурча

44
-1 для розкриття таємної особи Бетмена.
квітня

2
@Silviu Burcea - Я твердо погоджуюся, що приклад Брюса Уейна - не гарний приклад. З одного боку, якщо глобальне редагування тексту змінило назви 'ceoOfWayneIndustries' і 'batman' на 'p' (якщо не було зіткнень імені чи зміни обсягу), а семантика програми змінилася, то їх щось щось порушено. Об'єкт, на який посилається, представляє реальне значення, а не ім'я змінної в програмі. Щоб мати різну семантику, це або інший об'єкт, або посилається на щось з більшою поведінкою, ніж на посилання (яке повинно бути прозорим), і тому не є прикладом 2+ посилань.
gbulmer

2
Хоча приклад Брюса Уейна як написаний може не спрацювати, я вважаю, що заявлений намір є правильним. Можливо, більш близький приклад може бути Persona batman = new Persona("Batman"); Persona bruce = new Persona("Bruce Wayne"); Persona currentPersona = batman;- де у вас є кілька можливих значень (або список доступних значень) та посилання на поточно активний / вибраний.
Dan Puzey

1
@gbulmer: Я б сказав, що ви не можете мати посилання на два об'єкти; опорні currentPersonaточки до одного чи іншого об'єкта, але ніколи і того і іншого. Зокрема, це може легко бути можливо , що currentPersonaніколи і НЕ встановлено bruce, в цьому випадку це, звичайно , не є посиланням на два об'єкти. Я б сказав, що в моєму прикладі обидва batmanі currentPersonaпосилаються на один і той же екземпляр, але служать різному семантичному значенню в рамках програми.
Dan Puzey

16

Це насправді напрочуд глибоке питання! Досвід сучасних C ++ (та мов, які беруть із сучасних C ++, таких як Rust) говорить про те, що дуже часто ви цього не хочете! Для більшості даних потрібно мати єдину або унікальну ("володіючу") посилання. Ця ідея також є однією з головних причин систем лінійного типу .

Однак, навіть тоді ви хочете отримати кілька короткочасних "запозичених" посилань, які використовуються для короткого доступу до пам'яті, але не тривають протягом значної частини часу існування даних. Найчастіше, коли ви передаєте об'єкт як аргумент іншій функції (Параметри теж змінні!):

void encounter(Dog a) {
  hissAt(a);
}

void hissAt(Dog b) {
  // ...
}

Менш поширений випадок, коли ви використовуєте один з двох об'єктів залежно від умови, роблячи по суті те ж саме, незалежно від того, який ви вибрали:

Dog a, b;
Dog goodBoy = whoseAGoodBoy ? a : b;
feed(goodBoy);
walk(goodBoy);
pet(goodBoy);

Повертаючись до більш поширених застосувань, але залишаючи позаду локальні змінні, ми переходимо до полів: Наприклад, у віджетів у структурах графічного інтерфейсу часто є батьківські віджети, тому ваш великий кадр, що містить десять кнопок, повинен мати принаймні десять посилань, що вказують на нього (плюс ще кілька від його батьків і, можливо, від слухачів подій тощо). Будь-який тип об'єктного графіка та деякі види дерев об'єктів (ті, у яких посилання на батьків / братів і сестер) мають декілька об'єктів, що стосуються одного і того ж об'єкта. І практично кожен набір даних насправді є графіком ;-)


3
"Менш поширений випадок" є досить поширеним: ви маєте об'єкти, які зберігаються у списку чи карті, витягніть потрібний та виконайте необхідні операції. Ви не стираєте їх посилання з карти чи списку.
SJuan76

1
@ SJuan76 "Менш поширений випадок" стосується виключно використання локальних змінних, все, що стосується структур даних, підпадає під останню точку.

Це ідеал лише однієї посилання через управління пам’яттю з підрахунком посилань? Якщо так, то варто згадати, що мотивація не стосується інших мов з різними збирачами сміття (наприклад, C #, Java, Python)
MarkJ

@MarkJ Лінійні типи - це не просто ідеал, багато існуючий код невідомо відповідає йому, тому що це семантично правильна річ у досить багатьох випадках. У нього є потенційні переваги щодо продуктивності (але ні для підрахунку посилань, ні для відстеження ГК, це допомагає лише тоді, коли ви використовуєте іншу стратегію, яка фактично використовує ці знання, щоб опустити як знижки, так і трасування). Більш цікавим є його застосування до простого, детермінованого управління ресурсами. Подумайте, що RAII і C ++ 11 рухають семантику, але краще (застосовується частіше і помилки стикаються компілятором).

6

Тимчасові змінні: врахуйте наступний псевдокод.

Object getMaximum(Collection objects) {
  Object max = null;
  for (Object candidate IN objects) {
    if ((max is null) OR (candidate > max)) {
      max = candidate;
    }
  }
  return max;
}

Змінні max та candidateможуть вказувати на один і той же об'єкт, але призначення змінної змінюється, використовуючи різні правила та в різний час.


3

Щоб доповнити інші відповіді, ви також можете переглядати структуру даних по-іншому, починаючи з того самого місця. Наприклад, якщо у вас є BinaryTree a = new BinaryTree(...); BinaryTree b = a, ви можете пройти вниз по крайньому лівому шляху дерева aі по його крайньому правому шляху b, використовуючи щось на зразок:

while (!a.equals(null) && !b.equals(null)) {
    a = a.left();
    b = b.right();
}

Минув час, коли я написав Java, і цей код може бути не правильним чи розумним. Прийміть це більше як псевдокод.


3

Цей метод чудовий, коли у вас є кілька об'єктів, які всі передзвонюють іншому об'єкту, який можна використовувати безтекстовно.

Наприклад, якщо у вас є інтерфейс з вкладками, у вас можуть бути Tab1, Tab2 і Tab3. Ви також можете мати можливість використовувати загальну змінну незалежно від того, на якій вкладці перебуває користувач, щоб спростити ваш код і зменшити необхідність з'ясовувати на ходу і над якою вкладкою знаходиться ваш користувач.

Tab Tab1 = new Tab();
Tab Tab2 = new Tab();
Tab Tab3 = new Tab();
Tab CurrentTab = new Tab();

Потім на кожній з пронумерованих вкладок onClick ви можете змінити CurrentTab на посилання на цю Tab.

CurrentTab = Tab3;

Тепер у своєму коді ви можете безкарно зателефонувати на "CurrentTab", не знаючи, на якій вкладці ви фактично перебуваєте. Ви також можете оновити властивості CurrentTab, і вони автоматично перетікають вниз на посилання.


3

Існує безліч сценаріїв, в яких , щоб бути корисним, b повинно бути посилання на невідомий a. Зокрема:

  • Будь-який час ти не знаєш чого b вказує час компіляції.
  • Будь-який час, коли вам потрібно повторити колекцію, відомий під час компіляції чи ні
  • У будь-який час у вас обмежена сфера застосування

Наприклад:

Параметри

public void DoSomething(Thing &t) {
}

t є посиланням на змінну із зовнішньої області.

Повернені значення та інші умовні значення

Thing a = Thing.Get("a");
Thing b = Thing.Get("b");
Thing biggerThing = Thing.max(a, b);
Thing z = a.IsMoreZThan(b) ? a : b;

biggerThingі zкожне посилання на aабо b. Ми не знаємо, що під час компіляції.

Лямбда та їх повернені значення

Thing t = someCollection.FirstOrDefault(x => x.whatever > 123);

xє параметром (приклад 1 вище) і tє зворотним значенням (приклад 2 вище)

Колекції

indexByName.add(t.name, t);
process(indexByName["some name"]);

index["some name"]є значною мірою більш витонченим на вигляд b. Це псевдонім до об'єкта, який був створений і заповнений в колекцію.

Петлі

foreach (Thing t in things) {
 /* `t` is a reference to a thing in a collection */
}

t - посилання на елемент, повернутий ітератором (приклад 2) (попередній приклад).


Ваші приклади важко наслідувати. Не зрозумійте мене неправильно, я пережив їх, але мені довелося над цим працювати. Надалі я пропоную розділити приклади коду на окремі блоки (з деякими зауваженнями, що конкретно пояснюють кожен), не ставлячи кілька непов'язаних прикладів в один блок.
Басинатор

1
@HCBPshenanigans Я не очікую, що ти зміниш обрані відповіді; але я оновив шахту, щоб, сподіваюся, допомогти читати і заповнити деякі випадки використання, відсутні у вибраній відповіді.
svidgen

2

Це важливий момент, але ІМХО варто розуміти.

Усі мови OO завжди роблять копії посилань і ніколи не копіюють об'єкт "невидимим". Це було б багато важче писати програми , якщо мови OO працювали інакше. Наприклад, функції та методи ніколи не могли оновити об’єкт. Ява та більшість мов OO майже неможливо було б використовувати без значної додаткової складності.

Об'єкт у програмі повинен мати певне значення. Наприклад, це репрезентує щось конкретне в реальному фізичному світі. Зазвичай має сенс мати багато посилань на одне і те ж. Наприклад, мою домашню адресу можна надати багатьом людям та організаціям, і ця адреса завжди посилається на одне фізичне місце розташування. Отже, перший пункт полягає в тому, що об'єкти часто представляють щось конкретне, реальне або конкретне; і тому можливість мати багато посилань на одне і те ж є надзвичайно корисним. Інакше було б важче написати програми.

Кожен раз, коли ви передаєте aяк аргумент / параметр іншій функції, наприклад, виклику
foo(Dog aDoggy);
або застосуванню методу до a, базовий програмний код робить копію посилання, щоб створити другу посилання на той самий об'єкт.

Далі, якщо код з скопійованим посиланням знаходиться в іншому потоці, то обидва можуть бути використані одночасно для доступу до одного і того ж об'єкта.

Тож у більшості корисних програм буде кілька посилань на один і той же об’єкт, оскільки це семантика більшості мов програмування OO.

Тепер, якщо ми подумаємо про це, оскільки передача посилань є єдиним механізмом, доступним у багатьох мовах ОО (C ++ підтримує обидва), ми можемо очікувати, що це буде "правильне" поведінка за замовчуванням .

IMHO з використанням посилань є правильним за замовчуванням з кількох причин:

  1. Це гарантує, що значення об'єкта, який використовується у двох різних місцях, однакове. Уявіть, як розміщуєте об’єкт у двох різних структурах даних (масиви, списки тощо) та виконуєте деякі операції над об'єктом, який його змінює. Це може бути кошмаром для налагодження. Що ще важливіше, це один і той же об’єкт в обох структурах даних, або програма має помилку.
  2. Ви можете радісно рефакторний код на кілька функцій або об'єднати код з кількох функцій в одну, і семантика не змінюється. Якби мова не передбачала еталонної семантики, було б ще складніше змінити код.

Існує також аргумент ефективності; робити копії цілих об'єктів менш ефективно, ніж копіювати посилання. Однак я думаю, що це не вдається. Кілька посилань на один і той же об'єкт мають більше сенсу і простіші у використанні, оскільки вони відповідають семантиці реального фізичного світу.

Отже, IMHO, зазвичай має сенс мати кілька посилань на один і той же об’єкт. У незвичайних випадках, коли це не має сенсу в контексті алгоритму, більшість мов надають можливість зробити «клон» або глибоку копію. Однак це не за замовчуванням.

Я думаю, що люди, які стверджують, що це не повинно бути за замовчуванням, використовують мову, яка не забезпечує автоматичне вивезення сміття. Наприклад, старомодний C ++. Проблема полягає в тому, що їм потрібно знайти спосіб збирати "мертві" об'єкти, а не відновлювати об'єкти, які все ще можуть знадобитися; наявність кількох посилань на один і той же об’єкт робить це важко.

Я думаю, якщо C ++ мав достатньо недорогий збір сміття, так що всі об'єкти, на які посилаються, збирають сміття, значна частина заперечень відпадає. Ще будуть випадки, коли референтна семантика - це не те, що потрібно. Однак, на моєму досвіді, люди, які можуть ідентифікувати ці ситуації, також, як правило, здатні вибрати відповідну семантику.

Я вважаю, що є певні докази того, що велика кількість коду в програмі C ++ є для обробки або зменшення збору сміття. Однак написання та підтримка такого роду "інфраструктурного" коду додає витрат; саме там можна зробити мову простішою у використанні чи надійнішою. Так, наприклад, мова Go розроблена з акцентом на усунення деяких недоліків C ++, і у неї немає іншого вибору, крім збирання сміття.

Це, звичайно, не має значення в контексті Java. Він також був розроблений, щоб бути простим у використанні, а також збирання сміття. Отже, наявність декількох посилань є семантикою за замовчуванням і є відносно безпечною в тому сенсі, що об'єкти не відновлюються, поки є посилання на них. Звичайно, їх може утримувати структура даних, тому що програма не є належним чином приведена в дію, коли вона дійсно закінчилася об'єктом.

Отже, обертаючись навколо вашого запитання (з невеликим узагальненням), коли ви хочете більше, ніж одне посилання на один і той же об’єкт? Дуже багато в кожній ситуації, про яку я можу придумати. Вони є семантикою за замовчуванням для більшості мов механізму передачі параметрів. Я припускаю, що це тому, що семантика поводження з об'єктами, що існує в реальному світі, за замовчуванням в значній мірі повинна бути посиланням (адже фактичні об'єкти там є).

Будь-яку іншу семантику було б важче обробити.

Dog a = new Dog("rover");  // initialise with name 
DogList dl = new DogList()
dl.add(a)
...
a.setOwner("Mr Been")

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

пізніше собаку продають:

soldDog = dl.lookupOwner("rover", "Mr Been")
soldDog.setOwner("Mr Mcgoo")

Цей вид обробки є звичайним і нормальним. Тож довідкова семантика є типовою, оскільки вона зазвичай має найбільше значення.

Підсумок: Завжди має сенс мати кілька посилань на один і той же об'єкт.


хороший момент, але це краще підходить як коментар
Bassinator

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

1

Звичайно, ще один сценарій, де ви можете закінчитись:

Dog a = new Dog();
Dog b = a;

це коли ви підтримуєте код і bраніше використовували собаку чи інший клас, але зараз це обслуговує a.

Як правило, в середньостроковій перспективі вам слід переробити весь код для посилання aбезпосередньо, але це може не статися відразу.


1

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

Карти ідентичності давали остаточний локальний магазин сутностей, щоб ми могли уникнути наявності двох або більше окремих представлень. Коли ми представляємо один і той же об’єкт двічі, наш клієнт ризикує викликати проблему одночасності, якщо одна посилання на об'єкт зберігає зміни свого стану до того, як це зробить інший інший екземпляр. Ідея полягає в тому, що ми хочемо забезпечити, щоб наш клієнт завжди мав остаточне посилання на наш об'єкт / об'єкт.


0

Я використовував це, коли писав розв'язувач судоку. Коли я знаю номер комірки при обробці рядків, я хочу, щоб стовпець, що містить, також знав номер цієї комірки під час обробки стовпців. Отже, і стовпці, і рядки - це масиви об'єктів Cell, які перекриваються. Точно так, як показала прийнята відповідь.


-2

У веб-додатках Object Relational Mappers можуть використовувати ледаче завантаження, щоб усі посилання на один і той же об’єкт бази даних (принаймні, в межах однієї теми) вказували на одне і те ж.

Наприклад, якщо у вас були дві таблиці:

Собаки:

  • id | власник_id | назва
  • 1 | 1 | Кора Кент

Власники:

  • id | назва
  • 1 | я
  • 2 | ти

Якщо ваш ORM може зробити декілька підходів, якщо здійснюються наступні дзвінки:

dog = Dog.find(1)  // fetch1
owner = Owner.find(1) // fetch2
superdog = owner.dogs.first() // fetch3
superdog.name = "Superdog"
superdog.save! // write1
owner = dog.owner // fetch4
owner.name = "Mark"
owner.save! // write2
dog.owner = Owner.find(2)
dog.save! // write3

У наївній стратегії всі дзвінки до моделей та відповідних посилань отримують окремі об'єкти. Dog.find(), Owner.find(), owner.dogs, І dog.ownerрезультат в базі даних потрапив в перший раз, після чого вони будуть збережені в пам'ять. І так:

  • база даних отримується щонайменше 4 рази
  • dog.owner не те саме, що superdog.owner (вибирається окремо)
  • dog.name не те саме, що ім'я superdog.
  • dog і superdog намагаються записати в один рядок і перезаписать результати один одного: write3 скасує зміну імені в write1.

Без довідки у вас більше завантажень, ви використовуєте більше пам’яті та вводите можливість перезапису попередніх оновлень.

Припустимо, ваш ORM знає, що всі посилання на рядок 1 таблиці собак повинні вказувати на одне і те ж. Потім:

  • fetch4 можна усунути, оскільки в пам'яті є об'єкт, що відповідає Owner.find (1). fetch3 все одно призведе до принаймні сканування індексу, оскільки там можуть бути інші собаки, що належать власнику, але це не спровокує пошук рядків.
  • собака і супердог вказують на один і той же об’єкт.
  • dog.name та superdog.name вказують на один і той же об'єкт.
  • dog.owner та superdog.owner вказують на один і той же об’єкт.
  • write3 не замінює зміни в write1.

Іншими словами, використання посилань допомагає кодифікувати принцип єдиної точки істини (принаймні у цій темі), що є рядком у базі даних.

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