细节注意
最后更新于:2022-04-02 04:32:55
## 细节注意
一些重要但是容易被忽略的细节记录。不起眼但很重要的知识。

家里的电器坏了,90%的原因是因为没有插电源插头。
写代码也是这样,很多时候往往是因为不够细心而出了问题,比如写错变量名、多了空格等,所以出现问题时最应该先细心的检查一遍代码在做调试。
*****
### 判断细节
```php
if (0 == '') {
echo "output";
}
if (0 == 'str') {
echo "output";
}
if (0 == '1') {
echo "not output";
}
if (0 == '1str') {
echo "not output";
}
if ('0' == '') {
echo "not output";
}
if ('0' == 'str') {
echo "not output";
}
----
// 整形转换实验
var_dump((int) '01'); // 1
var_dump((int) '0 1'); // 0
var_dump((int) '0str'); // 0
var_dump((int) '0 str'); // 0
if (0 == '01') {
echo "no output";
}
if (0 == '0str') {
echo "output";
}
if (0 == '0 1') {
echo "output";
}
if (0 == '0 str') {
echo "output";
}
```
字符串 `'0'` 和整数 `0` 是不一样的,在开发中要尤其注意这个容易忽略的细节。
字符串类型 与 整形 作比较时,字符串会自动转换成 整形 在与之比较,**所以 开发时严格要求必须使用 全等比较,不要使用自动转换的特性,除了提高性能外最重要的是能够避免这种不易发现的安全问题。**
----
### 判断尽量使用全等,不要让其自动转换布尔再比较
php7在线执行 http://www.dooccn.com/php7/
```php
var_dump((bool) 0); // false
var_dump((bool) -1); // true
var_dump((bool) '-1'); // true
var_dump((bool) '0'); // false
var_dump((bool) 'str'); // true
if (0 == '') {
echo 's'; // output
}
if (0 == 'str') {
echo 's'; // output
}
if ('0' == '') {
echo 's'; // no output
}
```
两个类型不同的表达式进行比较时,会先各自自动换成布尔值后在进行比较,**只有当两者类型不一致时才会发生类型自动转换。**
>[tip] 当你要用比较时,永远优先考虑使用全等式。
尽量只让类型确定的表达式参与比较,类型不同时也可以手动转换成一致类型后再全等比较,而不要依赖自动的布尔转换,这样做的原因不仅仅是因为自动转换的性能消耗问题,更重要的是为了避免某些隐蔽的错误发生,使程序更加健壮。
>[tip] 纠正:上面说,类型不同的表达式比较时会自动转换为布尔值,这是不完全正确的,应该为:和布尔值比较时 转换为布尔值,和整形比较时,转换为整形后再比较。
----
### php与js关于判断的区别
```php
if ('string' == 0) {
echo "output";
}
```
js版本:比较上PHP和js是不同的,这有点出乎我的意料
javascript:
```javascript
var a = 'string'; a == 0;
// false
```
*****
### 使用不存在变量报错的细节
使用不存在的变量会报错,但有一种情况例外:
```php
echo $a; // 报错 Notice: Undefined variable: a
$a = null;
echo $a['k']; // 不会报错,值为null
或者
$a = false;
echo $a['k']; // 不会报错,值为null
```
这个细节很重要,因为在严格框架下面,我们总会期望当使用不存在的变量时来抛出异常,如果业务逻辑依赖于此,就得小心了。
*****
#### MYSQL decimal(10,2) 四舍五入问题
```php
# number_format 也会四舍五入
echo number_format(12.088, 2, '.', ''); // 12.09
// 还指望mysql自动保留两位小数,不进行任何四舍五入呢,没想到在 MySQL5.5.53 版本下,会进行四舍五入(12.088 => 12.09),坑啊!看来任何时候都不要指望和依赖外部啊。(目前发现不论什么版本的MySQL 都会直接四舍五舍的)
// 金额格式化(保留两位小数,四舍五舍)
function priceFormat($val)
{
// 保留两位小数不四舍五入
return substr(sprintf("%.3f", $val), 0, -1);
}
```
>[danger] 任何时候都不要指望和依赖外部,所有数据直接算好,交给数据库原原本本的存就可以,不要让数据库参与任何的逻辑部分,以及数据处理。
所以需要自己实现一个,但这也引发一个思考,那就是业务中到底需要保留多少位小数,以及如何对待多余的小数位。最简单粗暴的方式就是 四舍五入 或 四舍五舍,不同交易场景下,这两种方案都有不同的副作用,比如少收或多给。复杂的可能需要考虑银行家算法。
*****
#### 浮点数计算问题
[php浮点数的精度问题深究 - php小松 - CSDN博客](https://blog.csdn.net/a454213722/article/details/52135462)
[PHP浮点数的一个常见问题的解答 | 风雪之隅](http://www.laruence.com/2013/03/26/2884.html)
[关于PHP浮点数你应该知道的(All 'bogus' about the float in PHP) | 风雪之隅](http://www.laruence.com/2011/12/19/2399.html)
[intval遇到小数为什么会减1-CSDN论坛](https://bbs.csdn.net/topics/390789758)
[PHP: BC 数学 函数 - Manual](https://www.php.net/manual/zh/ref.bc.php)
[PHP: GMP 函数 - Manual](https://www.php.net/manual/zh/ref.gmp.php)
```php
// 安全的数字计算方式
// https://www.cnblogs.com/phpfensi/p/8143367.html
// https://www.cnblogs.com/jiqing9006/p/5531687.html
// http://php.net/manual/zh/function.bcdiv.php
// https://blog.csdn.net/LJFPHP/article/details/82255389
// https://www.cnblogs.com/phpper/p/7664069.html
//$m和$n代表传入的两个数值,主要就是这两个数值之间的比较
//$x代表传入的方法,比如是;add,sub等
//$scale 代表传入的小数点位数。这个根据需求更改即可
function calc($m, $n, $x = 'add', $scale = 2)
{
$errors = array(
'被除数不能为零',
'负数没有平方根',
);
switch ($x) {
case 'add':
$t = bcadd($m, $n, $scale);
break;
case 'sub':
$t = bcsub($m, $n, $scale);
break;
case 'mul':
$t = bcmul($m, $n, $scale);
break;
case 'div':
if ($n != 0) {
$t = bcdiv($m, $n, $scale);
} else {
return $errors[0];
}
break;
case 'pow':
$t = bcpow($m, $n, $scale);
break;
case 'mod':
if ($n != 0) {
$t = bcmod($m, $n, $scale);
} else {
return $errors[0];
}
break;
case 'sqrt':
if ($m >= 0) {
$t = bcsqrt($m);
} else {
return $errors[1];
}
break;
}
return $t;
}
```
*****
#### tp $db->find(null) 注意
```php
Db::name('user')->find($userId);
```
如果 `$userId` 是 `null` ,那么 `find(null)` 会查询出来表的第一条数据,由于我们的疏忽,没有 `(int) $userId` ,很可能就造成业务逻辑不符合预期,甚至引起严重而隐秘的BUG。
突然感觉到参数类型严格限制语言的好处了,确实能在很大程度上帮助我们在开发时避免这类错误问题。
*****
### intval() 整形转换问题
> 不管变量前面有多少个0,且数字都小于8,它会当作是八进制数转换成十进制数
[php中intval()函数 - ann_glx - 博客园](https://www.cnblogs.com/anns/p/3494195.html)
*****
#### PHP intval() 处理大整形问题
可以使用:
```php
$paysn = floatval($_POST['paysn']);
```
不能在使用 `intval()`了
[php关于数字防注入,intval溢出,intval - u010412301的博客 - CSDN博客](http://blog.csdn.net/u010412301/article/details/55046733)
[PHP长整型在32位系统中强制转化溢出 - CleverCode的博客 - CSDN博客](http://blog.csdn.net/clevercode/article/details/46423103)
[PHP-php使用intval长度超限的问题? - 德问:编程社交问答](http://www.dewen.net.cn/q/3969)
last update:2018-2-9 10:48:00
*****
### 条件判断优先级问题之括号
```php
// 错误,不符合预期,且不易发现
if (!$info = $db->lock(true)->getRow($sql) || $info['s_patent_claim_status'] != 0) {}
// 正确,符合预期
if (!($info = $db->lock(true)->getRow($sql)) || $info['s_patent_claim_status'] != 0) {}
// 正确,符合预期
if ($patentId && $recordInfo = $db->lock(true)->getRow($sql)) {}
```
所以必须细心谨慎对待这类条件判断问题,越不起眼越往往越容易出错,要确保每个功能上线前都通过完备了测试。
*****
### mysql非严格模式下注意的细节
```
`status` tinyint(4) unsigned NOT NULL DEFAULT 0 COMMENT ''
```
unsigned 无符号的,在非严格模式下, `SET status = -1` 不会更改任何数据,也不会报错,这时就需要注意了。
需要详细的测试,程序要健壮,一定要在严格模式环境下进行开发,对返回进行检查,及时发现问题。
*****
### var_export对标量不友好,特别是浮点型
```php
echo var_export(1.4, true); // 1.3999999999999999
echo var_export('1.4', true); // '1.4'
```
所以如果用到了var_export, 请判断一下是否为标量:
```php
if (!is_scalar($value)) {
$val = var_export($value, true);
} else {
$val = $value;
}
```
*****
### @ 关键字慎用!!!
@ 屏蔽错误显示,页面出错不会往下执行,但是页面不会显示任何报错信息!这让人很无语,如果不是确定的代码,请不要使用@
不然没有错误信息怎么调试呢,错误也很重要,要知道错误、BUG也是程序的一部分,也和程序本身一样的同等重要,没有错误信息、错误处理的程序是不完整的,是没有灵魂的。
----
### 计算时注意数据类型是否为数字
这是js中的情况:
```javascript
1 + '9'
// "19"
```
php也有类似的问题,总之任何时候不要忘记,当你想要计算时,是否严格验证了计算对象的数据类型。
----
### left join 左连一对多问题
a LEFT JOIN b ON a.id = b.mid
a INNER JOIN b ON a.id = b.mid
如果 a只有一条,但是对应的 b有两条,**那么最终结果是两条** ,这点容易让人忽略掉
|id|name|
|---|---|
|10| name |
|id|mid|title|
|---|---|---|
|1|10| title1 |
|2|10| title2 |
>[tip] 如果右表 `ON` 外键字段有重复的,那么就会出现重复数据
~~~
1. select DISTINCT a.id, a.name, b.* from a left join b on a.id = b.mid 重复
2. select DISTINCT a.id, a.name from a left join b on a.id = b.mid 不重复
3. select a.id, a.name from a left join b on a.id = b.mid GROUP BY a.id 不重复
4. select group_concat(DISTINCT a.id) as id,a.name, b.* from a left join b on a.id = b.mid 不重复
https://blog.csdn.net/u010003835/article/details/79154457
DISTINCT 表示对后面的所有参数的拼接取 不重复的记录,相当于 把 SELECT 表达式的项 拼接起来选唯一值。
即:行唯一,所以 上面 1 还是重复,2 不重复
~~~
[https://segmentfault.com/a/1190000017067294](https://segmentfault.com/a/1190000017067294)
[数据库表连接的简单解释 - 阮一峰的网络日志](http://www.ruanyifeng.com/blog/2019/01/table-join.html)
[数据库的最简单实现 - 阮一峰的网络日志](http://www.ruanyifeng.com/blog/2014/07/database\_implementation.html)

ps: 待研究 on 和 where 的区别,对上面的情况来说理论上 where 写 on 效果也是一样的,甚至提前缩小了范围表连接会更小
on a.id = b.mid where a.id = 1
表关系:条件两边都是表的字段,而不是其它值,如:a.id = b.mid
使用表连接,表关系不可无,并且 on 中只有写 表关系 才有效,如果写了 where 条件 会直接被忽略掉。
需要注意的是,表关系 也可以写在 where 中,但是不建议这样,应该都写在 on 中提前缩小表连接范围。
上面说法错误,正确如下:
1. 左联或右连时 on 都不会缩小主表范围,内联可以缩小范围。
2. on 只是查找副表数据与其连接,副表没有数据不会影响结果(副表字段都是 null)。
3. where 是最终对数据行进行过滤
4. ~~on 上写主表条件没有作用(因为这个条件只是查找副表数据)~~,但可以写副表 条件 缩小 副表范围
5. ~~如果要过滤最终结果,只能依靠 where~~
6. where/join on 在 INNER/RIGHT 时没什么区别,但是 LEFT 时就有很大区别:on 上只能过滤 副表的数据,并不能像 where 一样 过滤最终数据,导致如果主表数据多,最终结果可能不能如愿。
----
### js篇:不要使用 “连相等赋值”
```javascript
function a() {
var b = d = 1;
}
a();
d; // 1
// d 成了全局的了,如果你想将d赋值给b,就不要这样写 “连相等赋值”,而是这样:
function a() {
var d = 1, b = d;
}
a();
```
----
### mysql_insert_id() 受 insert 和update影响
所以 mysql_insert_id 不一定是取到 最后 insert 的id,如果中间 有update,则返回0 。
*****
### 注意隐含产生的引用
```php
$arr = [['a'], ['b']];
var_dump($arr);
foreach ($arr as &$item) {
foreach ($item as &$value) {
}
// unset($value);
}
// unset($item);
var_dump($arr);
```
```
array(2) {
[0]=>
array(1) {
[0]=>
string(1) "a"
}
[1]=>
array(1) {
[0]=>
string(1) "b"
}
}
array(2) {
[0]=>
array(1) {
[0]=>
string(1) "a"
}
[1]=>
&array(1) {
[0]=>
&string(1) "b"
}
}
```
----
### mysql 千万不要使用 id != null
SELECT * FROM `sp_led_mould` WHERE id != null; 没有结果
SELECT * FROM `sp_led_mould` WHERE id is not NULL; 才会有结果
mysql version: 5.6.16-log
----
### 注意不要 在 tp 同一模型上 上做多次更新
[更新 · ThinkPHP5.0完全开发手册 · 看云](https://www.kancloud.cn/manual/thinkphp5/135189)
> 注意不要在一个模型实例里面做多次更新,会导致部分重复数据不再更新,正确的方式应该是先查询后更新或者使用模型类的`update`方法更新。
~~~
关于多次调用save更新只有第一次更新的数据成功,后面都不成功的,可以在调用save前调用->force()来强制更新,或者调用update更新但是不要调用where方法。调用force的原因是save更新后会$this->origin = $this->data;而更新时又会调用getChangedData检查这个属性,不调用force就会只更新上次没更新过的字段,所以才会导致循环save只有第一条成功。模型的update就不会有这个问题,因为这个方法每次都是重新new一个实例
----
foreach(\[1,2,3\] as $k=>$v){
$user->save($data,\[id=>$v\]);
}
foreach更新只能更新第一条数据,什么原因?
~~~
----
last update: 2019-5-28 23:42:01
';