为何Perl三元条件运算符处理未定义哈希引用键与普通变量行为不同?
为什么Perl的三元条件运算符处理未定义哈希引用键时,和处理普通未定义变量的行为不一样?比如当$form未定义但$str已定义时,表达式($str) ? $form = $str : $form = $form;总能把$str的值赋给$form;但换成未定义的哈希键后,这个表达式就不生效了。
我写了一个脚本演示这个异常行为,所有测试里的“Use of uninitialized value”警告是故意保留的,因为我就是在测试未定义值的情况。
#!/usr/bin/env perl # ternary operator # condition ? if True : if False # Why does the ternary operator behave different with a undefined hashref key # than a regular undefined $ variable ? use strict; use warnings; use diagnostics; my $i = 1; my $form = { vc => 'customer', customer_id => ''}; my $ref = { customer_id => 12345 }; &string_test;# passes print "-" x 80; &mixed_test_ref; # passes print "-" x 80; &mixed_test; #fails print "-" x 80; &hashref_test; #fails print "-" x 80; &works_always; print "-" x 80; print "\n"; sub string_test { my $str = '1234'; my $form; # not defined print qq| Why does the ternary operator behave different with a undefined hashref key than a regular undefined \$ variable ? First with a undefined variable called form: Before: str : '$str' \n form : $form |; # will throw undefined warning # ternary operator #condition ? if True : if False ($str) ? $form = $str : $form = $form; print qq| Always works: str : '$str' \n form: $form \n|; } sub mixed_test_ref { my $local; print qq| Mixed test ref Before: form->{'vc'} '$form->{'vc'}' \n ref->{"$form->{'vc'}_id"} : '$ref->{"$form->{'vc'}_id"}' \n |; # ternary operator #condition ? if True : if False ($ref->{"$form->{'vc'}_id"}) ? $local = $ref->{"$form->{'vc'}_id"} : $local = $local; print qq| After: form->{'vc'} '$form->{'vc'}' \n ref->{"$form->{'vc'}_id"} : '$ref->{"$form->{'vc'}_id"}' \n local: '$local' \n |; if ($local = $ref->{"$form->{'vc'}_id"}) {print "Pass\n"} else {print "Fail\n"} } sub mixed_test { # $form->{"$form->{'vc'}_id_$i"} is not defined # setting it to empty string makes it pass #$form->{"$form->{'vc'}_id_$i"} = ''; delete $form->{"$form->{'vc'}_id_$i"}; my $str = '1234'; print qq| mixed test Before: str : '$str' \n form: '$form->{"$form->{'vc'}_id_$i"}' \n |; # ternary operator #condition ? if True : if False ($str) ? $form->{"$form->{'vc'}_id_$i"} = $str : $form->{"$form->{'vc'}_id_$i"} = $form->{"$form->{'vc'}_id_$i"}; if ( $form->{"$form->{'vc'}_id_$i"} == $str ) { print qq|Pass\ After: str : '$str' \n form: $form->{"$form->{'vc'}_id_$i"} \n|; } else {print "Fail\n"} } sub hashref_test { # $form->{"$form->{'vc'}_id_$i"} is not defined # setting it to empty string makes it pass #$form->{"$form->{'vc'}_id_$i"} = ''; delete $form->{"$form->{'vc'}_id_$i"}; print qq| hash ref test Before: form->{'vc'} '$form->{'vc'}' \n ref->{"$form->{'vc'}_id"} : '$ref->{"$form->{'vc'}_id"}' \n form->{"$form->{'vc'}_id_$i"} : $form->{"$form->{'vc'}_id_$i"} \n |; # ternary operator #condition ? if True : if False $ref->{"$form->{'vc'}_id"} ? $form->{"$form->{'vc'}_id_$i"} = $ref->{"$form->{'vc'}_id"} : $form->{"$form->{'vc'}_id_$i"} = $form->{"$form->{'vc'}_id_$i"}; if ($form->{"$form->{'vc'}_id_$i"}) {print "Passes and I am amazed\n"} else { print qq| Why does this not work? After: form->{'vc'} '$form->{'vc'}' \n ref->{"$form->{'vc'}_id"} : '$ref->{"$form->{'vc'}_id"}' \n form->{"$form->{'vc'}_id_$i"} : $form->{"$form->{'vc'}_id_$i"} \n|; } } sub works_always { if ($ref->{"$form->{'vc'}_id"}) { $form->{"$form->{'vc'}_id_$i"} = $ref->{"$form->{'vc'}_id"} } # not necessary but for completeness else { $form->{"$form->{'vc'}_id_$i"} = $form->{"$form->{'vc'}_id_$i"} } print qq| Always works as expected with if statement: form->{'vc'} '$form->{'vc'}' \n ref->{"$form->{'vc'}_id"} : '$ref->{"$form->{'vc'}_id"}' \n form->{"$form->{'vc'}_id_$i"} : $form->{"$form->{'vc'}_id_$i"} \n|; }
这根本不是哈希键和普通变量的差异问题,而是运算符优先级导致的解析错误。
Perl中,三元运算符?:的优先级高于赋值运算符=,所以你写的表达式:
($str) ? $form->{"key"} = $str : $form->{"key"} = $form->{"key"};
会被Perl解析成:
( ($str) ? ($form->{"key"} = $str) : $form->{"key"} ) = $form->{"key"};
当$str为真时,三元表达式的第一部分$form->{"key"} = $str会执行,它的返回值是$str。然后Perl会尝试把$form->{"key"}(此时还未在哈希中创建,值为undef)赋值给这个返回值——但$str是字符串常量,不是左值,这个赋值操作会被忽略,导致哈希键最终没有被设置。
而在普通变量的场景中,$form是左值,所以即使解析成(($str) ? ($form = $str) : $form) = $form,赋值给左值是允许的,最终$form会被两次赋值(第一次是$str,第二次是$form自己,结果还是$str),所以看起来“正常工作”,但这其实是巧合,写法本身就是错误的。
正确的写法
你应该把赋值操作放在三元运算符的外面,让三元运算符返回要赋值的值:
$form->{"$form->{'vc'}_id_$i"} = $str ? $str : $form->{"$form->{'vc'}_id_$i"};
或者更简洁的写法(利用Perl的短路特性):
$form->{"$form->{'vc'}_id_$i"} ||= $str;
如果只是想在目标值未定义时赋值,也可以用:
$form->{"$form->{'vc'}_id_$i"} = $str unless defined $form->{"$form->{'vc'}_id_$i"};
另外,你脚本里的works_always子函数用if语句能正常工作,是因为if的代码块不存在优先级问题,赋值操作是明确在条件为真时执行的。
内容的提问来源于stack exchange,提问作者turtle

