Как я могу изменить скалярную ссылку, переданную в ссылку на подпрограмму?

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

package Converter;
...
sub convert
{
    my ($self, $filename, $coderef) = @_;

    if ($filename =~ /html?$/i) {
        $self->_convert_html($filename, $coderef);
    }
}

sub _convert_html
{
    my ($self, $filename, $coderef) = @_;

    my $html = $self->slurp($filename);
    $coderef->(\$html); #this modifies the html
    $self->save_to_file($filename, $html);
}

который затем вызывается:

Converter->new->convert("./whatever.html", sub { s/<html>/<xml>/i });

Я пробовал несколько разных вещей в этом направлении, но я продолжаю получать «Использование неинициализированного значения в замене (s///)». Есть ли способ сделать то, что я пытаюсь сделать?

Спасибо


person Mark    schedule 17.05.2010    source источник
comment
Прежде чем вы прочитаете какой-либо из ответов ниже: вы можете попробовать добавить несколько операторов печати на каждом уровне подпрограммы, чтобы увидеть, действительно ли то, что вы получаете в качестве аргументов, соответствует тому, что, по вашему мнению, вы должны получить. Подсказка: оператор печати внутри кода подстановки должен привести вас к ответу.   -  person Ether    schedule 18.05.2010


Ответы (3)


Если бы это был я, я бы не стал изменять скалярную ссылку и просто вернул бы измененное значение:

sub _convert_html
{
    my ($self, $filename, $coderef) = @_;

    my $html = $self->slurp($filename);
    $html = $coderef->( $html ); #this modifies the html
    $self->save_to_file($filename, $html);
}

Однако, если вы хотите изменить аргументы подпрограммы, стоит знать, что все подаргументы в Perl передаются по ссылке (элементы @_ имеют псевдонимы для аргументов вызова подпрограммы). Таким образом, ваша конверсионная подпрограмма может выглядеть так:

sub { $_[0] =~ s/<html>/<xml>/ }

Но если вы действительно хотите работать с $_, как в желаемом примере кода, вам нужно сделать так, чтобы _convert_html() выглядело так:

sub _convert_html
{
    my ($self, $filename, $coderef) = @_;

    my $html = $self->slurp($filename);

    $coderef->() for $html;

    $self->save_to_file($filename, $html);
}

for — это простой способ правильно локализовать $_. Вы также можете сделать:

sub _convert_html
{
    my ($self, $filename, $coderef) = @_;

    local $_ = $self->slurp($filename);

    $coderef->();

    $self->save_to_file($filename, $_);
}
person daotoad    schedule 17.05.2010
comment
Спасибо за подробное объяснение :-) - person Mark; 18.05.2010
comment
Это много копий: сначала в стек аргументов, затем измените его и скопируйте обратно в стек аргументов. Это может повредить, если вам нужно обработать много-много страниц. - person brian d foy; 18.05.2010
comment
обратите внимание, что если скорость важна, local быстрее, чем for с одним элементом - person Eric Strom; 18.05.2010
comment
обратите внимание, что если вы заметили разницу в скорости между local и for, вы делаете что-то еще неправильно. - person jrockway; 18.05.2010
comment
Кроме того, Sub::AliasedUnderscore — более чистый способ, чем локальный или for. - person jrockway; 18.05.2010
comment
@brian, да, это может быть много копирования - тем более, что похоже, что save_to_file() тоже делает копию. Тем не менее, я обычно предпочитаю работать с копиями, когда это возможно; жуткого действия на расстоянии багов не будет, если работать только с копиями. Однако, если профилирование или предполагаемое использование предполагает, что нам нужно быть более эффективными, то я бы определенно положил конец дополнительному копированию и использовал ссылки для ускорения работы. Моя позиция по умолчанию — отдавать предпочтение надежности моих API, а не абсолютной эффективности. Есть много случаев, когда повышение эффективности стоит некоторых жертв. - person daotoad; 18.05.2010
comment
@jrockway, хороший модуль. Есть ли причина, по которой вы локализуете typeglob (*_), а не просто $_? - person daotoad; 18.05.2010

Помните, что s/// сам по себе работает с $_, но ваша скалярная ссылка передается в подпрограмму обратного вызова в качестве аргумента и, следовательно, находится в массиве @_.

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

sub { my ( $ref ) = @_; $$ref =~ s/<html>/<xml>/i }

Или вы можете воспользоваться преимуществом псевдонимов аргументов подпрограммы Perl и изменить их напрямую:

sub _convert_html { 
    ...
    $coderef->( $html );
}

а потом

sub { $_[0] =~ s/<html>/<xml>/i }

(Это фактически изменит исходную строку, если аргумент является скалярной переменной, а не литеральной строкой.)

person friedo    schedule 17.05.2010

Попробуй это:

Converter->new->convert("./whatever.html", sub { ${$_[0]} =~ s/<html>/<xml>/i; });

Вы получаете предупреждение о неинициализированном значении, потому что подстановке не дается ничего для работы ($_ не определено в своей области). Вам нужно сказать ему, где найти его значение (в @_, в качестве ссылки).

Если вы хотите быть необычным, вы можете заставить coderef работать со всеми своими аргументами по умолчанию:

sub { map { $$_ =~ s/<html>/<xml>/i } @_ }
person Ether    schedule 17.05.2010
comment
Для примера кода карты мне интересно, каким бы я хотел, чтобы возвращаемое значение было. :) - person brian d foy; 18.05.2010
comment
@брайан: действительно; обычно я бы написал подпрограмму преобразования, чтобы вернуть новое значение (я), а не использовать ссылки. - person Ether; 18.05.2010