Как мога да променя скаларна препратка, предадена на подпрограма?

Имам функция за конвертиране на документи в различни формати, която след това извиква друга функция въз основа на типа документ. Това е доста ясно за всичко, освен 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
Преди да прочетете някой от отговорите по-долу: може да искате да опитате да добавите някои оператори за печат на всяко ниво на подпрограма, за да видите дали това, което получавате като аргументи, наистина съвпада с това, което смятате, че трябва да получите. Съвет: операторът за печат в заместващия coderef трябва да ви отведе до отговора.   -  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);
}

Въпреки това, ако искате да промените аргументите на sub, струва си да знаете, че всички sub-аргументи се предават по препратка в Perl (елементите на @_ са свързани с аргументите на sub-извикването). Така че вашият подконверсия може да изглежда така:

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 е по-чист начин от local или 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/// сам по себе си оперира с $_, но вашата скаларна референция се предава на вашия subback callback като аргумент и следователно е в масива @_.

Така че можете просто да промените вашия subback call към нещо като това:

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
@brian: наистина; обикновено бих написал преобразуващ sub, за да върна новата стойност(и), вместо да използвам препратки. - person Ether; 18.05.2010