Por que tantos idiomas são passados ​​por valor?

36

Mesmo os idiomas em que você tem manipulação explícita de ponteiros como C, são sempre passados ​​por valor (você pode passá-los por referência, mas esse não é o comportamento padrão).

Qual é o benefício disso, por que tantos idiomas são transmitidos por valores e por que outros são transmitidos por referência ? (Entendo que Haskell é passado por referência, embora não tenha certeza).

Meu código não possui bugs
fonte
5
void acceptEntireProgrammingLanguageByValue(C++);
21812 Thomas Eding
4
Poderia ser pior. Algumas línguas antigas também permitiu chamada pelo nome
hugomg
25
Na verdade, em C você não pode passar por referência. Você pode passar um ponteiro por valor , que é muito semelhante a passagem por referência, mas não é a mesma coisa. No C ++, no entanto, você pode passar por referência.
Mason Wheeler
@Mason Wheeler você pode elaborar mais ou adicionar algum link, beacuase sua declaração não está claro para mim como eu não sou guru C / C ++, apenas um programador regular, graças
Betlista
1
@ Betlista: Com passagem por referência, você pode escrever uma rotina de troca que se parece com isso: temp := a; a := b; b := temp;E quando retornar, os valores de ae bserão trocados. Não há como fazer isso em C; você tem que passar ponteiros para ae be a swaprotina tem de agir sobre os valores que eles apontam.
Mason Wheeler

Respostas:

58

Passar por valor é geralmente mais seguro que passar por referência, porque você não pode modificar acidentalmente os parâmetros para seu método / função. Isso torna o idioma mais simples de usar, pois você não precisa se preocupar com as variáveis ​​que atribui a uma função. Você sabe que eles não serão alterados, e isso é frequentemente o que você espera .

No entanto, se você deseja modificar os parâmetros, é necessário ter alguma operação explícita para esclarecer isso (passe um ponteiro). Isso forçará todos os chamadores a fazerem a chamada de maneira um pouco diferente ( &variable, em C) e explicita que o parâmetro variável pode ser alterado.

Portanto, agora você pode assumir que uma função não alterará seu parâmetro variável, a menos que seja explicitamente marcado para fazê-lo (exigindo que você passe um ponteiro). Essa é uma solução mais segura e limpa do que a alternativa: suponha que tudo pode alterar seus parâmetros, a menos que eles digam que não podem.

Oleksi
fonte
4
As matrizes +1 que se deterioram em ponteiros apresentam uma exceção notável, mas a explicação geral é boa.
precisa saber é o seguinte
6
@dasblinkenlight: matrizes são uma ferida em C :(
Matthieu M.
55

Chamada por valor e chamada por referência são técnicas de implementação que foram confundidas com os modos de passagem de parâmetros há muito tempo.

No começo, havia FORTRAN. O FORTRAN só tinha chamada por referência, uma vez que as sub-rotinas precisavam modificar seus parâmetros, e os ciclos de computação eram muito caros para permitir vários modos de passagem de parâmetros, e não se sabia o suficiente sobre programação quando o FORTRAN foi definido pela primeira vez.

ALGOL surgiu com chamada por nome e chamada por valor. Chamada por valor era para coisas que não deveriam ser alteradas (parâmetros de entrada). Chamada por nome era para parâmetros de saída. A chamada por nome acabou por ser um grande problema, e o ALGOL 68 a abandonou.

A PASCAL forneceu chamada por valor e chamada por referência. Não foi possível ao programador dizer ao compilador que estava passando um objeto grande (geralmente uma matriz) por referência, para evitar explodir a pilha de parâmetros, mas que o objeto não deve ser alterado.

PASCAL adicionou ponteiros ao léxico do design de idiomas.

C forneceu chamada por valor e simulação por chamada, definindo um operador kludge para retornar um ponteiro para um objeto arbitrário na memória.

Os idiomas posteriores copiaram C, principalmente porque os designers nunca haviam visto mais nada. É provavelmente por isso que a chamada por valor é tão popular.

O C ++ adicionou um kludge em cima do C kludge para fornecer chamada por referência.

Agora, como resultado direto de chamada por valor vs. chamada por referência vs. chamada por ponteiro kludge, C e C ++ (programadores) têm dores de cabeça horríveis com ponteiros const e ponteiros para const (somente leitura) objetos.

Ada conseguiu evitar todo esse pesadelo.

O Ada não possui chamada explícita por valor x chamada por referência. Em vez disso, Ada possui parâmetros (que podem ser lidos, mas não escritos), parâmetros de saída (que DEVEM ser escritos antes que possam ser lidos) e parâmetros de saída, que podem ser lidos e escritos em qualquer ordem. O compilador decide se um parâmetro específico é passado por valor ou por referência: é transparente para o programador.

John R. Strohm
fonte
1
+1. Esta é a única resposta que eu vi aqui até agora que realmente responde à pergunta e faz sentido e não a usa como uma desculpa transparente para um discurso pró-FP.
Mason Wheeler
11
+1 em "Idiomas posteriores copiaram C, principalmente porque os designers nunca haviam visto mais nada". Toda vez que vejo um novo idioma com constantes octais com prefixo 0, morro um pouco por dentro.
Librik
4
Esta poderia ser a primeira frase da Bíblia da programação: In the beginning, there was FORTRAN.
1
Com relação ao ADA, se uma variável global for passada para um parâmetro in / out, o idioma impedirá que uma chamada aninhada a examine ou modifique? Caso contrário, acho que haveria uma clara diferença entre os parâmetros in / out e ref. Pessoalmente, gostaria que os idiomas suportassem explicitamente todas as combinações de "entrada / saída / entrada + saída" e "por valor / por ref / don't-care", para que os programadores pudessem oferecer o máximo de clareza quanto à sua intenção, mas otimizadores teria flexibilidade máxima na implementação [desde que esteja de acordo com a intenção do programador].
22714
2
@ Neikos Há também o fato de que o 0prefixo é inconsistente com todos os outros prefixos dez que não sejam de base. Isso pode ser um pouco desculpável em C (e principalmente em linguagens compatíveis com a fonte, por exemplo, C ++, Objective C), se octal foi a única base alternativa quando introduzida, mas quando as linguagens mais modernas usam ambos 0xe 0desde o início, apenas as faz parecer mal. pensando.
16bit16
13

A passagem por referência permite efeitos colaterais não intencionais muito sutis, que são muito difíceis de serem quase impossíveis de rastrear quando começam a causar o comportamento não intencional.

A passagem por valor, especialmente final, staticou os constparâmetros de entrada fazem com que toda essa classe de erros desapareça.

Linguagens imutáveis ​​são ainda mais determinísticas e mais fáceis de raciocinar e entender o que está acontecendo e o que se espera que saia de uma função.


fonte
7
É uma daquelas coisas que você descobre depois de tentar depurar o código VB 'antigo', onde tudo é por referência como padrão.
jfrankcarr
Talvez seja apenas porque minha formação é diferente, mas não tenho idéia de que tipo de "efeitos colaterais indesejados muito sutis que são muito difíceis de serem quase impossíveis de rastrear" de que você está falando. Estou acostumado a Pascal, onde por-valor é o padrão, mas por-referência pode ser usada marcando explicitamente o parâmetro (basicamente o mesmo modelo que o C #) e nunca tive isso que causasse problemas para mim. Eu posso ver como isso seria problemático no "VB antigo", onde by-ref é o padrão, mas quando by-ref é opcional, ele faz você pensar sobre isso enquanto está escrevendo.
Mason Wheeler
4
@MasonWheeler, que tal novatos que surgem atrás de você, copia o que você fez sem entendê-lo e começa a manipular esse estado em todo o lugar indiretamente, o mundo real acontece o tempo todo. Menos indireção é uma coisa boa. Imutável é o melhor.
1
@MasonWheeler Os exemplos simples de alias, como os Swaphacks que não usam uma variável temporária, embora provavelmente não sejam um problema, mostram o quão difícil pode ser um exemplo mais complexo para depurar.
Mark Hurd
1
@MasonWheeler: O exemplo clássico do FORTRAN, que levou ao provérbio "Variáveis ​​não; constantes não", passaria uma constante de ponto flutuante como 3,0 para uma função que modifica o parâmetro passado. Para qualquer constante de ponto flutuante que foi passada para uma função, o sistema criaria uma variável "oculta" que foi inicializada com o valor apropriado e que poderia ser passada para funções. Se a função adicionar 1,0 ao seu parâmetro, um subconjunto arbitrário de 3,0 no programa poderá repentinamente se tornar 4,0.
Supercat 5/16
8

Por que tantos idiomas são passados ​​por valor?

O objetivo de dividir programas grandes em pequenas sub-rotinas é que você pode raciocinar sobre as sub-rotinas independentemente. Passagem por referência quebra essa propriedade. (Como o estado mutável compartilhado.)

Mesmo os idiomas em que você tem manipulação explícita de ponteiros como C, são sempre passados ​​por valor (você pode passá-los por referência, mas esse não é o comportamento padrão).

Na verdade, C é sempre passagem por valor, nunca passagem por referência. Você pode pegar um endereço de algo e passar esse endereço, mas o endereço ainda será passado por valor.

Qual é o benefício disso, por que tantos idiomas são transmitidos por valores e por que outros são transmitidos por referência ?

Há duas razões principais para usar o passe por referência:

  1. simulando vários valores de retorno
  2. eficiência

Pessoalmente, acho que o número 1 é falso: quase sempre é uma desculpa para API ruim e / ou design de linguagem:

  1. Se você precisar de vários valores de retorno, não os simule, basta usar um idioma que os suporte.
  2. Você também pode simular vários valores de retorno empacotando-os em alguma estrutura de dados leve, como uma tupla. Isso funciona particularmente bem se o idioma oferecer suporte à correspondência ou desestruturação de padrões. Por exemplo, Ruby:

    def foo
      # This is actually just a single return value, an array: [1, 2, 3]
      return 1, 2, 3
    end
    
    # Ruby supports destructuring bind for arrays: a, b, c = [1, 2, 3]
    one, two, three = foo
    
  3. Muitas vezes, você nem precisa de vários valores de retorno. Por exemplo, um padrão popular é que a sub-rotina retorna um código de erro e o resultado real é gravado de volta por referência. Em vez disso, você deve lançar uma exceção se o erro for inesperado ou retornar um Either<Exception, T>se o erro for esperado. Outro padrão é retornar um booleano que informa se a operação foi bem-sucedida e retornar o resultado real por referência. Novamente, se a falha for inesperada, você deve lançar uma exceção, se a falha for esperada, por exemplo, ao procurar um valor em um dicionário, você deve retornar umMaybe<T> .

A passagem por referência pode ser mais eficiente que a passagem por valor, porque você não precisa copiar o valor.

(Entendo que Haskell é passado por referência, embora não tenha certeza).

Não, Haskell não é passado por referência. Nem é passar por valor. Passagem por referência e passagem por valor são estratégias estritas de avaliação, mas Haskell não é rigoroso.

De fato, a Especificação Haskell não especifica nenhuma estratégia de avaliação específica. A maioria das implementações da Hakell usa uma combinação de chamada por nome e chamada por necessidade (uma variante de chamada por nome com memorização), mas o padrão não exige isso.

Observe que, mesmo para idiomas estritos, não faz sentido distinguir entre passar por referência ou passar por valor para idiomas funcionais, pois a diferença entre eles só pode ser observada se você alterar a referência. Portanto, o implementador é livre para escolher entre os dois, sem quebrar a semântica da linguagem.

Jörg W Mittag
fonte
9
"Se você precisar de vários valores de retorno, não os simule, basta usar uma linguagem que os suporte". Isso é um pouco estranho de se dizer. É basicamente dizendo "se o idioma pode fazer tudo que você precisa, mas uma característica que você provavelmente vai precisar de menos de 1% do seu código - mas você ainda vai precisar dele para que 1% - não pode ser feito em de uma maneira particularmente limpa, seu idioma não é bom o suficiente para o seu projeto e você deve reescrever tudo em outro idioma ". Desculpe, mas isso é simplesmente ridículo.
Mason Wheeler
+1 Eu concordo totalmente com este ponto O ponto de dividir programas grandes em pequenas sub-rotinas é que você pode raciocinar sobre as sub-rotinas independentemente. Passagem por referência quebra essa propriedade. (Como faz compartilhada estado mutável.)
Rémi
3

Há um comportamento diferente, dependendo do modelo de chamada da linguagem, do tipo de argumentos e dos modelos de memória da linguagem.

Com tipos nativos simples, a passagem por valor permite que você passe o valor pelos registradores. Isso pode ser muito rápido, já que o valor não precisa ser carregado da memória nem salvado de volta. Otimização semelhante também é possível simplesmente reutilizando a memória usada pelo argumento depois que o chamado é concluído, sem arriscar mexer com a cópia do objeto do chamador. Se o argumento fosse um objeto temporário, você provavelmente salvaria uma cópia completa fazendo isso (o C ++ 11 torna esse tipo de otimização ainda mais óbvio com a nova referência correta e sua semântica de movimento).

Em muitas linguagens OO (C ++ é mais uma exceção nesse contexto), você não pode transmitir um objeto por valor. Você é forçado a passar por referência. Isso torna o código polimórfico por padrão e está mais alinhado com a noção de instâncias próprias do OO. Além disso, se você deseja passar por valor, você deve fazer uma cópia, reconhecendo o custo do desempenho que gerou essa ação. Nesse caso, o idioma escolhe para você a abordagem com maior probabilidade de oferecer o melhor desempenho.

No caso de linguagens funcionais, acho que passar por valor ou referência é simplesmente uma questão de otimização. Como as funções nessa linguagem são puras e livres de efeitos colaterais, não há realmente nenhuma razão para copiar o valor, exceto a velocidade. Tenho certeza de que essa linguagem geralmente compartilhava a mesma cópia de objetos com os mesmos valores, uma possibilidade disponível apenas se você usasse uma semântica de referência de passagem (const). O Python também usou esse truque para seqüências inteiras e comuns (como métodos e nomes de classes), explicando por que números inteiros e seqüências de caracteres são objetos constantes no Python. Isso também ajuda a otimizar o código novamente, permitindo, por exemplo, comparação de ponteiros em vez de comparação de conteúdo e fazendo uma avaliação lenta de alguns dados internos.

Fabien Ninoles
fonte
2

Você pode passar uma expressão por valor, é natural. Passar expressão por referência (temporária) é ... estranho.

Herby
fonte
1
Além disso, passar a expressão por referência temporária pode levar a erros ruins (em linguagem com estado), quando você a altera com facilidade (já que é apenas temporária), mas depois sai pela culatra quando você realmente passa uma variável e você deve planejar uma solução feia como passar foo +0 em vez de foo.
usar o seguinte
2

Se você passar por referência, estará efetivamente sempre trabalhando com valores globais, com todos os problemas das globais (ou seja, escopo e efeitos colaterais indesejados).

As referências, assim como as globais, às vezes são benéficas, mas não devem ser sua primeira escolha.

jmoreno
fonte
3
Suponho que você quis dizer passar por referência na primeira frase?
jk.