Por que os objetos são passados ​​por referência?

31

Um jovem colega de trabalho que estudava OO me perguntou por que cada objeto é passado por referência, que é o oposto de tipos ou estruturas primitivos. É uma característica comum de linguagens como Java e C #.

Não consegui encontrar uma boa resposta para ele.

Quais são as motivações para essa decisão de design? Os desenvolvedores dessas linguagens estavam cansados ​​de ter que criar ponteiros e typedefs sempre?

Gustavo Cardoso
fonte
2
Você está perguntando por que Java e C # você passa parâmetros por referência em vez de por valor ou por referência em vez de por ponteiro?
11111 robert
@ Robert, existe no alto nível alguma diferença entre "referência em vez de ponteiro"? Você acha que eu deveria mudar o título para algo como 'por que o objeto sempre é referência? "?
Gustavo Cardoso
Referências são ponteiros.
compman
2
@Anto: Uma referência Java é, de todas as formas, idêntica a um ponteiro C usado corretamente (usado corretamente: sem conversão de tipo, não definido para memória inválida, não definido por literal).
Zan Lynx
5
Também para ser realmente pedante, o título está incorreto (pelo menos no que diz respeito ao .net). Os objetos NÃO são passados ​​por referência, as referências são passadas por valor. Quando você passa um objeto para um método, o valor de referência é copiado para uma nova referência no corpo do método. Acho uma pena que "os objetos sejam passados ​​por referência" tenha entrado na lista de citações comuns de programadores quando está incorreto e leva a um entendimento mais pobre das referências para novos programadores que estão iniciando.
SecretDeveloper 11/03/11

Respostas:

15

Os motivos básicos se resumem a isso:

  • Os ponteiros são técnicos para acertar
  • Você precisa de ponteiros para implementar certas estruturas de dados
  • Você precisa de ponteiros para ser eficiente no uso da memória
  • Você não precisa da indexação manual da memória para funcionar se não estiver usando o hardware diretamente.

Portanto, referências.

Paul Nathan
fonte
32

Resposta Simples:

Minimizar o consumo de memória
e
o tempo da CPU para recriar e fazer uma cópia profunda de cada objeto passado em algum lugar.

Tim
fonte
Eu concordo com você, mas acho que também há alguma motivação estética ou de design de OO nesse sentido.
Gustavo Cardoso
9
@Gustavo Cardoso: "alguma motivação estética ou de design de OO". Não. É simplesmente uma otimização.
31511 S.Lott
6
@ S.Lott: Não, faz sentido em termos de OO passar por referência porque, semanticamente, você não deseja fazer cópias de objetos. Você deseja passar o objeto em vez de uma cópia dele. Se você está passando por valor, isso quebra a metáfora de OO, porque você tem todos esses clones de objetos sendo gerados em todo o lugar que não fazem sentido em um nível superior.
intuited
@Gustavo: Acho que estamos discutindo o mesmo ponto. Você menciona a semântica da OOP e se refere à metáfora da OOP como razões adicionais à minha. Parece-me que os criadores do OOP fez isso da maneira que eles fizeram para "minimizar o consumo de memória" e "Salvar em tempo de CPU"
Tim
14

No C ++, você tem duas opções principais: retornar por valor ou retornar por ponteiro. Vejamos o primeiro:

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

Supondo que seu compilador não seja inteligente o suficiente para usar a otimização do valor de retorno, o que acontece aqui é o seguinte:

  • newObj é construído como um objeto temporário e colocado na pilha local.
  • Uma cópia do newObj é criada e retornada.

Fizemos uma cópia do objeto sem sentido. Isso é uma perda de tempo de processamento.

Vejamos o retorno por ponteiro:

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

Eliminamos a cópia redundante, mas agora introduzimos outro problema: criamos um objeto no heap que não será destruído automaticamente. Nós temos que lidar com isso nós mesmos:

MyClass someObj = getNewObject();
delete someObj;

Saber quem é responsável por excluir um objeto alocado dessa maneira é algo que só pode ser comunicado por comentários ou por convenção. Isso facilmente leva a vazamentos de memória.

Muitas soluções alternativas foram sugeridas para resolver esses dois problemas - otimização do valor de retorno (em que o compilador é inteligente o suficiente para não criar a cópia redundante em retorno por valor), passando uma referência ao método (para que a função seja injetada em um objeto existente em vez de criar um novo), indicadores inteligentes (para que a questão da propriedade seja discutível).

Os criadores de Java / C # perceberam que sempre retornando objetos por referência era uma solução melhor, especialmente se a linguagem o suportasse nativamente. Ele se vincula a muitos outros recursos que os idiomas possuem, como coleta de lixo, etc.

Formiga
fonte
O retorno por valor é ruim o suficiente, mas o valor por passagem é ainda pior quando se trata de objetos, e acho que esse era o problema real que eles estavam tentando evitar.
Mason Wheeler
com certeza você tem um ponto válido. Mas o problema de design de OO que @Mason apontou foi a motivação final da mudança. Não havia nenhum significado para manter a diferença entre referência e valor quando você apenas deseja usar a referência.
Gustavo Cardoso
10

Muitas outras respostas têm boas informações. Gostaria de acrescentar um ponto importante sobre a clonagem que foi apenas parcialmente abordado.

Usar referências é inteligente. Copiar coisas é perigoso.

Como outros já disseram, em Java, não existe um "clone" natural. Este não é apenas um recurso ausente. Você nunca deseja copiar, quer queira quer não *, superficial ou profundamente) todas as propriedades de um objeto. E se essa propriedade fosse uma conexão com o banco de dados? Você não pode simplesmente "clonar" uma conexão com o banco de dados mais do que um humano. A inicialização existe por um motivo.

Cópias profundas são um problema próprio - até onde você realmente vai? Você definitivamente não pode copiar nada que seja estático (incluindo Classobjetos).

Portanto, pela mesma razão pela qual não há clone natural, os objetos passados ​​como cópias criariam insanidade . Mesmo se você pudesse "clonar" uma conexão com o banco de dados - como você garantiria que ela estivesse fechada?


* Veja os comentários - com esta declaração "never", quero dizer um clone automático que clona todas as propriedades. O Java não forneceu um, e provavelmente não é uma boa ideia para você, como usuário da linguagem, criar o seu próprio, pelos motivos listados aqui. A clonagem apenas de campos não transitórios seria um começo, mas mesmo assim você precisaria ser cuidadoso ao definir transientonde apropriado.

Nicole
fonte
Tenho problemas para entender o salto de boas objeções para a clonagem em determinadas condições, para a afirmação de que nunca é necessária. E eu tenho encontrado situações em que foi necessária uma cópia exata, onde há funções estáticas, onde envolvidos, sem IO ou conexões abertas poderia estar em questão ... Eu entendo os riscos da clonagem, mas eu não posso ver o cobertor nunca mais .
Inca
2
@Inca - Você pode estar me entendendo mal. Implementado intencionalmente cloneé bom. Por "quer ou não", quero dizer copiar todas as propriedades sem pensar nisso - sem intenção intencional. Os designers da linguagem Java forçaram essa intenção exigindo a implementação de clone.
Nicole
Usar referências a objetos imutáveis ​​é inteligente. Tornar valores simples como Date mutáveis ​​e, em seguida, criar várias referências a eles não é.
Kevin cline
@NickC: A principal razão pela qual a clonagem é perigosa é que linguagens / frameworks como Java e .net não têm meios de indicar declarativamente se uma referência encapsula estado mutável, identidade, ambos ou nenhum. Se o campo contiver uma referência de objeto que encapsule estado mutável, mas não identidade, a clonagem do objeto exige que o objeto que contém o estado seja duplicado e uma referência a essa duplicata seja armazenada no campo. Se a referência encapsular a identidade, mas não o estado mutável, o campo na cópia deverá se referir ao mesmo objeto que no original.
Supercat
O aspecto da cópia em profundidade é um ponto importante. A cópia de objetos é problemática quando eles contêm referências a outros objetos, principalmente se o gráfico de objetos contiver objetos mutáveis.
Caleb
4

Os objetos são sempre referenciados em Java. Eles nunca são passados ​​em torno de si.

Uma vantagem é que isso simplifica o idioma. Um objeto C ++ pode ser representado como um valor ou uma referência, criando a necessidade de usar dois operadores diferentes para acessar um membro: .e ->. (Existem razões pelas quais isso não pode ser consolidado; por exemplo, ponteiros inteligentes são valores que são referências e precisam mantê-los distintos.) O Java precisa apenas ..

Outra razão é que o polimorfismo deve ser feito por referência, não por valor; um objeto tratado por valor está lá e tem um tipo fixo. É possível estragar tudo em C ++.

Além disso, o Java pode alternar a atribuição / cópia padrão / o que for. No C ++, é uma cópia mais ou menos profunda, enquanto no Java é uma simples atribuição / cópia / qualquer coisa, com .clone()e assim por diante , no caso de você precisar copiar.

David Thornley
fonte
Às vezes, fica muito feio quando você usa '(* object) ->'
Gustavo Cardoso
1
Vale ressaltar que o C ++ faz distinção entre ponteiros, referências e valores. SomeClass * é um ponteiro para um objeto. SomeClass & é uma referência a um objeto. SomeClass é um tipo de valor.
Ant
Já perguntei ao @Rober sobre a pergunta inicial, mas também o farei aqui: a diferença entre * e & em C ++ é apenas uma coisa de baixo nível tecnológico, não é? Eles são, em alto nível, semanticamente eles são os mesmos.
Gustavo Cardoso
3
@Gustavo Cardoso: A diferença é semântica; em um nível técnico baixo, eles geralmente são idênticos. Um ponteiro aponta para um objeto ou é NULL (um valor inválido definido). A menos que constseu valor possa ser alterado para apontar para outros objetos. Uma referência é outro nome para um objeto, não pode ser NULL e não pode ser recolocado. Geralmente é implementado pelo simples uso de ponteiros, mas esse é um detalhe da implementação.
David Thornley
+1 para "o polimorfismo deve ser feito por referência". Esse é um detalhe incrivelmente crucial que a maioria das outras respostas ignorou.
Doval
4

Sua declaração inicial sobre objetos C # sendo passados ​​por referência não está correta. No C #, os objetos são tipos de referência, mas, por padrão, são passados ​​por valor, assim como os tipos de valor. No caso de um tipo de referência, o "valor" que está sendo copiado como um parâmetro do método de passagem por valor é a própria referência; portanto, as alterações nas propriedades dentro de um método serão refletidas fora do escopo do método.

No entanto, se você fosse redesignar a própria variável de parâmetro dentro de um método, verá que essa alteração não é refletida fora do escopo do método. Por outro lado, se você realmente passar um parâmetro por referência usando a refpalavra - chave, esse comportamento funcionará conforme o esperado.

newdayrising
fonte
3

Resposta rápida

Os designers de Java e de linguagens semelhantes queriam aplicar o conceito "tudo é um objeto". E passar dados como referência é muito rápido e não consome muita memória.

Comentário adicional chato estendido

Entretanto, essas linguagens usam referências a objetos (Java, Delphi, C #, VB.NET, Vala, Scala, PHP), a verdade é que as referências a objetos são ponteiros para objetos disfarçados. O valor nulo, a alocação de memória, a cópia de uma referência sem copiar todos os dados de um objeto, todos eles são indicadores de objetos, não objetos simples !!!

No Object Pascal (não Delphi) e no C ++ (não Java, não C #), um objeto pode ser declarado como uma variável alocada estática e também com uma variável alocada dinâmica, através do uso de um ponteiro ("referência de objeto" sem " sintaxe do açúcar "). Cada caso usa certa sintaxe e não há como ficar confuso como em Java "e amigos". Nessas linguagens, um objeto pode ser passado como valor ou como referência.

O programador sabe quando uma sintaxe de ponteiro é necessária e quando não é necessária, mas em Java e em linguagens similares, isso é confuso.

Antes que o Java existisse ou se tornasse popular, muitos programadores aprenderam OO em C ++ sem ponteiros, passando por valor ou por referência quando necessário. Quando passam de aplicativos de aprendizagem para aplicativos de negócios, geralmente usam ponteiros de objetos. A biblioteca QT é um bom exemplo disso.

Quando aprendi Java, tentei seguir o conceito de tudo é um objeto, mas fiquei confuso com a codificação. Eventualmente, eu disse "ok, são objetos alocados dinamicamente com um ponteiro com a sintaxe de um objeto alocado estaticamente" e não tive problemas para codificar novamente.

umlcat
fonte
3

Java e C # assumem o controle da memória de baixo nível. A "pilha" onde residem os objetos que você cria vive sua própria vida; por exemplo, o coletor de lixo colhe objetos sempre que preferir.

Como existe uma camada separada de indireção entre seu programa e esse "heap", as duas maneiras de se referir a um objeto, por valor e por ponteiro (como em C ++), tornam-se indistinguíveis : você sempre se refere a objetos "por ponteiro" para em algum lugar na pilha. É por isso que essa abordagem de design faz da referência por referência a semântica padrão da atribuição. Java, C #, Ruby, etc.

O acima mencionado diz respeito apenas a linguagens imperativas. Nas línguas mencionadas acima do controle sobre a memória é passado para o tempo de execução, mas o design linguagem também diz "hey, mas, na verdade, não é a memória, e não são os objetos, e eles fazem ocupar a memória". As linguagens funcionais abstraem ainda mais, excluindo o conceito de "memória" de sua definição. É por isso que a passagem por referência não se aplica necessariamente a todos os idiomas em que você não controla a memória de baixo nível.

P Shved
fonte
2

Eu posso pensar em alguns motivos:

  • Copiar tipos primitivos é trivial, geralmente se traduz em uma instrução de máquina.

  • Copiar objetos não é trivial, o objeto pode conter membros que são objetos em si. Copiar objetos é caro em tempo e memória da CPU. Existem até várias maneiras de copiar um objeto, dependendo do contexto.

  • A passagem de objetos por referência é barata e também se torna útil quando você deseja compartilhar / atualizar as informações do objeto entre vários clientes do objeto.

  • Estruturas de dados complexas (especialmente aquelas recursivas) requerem ponteiros. Passar objetos por referência é apenas uma maneira mais segura de passar ponteiros.

jeroko
fonte
1

Porque, caso contrário, a função deve ser capaz de criar automaticamente uma cópia (obviamente profunda) de qualquer tipo de objeto que é passado para ela. E geralmente não se pode adivinhar. Portanto, você teria que definir a implementação do método copy / clone para todos os seus objetos / classes.

David
fonte
Poderia apenas fazer uma cópia superficial e manter os valores reais e indicadores para outros objetos?
Gustavo Cardoso
#Gustavo Cardoso, então você pode modificar outros objetos através deste, é o que você esperaria de um objeto NÃO passado como referência?
David
0

Como o Java foi projetado como um C ++ melhor, e o C # foi projetado como um Java melhor, e os desenvolvedores dessas linguagens estavam cansados ​​do modelo de objeto C ++ fundamentalmente quebrado, no qual objetos são tipos de valor.

Dois dos três princípios fundamentais da programação orientada a objetos são herança e polimorfismo, e tratar objetos como tipos de valor em vez de tipos de referência causa estragos em ambos. Quando você passa um objeto para uma função como parâmetro, o compilador precisa saber quantos bytes passar. Quando seu objeto é um tipo de referência, a resposta é simples: o tamanho de um ponteiro, o mesmo para todos os objetos. Mas quando seu objeto é um tipo de valor, ele deve passar o tamanho real do valor. Como uma classe derivada pode adicionar novos campos, isso significa sizeof (derivado)! = Sizeof (base), e o polimorfismo sai pela janela.

Aqui está um programa C ++ trivial que demonstra o problema:

#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;
}

A saída desse programa não é o que seria para um programa equivalente em qualquer linguagem OO sã, porque você não pode passar um objeto derivado por valor para uma função que espera um objeto base; portanto, o compilador cria um construtor de cópia oculto e passa uma cópia da parte pai do objeto filho , em vez de passar o objeto filho como você pediu. Os truques semânticos ocultos como esse são o motivo pelo qual a passagem de objetos por valor deve ser evitada em C ++ e não é possível em quase todas as outras linguagens OO.

Mason Wheeler
fonte
Muito bom ponto. No entanto, concentrei-me nos problemas de retorno, pois trabalhar com eles exige bastante esforço; este programa pode ser corrigido com a adição de um único e comercial: void foo (Parent & a)
Ant
OO realmente não funciona direito, sem ponteiros #
Gustavo Cardoso
-1.000000000000
P
5
É importante lembrar que Java é passagem por valor (passa referências de objeto por valor, enquanto primitivas são passadas apenas por valor).
Nicole
3
@ Pavel Shved - "dois é melhor que um!" Ou, em outras palavras, mais corda para se pendurar.
Nicole Nicole
0

Porque não haveria polimorfismo de outra maneira.

Na programação OO, você pode criar uma Derivedclasse maior a partir de uma Basee depois passá-la para funções que esperam uma Base. Muito trivial eh?

Exceto que o tamanho do argumento de uma função é fixo e determinado em tempo de compilação. Você pode argumentar quanto quiser, o código executável é assim e os idiomas precisam ser executados em um ponto ou outro (idiomas puramente interpretados não são limitados por isso ...)

Agora, há um dado bem definido em um computador: o endereço de uma célula de memória, geralmente expresso como uma ou duas "palavras". É visível como ponteiros ou referências em linguagens de programação.

Portanto, para passar objetos de comprimento arbitrário, a coisa mais simples a fazer é passar um ponteiro / referência a esse objeto.

Essa é uma limitação técnica da programação OO.

Mas, como para tipos grandes, você geralmente prefere passar referências de qualquer maneira para evitar a cópia, geralmente não é considerado um grande golpe :)

Há uma conseqüência importante, porém, em Java ou C #, ao passar um objeto para um método, você não tem idéia se seu objeto será modificado pelo método ou não. Isso torna a depuração / paralelização mais difícil, e esse é o problema que as Linguagens Funcionais e a Referência Transparente estão tentando resolver -> copiar não é tão ruim (quando faz sentido).

Matthieu M.
fonte
-1

A resposta está no nome (bem, quase de qualquer maneira). Uma referência (como um endereço) apenas se refere a outra coisa, um valor é outra cópia de outra coisa. Tenho certeza de que alguém provavelmente mencionou algo com o seguinte efeito, mas haverá circunstâncias em que um e não o outro é adequado (segurança da memória versus eficiência da memória). É tudo sobre como gerenciar a memória, memória, memória ...... MEMÓRIA! : D

Dark Star1
fonte
-2

Tudo bem, então não estou dizendo que é exatamente por isso que os objetos são tipos de referência ou são passados ​​por referência, mas posso dar um exemplo de por que essa é uma ideia muito boa a longo prazo.

Se não me engano, quando você herda uma classe em C ++, todos os métodos e propriedades dessa classe são fisicamente copiados para a classe filho. Seria como escrever o conteúdo dessa classe novamente dentro da classe filho.

Portanto, isso significa que o tamanho total dos dados em sua classe filho é uma combinação dos itens da classe pai e da classe derivada.

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;

}   

O que mostraria a você:

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

Isso significa que se você tiver uma grande hierarquia de várias classes, o tamanho total do objeto, conforme declarado aqui, seria a combinação de todas essas classes. Obviamente, esses objetos seriam consideravelmente grandes em muitos casos.

A solução, acredito, é criá-lo na pilha e usar ponteiros. Isso significa que o tamanho de objetos de classes com vários pais seria administrável, de certa forma.

É por isso que usar referências seria um método mais preferível para fazer isso.

Dhaval Anjaria
fonte
1
este não parece oferecer nada substancial sobre pontos feitos e explicada em 13 respostas anteriores
mosquito