问题
I want to use a superclass sort which uses a subclass compare function. I've tried to distill the nature of the question in the following code. This isn't the "production" code, but is presented here for illustration. It's tested.
#!/usr/bin/perl
# $Id: foo,v 1.10 2019/02/23 14:14:33 bennett Exp bennett $
use strict;
use warnings;
package Fruit;
use Scalar::Util 'blessed';
sub new {
my $class = shift;
my $self = bless({}, $class);
$self->{itemList} = [];
warn "Called with class ", blessed $self, "\n";
return $self;
}
package Apples;
use parent qw(-norequire Fruit);
sub mySort {
my $self = shift;
@{$self->{itemList}} = sort compare @{$self->{itemList}};
return $self;
}
sub compare {
$a->{mass} <=> $b->{mass};
}
package main;
my $apfel = Apples->new();
push(@{$apfel->{itemList}}, { "name" => "grannysmith", "mass" => 12 });
push(@{$apfel->{itemList}}, { "name" => "macintosh", "mass" => 6 });
push(@{$apfel->{itemList}}, { "name" => "Alkmene", "mass" => 8 });
$apfel->mySort();
for my $f (@{$apfel->{itemList}}) {
printf("%s is %d\n", $f->{name}, $f->{mass});
}
exit 0;
What I want to do is to move mySort() to the abstract superclass Fruit. I've tried a number ways of addressing the $self->compare() subroutine, but I'm not having much luck.
Any thoughts?
I've gotten it to call the correct subroutine, but never with the correct $a and $b. I've left all of my failed attempts out of this question in the hopes that someone will know right away how to move the mySort() to the Fruit package so that I can sort my oranges with the same subroutine.
回答1:
You've got two problems. First, you need the mySort function in the super class to call the compare function for the correct subclass. Second, you need the compare function in the subclass to be able to receive the two elements it wants to compare from a call in a different package.
It's not clear whether you worked out a solution to the first problem, but one solution is to use UNIVERSAL::can to find out the right comparison method.
package Fruit;
sub mySort {
my $self = shift;
my $compare_func = $self->can("compare");
@{$self->{itemList}} = sort $compare_func @{$self->{itemList}};
}
This will find the correct subclass compare function and use it in the sort call.
Now the issue in the Apples::compare function will be that when Fruit::mySort is ready to compare a couple of elements, it will set the package variables $Fruit::a and $Fruit::b, not $Apples::a and $Apples::b. So your Apples::compare function must be prepared for this. Here are a couple of solutions:
package Apples;
sub compare {
package Fruit;
$a->{mass} <=> $b->{mass};
}
or
sub compare {
$Fruit::a->{mass} <=> $Fruit::b->{mass}
}
or more defensively,
package Apples;
sub compare {
my $pkg = caller;
if ($pkg ne __PACKAGE__) {
no strict 'refs';
$a = ${"${pkg}::a"};
$b = ${"${pkg}::b"};
}
$a->{mass} <=> $b->{mass}
}
Update: I thought about making a subroutine attribute that would copy $a and $b values into the correct package, but after benchmarking it and thinking about alternatives, I decided against it. Here were my results for posterity:
Consider three sort routines (that might be in another package and hard to use from the current package)
sub numsort { $a <=> $b }
sub lexsort { $a cmp $b }
sub objsort { $a->{value} <=> $b->{value} }
Here are some ways we can make these packages accessible:
implement a subroutine attribute to prepare the
$aand$bvariables in the right package. Implementation is too long to include here, but the sub declaration would look likesub numsort : CrossPkg { $a <=> $b }rewrite the comparison function to compare
$_[0]and$_[1]instead of$aand$b, and use a wrapper in thesortcallsub lexcmp { $_[0] cmp $_[1] } ... @output = sort { lexcmp($a,$b) } @inputPerform the sort call in the correct package, so it sets the correct
$aand$bvalues.@output = do { package OtherPackage; sort numsort @input };
And here are the benchmarking results. The local method is the ordinary sort call with no cross-package issues.
Rate attrib-numsort wrap-numcmp local-numsort repkg-numsort
attrib-numsort 1.17/s -- -90% -96% -96%
wrap-numcmp 11.6/s 885% -- -61% -64%
local-numsort 29.5/s 2412% 155% -- -8%
repkg-numsort 32.2/s 2639% 178% 9% --
Rate attrib-lexsort repkg-lexsort wrap-lexcmp local-lexsort
attrib-lexsort 3.17/s -- -12% -14% -17%
repkg-lexsort 3.60/s 13% -- -2% -5%
wrap-lexcmp 3.68/s 16% 2% -- -3%
local-lexsort 3.80/s 20% 6% 3% --
Rate attrib-objsort wrap-objcmp local-objsort repkg-objsort
attrib-objsort 1.22/s -- -81% -88% -89%
wrap-objcmp 6.32/s 417% -- -38% -44%
local-objsort 10.1/s 730% 61% -- -10%
repkg-objsort 11.3/s 824% 79% 11% --
Summary: overhead is less of a concern with lexsort,
where each comparison takes more time. The attribute
approach is dead on arrival. Setting the package going into
the sort call has the
best results -- more or less no overhead -- but it isn't
suitable for this application (in an object hierarchy).
Rewriting the comparison function and wrapping the function
in the sort call isn't too bad of a performance drop-off,
and it works in an object hierarchy, so the final
recommendation is:
package Fruit;
sub compare { ... }
sub mySort {
my $self = shift;
@{$self->{itemList}} =
sort { $self->can("compare")->($a,$b) } @{$self->{itemList}};
}
package Apples;
our @ISA = qw(Fruit)
sub compare { $_[0]->{mass} <=> $_[1]->{mass} }
回答2:
The punctuation variables such as $_[1] are called "super-globals" because they refer to the variable in the main:: namespace.[2] In other words, no matter what's the current package, $_ is short for $main::_.
$a and $b aren't super-globals. They are ordinary package variables. sort populates the $a and $b of the package in which the sort is found, which leads to problems if sort and the compare function are found in different packages. This means that moving mySort to Fruit:: will cause sort to populate $Fruit::a and $Fruit::b, but your compare function reads $Apple::a and $Apple::b.
There are a few solutions you could use when multiple packages are involved, but the simplest is to use the ($$) prototype on the compare function. This causes sort to pass the values to compare as arguments instead of using $a and $b.
package Foo;
my $compare = \&Bar::compare;
my @sorted = sort $compare @unsorted;
package Bar;
sub compare($$) { $_[0] cmp $_[1] }
sort calls the sub as a function, not a method. If you want it called as a method, you'll need a wrapper.
package Foo;
my @sorted = sort { Bar->compare($a, $b) } @unsorted;
package Bar;
sub compare { $_[1] cmp $_[2] }
That said, the idea of having sort in one class and the sorter in a sub class is fundamentally flawed. You can presumably have a list that contains both Apples and Oranges, so how can you determine which compare method to call?
package Foo;
my @sorted = sort { ???->compare($a, $b) } @unsorted;
package Bar;
sub compare { $_[1] cmp $_[2] }
And a few named ones too such as
STDIN.By using a fully-qualified name (e.g.
$package::_), you can access the punctuation variables of other packages. These have no special meaning; they aren't used by Perl itself.
回答3:
The variables $a and $b are used by sort as package variables in the same package that sort was called, so in order for the child class to see them, you could try this.
In the parent class:
sub mySort {
my $self = shift;
@{$self->{itemList}} = sort { $self->compare($a, $b) } @{$self->{itemList}};
return $self;
}
In the child class:
sub compare {
my ( $self, $a, $b ) = @_;
$a->{mass} <=> $b->{mass};
}
来源:https://stackoverflow.com/questions/54842607/can-i-call-a-superclass-sort-with-a-subclass-compare-in-perl