Перл 5.30.0. Библиотеки актуальны на сегодняшний день.
Я ожидаю, что Scalar::Util::readonly вернет некоторое истинное значение, если хеш доступен только для чтения, и это действительно так:
perl -MReadonly -M'Scalar::Util qw(readonly)' -E'
say readonly(%ENV);
Readonly::Hash %ENV => %ENV;
say readonly(%ENV);
'
0
134283264
... За исключением случаев, когда я также хочу использовать Types::Standard, тогда Scalar::Util::readonly больше не работает?!
perl -MReadonly -M'Scalar::Util qw(readonly)' -MTypes::Standard -E'
say readonly(%ENV);
Readonly::Hash %ENV => %ENV;
say readonly(%ENV);
'
0
0
Я просмотрел открытые проблемы для Types::Standard, но ничего не выскакивает из прямого описания моей проблемы.
Что здесь происходит ?
Это неправильный способ использования readonly
.
Невозможно передать хэш сабвуферу. Только скаляры могут быть переданы в качестве аргументов подпрограммам. Прототипы можно использовать, чтобы создать впечатление, что вы передаете хеш подпрограмме, но здесь это не так.
$ perl -E'
use Scalar::Util qw( readonly );
say prototype( "readonly" ) // "[none]";
'
$
Этот прототип означает, что
readonly( %ENV )
означает
&readonly( scalar( %ENV ) )
Он не проверяет, доступен ли %ENV
только для чтения; он проверяет, доступно ли значение, полученное в результате оценки %ENV
в скалярном контексте, только для чтения. Это совершенно неправильно.
Scalar::Util::readonly
нельзя использовать для проверки того, является ли хэш (или массив) readonly
, только скаляры.
Так как же проверить, доступен ли хеш только для чтения?
Что ж, Perl предоставляет встроенную подпрограмму, которая работает точно так же, как Scalar::Util::readonly
, называемую Internals::SvREADONLY
. В отличие от readonly
, SvREADONLY
работает с массивами как с хэшами, так и со скалярами.
$ perl -E'say prototype( "Internals::SvREADONLY" ) // "[none]";'
\[$%@];$
Этот прототип вызывает передачу ссылки на первый аргумент вместо самого аргумента. Как таковой,
Internals::SvREADONLY( %x )
сокращение от
&Internals::SvREADONLY( \%x )
Дело в том, что хэш, возвращаемый Readonly::Hash
, на самом деле не доступен только для чтения. Так что Internals::SvREADONLY
не более полезен, чем Scalar::Util::readonly
.
$ perl -E'
use Readonly qw( );
say Internals::SvREADONLY( %x ) ?1:0;
Readonly::Hash %x => %x;
say Internals::SvREADONLY( %x ) ?1:0;
'
0
0
Readonly::Hash
использует tie
для перехвата попыток изменить хэш.
$ perl -E'
use Devel::Peek qw( Dump );
use Readonly qw( );
Readonly::Hash %x => %x;
Dump( %x );
'
SV = PVHV(0x561f1e51b340) at 0x561f1e5435a8
REFCNT = 1
FLAGS = (RMG,OOK,SHAREKEYS) <--- No READONLY flag.
MAGIC = 0x561f1e558290
MG_VIRTUAL = &PL_vtbl_pack
MG_TYPE = PERL_MAGIC_tied(P) <--- tie() magic was added
MG_FLAGS = 0x02 to intercept attempts
REFCOUNTED to change the hash.
MG_OBJ = 0x561f1e515680
SV = IV(0x561f1e515670) at 0x561f1e515680
REFCNT = 1
FLAGS = (ROK)
RV = 0x561f1e5d39b8
SV = PVHV(0x561f1e51b400) at 0x561f1e5d39b8
REFCNT = 1
FLAGS = (OBJECT,SHAREKEYS)
STASH = 0x561f1e5d3c88 "Readonly::Hash"
ARRAY = 0x0
KEYS = 0
FILL = 0
MAX = 7
AUX_FLAGS = 0
ARRAY = 0x561f1e541950
KEYS = 0
FILL = 0
MAX = 7
RITER = -1
EITER = 0x0
RAND = 0x2685e09f
Вот как модуль проверяет, сделал ли он уже хэш доступным только для чтения:
tied( %x ) =~ 'Readonly::Hash'
Так почему же используется разница в выводе после Readonly::Hash
?
Хотя вопрос спорный, на него все еще интересно ответить.
Ну, это из-за ошибки в Readonly::Hash
: она возвращает неправильное значение в скалярном контексте.
$ perl -E'
use Readonly qw( );
my %x = ( a=>4, b=>5, c=>6 );
say scalar( %x );
Readonly::Hash %x => %x;
say scalar( %x );
'
3
1
Когда %x
используется в скалярном контексте, Perl возвращает количество элементов в хэше.[1]
С другой стороны, магия, добавленная Readonly::Hash
, заставляет возвращать истинное значение, когда хэш не пуст, и ложное значение, когда хэш пуст.
И в этом заключается разница.
Perl возвращает счетчик как временный скаляр. Он создается для хранения возвращаемого значения и будет освобожден после того, как вызывающая сторона сможет его скопировать. Нет смысла тратить время на то, чтобы сделать его доступным только для чтения.[2]
Readonly::Hash, с другой стороны, возвращает не просто любые истинные или ложные скаляры. Он возвращает одни и те же скаляры true и false, возвращаемые всеми операторами Perl, которые возвращают true или false. Не копии, а те самые скаляры, &PL_sv_yes
и &PL_sv_no
.[3] Эти скаляры доступны только для чтения.[4]
Так почему же Types::Standard имеет эффект?
Хотя вопрос спорный, на него все еще интересно ответить.
К сожалению, я не разобрался с этим.
Так было не всегда. Он никогда не возвращал только значение true/false, но старое значение практически было полезно только как значение true/false.
С некоторым трудом его можно было бы модифицировать (my $r = \scalar(%x); ++$$r
), но смысла в этом не было бы. Это не повлияет на хэш.
Интересный факт: наряду с &PL_sv_undef
они являются единственными тремя статически распределенными скалярами.
Они доступны только для чтения, потому что мы бы не хотели, чтобы 4 == 5
начала возвращать истинное значение, потому что &PL_sv_no
было случайно изменено.
Re "Я посмотрел на альтернативы Readonly", Почему? Проблема заключалась в том, что вы использовали readonly
. Если это связано с тем, что %ro
возвращает неверный результат в скалярном контексте, просто используйте вместо этого keys(%ro)
(в скалярном контексте).
Отличный обзор, спасибо. Я рассмотрел альтернативы только для чтения, но все они, похоже, ведут себя немного по-разному — например, Const::Fast слишком строг (для меня) при доступе к несуществующим ключам («Попытка доступа к запрещенному ключу»).