Лучший способ использовать метод "isa"?

Что такое "лучший" способ использовать "isa()" надежно? Другими словами, он корректно работает с любым значением, а не только с объектом.

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

В этом вопросе упоминаются два подхода, которые кажутся надежными (обратите внимание, что старый стиль UNIVERSAL::isa() не должен использоваться, с причинами, хорошо задокументированными в ответах на этот вопрос Q ):

eval { $x->isa("Class") }
#and check [email protected] in case $x was not an object, in case $x was not an object

use Scalar::Util 'blessed';
blessed $x && $x ->isa($class);

Первый использует eval, второй использует B:: (по крайней мере, для не-XS-аромата Scalar:: Util).

Первое не работает корректно, если $x - это скаляр, содержащий имя класса, как показано ниже, поэтому я склоняюсь к # 2 (используя blessed), если только somoene не указывает на вескую причину.

$ perl5.8 -e '{use IO::Handle;$x="IO::Handle";
  eval {$is = $x->isa("IO::Handle")}; print "$is:[email protected]\n";}'          
1:

Есть ли какие-либо объективные причины для выбора одного из этих двух подходов (или третьего я не знаю), таких как производительность, а не обработка какого-либо специального случая и т.д.?

Ответы

Ответ 1

Реализация Scalar::Util категорически лучше. Это позволяет избежать накладных расходов eval {}, которые всегда приводят к установке дополнительной переменной.

perl -we'[email protected]=q[foo]; eval {}; print [email protected]'

Реализация Scalar::Util легче читать (она не умирает по причине, неизвестной коду). Если eval терпит неудачу, я верю, что происходит, когда вы проходите назад в дереве до состояния до eval - так достигается состояние сброса. Это связано с дополнительными расходами на отказ.

Бенчмарки

Не объект вообще

          Rate eval   su
eval  256410/s   -- -88%
su   2222222/s 767%   --

Прохождение объектов isa check

          Rate   su eval
su   1030928/s   -- -16%
eval 1234568/s  20%   --

Ошибка объекта isa check

         Rate   su eval
su   826446/s   --  -9%
eval 909091/s  10%   --

Тестовый код:

use strict;
use warnings;
use Benchmark;
use Scalar::Util;

package Foo;

Benchmark::cmpthese(
    1_000_000
    , {
        eval => sub{ eval{ $a->isa(__PACKAGE__) } }
        , su => sub { Scalar::Util::blessed $a && $a->isa(__PACKAGE__) }
    }
);

package Bar;

$a = bless {};

Benchmark::cmpthese(
    1_000_000
    , {
        eval => sub{ eval{ $a->isa(__PACKAGE__)} }
        , su => sub { Scalar::Util::blessed $a && $a->isa(__PACKAGE__) }
    }
);

package Baz;

$a = bless {};

Benchmark::cmpthese(
    1_000_000
    , {
        eval => sub{ eval{ $a->isa('duck')} }
        , su => sub { Scalar::Util::blessed $a && $a->isa( 'duck' ) }
    }
);

Я использовал это perl, v5.10.1 (*), построенный для i486-linux-gnu-thread-multi, и Scalar::Util, 1.21

Ответ 2

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

use Scalar::Util 'blessed';

my $isa = sub {blessed $_[0] and $_[0]->isa($_[1])};

my $obj;

if ($obj->$isa('object')) { ... } # returns false instead of throwing an error 

$obj = {};

if ($obj->$isa('object')) { ... } # returns false as well

bless $obj => 'object';

if ($obj->$isa('object')) { say "we got an object" }

Обратите внимание, что $obj->$isa(...) - это просто другое написание $isa->($obj, ...), поэтому вызов метода фактически не выполняется (поэтому он избегает бросать какие-либо ошибки).

И вот какой-то код, который позволит вам называть isa на что угодно, а затем проверять результат (вдохновленный ответом Axeman):

{package ISA::Helper;
    use Scalar::Util;
    sub new {
        my ($class, $obj, $type) = @_;
        my $blessed = Scalar::Util::blessed $obj;
        bless {
            type    => $type,
            obj     => $obj,
            blessed => $blessed,
            isa     => $blessed && $obj->isa($type)
        } => $class
    }
    sub blessed         {$_[0]{blessed}}
    sub type            {$_[0]{isa}}
    sub ref     {ref     $_[0]{obj}}
    sub defined {defined $_[0]{obj}}

    use overload fallback => 1,
                 bool     => sub {$_[0]{isa}};
    sub explain {
        my $self = shift;
        $self->type    ? "object is a $$self{type}" :
        $self->blessed ? "object is a $$self{blessed} not a $$self{type}" :
        $self->ref     ? "object is a reference, but is not blessed" :
        $self->defined ? "object is defined, but not a reference"
                       : "object is not defined"
    }
}
my $isa = sub {ISA::Helper->new(@_)};

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

my @items = (
    undef,
    5,
    'five',
    \'ref',
    bless( {} => 'Other::Pkg'),
    bless( {} => 'My::Obj'),
);

for (@items) {
    if (my $ok = $_->$isa('My::Obj')) {
        print 'ok: ', $ok->explain, "\n";
    } else {
        print 'error: ', $ok->explain, "\n";
    }
}

print undef->$isa('anything?')->explain, "\n";

my $obj = bless {} => 'Obj';
print $obj->$isa('Obj'), "\n";

my $ref = {};
if (my $reason = $ref->$isa('Object')) {
    say "all is well"
} else {
    given ($reason) {
        when (not $_->defined) {say "not defined"}
        when (not $_->ref)     {say "not a reference"}
        when (not $_->blessed) {say "not a blessed reference"}
        when (not $_->type)    {say "not correct type"}
    }
}

это печатает:

error: object is not defined
error: object is defined, but not a reference
error: object is defined, but not a reference
error: object is a reference, but is not blessed
error: object is a Other::Pkg not a My::Obj
ok: object is a My::Obj
object is not defined
1
not a blessed reference

Если кто-то считает, что это действительно полезно, дайте мне знать, и я поставлю его на CPAN.

Ответ 3

Это может показаться немного грубым для Perl, но ни один из них не идеален. И то, и другое скрывает тот факт, что объекты привязаны к Perl. blessed идиома многословна и содержит более пары простых частей.

blessed( $object ) && object->isa( 'Class' )

Я бы предпочел что-то вроде этого:

object_isa( $object, 'Class' )

Нет логической операции, чтобы ошибиться, и большинство негодных использований будет отсеяно компилятором. (Кавычки не закрываются, запятая отсутствует, парены не закрываются, вместо этого вызывается object_isa...)

Для этого потребуются неопределенные скаляры, простые скаляры (если они не являются именем класса, являющегося Class), необъявленные ссылки и благословенные ссылки, которые не расширяют "класс" и говорят вам, что нет, они не являются объектами Class. Если мы не хотим идти по маршруту autobox -ing всего, нам понадобится функция, которая нам просто говорит.

Возможно, для $how_close может быть третий параметр, но также может быть что-то вроде этого:

if ( my $ranking = object_isa( $object, 'Class' )) { 
   ...
}
else { 
   given ( $ranking ) { 
       when ( NOT_TYPE )    { ... }
       when ( NOT_BLESSED ) { ... }
       when ( NOT_REF )     { ... }
       when ( NOT_DEFINED ) { ... }
   }
}

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

Тем не менее, она может иметь несколько членов: EXACTLY, INHERITS, IMPLEMENTS, AGGREGATES или даже MOCKS

Я тоже устал набирать это:

$object->can( 'DOES' ) && $object->DOES( 'role' )

потому что я пытаюсь реализовать ориентированные на будущее DOES в меньшем количестве (на идее, что люди могут осудить мой загрязняющий UNIVERSAL) на них.