 函數(shù))
前言range()用來生成一個(gè)連續(xù)序列的數(shù)組是 PHP 里少數(shù)幾個(gè)「天生就返回?cái)?shù)組」的函數(shù)之一。正因?yàn)樗粗唵魏芏嗳嗽谟盟臅r(shí)候帶著三個(gè)不準(zhǔn)確的印象第一以為它只能生成數(shù)字。它同樣能生成字符序列range(a, z)是常見的寫法。但要注意兩個(gè)邊界都是字符串時(shí)生成的是字節(jié)序列——range(A, z)會(huì)順帶把[ \ ] ^ _這幾個(gè)符號(hào)也包進(jìn)來因?yàn)樗鼈冋寐湓贏和z的 ASCII 區(qū)間之間。第二以為負(fù)的步長能自動(dòng)得到降序。PHP 8.3.0 起如果區(qū)間是遞增的起點(diǎn)小于等于終點(diǎn)卻給了負(fù)步長會(huì)直接拋ValueError。而在 PHP 8.2 及以前range()會(huì)去「猜」方向結(jié)果常常與預(yù)期相反。第三以為它像別的語言那樣是左閉右開區(qū)間。PHP 的range()是閉區(qū)間結(jié)果里包含終點(diǎn)。本文按官方手冊講清range()的類型推導(dǎo)規(guī)則、PHP 8.3 引入的錯(cuò)誤與警告、字符序列的坑以及大區(qū)間下的內(nèi)存問題。一、簽名與返回類型推導(dǎo)range(string|int|float $start, string|int|float $end, int|float $step 1): array返回的是數(shù)組但元素的類型取決于參數(shù)類型規(guī)則寫在手冊里條件結(jié)果兩個(gè)邊界都是字符串且$step是整數(shù)字節(jié)序列通常是 ASCII 字符$start、$end、$step中至少有一個(gè)是浮點(diǎn)數(shù)浮點(diǎn)數(shù)序列其余情況整數(shù)序列方向由邊界決定$start小于等于$end時(shí)遞增否則遞減。步長$step只負(fù)責(zé)「每兩個(gè)元素之間差多少」不負(fù)責(zé)決定方向。?php // 適用于 PHP 8.0echo implode(, , range(0, 5)), PHP_EOL; // 0, 1, 2, 3, 4, 5echo implode(, , range(5, 0)), PHP_EOL; // 5, 4, 3, 2, 1, 0echo implode(, , range(0, 100, 25)), PHP_EOL; // 0, 25, 50, 75, 100echo implode(, , range(c, a)), PHP_EOL; // c, b, a注意最后一個(gè)元素手冊說明結(jié)果數(shù)組的最后一個(gè)元素要么是$end要么是它的前一個(gè)元素取決于步長。range(0, 10, 4)得到[0, 4, 8]——12超了終點(diǎn)所以停在8。二、PHP 8.3 引入的錯(cuò)誤與警告PHP 8.3.0 對range()做了一次行為規(guī)范化這是使用它時(shí)必須知道的分界線情況PHP 8.3.0 起的行為$step為0拋ValueError$start、$end、$step中存在非有限值INF、-INF、NAN拋ValueError$step為負(fù)但區(qū)間是遞增的$start $end拋ValueError$start或$end是空字符串發(fā)出E_WARNING空串按0處理$start或$end是多字節(jié)的非數(shù)字字符串發(fā)出E_WARNING一個(gè)邊界是數(shù)字導(dǎo)致另一個(gè)字符串邊界被隱式轉(zhuǎn)成整數(shù)發(fā)出E_WARNING$step是浮點(diǎn)但小數(shù)部分為 0如2.0視為整數(shù)2結(jié)果數(shù)組是整數(shù)兩個(gè)邊界都是字符串一律生成字節(jié)序列舊版本里若一邊是數(shù)字串另一邊會(huì)被隱式轉(zhuǎn)成整數(shù)也就是說range(1, 5, -1)在 PHP 8.2 上是能跑出結(jié)果的方向被「猜」成升序在 PHP 8.3 上會(huì)直接報(bào)錯(cuò)。升級到 PHP 8.3 時(shí)這類調(diào)用是最典型的兼容性炸彈應(yīng)該在遷移前全局搜一遍。三、字符序列好看但危險(xiǎn)range(a, z)很直觀但它生成的其實(shí)是字節(jié)區(qū)間。ASCII 里大寫字母、小寫字母被若干符號(hào)隔開?php // 適用于 PHP 8.0$a range(a, z);$A range(A, Z);$all range(A, z);echo count($a), , count($A), , count($all), PHP_EOL;echo implode(, $all), PHP_EOL;26 26 58ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_abcdefghijklmnopqrstuvwxyzrange(A, z)得到的 58 個(gè)元素里中間的[、\、]、^、_、反引號(hào)并不是字母。需要「全部字母」時(shí)必須分兩段再合并?php // 適用于 PHP 8.0$letters array_merge(range(A, Z), range(a, z));echo count($letters), PHP_EOL; // 52四、實(shí)戰(zhàn)示例1. 用range()生成分頁頁碼窗口?php // 適用于 PHP 8.0declare(strict_types1);/*** 生成當(dāng)前頁附近的頁碼窗口例如當(dāng)前第 5 頁、總 20 頁、窗口 5 → 3,4,5,6,7** return listint*/function pageWindow(int $current, int $total, int $size 5): array{if ($total 1) {return [];}$half intdiv($size, 2);$start max(1, $current - $half);$end min($total, $start $size - 1);// 靠近尾頁時(shí)向左補(bǔ)齊保證窗口寬度盡量等于 $size$start max(1, $end - $size 1);return range($start, $end); // 閉區(qū)間兩端都包含}var_dump(pageWindow(1, 20)); // [1, 2, 3, 4, 5]var_dump(pageWindow(10, 20)); // [8, 9, 10, 11, 12]var_dump(pageWindow(20, 20)); // [16, 17, 18, 19, 20]2. 大區(qū)間不要用range()range()會(huì)一次性構(gòu)造完整的數(shù)組。生成 1000 萬個(gè)整數(shù)就要在內(nèi)存里放 1000 萬個(gè)元素如果還要對它們做array_map()中間還會(huì)再產(chǎn)生一份數(shù)組。需要惰性序列時(shí)用生成器generator?php // 適用于 PHP 8.0declare(strict_types1);/*** 與 range($start, $end, $step) 等價(jià)的惰性版本內(nèi)存占用與區(qū)間長度無關(guān)。** return Generatorint, int*/function intRange(int $start, int $end, int $step 1): Generator{if ($step 0) {throw new ValueError(步長必須是正整數(shù));}if ($start $end) {for ($i $start; $i $end; $i $step) {yield $i;}} else {for ($i $start; $i $end; $i - $step) {yield $i;}}}$sum 0;foreach (intRange(1, 1_000_000) as $n) {$sum $n;}echo $sum, PHP_EOL; // 500000500000這段代碼的內(nèi)存占用是恒定的無論區(qū)間多大。需要數(shù)組時(shí)再iterator_to_array()轉(zhuǎn)換這樣「是否真的需要全部元素」就成了一個(gè)顯式?jīng)Q定。3. 生成時(shí)間點(diǎn)時(shí)注意浮點(diǎn)誤差?php // 適用于 PHP 8.0// 用整數(shù)運(yùn)算生成「每 15 分鐘一個(gè)時(shí)間點(diǎn)」避開浮點(diǎn)累加誤差$base strtotime(2024-01-01 00:00:00);$slots array_map(static fn (int $i): string date(Y-m-d H:i, $base $i * 900),range(0, 95) // 一天 96 個(gè) 15 分鐘時(shí)段);echo $slots[0], ... , $slots[95], PHP_EOL;常見坑點(diǎn)?range(1, 5, -1)想得到降序 —— PHP 8.3.0 起會(huì)拋ValueError遞增區(qū)間配負(fù)步長PHP 8.2 及以前方向靠猜結(jié)果與預(yù)期相反。? 方向交給邊界決定range(5, 1)就是降序真要用負(fù)步長寫成range(5, 1, -1)。?range(1, 10, 0)想「不按步長、逐個(gè)取」—— 舊版本里是死循環(huán)式的告警PHP 8.3.0 起拋ValueError。? 步長恒為正整數(shù)需要逐個(gè)取就省略第三個(gè)參數(shù)。? 用range(A, z)當(dāng)「所有字母」—— 會(huì)帶進(jìn)[、\、]、^、_、反引號(hào)這 6 個(gè)非字母字符。?array_merge(range(A, Z), range(a, z))。? 以為range()是左閉右開、不含終點(diǎn) —— PHP 是閉區(qū)間range(1, 3)得到[1, 2, 3]。? 想要「從 0 開始的 N 個(gè)值」寫range(0, $n - 1)用for ($i 0; $i $n; $i)時(shí)要記得這里才是開區(qū)間。? 用range()生成很大的區(qū)間再array_map()—— 數(shù)組先完整構(gòu)造出來內(nèi)存峰值隨區(qū)間長度線性上升通常直接 OOM。? 用for循環(huán)或生成器惰性產(chǎn)生確實(shí)要數(shù)組時(shí)才調(diào)用iterator_to_array()。? 用浮點(diǎn)步長累加并期望精確值 ——range(0, 1, 0.1)的元素受二進(jìn)制浮點(diǎn)表示影響末位可能不是0.1的精確倍數(shù)用來做等值比較會(huì)失敗。? 需要精確小數(shù)時(shí)用整數(shù)運(yùn)算先乘成整數(shù)再除比較用abs($a - $b) 1e-9或先round()。? 依賴range()的元素類型 ——range(1, 5, 2.0)在 PHP 8.3.0 起會(huì)返回整數(shù)數(shù)組浮點(diǎn)步長若沒有小數(shù)部分會(huì)被當(dāng)成整數(shù)在舊版本上可能是浮點(diǎn)。? 需要浮點(diǎn)結(jié)果時(shí)顯式轉(zhuǎn)換array_map(floatval, range(1, 5))。? 用range()生成數(shù)組鍵后忘了 PHP 的鍵類型規(guī)則 ——array_combine(range(1, 3), $values)得到的鍵是整數(shù)1, 2, 3不是字符串1,2,3PHP 數(shù)組會(huì)把純數(shù)字字符串鍵自動(dòng)轉(zhuǎn)成整數(shù)。? 需要字符串鍵就顯式轉(zhuǎn)換array_map(strval, range(1, 3))或直接在鍵里加前綴??偨Y(jié)要點(diǎn)結(jié)論簽名range(string\區(qū)間閉區(qū)間包含$end最后一個(gè)元素可能小于$end步長不整除時(shí)方向由$start與$end的大小決定不由步長的正負(fù)決定元素類型雙字符串 整數(shù)步長 → 字節(jié)序列含浮點(diǎn) → 浮點(diǎn)序列否則整數(shù)序列PHP 8.3.0 新增錯(cuò)誤步長為 0、非有限值、遞增區(qū)間配負(fù)步長均拋ValueError字符序列是按字節(jié)區(qū)間生成的range(A,z)會(huì)帶進(jìn) 6 個(gè)非字母字符內(nèi)存一次性構(gòu)造整個(gè)數(shù)組大區(qū)間應(yīng)改用生成器或for循環(huán)一句話總結(jié)range()的方向由兩個(gè)邊界決定$step只控制間距記住它是閉區(qū)間、記住 PHP 8.3 起對非法步長不再「猜」而是拋ValueError再用生成器處理大區(qū)間它就是一個(gè)可靠的工具。