Só um adendo - e eu sei que o texto está fazendo simplificações para ficar mais didático, mas não tem jeito, eu sou chato e pedante, então vamos lá:
Essa questão do array ser um ponteiro para o primeiro elemento é verdade em C. Mas em JavaScript e Python (e em várias outras linguagens de mais alto nível), não é bem assim. Essas linguagens abstraem as estruturas de forma que, na prática, não necessariamente será um ponteiro direto para o array.
Por exemplo, vejamos a implementação mais comum do Python (que é escrita em C, e chamada de CPython). Neste caso, a lista é implementada como um struct
:
typedef struct {
PyObject_VAR_HEAD
/* Vector of pointers to list elements. list[0] is ob_item[0], etc. */
PyObject **ob_item;
/* ob_item contains space for 'allocated' elements. The number
* currently in use is ob_size.
* Invariants:
* 0 <= ob_size <= allocated
* len(list) == ob_size
* ob_item == NULL implies ob_size == allocated == 0
* list.sort() temporarily sets allocated to -1 to detect mutations.
*
* Items must normally not be NULL, except during construction when
* the list is not yet visible outside the function that builds it.
*/
Py_ssize_t allocated;
} PyListObject;
Ou seja, é uma estrutura que contém o tamanho alocado e um array de ponteiros para os elementos da lista. Isso quer dizer que na verdade, por debaixo dos panos, não estamos passando um ponteiro para o array, e sim um ponteiro para o struct
(que internamente manipulará o array de fato).
Mas isso é no nível mais interno da implementação. No código Python em si, tudo que vemos são variáveis, que no fundo são referências para objetos (e referência pode ser só o ponteiro/endereço, ou ainda alguma outra estrutura que abstrai esses detalhes). Até mesmo se fizermos x = 1
, x
será uma referência para o valor 1
(que é implementando como um objeto int
). E claro que em outras linguagens pode ser diferente.
Quanto ao fato de ser mutável ou imutável, não tem relação direta com passagem por valor e referência. Vc pode ter tipos imutáveis que são passados por referência, só que aí não vai ser possível mudá-lo porque a imutabilidade está no tipo, não no fato de vc ter uma referência ao mesmo.
Passagem por valor ou por referência
Aliás, sobre passagem por valor e por referência, tem um detalhe bem sutil que muita gente confunde. E pra isso copiei os exemplos abaixo daqui.
Primeiro, este código em C:
void function2(int *param) {
printf("I've received value %d\n", *param);
(*param)++;
}
int main(void) {
int variable = 111;
function2(&variable);
printf("variable %d\n", variable);
return 0;
}
Saída:
I've received value 111
variable=112
Repare que o valor da variável foi alterado dentro da função, dando a impressão de que ela foi passada por referência. Mas na verdade, o que foi passado para a função foi um ponteiro para a variável (&variable
, o endereço da variável). E esse endereço é passado por valor (é feita uma cópia do endereço, e a cópia que é usada na função).
O que acontece é que, dentro da função, o operador *
desreferencia o ponteiro (pega o valor que está no endereço para o qual ele aponta) e aí o valor que está lá pode ser modificado. Mas isso não quer dizer que a variável foi passada por referência, e sim que o ponteiro/endereço para a variável foi passado por valor.
E como ele comprova isso? Com este código:
void function2(int *param) {
printf("address param is pointing to %d\n", param);
param = NULL;
}
int main(void) {
int variable = 111;
int *ptr = &variable;
function2(ptr);
printf("address ptr is pointing to %d\n", ptr);
return 0;
}
A saída é:
address param is pointing to -1846583468
address ptr is pointing to -1846583468
Se o ponteiro fosse passado por referência, então depois da função ele seria nulo, mas não é o caso. Isso porque a função recebe o ponteiro por valor (ela recebe uma cópia do endereço), e qualquer alteração feita neste ponteiro dentro da função não é refletida fora dela, por isso o ponteiro continua apontando para o mesmo endereço depois que a função é chamada. Então em C vc tem apenas a ilusão de passagem por referência (ou uma simulação, feita de maneira "esperta" com os ponteiros).
Aliás, se fizer assim, também dá o mesmo resultado:
function2(&variable);
printf("address ptr is pointing to %d\n", &variable);
Pois a função recebe uma cópia do ponteiro/endereço, e a alteração feita dentro dela (param = NULL
) não se reflete fora dela.
Algo similar ocorre em Java, JS, Pyhton, etc. A função a grosso modo recebe uma referência (outro nome bonito para o ponteiro ou endereço, mas pode ter mais detalhes de implementação que os abstraem), mas na verdade, ela recebe uma cópia desta referência (ou uma cópia do endereço). E como as operações são feitas no endereço, o resultado é o objeto original sendo modificado. Por exemplo, em Python:
def f(x):
x.append(4)
lista = [1, 2, 3]
f(lista)
print(lista) # [1, 2, 3, 4]
A função recebe a referência à lista (uma "cópia do endereço" dela) e atua em cima desta cópia. O que acontece é que métodos são sempre chamados no objeto para o qual a referência aponta. E como foi feita uma "cópia do endereço", o método é chamado no mesmo endereço do objeto original, e por isso este é modificado.
Mas se fizer assim:
def f(x):
x = [4, 5, 6]
lista = [1, 2, 3]
f(lista)
print(lista) # [1, 2, 3]
Aí a lista não é modificada. Isso porque a atribuição x = [4, 5, 6]
foi feita na cópia, e não na referência original. Afinal, a função recebe uma cópia da referência, e uma atribuição cria um objeto novo (com um novo endereço, e portanto, outra referência). Mas como x
é uma cópia, a atribuição a ele não altera a lista fora da função. Para mais detalhes, veja aqui.
E vale lembrar que a função sempre receberá uma "cópia do endereço", mesmo se o tipo for imutável. A imutabilidade não tem relação direta com o tipo de passagem, são caraterísticas independentes.
Então o que é passagem por referência?
Uma linguagem que tem passagem por referência de fato é C#:
public class Test
{
public static void Main()
{
int[] array = { 1, 2, 3 };
f(ref array);
foreach (int n in array)
Console.WriteLine(n);
}
public static void f(ref int[] array)
{
array = new[]{4, 5, 6};
}
}
O código acima imprime 4 5 6
, pois a função recebe a referência do array (indicado por ref
), e não uma cópia. Por isso é possível atribuir outro array dentro da função, e esta mudança se reflete fora dela.
Se removermos os ref
's do código, aí passa a ser passagem por valor normal: é feita uma cópia da referência, igual ao exemplo anterior com Python. E por isso a atribuição dentro da função é feita na cópia e isso não altera o array fora da função. Por isso, se remover os ref
's, o código imprimirá 1 2 3
.
Só pra dar outro exemplo, PHP também tem passagem por referência, bastando colocar &
antes do parâmetro:
function f(&$var) { // & indica que $var é passada por referência
$var = 1;
}
$x = 'oi';
f($x);
echo $x; // 1
Se removermos o &
antes de $var
, aí deixa de ser passagem por referência e o código imprime "oi".