Prefacio
Este artículo es un ejemplo de cómo podemos investigar el comportamiento de las funciones de la Biblioteca estándar de Swift para desarrollar nuestro conocimiento no solo en la documentación de la Biblioteca sino también en su código fuente.
Errores irrecuperables
Todos los eventos que los programadores llaman "errores" se pueden separar en dos tipos.
- Eventos causados por factores externos como una falla de conexión de red.
- Eventos causados por un error del programador, como llegar a un caso de operador de interruptor que no debería ser accesible.
Los eventos del primer tipo se procesan en un flujo de control regular. Por ejemplo, reaccionamos a la falla de la red mostrando un mensaje a un usuario y configurando una aplicación para esperar la recuperación de la conexión de red.
Intentamos descubrir y eliminar eventos del segundo tipo lo antes posible antes de que el código entre en producción. Uno de los enfoques aquí es ejecutar algunas comprobaciones de tiempo de ejecución que finalizan la ejecución del programa en un estado depurable e imprimir un mensaje con una indicación de en qué parte del código ha ocurrido el error.
Por ejemplo, un programador puede terminar la ejecución si no se proporcionó el inicializador requerido pero se lo llamó. Eso se notará y arreglará invariablemente durante la primera prueba.
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
Otro ejemplo es el cambio entre índices (supongamos que por alguna razón no puede usar la enumeración).
switch index { case 0:
Nuevamente, un programador provocará un bloqueo durante la depuración aquí para notar inevitablemente un error en la indexación.
Hay cinco funciones de terminación de la Biblioteca estándar de Swift (como para Swift 4.2).
func precondition(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line)
func preconditionFailure(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) -> Never
func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line)
func assertionFailure(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line)
func fatalError(_ message: @autoclosure () -> String = default, file: StaticString = #file, line: UInt = #line) -> Never
¿Cuál de las cinco funciones de terminación deberíamos preferir?
Código fuente vs documentación
Veamos el código fuente . Podemos ver lo siguiente de inmediato:
- Cada una de estas cinco funciones termina la ejecución del programa o no hace nada.
- La posible terminación ocurre de dos maneras.
_assertionFailure(_:_:file:line:flags:)
un conveniente mensaje de depuración llamando a _assertionFailure(_:_:file:line:flags:)
.- Sin el mensaje de depuración simplemente llamando a
Builtin.condfail(error._value)
o Builtin.int_trap()
.
- La diferencia entre las cinco funciones de terminación radica en las condiciones bajo las cuales sucede todo lo anterior.
fatalError(_:file:line)
llama a _assertionFailure(_:_:file:line:flags:)
incondicionalmente.- Las otras cuatro funciones de terminación evalúan las condiciones llamando a las siguientes funciones de evaluación de configuración. (Comienzan con un guión bajo, lo que significa que son internos y no deben ser llamados directamente por un programador que usa Swift Standard Library).
_isReleaseAssertConfiguration()
_isDebugAssertConfiguration()
_isFastAssertConfiguration()
Ahora echemos un vistazo a la documentación . Podemos ver lo siguiente de inmediato.
fatalError(_:file:line)
imprime incondicionalmente un mensaje dado y detiene la ejecución .- Los efectos de las otras cuatro funciones de terminación varían según el indicador de compilación utilizado:
-Onone
, -O
, -Ounchecked
. Por ejemplo, mire la documentación de preconditionFailure(_:file:line:)
. - Podemos establecer estos indicadores de compilación en Xcode a través de la
SWIFT_OPTIMIZATION_LEVEL
compilación del compilador SWIFT_OPTIMIZATION_LEVEL
. - También sabemos por la documentación de Xcode 10 que se introduce un indicador de optimización más,
-Osize
. - Por lo tanto, tenemos que considerar los cuatro indicadores de compilación de optimización.
-Onone
(no optimizar)-O
(optimizar para la velocidad)-Osize
(optimizar para el tamaño)-Ounchecked
(apague muchas comprobaciones del compilador)
Podemos concluir que la configuración evaluada en las cuatro funciones de terminación se establece mediante estos indicadores de compilación.
Ejecución de funciones de evaluación de configuración
Aunque las funciones de evaluación de la configuración están diseñadas para uso interno, algunas de ellas son públicas para fines de prueba , y podemos probarlas a través de CLI dando los siguientes comandos en Bash.
$ echo 'print(_isFastAssertConfiguration())' >conf.swift $ swift conf.swift false $ swift -Onone conf.swift false $ swift -O conf.swift false $ swift -Osize conf.swift false $ swift -Ounchecked conf.swift true
$ echo 'print(_isDebugAssertConfiguration())' >conf.swift $ swift conf.swift true $ swift -Onone conf.swift true $ swift -O conf.swift false $ swift -Osize conf.swift false $ swift -Ounchecked conf.swift false
Estas pruebas y la inspección del código fuente nos llevan a las siguientes conclusiones aproximadas.
Hay tres configuraciones mutuamente excluyentes.
- La configuración de lanzamiento se establece proporcionando una
-Osize
compilación -O
o -Osize
. - La configuración de depuración se establece proporcionando un
-Onone
compilación -Onone
o ningún indicador de optimización. _isFastAssertConfiguration()
se evalúa como true
si se establece un -Ounchecked
compilación -Ounchecked
. Aunque esta función tiene una palabra "rápido" en su nombre, no tiene nada que ver con la optimización de la velocidad de la bandera de construcción.
NB: Estas conclusiones no son la definición estricta de cuándo tienen lugar las compilaciones de depuración o de lanzamiento . Es un tema más complejo. Pero estas conclusiones son correctas para el contexto de terminación del uso de funciones.
Simplificando la imagen
-Ounchecked
No veamos para qué -Ounchecked
indicador -Ounchecked
(es irrelevante aquí) sino cuál es su función en el contexto de la finalización del uso de funciones.
- La documentación para la
precondition(_:_:file:line:)
y assert(_:_:file:line:)
dice: "En -Ounchecked
compilaciones sin -Ounchecked
, la condición no se evalúa, pero el optimizador puede suponer que siempre se evalúa como verdadero. El incumplimiento de esa suposición es un grave error de programación ". - La documentación para
preconditionFailure(_:file:line)
y assertionFailure(_:file:line:)
dice: "En -Ounchecked
compilaciones sin -Ounchecked
, el optimizador puede suponer que esta función nunca se llama. Si no se cumple esa suposición, se trata de un grave error de programación. " - Podemos ver en el código fuente que la evaluación de
_isFastAssertConfiguration()
a true
no debería suceder . (Si sucede, se llama a _conditionallyUnreachable()
extraño. Consulte las líneas 136 y _conditionallyUnreachable()
)
Hablando más directamente, no debe permitir el alcance de las siguientes cuatro funciones de terminación con el -Ounchecked
compilación -Ounchecked
establecido para su programa.
precondition(_:_:file:line:)
preconditionFailure(_:file:line)
assert(_:_:file:line:)
assertionFailure(_:file:line:)
Use solo fatalError(_:file:line)
mientras aplica -Ounchecked
y al mismo tiempo permite que el punto de su programa con la fatalError(_:file:line)
sea accesible.
El papel de una verificación de condición
Dos de las funciones de terminación nos permiten verificar las condiciones. La inspección del código fuente nos permite ver que si la condición falla, el comportamiento de la función es el mismo que el de su primo respectivo:
precondition(_:_:file:line:)
convierte en preconditionFailure(_:file:line)
,assert(_:_:file:line:)
convierte en assertionFailure(_:file:line:)
.
Ese conocimiento facilita el análisis posterior.
Configuraciones de lanzamiento vs depuración
Eventualmente, la documentación adicional y la inspección del código fuente nos permiten formular la siguiente tabla.

Ahora está claro que la opción más importante para un programador es cómo debería ser el comportamiento del programa en la versión si una verificación de tiempo de ejecución revela un error.
El punto clave aquí es que assert(_:_:file:line:)
y assertionFailure(_:file:line:)
hacen que el impacto de la falla del programa sea menos severo. Por ejemplo, una aplicación de iOS puede haber corrompido la IU (ya que fallaron algunas comprobaciones importantes de tiempo de ejecución) pero no se bloqueará.
Pero ese escenario puede no ser el que deseabas. Tienes una opción
Never
devuelva el tipo
Never
se utiliza como un tipo de función de retorno que arroja un error incondicionalmente, atrapa o no termina normalmente. Ese tipo de funciones en realidad no regresan, nunca regresan.
Entre las cinco funciones de terminación, solo preconditionFailure(_:file:line)
y fatalError(_:file:line)
devuelven Never
porque solo estas dos funciones detienen incondicionalmente las ejecuciones del programa y, por lo tanto, nunca regresan.
Aquí hay un buen ejemplo de cómo utilizar Never
escriba en una aplicación de línea de comandos. (Aunque este ejemplo no utiliza las funciones de terminación de Swift Standard Library, sino la función estándar C exit()
).
func printUsagePromptAndExit() -> Never { print("Usage: command directory") exit(1) } guard CommandLine.argc == 2 else { printUsagePromptAndExit() }
Si printUsagePromptAndExit()
devuelve Void
lugar de Never
, aparece un error de tiempo de compilación con el mensaje, " el cuerpo 'guard' no debe fallar , considere usar un 'return' o 'throw' para salir del alcance ". Al usar Never
, está diciendo de antemano que nunca sale del ámbito y, por lo tanto, el compilador no le dará un error de tiempo de compilación. De lo contrario, debe agregar return
al final del bloque de código de protección, que no se ve bien.
Comida para llevar
- No importa qué función de terminación usar si está seguro de que todas sus comprobaciones de tiempo de ejecución son relevantes solo para la configuración de depuración .
- Use solo
fatalError(_:file:line)
mientras aplica -Ounchecked
y al mismo tiempo permite que el punto de su programa con la fatalError(_:file:line)
sea accesible. - Use
assertionFailure(_:file:line:)
assert(_:_:file:line:)
y assertionFailure(_:file:line:)
si le preocupa que las comprobaciones de tiempo de ejecución puedan fallar de alguna manera en la versión. Al menos tu aplicación no se bloqueará. - Use
Never
para que su código se vea ordenado.
Enlaces utiles