Next Previous Contents

Unix Review Column 30 -- Глубокое копирование и рекурсия

Randal Schwartz

Февраль 2000

Перевод Anton Petrusevich <casus@mail.ru> и Alex Ott <ott@phtd.tpu.edu.ru>

Один из модулей, который я обнаружил на CPAN имел проблему с созданием нескольких объектов. Отдельные объекты создавались нормально, но при дальнейшем одновременном использовании объектов, некоторые данные из одного объекта таинственным способом оказывались в другом объекте, даже если первый объект был освобожден!

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

Что такое глубокое копирование и почему оно нужно нам? Давайте начнем с простого примера и вернемся к задаче, с которой я столкнулся.

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

  while (@x = getpwent()) {
    $info{$x[0]} = \@x;
  }
  for (sort keys %info) {
    print "$_ => @{$info{$_}}\n"
  }

Что? Где будут находиться данные? Мы сохранили ссылку на данные в значение хеша. Хорошо, может сделаем код более ясным:

  while (@x = getpwent()) {
    $info{$x[0]} = \@x;
    print "$info{$x[0]}\n";
  }

На моей машине, это несколько раз выдает ARRAY(0x80ce7fc), по одному разу для каждого из пользователей в системе. Так что это значит? Это значит, что мы используем один и тот же массив, и таким образом у нас нет различных ссылок, указывающих на разные массивы, у нас есть один массив, с множеством ссылок, указывающих на одни и те же данные. Таким образом, при последнем проходе цикла, массив @x будет опустошен и все ссылки на массив будут указывать на одинаковые пустые данные.

Это происходит, поскольку мы выполнили поверхностное копирование ссылки на массив @x в значение хеша, что копирует только указатель, а не содержимое массива. Что нам нужно, чтобы копировать не только ссылку, но и данные, на которые указывает данная ссылка. В нашем случае, это достаточно просто:

  while (@x = getpwent()) {
    $info{$x[0]} = [@x];
  }
  for (sort keys %info) {
    print "$_ => $info{$_} => @{$info{$_}}\n"
  }

И вы можете заметить, что мы для каждого из элементов хеша мы получили разные ссылки на массивы, указывающие на независимые копии 9-элементного массива, вначале содержавшегося в массиве @x. Этот код работе, поскольку мы создавали анонимные ссылки на массивы с помощью выражения [@x], что также дает анонимному массиву начальное значение, копируя элементы из массива @x.

Вот основной принцип глубокого копирования: для обеспечения полной независимости необходимо выполнять копирование не только указателя верхнего уровня, но и всех данных внутри структуры данных.

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

  while (my @x = getpwent()) {
    $info{$x[0]} = \@x;
  }
  for (sort keys %info) {
    print "$_ => $info{$_} => @{$info{$_}}\n"
  }

Здесь, каждый проход по циклу начинается с выделения совершенно лексически нового массива @x, вместо повторного использования существующей переменной, Так что когда на нее берется ссылка, и когда переменная выходит из области видимости в конце цикла, то переменная автоматически остается, уже как анонимная переменная.

Так что давайте вернемся к глубокому копированию. Вот другой пример. Предположим, что Fred и Barney используют один дом:

  $place = {
    Street => "123 Shady Lane",
    City => "Granite Junction",
  };
  $fred = {
    First_name => "Fred",
    Last_name => "Flintstone",
    Address => $place,
  };
  $barney = {
    First_name => "Barney",
    Last_name => "Rubble",
    Address => $place,
  };

Здесь заметьте, что $fred-&gt;{Address}{City} равен ``Granite Junction'', как мы и ожидали, этому значению равен и $barney-&gt;{Address}{City}. Но мы выполнили поверхностное копирование $place в значения обоих элементов Address. Это значит, что у нас нет двух копий данных, а просто один экземпляр данных. Мы можем убедиться в этом, когда мы изменим одно из значений. Давайте позволим Fred переехать в другое место:

  $fred->{Address}{Street} = "456 Palm Drive";
  $fred->{Address}{City} = "Bedrock";

Выглядит достаточно правильно. Но что случилось с Barney? Он переехал вместо с Fred!

print "@{$barney->{Address}}{qw(Street City)}\n";

Этот код выдает новый адрес Fred! Почему это случилось? Присвоение $place как адреса, в обоих случаях выполнило поверхностное копирование: обе структуры данных использовали общий указатель на общие данные улицы и адреса. Далее нам могло бы помочь глубокое копирование:

  $place = {
    Street => "123 Shady Lane",
    City => "Granite Junction",
  };
  $fred = {
    First_name => "Fred",
    Last_name => "Flintstone",
    Address => {%$place},
  };
  $barney = {
    First_name => "Barney",
    Last_name => "Rubble",
    Address => {%$place},
  };
  $fred->{Address}{Street} = "456 Palm Drive";
  $fred->{Address}{City} = "Bedrock";
  print "@{$barney->{Address}}{qw(Street City)}\n";

Здесь... теперь каждое поле Address является полностью отдельной копией, так что при обновлении одной из копий, остальные копии остаются без изменений. Это работает, поскольку как и в случае с выражением [@x], мы создаем новый анонимный хеш и берем ссылку на него.

Но что делать, если $place сам по себе является сложной структурой? Например, предположим, что улица в адресе состоит из названия и номера:

  $place = {
    Street => {
      Number => 123,
      Name => "Shady Lane",
    },
    City => "Granite Junction",
  };
  $fred = {
    First_name => "Fred",
    Last_name => "Flintstone",
    Address => {%$place},
  };
  $barney = {
    First_name => "Barney",
    Last_name => "Rubble",
    Address => {%$place},
  };

Теперь мы делаем то, что не является ни глубоким копированием, ни поверхностным копированием. Хеш $fred-&gt;{Address} отличается от хеша $barney-&gt;{Address}. Но они оба содержат значение, которое идентично ссылке на хеш $place-&gt;{Street}! Так, что если Fred просто переедет дальше по улице:

  $fred->{Address}{Street}{Number} = 456;

то Barney снова переедет вместе с ним! Теперь мы можем исправить положение применяя логику к процессу копирования адреса, для того чтобы скопировать и описание улицы:

  $fred = {
    First_name => "Fred",
    Last_name => "Flintstone",
    Address => {
      Street => {%{$place->{Street}}},
      City => $place->{City},
    },
  };

Но как вы можете видеть, процесс становится более и более извилистым. И что будет если мы заменим City на другую структуру, или добавим другой уровень к структуре Street.

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

  sub deep_copy {
    my $this = shift;
    if (not ref $this) {
      $this;
    } elsif (ref $this eq "ARRAY") {
      [map deep_copy($_), @$this];
    } elsif (ref $this eq "HASH") {
      +{map { $_ => deep_copy($this->{$_}) } keys %$this};
    } else { die "what type is $_?" }
  }

Эта подпрограмма получает один параметр: вершину дерева скалярных переменных и указателей на хеши и массивы. Если объект является скалярной переменной, то он просто возвращается в вызывающую программу, поскольку поверхностное копирование скаляра является и глубоким копированием. Если объект является ссылкой на массив, то мы из данных создаем с новый анонимный массив. Однако каждый из элементов массива также может являться структурой данных, так что нам необходимо выполнить глубокое копирование этих данных. Решение является очень простым: просто вызвать deep_copy для каждого из элементов. Аналогичным образом новая ссылка на хеш создается копированием каждого из элементов, включая глубокое копирование значений хеша. (Ключ хеша всегда является простым скаляром, так что он не нуждается в копировании, хотя это должно быть достаточно просто для добавления). Для того, чтобы убедиться в работоспособности, давайте передадим некоторые данные:

  $place = {
    Street => {
      Number => 123,
      Name => [qw(Shady Lane)],
    },
    City => "Granite Junction",
    Zip => [97007, 4456],
  };
  $place2 = $place;
  $place3 = {%$place};
  $place4 = deep_copy($place);

Хммм7 Как мы можем увидеть что мы сделали, и что было использовано совместно? Давайте добавим вызов стандартного библиотечного модуля, названного Data::Dumper:

  use Data::Dumper;
  $Data::Dumper::Indent = 1;
  print Data::Dumper->Dump(
    [$place, $place2, $place3, $place4],
    [qw(place place2 place3 place4)]
  );

И вот что выдается на моей машине:

  $place = {
    'City' => 'Granite Junction',
    'Zip' => [
      97007,
      4456
    ],
    'Street' => {
      'Name' => [
        'Shady',
        'Lane'
      ],
      'Number' => 123
    }
  };
  $place2 = $place;
  $place3 = {
    'City' => 'Granite Junction',
    'Zip' => $place->{'Zip'},
    'Street' => $place->{'Street'}
  };
  $place4 = {
    'City' => 'Granite Junction',
    'Zip' => [
      97007,
      4456
    ],
    'Street' => {
      'Number' => 123,
      'Name' => [
        'Shady',
        'Lane'
      ]
    }
  };

Давайте посмотрим на результаты. Data::Dumper позволяет мне увидеть, что $place2 является поверхностной копией $place, тогда как $place3 является промежуточной копией. Обратите внимание на элементы $place внутри $place3. И поскольку $place4 не содержит использованных до этого ссылок, то мы знаем, что эта переменная является полностью независимой глубокой копией. Успех! (Порядок элементов хеша является несовместимым, но при обычном использовании это является неважным и неопределяемым).

Теперь эта простая подпрограмма deep_copy будет давать сбой, если будут использоваться рекурсивные ссылки на данные (ссылки, которые указывают на данные выше по дереву). Для решения этой, вы можете взглянуть на метод dclone модуля Storable, который можно найти на CPAN.

Так, что когда вы используете = для структуры, то убедитесь, что вы знаете что делаете. Вместо поверхностного копирования вам может понадобиться глубокое копирование. Для получения дополнительной информации обратитесь к оперативной документации с помощью команд perldoc perldsc и perldoc perllol, или даже для описания основ, к документации выдаваемой командами perldoc perlref и perldoc perlreftut. Встретимся в следующий раз, наслаждайтесь!


Next Previous Contents