Почему объекты передаются по ссылке?


31

Молодой сотрудник, который изучал ОО, спросил меня, почему каждый объект передается по ссылке, что противоположно примитивным типам или структурам. Это общая характеристика языков, таких как Java и C #.

Я не мог найти хороший ответ для него.

Каковы мотивы для этого дизайнерского решения? Разработчики этих языков устали от необходимости каждый раз создавать указатели и определения типов?


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

@ Роберт, есть ли на высоком уровне какая-либо разница между «ссылкой вместо указателя»? Как вы думаете, я должен изменить название на что-то вроде «почему объект всегда является ссылкой?»?
Густаво Кардосо

Ссылки являются указателями.
комп

2
@Anto: ссылка на Java во всех отношениях идентична правильно используемому указателю C (правильно используется: не приведение типов, не установлено недопустимой памяти, не установлено литералом).
Zan Lynx

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

Ответы:


15

Основные причины сводятся к этому:

  • Указатели технические, чтобы получить право
  • Вам нужны указатели для реализации определенных структур данных
  • Вам нужны указатели, чтобы эффективно использовать память
  • Вам не нужна ручная индексация памяти для работы если вы не используете оборудование напрямую.

Отсюда и ссылки.


32

Простой ответ:

Минимизация потребления памяти
и
процессорного времени при воссоздании и выполнении глубокой копии каждого переданного объекта.


Я согласен с вами, но я думаю, что есть и эстетическая мотивация, или дизайн оригинального дизайна.
Густаво Кардосо

9
@Gustavo Cardoso: «какая-то эстетическая или оригинальная мотивация дизайна». Нет, это просто оптимизация.
S.Lott

6
@ S.Lott: Нет, в терминах ОО имеет смысл передавать по ссылке, потому что семантически вы не хотите делать копии объектов. Вы хотите передать объект, а не его копию. Если вы передаете по значению, это несколько нарушает метафору ОО, потому что у вас есть все эти клоны объектов, сгенерированные повсеместно, которые не имеют смысла на более высоком уровне.
интуитивно

@ Густаво: я думаю, что мы спорим о том же. Вы упоминаете семантику ООП и ссылаетесь на метафору ООП, которая является дополнительными причинами для меня. Мне кажется, что создатели ООП сделали так, как они сделали, чтобы «минимизировать потребление памяти» и «Экономия времени ЦП»
Тим

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

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

Использование ссылок разумно. Копировать вещи опасно.

Как говорили другие, в Java нет естественного «клона». Это не просто отсутствующая функция. Вы никогда не захотите просто копировать (волевым или глубоким) каждое свойство объекта. Что если это свойство было подключением к базе данных? Вы не можете просто «клонировать» соединение с базой данных больше, чем вы можете клонировать человека. инициализация существует по причине.

Глубокие копии - это отдельная проблема. Насколько глубоки вы на самом деле ? Вы определенно не можете скопировать что-либо статичное (включая любыеClass объекты).

Таким образом, по той же причине, по которой нет естественного клона, объекты, которые передаются как копии, создают безумие . Даже если бы вы могли «клонировать» соединение с БД - как бы вы теперь обеспечивали его закрытие?


* См. Комментарии. Под этим утверждением «никогда» я подразумеваю авто-клон, который клонирует каждое свойство. Java не предоставил его, и, вероятно, вам, как пользователю языка, не стоит создавать свой собственный язык по причинам, перечисленным здесь. Клонирование только непереходных полей было бы началом, но даже в этом случае вы должны быть внимательны при определении того, transientгде это уместно.


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

2
@ Инка - Вы, возможно, неправильно меня поняли. Преднамеренно реализовано cloneв порядке. Под «волей-неволей» я подразумеваю копирование всех свойств, не думая об этом - без целенаправленных намерений. Разработчики языка Java вынудили это сделать, потребовав пользовательскую реализацию clone.
Николь

Использование ссылок на неизменяемые объекты разумно. Создание простых значений, таких как изменяемая дата, а затем создание нескольких ссылок на них - нет.
Кевин Клайн

@NickC: Основная причина, по которой «клонировать вещи, волей и неволей» опасна, заключается в том, что языки / фреймворки, такие как Java и .net, не имеют никаких средств для декларативного указания, инкапсулирует ли ссылка изменяемое состояние, идентичность, и то, и другое, или ни то, ни другое. Если поле содержит ссылку на объект, которая инкапсулирует изменяемое состояние, но не идентичность, клонирование объекта требует, чтобы объект, содержащий состояние, был продублирован, а ссылка на этот дубликат сохранена в поле. Если ссылка содержит идентификатор, но не изменяемое состояние, поле в копии должно ссылаться на тот же объект, что и в оригинале.
суперкат

Глубокий аспект копирования является важным моментом. Копирование объектов проблематично, когда они содержат ссылки на другие объекты, особенно если граф объектов содержит изменяемые объекты.
Калеб

4

На объекты всегда ссылаются в Java. Они никогда не передаются вокруг себя.

Одним из преимуществ является то, что это упрощает язык. Объект C ++ может быть представлен как значение или ссылка, что создает необходимость использования двух разных операторов для доступа к члену: .и ->. (Есть причины, по которым это не может быть консолидировано; например, умные указатели - это значения, которые являются ссылками и должны сохранять их различимыми.) Java нуждается только в. .

Другая причина заключается в том, что полиморфизм должен быть сделан по ссылке, а не по значению; объект, обработанный значением, только там, и имеет фиксированный тип. Это возможно испортить в C ++.

Кроме того, Java может переключать назначение по умолчанию / копировать / что угодно. В C ++ это более или менее глубокая копия, в то время как в Java это простое назначение / копирование / что угодно указателя, с .clone()и тому подобное на случай, если вам нужно скопировать.


Иногда это становится ужасно, когда вы используете '(* object) ->'
Gustavo Cardoso

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) и C ++ (не Java, не C #) объект может быть объявлен как статическая распределенная переменная, а также с динамически распределенной переменной с помощью указателя («ссылка на объект» без »). сахарный синтаксис "). В каждом случае используется определенный синтаксис, и нет способа запутаться, как в Java «и друзья». В этих языках объект может передаваться как значение или как ссылка.

Программист знает, когда требуется синтаксис указателя, а когда нет, но в Java и других языках это сбивает с толку.

До того, как Java существовала или стала мейнстримом, многие программисты изучали ОО на С ++ без указателей, передавая по значению или по ссылке, когда это необходимо. При переходе от обучения к бизнес-приложениям они обычно используют указатели объектов. Библиотека QT является хорошим примером этого.

Когда я изучал Java, я пытался понять, что все является объектной концепцией, но запутался в кодировании. В конце концов, я сказал: «Хорошо, это объекты, динамически размещаемые с указателем с синтаксисом статически размещенного объекта», и у меня снова не возникло проблем с кодированием.


3

Java и C # получают контроль над памятью низкого уровня от вас. «Куча», в которой находятся создаваемые вами объекты, живет своей жизнью; например, сборщик мусора собирает объекты всякий раз, когда он предпочитает.

Поскольку между вашей программой и этой «кучей» существует отдельный уровень косвенности, два способа обращения к объекту по значению и по указателю (как в C ++) становятся неразличимыми : вы всегда обращаетесь к объектам «по указателю» на где-то в куче. Вот почему такой подход к проектированию делает передачу по ссылке семантикой присваивания по умолчанию. Java, C #, Ruby и так далее.

Вышесказанное касается только императивных языков. В языках , упомянутых выше контроля над памятью передаются во время выполнения, но язык дизайн также говорит : «Эй, но на самом деле, там есть память, и там есть объекты, и они делают занимают память». Функциональные языки абстрагируются еще дальше, исключая понятие «память» из их определения. Вот почему передача по ссылке не обязательно относится ко всем языкам, где вы не контролируете низкоуровневую память.


2

Я могу придумать несколько причин:

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

  • Копирование объектов не является тривиальным, объект может содержать элементы, которые сами являются объектами. Копирование объектов требует много времени и ресурсов процессора. Есть даже несколько способов копирования объекта в зависимости от контекста.

  • Передача объектов по ссылке обходится дешево, а также становится удобной, когда вы хотите поделиться / обновить информацию об объекте между несколькими клиентами объекта.

  • Сложные структуры данных (особенно те, которые являются рекурсивными) требуют указателей. Передача объектов по ссылке - это просто более безопасный способ передачи указателей.


1

Потому что в противном случае функция должна иметь возможность автоматически создавать (очевидно глубокую) копию любого типа объекта, который передается ей. И обычно это невозможно угадать, чтобы сделать это. Таким образом, вам придется определить реализацию метода copy / clone для всех ваших объектов / классов.


Может ли он просто сделать поверхностную копию и сохранить фактические значения и указатели на другие объекты?
Густаво Кардосо

# Густаво Кардосо, тогда вы можете изменить другие объекты через этот, это то, что вы ожидаете от объекта НЕ передается в качестве ссылки?
Дэвид

0

Потому что Java была разработана как лучший C ++, а C # была разработана как лучший Java, и разработчики этих языков устали от принципиально сломанной объектной модели C ++, в которой объекты являются типами значений.

Два из трех фундаментальных принципов объектно-ориентированного программирования - это наследование и полиморфизм, и трактовка объектов как типов значений вместо ссылочных типов приводит к хаосу в обоих случаях. Когда вы передаете объект в функцию в качестве параметра, компилятор должен знать, сколько байтов нужно передать. Когда ваш объект является ссылочным типом, ответ прост: размер указателя одинаков для всех объектов. Но когда ваш объект является типом значения, он должен передать фактический размер значения. Так как производный класс может добавлять новые поля, это означает sizeof (производный)! = Sizeof (base), и полиморфизм выходит за пределы окна.

Вот тривиальная программа 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, потому что вы не можете передать производный объект по значению функции, ожидающей базовый объект, поэтому компилятор создает конструктор скрытого копирования и передает копия родительской части объекта Child , вместо передачи объекта Child, как вы сказали. Скрытые семантические ошибки, подобные этой, являются причиной того, что в C ++ следует избегать передачи объектов по значению, и это невозможно вообще почти во всех других языках ОО.


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

ОО действительно не работает правильно без указателей
Густаво Кардосо

-1.000000000000
П Швед

5
Важно помнить, что Java передается по значению (она передает ссылки на объекты по значению, а примитивы передаются исключительно по значению).
Николь

3
@ Павел Швед - "два лучше, чем один!" Или, другими словами, больше веревки, чтобы повеситься.
Николь

0

Потому что иначе не было бы полиморфизма.

В ОО-программировании вы можете создать больший Derivedкласс из класса Base, а затем передать его функциям, ожидающим Baseединицу. Довольно тривиально, а?

За исключением того, что размер аргумента функции фиксирован и определяется во время компиляции. Вы можете утверждать, что хотите, исполняемый код похож на это, и языки должны выполняться в тот или иной момент (чисто интерпретируемые языки этим не ограничиваются ...)

Теперь существует один фрагмент данных, который четко определен на компьютере: адрес ячейки памяти, обычно выражаемый в виде одного или двух «слов». Это видно как указатели или ссылки в языках программирования.

Таким образом, для передачи объектов произвольной длины наиболее простой способ - передать указатель / ссылку на этот объект.

Это техническое ограничение ОО-программирования.

Но поскольку для больших типов вы, как правило, предпочитаете передавать ссылки, чтобы избежать копирования, это обычно не считается серьезным ударом :)

Однако есть одно важное следствие того, что в Java или C # при передаче объекта в метод вы не представляете, будет ли ваш объект изменен методом или нет. Это затрудняет отладку / распараллеливание, и это проблема, которую пытаются решить функциональные языки и прозрачная референция -> копирование не так уж и плохо (когда это имеет смысл).


-1

Ответ в названии (ну почти в любом случае). Ссылка (например, адрес) просто ссылается на что-то другое, значение - это еще одна копия чего-то еще. Я уверен, что кто-то, вероятно, упомянул что-то для следующего эффекта, но будут обстоятельства, при которых подходит один, а не другой (Безопасность памяти против Эффективности памяти). Все дело в управлении памятью, памятью, памятью ... ПАМЯТЬ! : D


-2

Хорошо, я не говорю, что это именно то, почему объекты являются ссылочными типами или передаются по ссылке, но я могу привести вам пример того, почему это очень хорошая идея в долгосрочной перспективе.

Если я не ошибаюсь, когда вы наследуете класс в C ++, все методы и свойства этого класса физически копируются в дочерний класс. Это было бы как писать содержимое этого класса снова внутри дочернего класса.

Таким образом, это означает, что общий размер данных в вашем дочернем классе является комбинацией материала в родительском классе и производном классе.

EG: #include

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 предыдущих ответах
комнат
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.