Чому об’єкти передаються за посиланням?


31

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

Я не зміг знайти для нього гарної відповіді.

Які мотиви для цього дизайнерського рішення? Чи втомилися розробники цих мов щоразу створювати вказівники та typedefs?


2
Ви запитуєте, чому Java та C # передають параметри за посиланням, а не за значенням чи за посиланням, а не за вказівником?
Роберт

@Robert, чи є на високому рівні будь-яка різниця між "посиланням замість покажчика"? Як ви вважаєте, я повинен змінити назву на щось на кшталт "чому об'єкт завжди є посиланням?"?
Густаво Кардосо

Посилання - покажчики.
комман

2
@Anto: Посилання Java у всіх відношеннях ідентична правильно використаному вказівнику C (правильно використовується: не типів, не встановлено на недійсну пам'ять, не встановлено літералом).
Зан Лінкс

5
Крім того, щоб бути дійсно педантичним, назва неправильна (принаймні, що стосується .net). Об'єкти НЕ передаються посиланням, посилання передаються за значенням. При передачі об'єкта методу опорне значення копіюється в нове посилання в тілі методу. Я думаю, що прикро, що "об'єкти передаються за посиланням" увійшли до списку загальних програмних цитат, коли вони невірні і призводять до того, що починається слабше розуміння посилань для нових програмістів.
SecretDeveloper

Відповіді:


15

Основні причини зводяться до цього:

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

Звідси посилання.


32

Простий відповідь:

Мінімізація споживання пам’яті
та
часу процесора на відтворенні та виконанні глибокої копії кожного переданого кудись об’єкта.


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

9
@ Густаво Кардосо: "якась естетична або проектна мотивація ОО". Ні. Це просто оптимізація.
С.Лотт

6
@ S.Lott: Ні, в ОО терміни є сенс передати посилання, тому що, семантично, ви не хочете робити копії об'єктів. Ви хочете передати об'єкт, а не його копію. Якщо ви проходите повз значення, це дещо порушує метафору ОО, оскільки у вас є всі ці клони об’єктів, що генеруються всюди, які не мають сенсу на більш високому рівні.
інтуїтивно,

@ Густаво: Я думаю, ми сперечаємось з тим самим моментом. Ви згадуєте семантику ООП і посилаєтесь на те, що метафора ООП є моїми додатковими причинами. Мені здається, що творці OOP зробили це так, як зробили, щоб "мінімізувати споживання пам'яті" та "Економити на час процесора"
Тім,

14

У C ++ у вас є два основні варіанти: повернення за значенням або повернення за вказівником. Розглянемо перший:

MyClass getNewObject() {
    MyClass newObj;
    return newObj;
}

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

  • newObj будується як тимчасовий об'єкт і розміщується на локальному стеку.
  • Копія newObj створюється та повертається.

Ми безглуздо зробили копію об’єкта. Це марна трата часу на обробку.

Давайте подивимось натомість зворотний вказівник:

MyClass* getNewObject() {
    MyClass newObj = new MyClass();
    return newObj;
}

Ми видалили зайву копію, але тепер ми ввели ще одну проблему: ми створили об’єкт на купі, який не буде автоматично знищений. Ми повинні з цим боротися самі:

MyClass someObj = getNewObject();
delete someObj;

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

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

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


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

напевно у вас є дійсна точка. Але проблема дизайну ОО, на яку вказував @Mason, була остаточною мотивацією змін. Не було сенсу зберігати різницю між посиланням і значенням, коли ви просто хочете використовувати посилання.
Густаво Кардосо

10

Багато інших відповідей мають добру інформацію. Я хотів би додати один важливий момент щодо клонування, який лише частково розглядався.

Використовувати посилання - розумно. Копіювати речі небезпечно.

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

Глибокі копії - це своя проблема - наскільки глибоко ви дійсно ходите? Ви точно не могли скопіювати нічого статичного (включаючи будь-які Classоб'єкти).

Тож з тієї ж причини, чому немає природного клону, об’єкти, передані як копії, створили б божевілля . Навіть якби ви могли «клонувати» з'єднання БД - як би ви тепер забезпечили його закриття?


* Дивіться коментарі - Під цим твердженням "ніколи" я маю на увазі авто-клон, який клонує кожну власність. Java не надала жодного, і, мабуть, це не гарна ідея для вас, як користувача мови, щоб створити свою власну, з перерахованих тут причин. Клонування лише непрохідних полів було б початком, але навіть тоді вам потрібно буде старанно визначитись, transientде це доречно.


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

2
@Inca - Можливо, ти мене не розумієш. Навмисне реалізація clone- це добре. Під "вольовим-невільним" я маю на увазі копіювання всіх властивостей, не замислюючись про це - без цілеспрямованого наміру. Дизайнери мови Java змусили цей намір вимагаючи створеної користувачем реалізації clone.
Ніколь

Використовувати посилання на незмінні об’єкти - розумно. Створення простих значень, таких як Date mutable, а потім створення декількох посилань на них не є.
кевін клайн

@ NickC: Основна причина "клонування речей мимоволі" - це те, що мови / рамки, такі як Java та .net, не мають жодного способу декларативно вказати, чи посилання інкапсулює змінне стан, ідентичність, і те, і інше. Якщо поле містить посилання на об'єкт, який інкапсулює змінний стан, але не тотожність, клонування об'єкта вимагає дублювання об'єкта, у якому знаходиться стан, і посилання на цей дублікат, що зберігається в полі. Якщо посилання інкапсулює ідентичність, але не змінює стан, поле в копії має посилатися на той самий об'єкт, що і в оригіналі.
supercat

Аспект глибокої копії є важливим моментом. Копіювання об'єктів є проблематичним, коли вони містять посилання на інші об'єкти, особливо якщо графік об'єктів містить об'єкти, що змінюються.
Калеб

4

На об'єкти завжди посилаються на Java. Вони ніколи не проходять навколо себе.

Одна перевага полягає в тому, що це спрощує мову. Об'єкт C ++ може бути представлений у вигляді значення або посилання, що створює необхідність використання двох різних операторів для доступу до члена: .і ->. (Є причини, по яких це не може бути консолідовано; наприклад, смарт-покажчики - це значення, які є посиланнями, і повинні зберігати ці відмінності.) Java тільки потребує ..

Ще одна причина полягає в тому, що поліморфізм має здійснюватися шляхом посилання, а не значення; Об'єкт, оброблений значенням, є саме там і має фіксований тип. Це можна накрутити на C ++.

Також Java може перемикати призначення / копію за замовчуванням на будь-яке інше. У C ++ це більш-менш глибока копія, тоді як у Java це просте призначення / копія / вказівник / що завгодно, з тим чи іншим, .clone()якщо вам потрібно скопіювати.


Іноді стає по-справжньому некрасивим, коли ти використовуєш '(* об’єкт) ->'
Густаво Кардосо

1
Варто зазначити, що C ++ розрізняє покажчики, посилання та значення. SomeClass * - вказівник на об’єкт. SomeClass & - це посилання на об'єкт. SomeClass - тип значення.
Мурашка

Я вже задавав @Rober на первинному запитанні, але я це зроблю і тут: різниця між * та & на C ++ - це лише технічний предмет низького рівня, чи не так? Чи є вони на високому рівні, семантично вони однакові.
Густаво Кардосо

3
@ Густаво Кардосо: Різниця семантична; на низькому технічному рівні вони, як правило, однакові. Вказівник вказує на об’єкт, або NULL (визначене неправильне значення). Якщо тільки constїї значення можна змінити, щоб вказувати на інші об'єкти. Посилання - це інша назва об'єкта, не може бути NULL і не може бути повторно використана. Як правило, це реалізується простим використанням покажчиків, але це деталізація реалізації.
Девід Торнлі

+1 за "поліморфізм має бути здійснено шляхом посилання". Це неймовірно важлива деталь, яку більшість інших відповідей ігнорували.
Довал

4

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

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


3

Швидка відповідь

Дизайнери Java та подібних мов хотіли застосувати концепцію "все є об'єктом". А передача даних як еталон дуже швидка і не потребує багато пам'яті.

Додатковий розширений нудний коментар

Однак, ці мови використовують посилання на об'єкти (Java, Delphi, C #, VB.NET, Vala, Scala, PHP), правда полягає в тому, що посилання на об'єкти є вказівниками на маскування об'єктів. Нульове значення, розподіл пам’яті, копія посилання без копіювання цілих даних об’єкта, усі вони є вказівниками об’єктів, а не простими об’єктами !!!

У Object Pascal (не Delphi), anc C ++ (не Java, не C #) об'єкт можна оголосити статичною розподіленою змінною, а також з динамічною розподіленою змінною, використовуючи покажчик ("посилання на об'єкт" без " синтаксис цукру "). У кожному випадку використовується певний синтаксис, і немає ніякого способу заплутатися, як у Java "та друзі". У цих мовах об'єкт може бути переданий як значення або як еталон.

Програміст знає, коли потрібен синтаксис вказівника, а коли не потрібен, але в Java та подібних мовах це заплутано.

Перш ніж Java існувала або стала мейнстрімом, багато програмістів вивчали OO в C ++ без покажчиків, передаючи значення або посилаючись, коли це було потрібно. При переході від навчання до ділових додатків вони зазвичай використовують вказівники об'єктів. Бібліотека QT є хорошим прикладом цього.

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


3

Java та C # беруть на себе контроль над низькорівневою пам'яттю. "Купка", де розміщуються створені вами предмети, живе власним життям; наприклад, сміттєзбірник пожинає предмети коли завгодно.

Оскільки між вашою програмою та тією "купою" є окремий шар опосередкування, два способи посилання на об'єкт за значенням та вказівником (як у C ++) стають невідрізними : ви завжди посилаєтесь на об'єкти "за вказівником" на десь у купі. Ось чому такий дизайнерський підхід робить пропускний посилання семантикою призначення за замовчуванням. Java, C #, Ruby та ін.

Сказане стосується лише імперативних мов. У мовах , згаданих вище контролю над пам'яттю передаються під час виконання, але мова дизайн також говорить : «Ей, але на самому справі, там є пам'ять, і там є об'єкти, і вони роблять займають пам'ять». Функціональні мови ще більше абстрагуються, виключаючи поняття "пам'ять" з їх визначення. Ось чому передача посилань не обов'язково стосується всіх мов, де ви не контролюєте низькорівневу пам'ять.


2

Я можу придумати кілька причин:

  • Копіювання примітивних типів є тривіальним, воно зазвичай перекладається на одну машинну інструкцію.

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

  • Передача об'єктів за посиланням є дешевою, а також стає зручною, коли ви хочете поділитися / оновити інформацію про об'єкт між декількома клієнтами об'єкта.

  • Складні структури даних (особливо рекурсивні) потребують покажчиків. Передача об'єктів за посиланням - лише безпечніший спосіб передачі покажчиків.


1

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


Чи може це просто зробити неглибоку копію та зберегти фактичні значення та покажчики на інші об’єкти?
Густаво Кардосо

#Gustavo Cardoso, тоді ви могли б модифікувати інші об'єкти через цей, це те, що ви очікували від об'єкта, НЕ передається як посилання?
Девід

0

Оскільки Java була розроблена як краща C ++, а C # була розроблена як краща Java, а розробникам цих мов набридла принципово розбита об'єктна модель C ++, в якій об'єкти є типовими типами.

Два з трьох основоположних принципів об'єктно-орієнтованого програмування - це успадкування та поліморфізм, а трактування об'єктів як типів цінності замість еталонних типів спричиняє хаос із обох. Коли ви передаєте об'єкт функції як параметр, компілятору необхідно знати, скільки байтів пройти. Коли ваш об'єкт є еталонним типом, відповідь проста: розмір вказівника, однаковий для всіх об'єктів. Але коли ваш об'єкт є типом значення, він повинен передавати фактичний розмір значення. Оскільки похідний клас може додавати нові поля, це означає sizeof (похідне)! = Sizeof (база), а поліморфізм виходить у вікно.

Ось тривіальна програма C ++, яка демонструє проблему:

#include <iostream> 
class Parent 
{ 
public: 
   int a;
   int b;
   int c;
   Parent(int ia, int ib, int ic) { 
      a = ia; b = ib; c = ic;
   };
   virtual void doSomething(void) { 
      std::cout << "Parent doSomething" << std::endl;
   }
};

class Child : public Parent {
public:
   int d;
   int e;
   Child(int id, int ie) : Parent(1,2,3) { 
      d = id; e = ie;
   };
   virtual void doSomething(void) {
      std::cout << "Child doSomething : D = " << d << std::endl;
   }
};

void foo(Parent a) {
   a.doSomething();
}

int main(void)
{
   Child c(4, 5);
   foo(c);
   return 0;
}

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


Дуже хороший момент. Однак я зосередився на проблемах повернення, оскільки робота над ними забирає чимало зусиль; цю програму можна виправити, додавши єдину амперсанд: void foo (Parent & a)
Ant Ant

OO справді не справляється без покажчиків
Gustavo Cardoso

-1.000000000000
П Швед

5
Важливо пам’ятати, що Java - це pass-by-value (вона передає посилання на об'єкти за значенням, тоді як примітиви передаються виключно за значенням).
Ніколь

3
@Pavel Швед - "два краще, ніж один!" Або, іншими словами, більше мотузки, щоб повіситись.
Ніколь

0

Бо інакше не було б поліморфізму.

У програмуванні OO ви можете створити більший Derivedклас із Baseодного, а потім передати його функціям, які очікують Baseодного. Досить тривіально так?

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

Тепер є одна деталь даних, яка добре визначена на комп’ютері: адреса комірки пам’яті, як правило, виражена одним або двома «словами». Це видно як вказівники або посилання на мовах програмування.

Отже, для передачі об'єктів довільної довжини, найпростіше зробити це ввести покажчик / посилання на цей об’єкт.

Це технічне обмеження програмування ОО.

Але оскільки для великих типів ви все одно віддаєте перевагу передачі посилань, щоб уникнути їх копіювання, це, як правило, не вважається головним ударом :)

Є одне важливе наслідок, хоча в Java або C #, передаючи об'єкт методу, ви не маєте поняття, буде ваш об'єкт модифікований методом чи ні. Це ускладнює налагодження / паралелізацію, і це питання, яке намагаються вирішити функціональні мови та транспарентна довідка -> копіювання не так вже й погано (коли це має сенс).


-1

Відповідь у назві (ну майже у будь-якому разі). Посилання (як адреса) просто посилається на щось інше, значення - це ще одна копія чогось іншого. Я впевнений, що хтось, мабуть, згадав щось із наступним ефектом, але будуть обставини, коли підходить одне, а не друге (безпека пам’яті проти ефективності пам’яті). Це все про управління пам’яттю, пам’яттю, пам’яттю …… ПАМ’ЯТАЙТЕ! : D


-2

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

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

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

EG: #включити

class Top 
{   
    int arrTop[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};  

class Middle : Top 
{   
    int arrMiddle[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};  

class Bottom : Middle
{   
    int arrBottom[20] = {1,2,3,4,5,6,7,8,9,9,8,7,6,5,4,3,2,1};
};  

int main()
{   
    using namespace std;

    int arr[20];
    cout << "Size of array of 20 ints: " << sizeof(arr) << endl;

    Top top;
    Middle middle;
    Bottom bottom;

    cout << "Size of Top Class: " << sizeof(top) << endl;
    cout << "Size of middle Class: " << sizeof(middle) << endl;
    cout << "Size of bottom Class: " << sizeof(bottom) << endl;

}   

Що б вам показало:

Size of array of 20 ints: 80
Size of Top Class: 80
Size of middle Class: 160
Size of bottom Class: 240

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

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

Ось чому використання посилань було б більш кращим методом для цього.


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