字符串访问与修改
概述
PHP 将字符串视为由字符组成的数组,支持通过方括号偏移量来访问和修改单个字符。本章详细讲解字符串偏移访问的语法规则、边界条件、多字节字符处理以及相关的注意事项。
基础概念
字符串偏移访问
| 操作 | 语法 | 说明 |
|---|---|---|
| 读取字符 | $str[0] | 获取指定偏移量的字符 |
| 修改字符 | $str[0] = 'x' | 替换指定偏移量的字符 |
| 空字符串访问 | $str[0] | 返回空字符串,PHP 8.0+ 抛出警告 |
| 越界访问 | $str[100] | 返回空字符串,PHP 8.0+ 抛出警告 |
| 负偏移 | $str[-1] | PHP 7.1+ 从末尾开始访问 |
PHP 8.0 变更
PHP 8.0 起,访问空字符串或越界偏移时会抛出 E_WARNING,PHP 8.1 起使用 [] 读取越界偏移会抛出 Warning。应使用 substr() 作为替代。
语法与代码
基本偏移访问
php
<?php
declare(strict_types=1);
// 通过偏移量读取单个字符
$str = 'Hello, World!';
echo $str[0]; // 输出: H(第一个字符)
echo $str[4]; // 输出: o(第五个字符)
echo $str[-1]; // 输出: !(最后一个字符,PHP 7.1+)
echo $str{-2}; // 输出: d(花括号语法,PHP 7.4+ 废弃,PHP 8.0+ 移除)
// 遍历字符串中的每个字符(注意:对多字节字符不安全)
$word = 'PHP';
for ($i = 0, $len = strlen($word); $i < $len; $i++) {
echo $word[$i]; // P, H, P
}修改字符串字符
php
<?php
declare(strict_types=1);
// 通过偏移量修改字符
$str = 'Hello';
$str[0] = 'h'; // 修改为 'hello'
echo $str; // 输出: hello
$str[5] = '!'; // 在末尾追加字符
echo $str; // 输出: hello!
// 修改中间字符
$phone = '138-0000-0000';
$phone[3] = '-'; // 已经是 '-'
$phone[4] = '1'; // 修改为 138-1000-0000
echo $phone;
// 注意:不能用偏移方式修改不存在的位置(PHP 8.0+ 警告)
// $str[100] = 'x'; // PHP 8.0+: Warning负偏移访问(PHP 7.1+)
php
<?php
declare(strict_types=1);
// 负偏移 - 从字符串末尾开始计数
$str = 'abcdef';
echo $str[-1]; // 输出: f(最后一个字符)
echo $str[-2]; // 输出: e(倒数第二个)
echo $str[-3]; // 输出: d
// 负偏移也可以用于修改
$str[-1] = 'F';
echo $str; // 输出: abcdeF
// 负偏移越界
// echo $str[-100]; // PHP 7.1-7.4: 返回空字符串;PHP 8.0+: Warning字符串转为数组处理
当需要对字符串中的每个字符进行复杂操作时,可以将字符串拆分为数组。
php
<?php
declare(strict_types=1);
// str_split - 将字符串拆分为单字符数组
$str = 'Hello';
$chars = str_split($str);
// ['H', 'e', 'l', 'l', 'o']
// 按指定长度拆分
$chars = str_split('abcdefgh', 3);
// ['abc', 'def', 'gh']
// 反转字符串(多字节安全的方式见 mbstring 章节)
$reversed = implode('', array_reverse($chars));
// 检查字符串是否为回文
function isPalindrome(string $str): bool
{
$str = strtolower(preg_replace('/[^a-z0-9]/', '', $str));
return $str === implode('', array_reverse(str_split($str)));
}
var_dump(isPalindrome('racecar')); // bool(true)
var_dump(isPalindrome('hello')); // bool(false)详细说明
偏移访问的边界行为
PHP 对字符串偏移访问有特殊的边界处理规则,不同版本的行为有所不同:
| 场景 | PHP < 7.1 | PHP 7.1+ | PHP 8.0+ |
|---|---|---|---|
| 越界读取 | 返回空字符串,E_NOTICE | 返回空字符串 | 返回空字符串 + Warning |
| 空字符串读取 | 返回空字符串,E_NOTICE | 返回空字符串 | 返回空字符串 + Warning |
| 越界写入 | 静默追加 | 静默追加 | Warning(字符串中间部分不变) |
| 负偏移 | 报错 | 支持负偏移 | 支持负偏移 |
| 花括号语法 | 支持 | 废弃 | 移除 |
推荐做法
使用 substr() 函数替代偏移访问,它能更好地处理边界情况,并且对多字节字符串更友好。
方括号与花括号语法
PHP 历史上支持两种字符串偏移语法:$str[0] 和 $str{0}。PHP 7.4 将花括号语法标记为废弃,PHP 8.0 完全移除。
php
<?php
declare(strict_types=1);
$str = 'Hello';
// 推荐写法(始终兼容)
echo $str[0]; // H
// 废弃/移除写法
// echo $str{0}; // PHP 7.4: Deprecated,PHP 8.0: Fatal Error多字节字符串偏移
使用方括号偏移访问多字节字符串(如 UTF-8 中文)时,会按字节而非字符工作,导致错误结果。
php
<?php
declare(strict_types=1);
// 错误示范:直接用偏移访问多字节字符
$chinese = '你好世界';
echo $chinese[0]; // 输出: (不可见字节,不是'你')
echo $chinese[1]; // 输出: (不可见字节)
// 正确做法:使用 mb_substr
echo mb_substr($chinese, 0, 1, 'UTF-8'); // 输出: 你
echo mb_substr($chinese, 1, 1, 'UTF-8'); // 输出: 好
// 将多字节字符串拆分为字符数组
function mbStrSplit(string $str, string $encoding = 'UTF-8'): array
{
$result = [];
$len = mb_strlen($str, $encoding);
for ($i = 0; $i < $len; $i++) {
$result[] = mb_substr($str, $i, 1, $encoding);
}
return $result;
}
$chars = mbStrSplit('你好世界');
// ['你', '好', '世', '界']实战示例
单词首字母大小写转换(逐字符处理)
php
<?php
declare(strict_types=1);
function toggleCase(string $str): string
{
$chars = str_split($str);
return implode('', array_map(function (string $char): string {
if (ctype_upper($char)) {
return strtolower($character);
}
if (ctype_lower($char)) {
return strtoupper($character);
}
return $character;
}, $chars));
}
echo toggleCase('Hello World'); // 输出: hELLO wORLD字符串遮罩(敏感信息处理)
php
<?php
declare(strict_types=1);
// 手机号遮罩
function maskPhone(string $phone): string
{
$len = strlen($phone);
$maskLen = (int)floor($len * 0.4);
$start = (int)floor(($len - $maskLen) / 2);
for ($i = $start; $i < $start + $maskLen; $i++) {
if ($phone[$i] !== '-') {
$phone[$i] = '*';
}
}
return $phone;
}
echo maskPhone('138-0000-1234'); // 输出: 138-****-1234
echo maskPhone('13987654321'); // 输出: 139***54321Caesar 密码实现
php
<?php
declare(strict_types=1);
function caesarCipher(string $text, int $shift): string
{
$chars = str_split($text);
$shift = $shift % 26;
return implode('', array_map(function (string $char) use ($shift): string {
if (!ctype_alpha($char)) {
return $char;
}
$base = ctype_lower($char) ? ord('a') : ord('A');
$ord = ord($char) - $base;
$ord = ($ord + $shift + 26) % 26;
return chr($base + $ord);
}, $chars));
}
echo caesarCipher('Hello, World!', 3); // 输出: Khoor, Zruog!
echo caesarCipher('Khoor, Zruog!', -3); // 输出: Hello, World!字符串偏移与 substr 性能对比
php
<?php
declare(strict_types=1);
// 性能对比:$str[0] vs substr($str, 0, 1)
$str = str_repeat('Hello World! ', 100);
$n = 100000;
// 方式 1: 直接偏移
$start = microtime(true);
for ($i = 0; $i < $n; $i++) {
$char = $str[0];
}
$t1 = microtime(true) - $start;
// 方式 2: substr
$start = microtime(true);
for ($i = 0; $i < $n; $i++) {
$char = substr($str, 0, 1);
}
$t2 = microtime(true) - $start;
echo "偏移方式: {$t1}s\n";
echo "substr方式: {$t2}s\n";
// 直接偏移比 substr 略快,但差异极小安全的字符访问封装
php
<?php
declare(strict_types=1);
/**
* 安全获取字符串指定位置的字符
*/
function charAt(string $str, int $index): string
{
$len = strlen($str);
if ($index < 0) {
$index = $len + $index;
}
if ($index < 0 || $index >= $len) {
return '';
}
return $str[$index];
}
/**
* 多字节安全的字符获取
*/
function mbCharAt(string $str, int $index, string $encoding = 'UTF-8'): string
{
$len = mb_strlen($str, $encoding);
if ($index < 0) {
$index = $len + $index;
}
if ($index < 0 || $index >= $len) {
return '';
}
return mb_substr($str, $index, 1, $encoding);
}
echo charAt('Hello', 0); // H
echo charAt('Hello', -1); // o
echo charAt('Hello', 100); // ''(越界安全)
echo mbCharAt('你好', 0); // 你
echo mbCharAt('你好', -1); // 好字符串逐字符处理模式
php
<?php
declare(strict_types=1);
// 将字符串按字节逐字符处理
function processBytes(string $str, callable $callback): string
{
$result = '';
$len = strlen($str);
for ($i = 0; $i < $len; $i++) {
$result .= $callback($str[$i], $i);
}
return $result;
}
// 跳过第一个字符
echo processBytes('Hello', fn(string $c, int $i): string =>
$i === 0 ? '' : $c
);
// ello
// 每个字符加前缀
echo processBytes('ABC', fn(string $c, int $i): string =>
"[{$i}]{$c}"
);
// [0]A[1]B[2]C字符串编码检测与安全修改
php
<?php
declare(strict_types=1);
// 检查字符串是否为有效的 UTF-8
function isValidUtf8(string $str): bool
{
return $str === '' || mb_check_encoding($str, 'UTF-8');
}
// 安全修改(确保不截断多字节字符)
function safeCharReplace(string $str, int $index, string $replacement): string
{
if (!isValidUtf8($replacement) || mb_strlen($replacement, 'UTF-8') !== 1) {
throw new \InvalidArgumentException('Replacement must be a single UTF-8 character');
}
return mb_substr($str, 0, $index, 'UTF-8') . $replacement . mb_substr($str, $index + 1, null, 'UTF-8');
}
echo safeCharReplace('你好世界', 1, '中');
// 你中世界注意事项
多字节字符陷阱
方括号偏移 $str[0] 按字节操作,一个 UTF-8 中文字符占用 3 个字节。直接使用偏移会导致截断乱码。必须使用 mb_substr() 或 mb_str_split() 处理多字节字符串。
PHP 版本差异
PHP 8.0 以后,对空字符串或越界偏移的读写会产生 Warning。在生产环境中,应使用 substr() 等函数替代直接偏移访问,或先用 isset() 检查偏移是否存在。
字符串不可变性
虽然 PHP 允许通过偏移修改字符串字符,但字符串在内存中是按值传递的。每次修改都会创建一个新的字符串副本,大量逐字符修改会导致性能问题。
最佳实践
- 优先使用 substr():用
substr($str, 0, 1)替代$str[0],更安全、更可读 - 多字节字符串使用 mbstring:处理中文等 UTF-8 字符串时,始终使用
mb_*系列函数 - 大段修改使用 str_replace():需要替换多个字符时,使用
str_replace()而非逐字符修改 - 避免花括号语法:PHP 8.0+ 已移除
$str{0}语法,统一使用$str[0] - 敏感数据处理:使用偏移修改实现数据遮罩时,注意多字节字符和边界检查