一、函数定位与核心机制
usort是PHP内置的数组排序函数,从PHP4.0开始提供。名字里的u代表userfunction,意思是「按值排序,比较规则由用户自定义回调决定」。
当数组需要按某种非平凡(non-trivial)的标准排序时,比如按字符串长度、按对象属性、按自定义优先级,PHP内部的排序规则不够用,就得用usort把比较权交给开发者。
与相近函数的区分
先理清这几个排序函数的分工:
| 函数 | 排序对象 | 比较规则 | 是否保留键名 |
|---|---|---|---|
| sort | 值 | 内部规则 | 否,键名重建 |
| asort | 值 | 内部规则 | 是 |
| usort | 值 | 用户回调 | 否,键名重建 |
| uasort | 值 | 用户回调 | 是 |
| uksort | 键名 | 用户回调 | 是 |
usort和uasort的区别就在键名是否保留。usort会重建键名,uasort保留原键名。这个差异在关联数组排序时影响很大。
个人经验:在电商商品排序场景里,商品数组用商品ID作键名,需要按销量排序。一开始用usort,排完后商品ID键名全丢了,变成0、1、2的连续数字,后续按ID取商品全部失败。换成uasort后键名保留,问题解决。这个坑在第一次用usort时很容易中招,尤其是从sort迁移过来的开发者。
语法结构
bool usort(array &$array, callable $value_compare_func): bool
参数说明:
| 参数 | 是否必需 | 说明 |
|---|---|---|
| array | 必需 | 待排序的数组,传引用,函数直接修改原数组 |
| value_compare_func | 必需 | 用户自定义的值比较函数 |
返回值是布尔值:成功返回true,失败返回false。排序结果直接体现在传入的数组变量上,这一点和uksort一致。
回调返回值语义
回调函数接收两个元素值作为参数,返回整数:
-
第一个参数小于第二个参数,返回负数
-
两个参数相等,返回0
-
第一个参数大于第二个参数,返回正数
原文示例里有个值得注意的地方:return($a>$b)?-1:1;返回的是-1和1,把「大于」映射成-1,实现的是降序排序。返回值的正负决定排序方向,这是理解usort的关键。
用一张对照表说明:
| 回调返回逻辑 | 排序结果 |
|---|---|
($a>$b)?1:-1 |
升序 |
($a>$b)?-1:1 |
降序 |
$a<=>$b |
升序 |
$b<=>$a |
降序 |
PHP7之后推荐用太空船操作符<=>,它直接返回-1、0、1,比三元表达式简洁。
二、键名重建机制
usort会丢弃原有键名,把结果数组重新索引成从0开始的连续整数。这一点和array_unshift的数值键名重建类似,但usort是全部重建,不区分字符串键名还是数字键名。
$country = array("d" => "denmark", "a" => "australia", "b" => "bangladesh");
usort($country, "compare");
print_r($country);
输出:
Array
(
[0] => denmark
[1] => bangladesh
[2] => australia
)
原来的键名d、a、b全部丢失,变成0、1、2。
如果业务上需要保留键名,得用uasort。这一点在关联数组排序时必须先想清楚:排序后的结果是要按新顺序遍历,还是要按原键名访问。前者用usort,后者用uasort。
三、回调编写要点
回调必须对称。内部排序后,参数哪个来自前面、哪个来自后面是随机的。回调不能依赖参数顺序做逻辑分支,否则结果会漂移。
返回0的场景要处理。两个值相等时返回0,这是规范。如果漏掉这个判断,只返回1和-1,排序仍能跑,但内部算法遇到相等元素时的稳定性不如返回0。
回调里不要做副作用操作。内部排序会让回调被调用多次,调用次数和顺序不固定。在回调里写日志、改全局变量、做数据库查询,结果都不可预期。早期项目里我在回调里累加计数器想统计比较次数,结果发现同一个元素被比较了多次,计数对不上。
大数据量下注意性能。排序的时间复杂度大约是O(nlogn),回调本身的开销会被放大。如果数组有几千条以上,回调里避免做字符串拼接、正则匹配、数据库查询这类相对重的操作。必要时先把值预处理成规范形式,再排序。
比较对象属性时先取出来。如果数组元素是对象,回调里反复调用getter方增加开销。可以在排序前先把需要的属性提取到一个临时数组,排序后再映射回去。不过这样代码复杂度会上升,是否值得要看数据量。
四、项目实战示例
示例一:字符串降序排序
<?php
function codehub_cmp($a, $b) {
if ($a == $b) {
return 0;
}
return ($a > $b) ? -1 : 1;
}
$country = array("d" => "denmark", "a" => "australia", "b" => "bangladesh");
usort($country, "codehub_cmp");
print_r($country);
输出:
Array
(
[0] => denmark
[1] => bangladesh
[2] => australia
)
回调里($a>$b)?-1:1表示第一个参数大时返回负数,排在前面,实现降序。键名被重建。
示例二:数字升序排序
<?php
function codehub_num($a, $b) {
if ($a == $b) {
return 0;
}
return ($a < $b) ? -1 : 1;
}
$numbers = array(11, 56, 8, 96);
usort($numbers, "codehub_num");
print_r($numbers);
输出:
Array
(
[0] => 8
[1] => 11
[2] => 56
[3] => 96
)
回调里($a<$b)?-1:1实现升序。这里要注意,如果不用usort而用sort,结果一样,但sort的性能更好,因为比较规则在内部用C实现。只有当排序规则确实需要业务自定义时,才用usort。
示例三:字符串按字典序升序
<?php
function codehub_str($a, $b) {
if ($a == $b) {
return 0;
}
return ($a < $b) ? -1 : 1;
}
$language = array('php', 'java', 'c++', 'csharp');
usort($language, "codehub_str");
print_r($language);
输出:
Array
(
[0] => c++
[1] => csharp
[2] => java
[3] => php
)
按字符串字典序排列。这里有个细节:c++和csharp的比较,+的ASCII码是43,s的ASCII码是115,所以c++排在csharp前面。如果业务上想按字母顺序排列忽略符号,需要在回调里做额外处理,比如用strcmp前先过滤掉非字母字符。
示例四:按字符串长度排序
<?php
function codehub_len($a, $b) {
$lenA = strlen($a);
$lenB = strlen($b);
if ($lenA == $lenB) {
return 0;
}
return ($lenA < $lenB) ? -1 : 1;
}
$news = array('aajtak', 'abp', 'zee news', 'ndtv');
usort($news, "codehub_len");
print_r($news);
输出:
Array
(
[0] => abp
[1] => ndtv
[2] => aajtak
[3] => zee news
)
这个示例展示了usort的典型用途:按字符串长度排序,这是PHP内部排序规则做不到的。abp和ndtv长度都是3和4,aajtak长度6,zeenews长度8(含空格)。
示例五:用太空船操作符简化
<?php
function codehub_spaceship($a, $b) {
return $b <=> $a;
}
$scores = array(88, 72, 95, 60);
usort($scores, "codehub_spaceship");
print_r($scores);
输出:
Array
(
[0] => 95
[1] => 88
[2] => 72
[3] => 60
)
$b<=>$a把两个参数位置对调,等价于降序。PHP7及以上版本推荐这种写法,可读性比嵌套三元表达式好。
五、容易踩的坑
键名被重建导致关联丢失。usort会丢弃原有键名,关联数组排序后变成索引数组。如果业务上需要按原键名访问,用uasort。
返回值和排序方向的关系容易搞反。很多人以为返回1就是排在后面,实际上返回正数表示第一个参数排在第二个参数之后。降序和升序的区别就在返回值正负上,写的时候先在纸上画两个值验证一下。
传引用修改原数组。usort直接修改传入的数组,不返回新数组。如果业务上需要保留原数组,先复制一份。这个坑在多层函数调用里容易被忽略,导致上游数据被意外改动。
回调里做类型转换要谨慎。如果数组里混有字符串和数字,回调里的===会让"53"和53判为不相等。如果业务上需要把它们视为相同,得在回调里统一类型,比如都用(int)转换后再比。
失败时返回false但不报错。usort在参数类型不对时返回false,但不会抛出异常。如果不检查返回值,可能以为排序成功,实际上数组没动。写代码时很好加一层判断:
if (!usort($data, "codehub_cmp")) {
// 处理失败情况
}
稳定性问题。PHP8.0之前usort不保证排序稳定性,相等元素的相对顺序可能改变。PHP8.0及以后版本排序算法稳定,相等元素保持原顺序。如果代码要兼容旧版本,且业务依赖相等元素的顺序,需要在回调里加上二级比较条件,比如值相等时按键名比较。
六、本节课程知识要点
-
usort按值排序,比较规则由用户回调决定
-
回调返回负数表示第一个参数排前面,返回正数表示排后面
-
回调返回值的正负决定排序方向
-
第一个参数传引用,函数直接修改原数组,返回布尔值
-
键名会被重建,从0开始连续编号,字符串键名也丢失
-
需要保留键名时用uasort
-
PHP7及以上推荐用太空船操作符
<=>简化回调 -
简单排序优先用sort,只有非平凡排序才用usort
-
PHP8.0之前不保证排序稳定性,兼容旧版本时注意相等元素的顺序
七、适用场景
这个函数适合以下场景:
-
按字符串长度排序
-
按对象属性排序(比如按商品价格、按用户年龄)
-
按自定义优先级排序(比如按状态枚举的顺序)
-
多维数组按某个字段排序
-
混合类型数组按业务规则排序
不适合的场景:
-
简单的字母升序或数字升序,用sort性能更好
-
需要保留键名的关联数组排序,用uasort
-
按键名排序,用uksort或ksort
-
需要保留原数组不变,先复制再排序