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?
programming-languages
object-oriented
Gustavo Cardoso
fonte
fonte
Respostas:
Os motivos básicos se resumem a isso:
Portanto, referências.
fonte
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.
fonte
No C ++, você tem duas opções principais: retornar por valor ou retornar por ponteiro. Vejamos o primeiro:
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:
Fizemos uma cópia do objeto sem sentido. Isso é uma perda de tempo de processamento.
Vejamos o retorno por ponteiro:
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:
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.
fonte
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
Class
objetos).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
transient
onde apropriado.fonte
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 declone
.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.fonte
const
seu 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.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
ref
palavra - chave, esse comportamento funcionará conforme o esperado.fonte
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.
fonte
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.
fonte
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.
fonte
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.
fonte
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:
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.
fonte
Porque não haveria polimorfismo de outra maneira.
Na programação OO, você pode criar uma
Derived
classe maior a partir de umaBase
e depois passá-la para funções que esperam umaBase
. 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).
fonte
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
fonte
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
O que mostraria a você:
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.
fonte