1. 什么是哈希
哈希是1种数据结构,像数组一样可以容纳任意多的值,并可按需取用,与数组不同的是,哈希与数组的检索方式不同,数组以数字下标来检索,哈希以唯一的名字来检索,这个唯一的名字叫做键
哈希的字符串只能用普通的字符串表示,可以用任意字符串表达式作为哈希键,这些键必须是唯一的字符串
可以把哈希想象称一大桶数据,但是桶里没有第一个数据,只有一堆数据,哈希内的数据没有顺序,因此没有所谓的第1个元素,有的只是一堆键-值对的组合
哈希的键和值都是任意的标量,但哈希的键总会被转换成字符串,如果以数字表达式50 / 20
为键,那么这个键实际上为字符串“2.5”
根据perl“去除不必要限制”的原则,哈希的数据结构容量可以是任意大小,从没有任何键-值对的空哈希到填满内存的巨大哈希都可以
某些语言也有哈希表(比如awk语言),但这些语言在键-值对增多时检索速度会逐渐变慢。如果某个哈希包含300万个键-值对,从中提取任意一项数据还是会和原来一样快。所以我们不用担心大数据量的哈希读写性能。
虽然哈希的键是唯一的,但是哈希对应的值是可以重复的
哈希的值可以是数字、字符串、undef,或是这些类型的组合
但哈希的键必须是字符串且不可重复
2. 为何使用哈希?
需要将1组数据对应到另1组数据
可以把哈希当成一种极简数据库,每个键对应存储一块数据
如果我们要处理的问题的描述中带有“找出重复项”、“唯一的”、“交叉引用”、“查表” 之类的字眼的话,很可能需要用到哈希
3. 访问哈希元素
和访问数组的做法类似,只是用了花括号而非方括号来表示索引键,并且键表达式是字符串,而非数字
$hash{$some_key};
$family_name{'fred'} = 'flintstone';
$family_name{'barney'} = 'rubble';
foreach my $person (qw< barney fred >){
print "I've heard of $person $family_name{$person}\n";
}
哈希变量的命名和其它perl标识符相似,可以由字母、数字、下划线构成,但不能用数字开头
此外,哈希与标量、数组及子程序一样,有自己的命名空间,也就是说$family_name{‘fred’}和子程序 &family_name之间毫无关联
定义哈希键是可以使用任意表达式,字符串、简单标量变量等,求值结果按字符串当成键
$foo = 'bar';
print $family_name{$foo.'ney'} ; # 打印 'rubble'
若对某个已存在的哈希元素赋值,就会覆盖之前的值
$family_name{'fred'} = 'astaire'; # 给已有的元素赋上新值
$bedrock = $family_name{'fred'}; # 得到"astarie",之前的值已抛弃
哈希元素会因赋值而诞生
$family_name {'wilma'} = 'flintstone'; # 增加1个新的键-值对
$family_name {'betty'} .= $family_name{'barney'}; # 在需要的时候动态创建该元素
这种特性称为自动延展
访问哈希中不存在的值会得到undef
$granite = $family_name{'larry'}; # 没有larry这个键,所以值为undef
4. 访问整个哈希
用百分号作为前缀,来指代整个哈希,比如%family_name
为了定义和解构方便,哈希可以转换成列表,或者通过列表构造哈希
在列表上下文中对哈希赋值,列表中的每个元素将依次作为键-值对,构造一个新的哈希
%some_hash = ( 'foo', 35, 'bar', 12.4, 2.5, 'hello', 'wilma', 1.72e30, 'betty', "bye\n");
虽然可以使用任意长度的列表,但由于构成哈希的键-值对是偶数个,所以对应列表也应该有偶数个元素,否则最后1个键对应的值会是undef
在列表上下文中,哈希返回的是键-值对组成的列表
@any_array = %some_hash;
这个变换叫做哈希松绑(unwinding),键-值对重新变回列表,此时的键-值对和当初赋值时的顺序基本上不会相同
print "@any_array\n";
之所以顺序乱掉,是因为perl已经为哈希的快速检索对键-值对的存储作了特别排序
即使键-值对的顺序被打乱,列表里的每个键还是会黏着相应的值,所以无法知道某个键foo出现在列表的哪个位置,仍然可以确信对应值35会跟在后面
5. 哈希赋值
my %new_hash = %old_hash; # 语法上可行,但是很少这么做
上面这行代码会先把%old_hash松绑为键-值对列表,然后通过列表赋值重新构造每个键-值对,最终形成新的哈希%new_hash
可以对哈希做一些变形,比如反转键值对,得到逆向检索的新哈希
my %inverse_hash = reverse %any_hash;
这会把%any_hash解绑为键值对列表,然后利用reverse的列表翻转功能,获取新列表,达成键值互换,但是这种技巧只能在哈希值不重复的情况下才能生效,因为哈希的键必须唯一
键值对在哈希松绑后的顺序是无法预知的,所以无法预知最后哪些键值对会被覆盖
6. 胖箭头
使用列表对哈希赋值时很难区分键和值的成对关系
胖箭头(=>)只是逗号的另一种写法,在任何需要逗号的地方都可以用胖箭头代替
%some_hash = ( 'foo', 35, 'bar', 12.4, 2.5, 'hello', 'wilma', 1.72e30, 'betty', "bye\n" );
my %some_hash = (
'fred' => 'flintstone',
'dino' => undef,
'barney' => 'rubble',
'betty' => 'rubble',
);
列表末尾有个逗号,是为了便于维护,perl会直接忽略多余的逗号
由于表示键的字符串一般都用简单的词,所以胖箭头左侧的键可以直接省略引号,当不加引号会产生歧义时,则必须加上引号
my %some_hash = (
fred => 'flintstone',
dino => undef,
barney => 'rubble',
betty => 'rubble',
);
my %last_name = (
+ => 'flintstone';
);
如果键名只是由字母、数字和下划线组成,并且不以数字开头,就可以省略引号,这类无需引号的字符序列,我们称之为裸字(bareword),因为它是孤立存在的
另外一个常见允许省略键名引号的地方是:定义在花括号中的键名,$score{'fred'}
可以简写为$score{fred}
,不过如果花括号内的不是裸字,perl就会将其当作表达式先求值,然后把结果当作键名
$hash{bar.foo} = 1; # 构成键名'barfoo'
7. 哈希操作函数
keys和values函数
keys函数返回的是哈希的键列表
values函数返回的是哈希的值列表
如果哈希中没东西,则返回空列表
my %hash = ( 'a' => 1, 'b' => 2, 'c' => 3);
my @k = keys %hash;
my @v = values %hash;
返回的顺序可能会有所不同,因为哈希按自己检索的需要维护存储顺序,但返回的键列表及对应的值列表间的对应顺序是一致的
但如果新增某个元素的话,perl可能根据需要重新优化排列顺序,以保证后续高速检索的能力
在标量上下文中,keys和values函数都会返回哈希中键值对的个数,由于这个计算过程不需要对整个哈希进行遍历,因为非常高效
my $count = keys %hash; # 返回3,说明有3对键值
可以把哈希当成布尔表达式来判断真假
if (%hash){
print "That was a true value!\n";
}
只要哈希中至少有1个键值对,就返回真
8. each函数
可以用each函数来遍历哈希的每个键值对,每次调用each函数,它都会以列表形式返回2个元素,1个是键名,1个是值,再次调用它会返回下一组键值对,直到所有元素都遍历完毕,没有数据返回时,each函数会返回空列表
each在while循环中很有用
while (($key, $value) = each %hash){
print "$key => $value\n";
}
哈希会记着上次访问的位置,每个哈希都有1个迭代器(iterator)
由于每个哈希都有自己的迭代器,因此处理不同哈希的each调用可以嵌套。不过如果嵌套时each调用的是同一个哈希,就会相互干扰,最终出现混乱的结果
each返回键值对的顺序是乱的,但each函数返回的顺序和keys与values返回的顺序相同,也就是哈希的自然顺序,如果需要依次处理哈希,只要对键排序就行了
foreach $key (sort keys %hash){
$value = $hash{$key};
print "$key => $value";
}
9. 哈希的典型应用
哈希里有些元素的值并不为真
$books{"barney"} = 0; # barney现在没有借书
$books{"pebbles"} = undef; # pebbles新办了一张借书证
10. exists函数
检查哈希中是否存在某个键,可以用exists函数,键存在的话它会返回真,不存在的话它会返回假
if (exists $books{"dino"}){
print "Hey, there's a library card for dino!\n";
}
11. delete函数
delete能从哈希中删除指定的键及其对应的值,如果没有这样的键,就直接结束,不会出现任何警告或错误信息
my $person = 'betty';
delete $books{$person}; # 销毁$person的借书证
这并不是将undef存入哈希元素,在delete之后,哈希中就没有这个键了,但存入undef之后,键是一定会存在的
12. 哈希元素内插
可以将单一哈希元素内插到双引号引起的字符串中
foreach $person (sort keys %books){ # 按次序访问每位借阅者
if($books{$person}){
print "$person has $book{$person}"; # fred借了3本书
}
}
但这种方式不支持内插整个哈希,"%books"的结果只是"%books"!!!
在双引号中需要转义的魔力字符有 $ 和 @ , 双引号, \ 本身也需要转义,除此外,双引号中任何字符都只代表它们自己
在双引号中变量名后的撇号(’)、左方括号([)、左花括号({)、瘦箭头(->)和双冒号(::),它们是表示变量内容的一部分,而非字符本身
13. 特殊哈希 %ENV
从%ENV中读取键为PATH的值
print "PATH is $ENV{PATH}";
可以自己添加需要的环境变量
$ export CHARACTER = Fred # bash
C:\> set CHARACTER = Fred # windows
预先在外部设定好环境变量后,perl程序就可以读到它当前的值
print "CHARACTER is $ENV{CHARACTER}\n";