Передавать по ссылке или передавать по значению?

При изучении нового языка программирования одним из возможных препятствий, с которыми вы можете столкнуться, является вопрос, является ли язык по умолчанию передачей по значению или по ссылке .

Итак, вот мой вопрос всем вам, на вашем любимом языке, как это на самом деле делается? И каковы возможные подводные камни?

Ваш любимый язык, конечно, может быть любым, с чем вы когда-либо играли: популярным , малоизвестным , эзотерическим , новым , старым ...

Ответов (11)

Поскольку я еще не видел ответа Perl, я подумал, что напишу его.

Под капотом Perl эффективно работает как передача по ссылке. Переменные как аргументы вызова функции передаются по ссылке, константы передаются как значения только для чтения, а результаты выражений передаются как временные. Обычные идиомы для создания списков аргументов путем присвоения списков @_ или с помощью shift имеют тенденцию скрывать это от пользователя, создавая видимость передачи по значению:

sub incr {
  my ( $x ) = @_;
  $x++;
}

my $value = 1;
incr($value);
say "Value is now $value";

Это будет напечатано, Value is now 1 потому что $x++ он увеличил лексическую переменную, объявленную внутри incr() функции, а не переданную переменную. Этот стиль передачи по значению обычно является тем, что требуется большую часть времени, поскольку функции, которые изменяют свои аргументы, редко встречаются в Perl, и стиля следует избегать.

Однако, если по какой-то причине такое поведение особенно желательно, его можно достичь, работая непосредственно с элементами @_ массива, поскольку они будут псевдонимами для переменных, переданных в функцию.

sub incr {
  $_[0]++;
}

my $value = 1;
incr($value);
say "Value is now $value";

На этот раз он будет печатать Value is now 2, потому что $_[0]++ выражение увеличило фактическую $value переменную. Это работает так, что под капотом @_ находится не настоящий массив, как большинство других массивов (например, получаемых с помощью my @array ), а вместо этого его элементы строятся непосредственно из аргументов, переданных при вызове функции. Это позволяет вам создавать семантику передачи по ссылке, если это необходимо. Аргументы вызова функции, которые являются простыми переменными, вставляются в этот массив как есть, а константы или результаты более сложных выражений вставляются как временные объекты только для чтения.

Однако на практике это делается очень редко, потому что Perl поддерживает ссылочные значения; то есть значения, относящиеся к другим переменным. Обычно гораздо проще создать функцию, которая имеет очевидный побочный эффект для переменной, передав ссылку на эту переменную. Это явное указание читателю на сайте вызова о том, что действует семантика передачи по ссылке.

sub incr_ref {
  my ( $ref ) = @_;
  $$ref++;
}

my $value = 1;
incr(\$value);
say "Value is now $value";

Здесь \ оператор дает ссылку почти так же, как & оператор адресации в C.

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

С этой точки зрения языки сгруппированы следующим образом; каждая группа имеет одинаковую семантику передачи. Если вы считаете, что два языка не следует помещать в одну группу, я призываю вас привести пример, который их различает.

Подавляющее большинство языков, включая C , Java , Python , Ruby , JavaScript , Scheme , OCaml , Standard ML , Go , Objective-C , Smalltalk и т. Д. , Передаются только по значению . Передача значения указателя (некоторые языки называют его «ссылкой») не считается передачей по ссылке; нас беспокоит только переданная вещь, указатель, а не то, на что указывает.

Такие языки, как C++ , C# , PHP по умолчанию передаются по значению, как и языки выше, но функции могут явно объявлять параметры для передачи по ссылке, используя & или ref .

Perl всегда передается по ссылке; однако на практике люди почти всегда копируют значения после их получения, таким образом используя их в качестве передачи по значению.

Вот мой собственный вклад в язык программирования Java .

сначала код:

public void swap(int x, int y)
{
  int tmp = x;
  x = y;
  y = tmp;
}

вызов этого метода приведет к следующему:

int pi = 3;
int everything = 42;

swap(pi, everything);

System.out.println("pi: " + pi);
System.out.println("everything: " + everything);

"Output:
pi: 3
everything: 42"

даже использование «реальных» объектов даст аналогичный результат:

public class MyObj {
    private String msg;
    private int number;

    //getters and setters
    public String getMsg() {
        return this.msg;
    }


    public void setMsg(String msg) {
        this.msg = msg;
    }


    public int getNumber() {
        return this.number;
    }


    public void setNumber(int number) {
        this.number = number;
    }

    //constructor
    public MyObj(String msg, int number) {
        setMsg(msg);
        setNumber(number);
    }
}

public static void swap(MyObj x, MyObj y)
{
    MyObj tmp = x;
    x = y;
    y = tmp;
}

public static void main(String args[]) {
    MyObj x = new MyObj("Hello world", 1);
    MyObj y = new MyObj("Goodbye Cruel World", -1); 

    swap(x, y);

    System.out.println(x.getMsg() + " -- "+  x.getNumber());
    System.out.println(y.getMsg() + " -- "+  y.getNumber());
}


"Output:
Hello world -- 1
Goodbye Cruel World -- -1"

таким образом, ясно, что Java передает свои параметры по значению , поскольку значение pi и всего остального, а также объекты MyObj не меняются местами. имейте в виду, что "по значению" - единственный способ передать параметры методу в java. (например, такой язык, как c ++, позволяет разработчику передавать параметр по ссылке, используя ' & ' после типа параметра)

теперь сложная часть , или, по крайней мере, та часть, которая запутает большинство новых разработчиков Java: (заимствовано из javaworld )
Оригинальный автор: Тони Синтес

public void tricky(Point arg1, Point arg2)
{
    arg1.x = 100;
    arg1.y = 100;
    Point temp = arg1;
    arg1 = arg2;
    arg2 = temp;
}
public static void main(String [] args)
{
    Point pnt1 = new Point(0,0);
    Point pnt2 = new Point(0,0);
    System.out.println("X: " + pnt1.x + " Y: " +pnt1.y); 
    System.out.println("X: " + pnt2.x + " Y: " +pnt2.y);
    System.out.println(" ");
    tricky(pnt1,pnt2);
    System.out.println("X: " + pnt1.x + " Y:" + pnt1.y); 
    System.out.println("X: " + pnt2.x + " Y: " +pnt2.y);  
}


"Output
X: 0 Y: 0
X: 0 Y: 0
X: 100 Y: 100
X: 0 Y: 0"

хитрость успешно меняет значение pnt1! Это означало бы, что объекты передаются по ссылке, это не так! Правильным утверждением было бы: ссылки на объекты передаются по значению.

другие работы Тони Синтеса:

Метод успешно изменяет значение pnt1, даже если оно передается по значению; однако поменять местами pnt1 и pnt2 не удается! Это главный источник путаницы. В методе main () pnt1 и pnt2 - это не что иное, как ссылки на объекты. Когда вы передаете pnt1 и pnt2 методу tricky (), Java передает ссылки по значению, как и любой другой параметр. Это означает, что ссылки, переданные методу, на самом деле являются копиями исходных ссылок. На рисунке 1 ниже показаны две ссылки, указывающие на один и тот же объект после того, как Java передает объект методу.

Рисунок 1
(источник: javaworld.com )

Вывод или короче:

  • Java передает ему параметры по значению
  • "по значению" - единственный способ в java передать параметр методу.
  • использование методов из объекта, указанного в качестве параметра , изменит объект, поскольку ссылки указывают на исходные объекты. (если сам метод изменяет некоторые значения)

Полезные ссылки:

Вот еще одна статья для языка программирования c #

c # передает свои аргументы по значению (по умолчанию)

private void swap(string a, string b) {
  string tmp = a;
  a = b;
  b = tmp;
}

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

string x = "foo";
string y = "bar";
swap(x, y);

"output: 
x: foo
y: bar"

Однако, в отличие от Java C# делает дает разработчику возможность передать параметры по ссылке , это делается с помощью «исх» ключевое слово перед типом параметра:

private void swap(ref string a, ref string b) {
  string tmp = a;
  a = b;
  b = tmp;
} 

эта замена будет изменить значение ссылочного параметра:

string x = "foo";
string y = "bar";
swap(x, y);

"output: 
x: bar
y: foo"

В c # также есть ключевое слово out , и разница между ref и out тонкая. из msdn:

Вызывающий метод, который принимает выходной параметр , не обязан назначать переменную, переданную как выходной параметр перед вызовом; однако перед возвратом вызываемый должен назначить параметр out.

а также

В отличие от реф параметры которые считаются изначально назначены вызываемым. Таким образом, вызываемому не требуется назначать параметр ref перед использованием. Параметры ссылки передаются как в метод, так и из него.

небольшая ошибка, как и в java, заключается в том, что объекты, переданные по значению, все еще могут быть изменены с помощью их внутренних методов

заключение:

  • c # по умолчанию передает свои параметры по значению
  • но при необходимости параметры также можно передать по ссылке с помощью ключевого слова ref
  • внутренние методы из параметра, переданного по значению , изменят объект (если сам метод изменяет некоторые значения)

Полезные ссылки:

по стоимости

  • медленнее, чем по ссылке, так как система должна скопировать параметр
  • используется только для ввода

по ссылке

  • быстрее, так как передается только указатель
  • используется для ввода и вывода
  • может быть очень опасным при использовании вместе с глобальными переменными

Не забывайте, что есть также передача по имени и передача по значению-результату .

Передача по значению-результату аналогична передаче по значению с дополнительным аспектом, заключающимся в том, что значение устанавливается в исходной переменной, которая была передана в качестве параметра. Это может до некоторой степени избежать вмешательства в глобальные переменные. Очевидно, это лучше для многораздельной памяти, где передача по ссылке может вызвать сбой страницы ( Ссылка ).

Передача по имени означает, что значения вычисляются только тогда, когда они фактически используются, а не в начале процедуры. Алгол использовал передачу по имени, но интересным побочным эффектом является то, что очень сложно написать процедуру подкачки ( Ссылка ). Кроме того, выражение, переданное по имени, переоценивается каждый раз при обращении к нему, что также может иметь побочные эффекты.

Здесь есть хорошее объяснение для .NET.

Многие люди удивляются, что ссылочные объекты на самом деле передаются по значению (как в C#, так и в Java). Это копия адреса стека. Это предотвращает изменение метода, на которое фактически указывает объект, но по-прежнему позволяет методу изменять значения объекта. В C# можно передать ссылку по ссылке, что означает, что вы можете изменить место, на которое указывает фактический объект.

PHP также передается по значению.

<?php
class Holder {
    private $value;

    public function __construct($value) {
        $this->value = $value;
    }

    public function getValue() {
        return $this->value;
    }
}

function swap($x, $y) {
    $tmp = $x;
    $x = $y;
    $y = $tmp;
}

$a = new Holder('a');
$b = new Holder('b');
swap($a, $b);

echo $a->getValue() . ", " . $b->getValue() . "\n";

Выходы:

a b

Однако в PHP4 объекты обрабатывались как примитивы . Что значит:

<?php
$myData = new Holder('this should be replaced');

function replaceWithGreeting($holder) {
    $myData->setValue('hello');
}

replaceWithGreeting($myData);
echo $myData->getValue(); // Prints out "this should be replaced"

Python использует передачу по значению, но поскольку все такие значения являются ссылками на объекты, общий эффект сродни передаче по ссылке. Однако программисты Python больше думают о том, является ли тип объекта изменяемым или неизменным . Изменяемые объекты могут быть изменены на месте (например, словари, списки, определяемые пользователем объекты), тогда как неизменяемые объекты не могут (например, целые числа, строки, кортежи).

В следующем примере показана функция, которой передаются два аргумента, неизменяемая строка и изменяемый список.

>>> def do_something(a, b):
...     a = "Red"
...     b.append("Blue")
... 
>>> a = "Yellow"
>>> b = ["Black", "Burgundy"]
>>> do_something(a, b)
>>> print a, b
Yellow ['Black', 'Burgundy', 'Blue']

Строка a = "Red" просто создает локальное имя a для строкового значения "Red" и не влияет на переданный аргумент (который теперь скрыт, так как a с этого момента должен ссылаться на локальное имя). Присваивание не является операцией на месте, независимо от того, является ли аргумент изменяемым или неизменным.

b Параметр представляет собой ссылку на изменяемый объект списка, а .append() метод выполняет расширение в месте списка, лавируя на новом "Blue" строкового значения.

(Поскольку строковые объекты неизменяемы, у них нет методов, поддерживающих модификации на месте.)

После возврата из функции повторное присвоение не a имеет никакого эффекта, а расширение b явно показывает семантику вызова стиля передачи по ссылке.

Как упоминалось ранее, даже если аргумент for a является изменяемым типом, повторное присвоение внутри функции не является операцией на месте, и поэтому значение переданного аргумента не изменится:

>>> a = ["Purple", "Violet"]
>>> do_something(a, b)
>>> print a, b
['Purple', 'Violet'] ['Black', 'Burgundy', 'Blue', 'Blue']

Если вы не хотите, чтобы ваш список изменялся вызываемой функцией, вы должны вместо этого использовать неизменяемый тип кортежа (обозначенный круглыми скобками в буквальной форме, а не квадратными скобками), который не поддерживает метод на месте .append() :

>>> a = "Yellow"
>>> b = ("Black", "Burgundy")
>>> do_something(a, b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in do_something
AttributeError: 'tuple' object has no attribute 'append'

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

spaceused=: [: 7!:5 <
exectime =: 6!:2
big_chunk_of_data =. i. 1000 1000 100
passbyvalue =: 3 : 0
    $ y
    ''
)
locale =. cocreate''
big_chunk_of_data__locale =. big_chunk_of_data
passbyreference =: 3 : 0
    l =. y
    $ big_chunk_of_data__l
    ''
)
exectime 'passbyvalue big_chunk_of_data'
   0.00205586720663967
exectime 'passbyreference locale'
   8.57957102144893e_6

Очевидным недостатком является то, что вам нужно каким-то образом знать имя вашей переменной в вызываемой функции. Но этот метод может безболезненно перемещать большой объем данных. Вот почему, хотя технически это не передается по ссылке, я называю это «почти так».

По умолчанию ANSI / ISO C использует любой из них - это зависит от того, как вы объявляете свою функцию и ее параметры.

Если вы объявляете параметры функции как указатели, тогда функция будет передаваться по ссылке, а если вы объявите параметры функции как переменные, не являющиеся указателями, тогда функция будет передаваться по значению.

void swap(int *x, int *y);   //< Declared as pass-by-reference.
void swap(int x, int y);     //< Declared as pass-by-value (and probably doesn't do anything useful.)

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

float *FtoC(float temp)
{
    float c;
    c = (temp-32)*9/5;
    return &c;
}

Однако вы можете вернуть ссылку на статическую переменную или указатель, который был передан в списке параметров.

float *FtoC(float *temp)
{
    *temp = (*temp-32)*9/5;
    return temp;
}