PHPlay

Curso de PHP gratis · Capítulo 4 de 13 · con Dr. Juggle

Operadores en PHP: == frente a ===, type juggling y floats

PHP convierte tipos por su cuenta (type juggling), y eso tiene trampas. Aquí verás cuándo usar ===, cómo cambió la comparación en PHP 8 y por qué los floats no son exactos.

Aritmética

Los operadores de siempre: +, -, *, /, y algunos más:

  • % módulo: el resto de la división entera. 7 % 3 es 1. Muy útil para saber si un número es par ($n % 2 == 0) o para "dar la vuelta" (¡el efecto donut del campo de BIT-8!).
  • potencia: 2 10 es 1024.
  • intdiv($a, $b): división entera (sin decimales).
Pergamino del porque

¿Por qué 8 / 2 da int(4) pero 7 / 2 da float(3.5)? Porque en PHP / devuelve un entero solo si la división es exacta; si no, devuelve un float. Si quieres siempre un entero, usa intdiv().

El signo del resultado de % es el del primer operando: -7 % 3 es -1. Y dividir entre 0 lanza un DivisionByZeroError.

<?php
var_dump(7 / 2, 8 / 2);
var_dump(7 % 3, -7 % 3);
var_dump(2 ** 10, 2 ** -1);
var_dump(intdiv(7, 2));
echo 17 % 5, " ", 10 % 5, "\n";
Salida
float(3.5)
int(4)
int(1)
int(-1)
int(1024)
float(0.5)
int(3)
2 0

Precedencia: quién va primero

Igual que en matemáticas, * y / van antes que + y -. La tabla completa es larga, pero lo importante:

  1. (y es asociativo por la derecha: 2 3 2 = 2 9).
  2. !, ++, --, los casts como (int), el menos unario.
  3. *, /, %.
  4. +, -.
  5. . (concatenar).
  6. Comparaciones (<, ==...).
  7. &&, luego ||, luego ??, luego ?:.
  8. Asignaciones (=, +=...).
  9. and, xor, or (¡las últimas de todas!).
Cuidado

Rareza: -2 2 da -4, porque va antes que el menos: es -(2 ** 2).

Truco

Cuando dudes, pon paréntesis. No es de novatos: es de gente que quiere que su código se entienda.

<?php
echo 2 + 3 * 4, "\n";
echo (2 + 3) * 4, "\n";
echo 2 ** 3 ** 2, "\n";
echo -2 ** 2, " ", (-2) ** 2, "\n";
echo 100 / 10 / 2, "\n";
Salida
14
20
512
-4 4
5

Atajos: +=, ++ y compañía

Operar y guardar en la misma variable es tan común que tiene atajos: $x += 5 equivale a $x = $x + 5. También existen -=, =, /=, %=, *= y .=.

++ suma 1 y -- resta 1, pero hay dos formas:

  • ++$i (preincremento): primero suma y luego devuelve el valor nuevo.
  • $i++ (postincremento): devuelve el valor viejo y luego suma.

Solo importa si usas el resultado en la misma expresión. Si es una línea suelta ($i++;), da igual.

<?php
$sacos = 10;
$sacos += 5;
$sacos *= 2;
echo $sacos, "\n";
$i = 5;
echo $i++, " ", $i, "\n";
$j = 5;
echo ++$j, " ", $j, "\n";
Salida
30
5 6
6 6
Pregunta

¿Qué imprime $i = 5; echo $i++ + ++$i;?

  1. 10
  2. 11
  3. 12
  4. 13
Ver respuesta

C. 12

$i++ devuelve 5 (y $i pasa a 6). Después ++$i pone $i a 7 y devuelve 7. 5 + 7 = 12. ¡Por eso es mejor no mezclarlos en la misma expresión!

Comparar: == frente a ===

Hay dos formas de comparar igualdad:

  • == (igualdad débil): convierte los tipos si hace falta y luego compara. 5 == "5" es true.
  • === (igualdad estricta): el tipo también tiene que coincidir. 5 === "5" es false.

Y sus contrarios: != y !==.

Las conversiones de == dan resultados sorprendentes: "1" == "01" y "10" == "1e1" son true porque, si los dos strings parecen números, PHP los compara como números.

Pergamino del porque

El gran cambio de PHP 8: antes, al comparar un número con un texto no numérico, PHP convertía el texto a número: 0 == "hola" ¡era true! Provocaba agujeros de seguridad. Desde PHP 8, si el texto no es numérico, se compara como texto: 0 == "hola" es false. Si lees código antiguo, recuérdalo.

Cuidado

Usa === por defecto. Usa == solo cuando quieras la conversión a propósito.

<?php
var_dump(5 == "5", 5 === "5");
var_dump("1" == "01", "10" == "1e1", 100 == "1e2");
var_dump(0 == "hola", "abc" == 0);
var_dump(null == false, 0 == "", "0" == false);
var_dump(1.0 === 1);
Salida
bool(true)
bool(false)
bool(true)
bool(true)
bool(true)
bool(false)
bool(false)
bool(true)
bool(false)
bool(true)
bool(false)

Mayor, menor y la nave espacial

<, >, <=, >= comparan. Con dos strings no numéricos, comparan alfabéticamente byte a byte: las mayúsculas van antes que las minúsculas ("Z" < "a").

Pero si los dos strings son numéricos, se comparan como números: "10" > "9" es true. Si solo uno lo es... se comparan como texto: "10" < "9a".

La nave espacial <=> compara y devuelve -1 si el primero es menor, 0 si son iguales y 1 si es mayor. Será muy útil para ordenar cosas (capítulo 9).

<?php
var_dump("Z" < "a", "abc" < "abd");
var_dump("10" > "9", "10" < "9a");
echo 1 <=> 2, " ", 2 <=> 2, " ", 3 <=> 2, "\n";
echo "a" <=> "b", "\n";
Salida
bool(true)
bool(true)
bool(true)
bool(true)
-1 0 1
-1

Convertir tipos (casting)

Puedes convertir un valor a otro tipo a propósito con un cast: (int), (float), (string), (bool), (array).

  • (int)"12abc" → 12 (lee los dígitos del principio). (int)"abc" → 0. (int)12.9 → 12 (trunca, no redondea).
  • (string)1.0 → "1". (string)false → "".

¿Qué es verdadero y qué falso? Al convertir a bool, son false: false, 0, 0.0, "", "0", [] y null. Todo lo demás es true, incluidos "0.0", " " y "false".

Funciones útiles: intval(), floatval(), is_numeric(), is_int()...

Cuidado

Los casts nunca avisan. En cambio, en una operación aritmética, un texto como "5 manzanas" da un Warning, y "hola" * 2 directamente un TypeError en PHP 8.

<?php
var_dump((int)"12abc", (int)"abc", (int)12.9, (int)-12.9);
var_dump((float)"3.14 kg", (string)1.0, (string)false);
var_dump((bool)"0", (bool)"0.0", (bool)" ", (bool)[], (bool)0.0);
var_dump(is_numeric("12.5"), is_numeric("12a"));
var_dump("5 manzanas" + 1);
Salida
int(12)
int(0)
int(12)
int(-12)
float(3.14)
string(1) "1"
string(0) ""
bool(false)
bool(true)
bool(true)
bool(false)
bool(false)
bool(true)
bool(false)
int(6)
Pregunta

¿Cuál de estos valores se convierte en true?

  1. "0"
  2. ""
  3. "0.0"
  4. 0
Ver respuesta

C. "0.0"

Solo el string "0" exacto (y el vacío) son falsos. "0.0" es un texto no vacío distinto de "0", así que es verdadero.

Los floats mienten (un poco)

Prueba esto: 0.1 + 0.2 == 0.3 da false. ¿Está roto PHP? No: les pasa a casi todos los lenguajes.

Pergamino del porque

¿Por qué? Los float se guardan en binario (estándar IEEE 754). Igual que 1/3 en decimal es 0,3333... infinito, 0.1 en binario es periódico y no se puede guardar exacto. Se guarda la aproximación más cercana, y al sumar dos aproximaciones el error se nota: 0.30000000000000004.

echo redondea a 14 cifras y te "esconde" el error; var_dump te lo enseña.

Cómo comparar floats:

  • Con un margen: abs($a - $b) < 0.00001 (o PHP_FLOAT_EPSILON).
  • Redondeando: round($a, 2) == round($b, 2).
  • Para dinero, trabaja en céntimos con enteros.
<?php
$x = 0.1 + 0.2;
echo $x, "\n";
var_dump($x);
var_dump($x == 0.3);
var_dump(abs($x - 0.3) < PHP_FLOAT_EPSILON);
var_dump(round($x, 2) == 0.3);
Salida
0.3
float(0.30000000000000004)
bool(false)
bool(true)
bool(true)

Lógica, cortocircuito y ??

Operadores lógicos: && (y), || (o), ! (no). Siempre devuelven un bool.

Cortocircuito: en $a && $b, si $a es falso, PHP ni siquiera evalúa $b (el resultado ya es falso). Igual con || si el primero es verdadero. Se usa para proteger operaciones: $n != 0 && 10 / $n > 2.

El operador ?? (null coalescing) devuelve el de la izquierda si existe y no es null; si no, el de la derecha. ¡Y no da Warning si la variable no existe!

Cuidado

and y or existen, pero tienen una precedencia MÁS BAJA que =. Por eso $r = false or true; guarda false en $r. Usa && y ||.

<?php
var_dump(true && false, true || false, !true);
$n = 0;
var_dump($n != 0 && 10 / $n > 2);
$apodo = null;
echo $apodo ?? "sin apodo", "\n";
echo $noExiste ?? "valor por defecto", "\n";
$r = false or true;
var_dump($r);
Salida
bool(false)
bool(true)
bool(false)
bool(false)
sin apodo
valor por defecto
bool(false)

Ejercicios de Operadores en PHP resueltos

Intenta resolverlos antes de mirar la solución. En el juego puedes ejecutarlos y comprobarlos automáticamente.

Acertijo 1. Las muestras de Juggle

Juggle ha etiquetado cada frasco con una comparación. Tienes que decir qué resultado da cada una en PHP 8.

Responde a las seis preguntas.

  1. "5" == 5

    1. true
    2. false
    Ver respuesta

    A. true

  2. "5" === 5

    1. true
    2. false
    Ver respuesta

    B. false

  3. 0 == "hola" (en PHP 8)

    1. true
    2. false
    Ver respuesta

    B. false

  4. "abc" < "abd"

    1. true
    2. false
    Ver respuesta

    A. true

  5. (int)"7 enanitos"

    1. 0
    2. 7
    3. Error
    Ver respuesta

    B. 7

  6. 10 % 4 + 2 ** 2

    1. 6
    2. 18
    3. 8
    Ver respuesta

    A. 6

Encargo 2. Carros del mercado

Cada carro del mercado lleva $capacidad sacos. Tienes $sacos sacos. Ambas variables ya existen.

Calcula cuántos carros se llenan del todo y cuántos sacos sobran, e imprime exactamente (con los números calculados):

3 carros llenos y sobran 2 sacos
Pistas
  • intdiv($sacos, $capacidad) te da los carros llenos.
  • El resto de la división es $sacos % $capacidad.
Ver solución
<?php
$carros = intdiv($sacos, $capacidad);
$resto = $sacos % $capacidad;
echo "$carros carros llenos y sobran $resto sacos";

Predicción 3. El reactivo de incrementos

Juggle mezcla incrementos en una pócima. ¿Qué saldrá?

Escribe exactamente lo que imprime el programa.

<?php
$x = 3;
$y = $x++ + 2;
$z = ++$x * 2;
echo "$x $y $z";
    Ver solución
    5 5 10

    ¡Correcto! $y usó el 3 viejo; $z usó el 5 nuevo.

    Caza de bugs 4. La balanza que miente

    La balanza del laboratorio suma dos pesadas: 0.1 kg y 0.2 kg. Dice que NO pesan 0.3 kg. Juggle está indignado.

    Cambia la comparación para que el programa imprima bool(true), comparando los floats de forma segura. Sigue usando $total.

    Código de partida:

    <?php
    $a = 0.1;
    $b = 0.2;
    $total = $a + $b;
    var_dump($total == 0.3);
    
    Pistas
    • Compara la diferencia con un margen pequeño: abs($total - 0.3) < 0.00001.
    • O redondea antes de comparar: round($total, 2) == 0.3.
    Ver solución
    <?php
    $a = 0.1;
    $b = 0.2;
    $total = $a + $b;
    var_dump(abs($total - 0.3) < PHP_FLOAT_EPSILON);
    

    Misión del dron 5. Tornillos por heno

    Tuerca cambia 2 de heno por 1 tornillo. BIT-8 está en (0, 1).

    Cosecha las 3 hierbas de la fila de BIT-8 (hacia el ESTE). Luego calcula con intdiv() y % cuántos tornillos te dan y cuánto heno sobra, e imprime:

    Tornillos: 1, sobra: 1

    (calculado a partir de inventario(HENO)).

    Esta misión mueve al dron BIT-8 por un campo de cultivo: juégala en PHPlay para ver tu código animado.

    Pistas
    • $heno = inventario(HENO);
    • $tornillos = intdiv($heno, 2); y $sobra = $heno % 2;
    Ver solución
    <?php
    cosechar();
    mover(ESTE);
    cosechar();
    mover(ESTE);
    cosechar();
    $heno = inventario(HENO);
    $tornillos = intdiv($heno, 2);
    $sobra = $heno % 2;
    echo "Tornillos: $tornillos, sobra: $sobra";
    

    Chuleta: lo esencial

    • + - * /, % (resto), ** (potencia), intdiv(). / da int solo si la división es exacta.
    • Precedencia: ** > unarios > * / % > + - > . > comparaciones > && > || > ?? > ?: > = > and/or. Usa paréntesis.
    • +=, -=, *=, .=... $i++ devuelve el viejo; ++$i el nuevo.
    • == compara con conversión; === también el tipo. PHP 8: 0 == "hola" es false.
    • <=> devuelve -1, 0 o 1.
    • Casts: (int), (float), (string), (bool). Falsos: false 0 0.0 "" "0" [] null.
    • Floats: 0.1 + 0.2 != 0.3. Compara con abs($a - $b) < ε o round().
    • && y || hacen cortocircuito. $x ?? "defecto" sin Warning si no existe.