studio programacion móviles libros español desarrollo desarrollar curso aprende aplicaciones c++ exception coding-style language-design go

c++ - móviles - manual programacion android español pdf



¿Es la declaración de devolución de múltiples valores en el idioma "Ir" de Google una alternativa a las excepciones? (7)

Me parece que las alternativas de Google a las excepciones son:

  • GO: retorno de valores múltiples "return val, err;"
  • GO, C ++: cheques nulos (retorno anticipado)
  • GO, C ++: "manejar el maldito error" (mi término)
  • C ++: afirmar (expresión)

  • GO: defer / panic / recover son características de lenguaje agregadas después de que se hizo esta pregunta

¿Es el retorno de múltiples valores lo suficientemente útil como para actuar como una alternativa? ¿Por qué se "afirma" se consideran alternativas? ¿Google piensa que está bien si un programa se detiene si ocurre un error que no se maneja correctamente?

GO efectivo: valores de retorno múltiples

Una de las características inusuales de Go es que las funciones y los métodos pueden devolver múltiples valores. Esto se puede usar para mejorar un par de modismos torpes en los programas de C: devoluciones de error dentro de la banda (como -1 para EOF) y modificar un argumento.

En C, un error de escritura indica un error de escritura con el código de error guardado en una ubicación volátil. En Go, Write puede devolver un recuento y un error: "Sí, escribió algunos bytes pero no todos porque llenó el dispositivo". La firma de * File.Write en el paquete os es:

func (file *File) Write(b []byte) (n int, err Error)

y como dice la documentación, devuelve el número de bytes escritos y un error no nulo cuando n! = len (b). Este es un estilo común; Vea la sección sobre manejo de errores para más ejemplos.

GO efectivo: parámetros de resultados nombrados

Los "parámetros" de retorno o resultado de una función Go pueden recibir nombres y utilizarse como variables regulares, al igual que los parámetros entrantes. Cuando se nombran, se inicializan a los valores cero para sus tipos cuando comienza la función; Si la función ejecuta una declaración de retorno sin argumentos, los valores actuales de los parámetros de resultados se utilizan como valores devueltos.

Los nombres no son obligatorios pero pueden hacer el código más corto y claro: son documentación. Si nombramos los resultados de nextInt, se vuelve obvio que int devuelto es cuál.

func nextInt(b []byte, pos int) (value, nextPos int) {

Debido a que los resultados con nombre están inicializados y vinculados a un rendimiento sin adornos, se pueden simplificar y aclarar. Aquí hay una versión de io.ReadFull que los usa bien:

func ReadFull(r Reader, buf []byte) (n int, err os.Error) { for len(buf) > 0 && err == nil { var nr int; nr, err = r.Read(buf); n += nr; buf = buf[nr:len(buf)]; } return; }

¿Por qué Go no tiene excepciones?

Las excepciones son una historia similar. Se han propuesto varios diseños para excepciones, pero cada uno agrega una complejidad significativa al lenguaje y al tiempo de ejecución. Por su propia naturaleza, las excepciones abarcan funciones y quizás incluso goroutines; Tienen amplias implicaciones. También hay preocupación por el efecto que tendrían en las bibliotecas. Son, por definición, excepcionales, pero la experiencia con otros lenguajes que los admiten muestra que tienen un efecto profundo en la biblioteca y la especificación de la interfaz. Sería bueno encontrar un diseño que les permita ser verdaderamente excepcionales sin alentar errores comunes para que se conviertan en un flujo de control especial que requiera que cada programador compense.

Al igual que los genéricos, las excepciones siguen siendo un tema abierto.

Guía de estilo de Google C ++: Excepciones

Decisión:

A primera vista, los beneficios de usar excepciones superan los costos, especialmente en proyectos nuevos. Sin embargo, para el código existente, la introducción de excepciones tiene implicaciones en todo el código dependiente. Si las excepciones se pueden propagar más allá de un nuevo proyecto, también resulta problemático integrar el nuevo proyecto en el código sin excepciones existente. Debido a que la mayoría del código C ++ existente en Google no está preparado para lidiar con excepciones, es relativamente difícil adoptar un nuevo código que genere excepciones.

Dado que el código existente de Google no es tolerante a las excepciones, los costos de usar excepciones son algo mayores que los costos en un nuevo proyecto. El proceso de conversión sería lento y propenso a errores. No creemos que las alternativas disponibles a las excepciones, como los códigos de error y las afirmaciones, supongan una carga significativa.

Nuestro consejo contra el uso de excepciones no se basa en razones filosóficas o morales, sino prácticas. Debido a que nos gustaría usar nuestros proyectos de código abierto en Google y es difícil hacerlo si esos proyectos usan excepciones, también debemos advertir contra las excepciones en los proyectos de código abierto de Google. Las cosas probablemente serían diferentes si tuviéramos que hacerlo de nuevo desde cero.

GO: aplazar, pánico y recuperar

Las declaraciones diferidas nos permiten pensar en cerrar cada archivo inmediatamente después de abrirlo, garantizando que, independientemente del número de declaraciones devueltas en la función, los archivos se cerrarán.

El comportamiento de las declaraciones diferidas es sencillo y predecible. Hay tres reglas simples:

1. Los argumentos de una función diferida se evalúan cuando se evalúa la sentencia diferida.

En este ejemplo, la expresión "i" se evalúa cuando la llamada Println se aplaza. La llamada diferida imprimirá "0" después de que la función regrese.

func a() { i := 0 defer fmt.Println(i) i++ return }

2. Las llamadas a la función diferida se ejecutan en el orden Último Primero Primero Fuera después de que la función que la rodea vuelve. Esta función imprime "3210":

func b() { for i := 0; i < 4; i++ { defer fmt.Print(i) } }

3. Las funciones diferidas pueden leer y asignar a los valores de retorno nombrados de la función de retorno.

En este ejemplo, una función diferida incrementa el valor de retorno i después de que devuelve la función circundante. Por lo tanto, esta función devuelve 2:

func c() (i int) { defer func() { i++ }() return 1 }

Esto es conveniente para modificar el valor de retorno de error de una función; Veremos un ejemplo de esto en breve.

Panic es una función incorporada que detiene el flujo ordinario de control y comienza a entrar en pánico. Cuando la función F llama al pánico, la ejecución de F se detiene, cualquier función diferida en F se ejecuta normalmente, y luego F regresa a su interlocutor. Para la persona que llama, F se comporta como una llamada al pánico. El proceso continúa en la pila hasta que todas las funciones en el goroutine actual han regresado, momento en el que el programa falla. Los pánicos se pueden iniciar invocando el pánico directamente. También pueden ser causados ​​por errores de tiempo de ejecución, como los accesos de matriz fuera de los límites.

Recuperar es una función incorporada que recupera el control de un goroutine en pánico. Recuperar solo es útil dentro de funciones diferidas. Durante la ejecución normal, una llamada para recuperar devolverá cero y no tendrá ningún otro efecto. Si el goroutine actual está en pánico, una llamada para recuperar capturará el valor dado al pánico y reanudará la ejecución normal .

Aquí hay un programa de ejemplo que demuestra la mecánica del pánico y el aplazamiento:

<snip>

Para ver un ejemplo real de pánico y recuperación, consulte el paquete json en la biblioteca estándar de Go. Decodifica los datos codificados en JSON con un conjunto de funciones recursivas. Cuando se encuentra un JSON con formato incorrecto, el analizador llama al pánico para desenrollar la pila a la llamada de función de nivel superior, que se recupera del pánico y devuelve un valor de error adecuado (consulte las funciones "error" y "unmarshal" en decode.go) . Hay un ejemplo similar de esta técnica en la rutina de compilación del paquete regexp. La convención en las bibliotecas de Go es que incluso cuando un paquete utiliza pánico internamente, su API externa todavía presenta valores de retorno de error explícitos.

Otros usos de diferir (más allá del archivo. El ejemplo de cierre () dado anteriormente) incluyen la liberación de un mutex:

mu.Lock() defer mu.Unlock


Debe leer un par de artículos sobre excepciones para darse cuenta de que los valores devueltos no son excepciones. No en la forma C ''en banda'' o de ninguna otra manera.

Sin entrar en un argumento profundo, las excepciones están destinadas a ser lanzadas donde se encuentra la condición de error y capturadas donde la condición de error se puede manejar de manera significativa. Los valores de retorno solo se procesan en la primera función de la pila de jerarquía, que podría o no podría procesar el problema. Un ejemplo simple sería un archivo de configuración que pueda recuperar valores como cadenas y que también admita el procesamiento en declaraciones de devolución escritas:

class config { // throws key_not_found string get( string const & key ); template <typename T> T get_as( string const & key ) { return boost::lexical_cast<T>( get(key) ); } };

Ahora el problema es cómo manejar si no se encontró la clave. Si usa códigos de retorno (por ejemplo, en el camino), el problema es que get_as debe manejar el código de error de get y actuar en consecuencia. Como realmente no sabe qué hacer, lo único sensato es propagar manualmente el error en sentido ascendente:

class config2 { pair<string,bool> get( string const & key ); template <typename T> pair<T,bool> get_as( string const & key ) { pair<string,bool> res = get(key); if ( !res.second ) { try { T tmp = boost::lexical_cast<T>(res.first); } catch ( boost::bad_lexical_cast const & ) { return make_pair( T(), false ); // not convertible } return make_pair( boost::lexical_cast<T>(res.first), true ); } else { return make_pair( T(), false ); // error condition } } }

El implementador de la clase debe agregar código adicional para reenviar los errores, y ese código se entremezcla con la lógica real del problema. En C ++, esto es probablemente más oneroso que en un lenguaje diseñado para múltiples asignaciones ( a,b=4,5 ) pero aún así, si la lógica depende del posible error (aquí, llamar a lexical_cast solo se debe realizar si tenemos una cadena real ) entonces tendrás que almacenar en caché los valores en las variables de todos modos.


Esta pregunta es un poco difícil de responder objetivamente, y las opiniones sobre las excepciones pueden diferir bastante.

Pero si tuviera que especular, creo que la razón principal por la que no se incluyen las excepciones en Go es porque complica al compilador y puede llevar a implicaciones no triviales al escribir bibliotecas. Es difícil hacer las excepciones, y dieron prioridad a que algo funcione.

La diferencia principal entre el manejo de errores a través de valores de retorno y excepciones es que las excepciones obligan al programador a lidiar con condiciones inusuales. Nunca puede tener un "error silencioso" a menos que atrape explícitamente una excepción y no haga nada en el bloque catch. Por otro lado, obtiene puntos de retorno implícitos en todas partes dentro de las funciones que pueden llevar a otros tipos de errores. Esto es especialmente frecuente en C ++, donde gestiona la memoria de forma explícita y debe asegurarse de no perder nunca un puntero a algo que haya asignado.

Ejemplo de situación peligrosa en C ++:

struct Foo { // If B''s constructor throws, you leak the A object. Foo() : a(new A()), b(new B()) {} ~Foo() { delete a; delete b; } A *a; B *b; };

Los valores de retorno múltiples facilitan la implementación del manejo de errores basado en el valor de retorno sin tener que depender de los argumentos de las funciones, pero no cambia nada de manera fundamental.

Algunos idiomas tienen valores de retorno y excepciones múltiples (o mecanismos similares). Un ejemplo es Lua .


Este es un ejemplo de cómo pueden funcionar múltiples valores de retorno en c ++. Yo no escribiría este código yo mismo, pero no creo que esté completamente fuera de la cuestión utilizar este enfoque.

#include <iostream> #include <fstream> #include <string> using namespace std; // return value type template <typename T> struct RV { int mStatus; T mValue; RV( int status, const T & rv ) : mStatus( status ), mValue( rv ) {} int Status() const { return mStatus; } const T & Value() const {return mValue; } }; // example of possible use RV <string> ReadFirstLine( const string & fname ) { ifstream ifs( fname.c_str() ); string line; if ( ! ifs ) { return RV <string>( -1, "" ); } else if ( getline( ifs, line ) ) { return RV <string>( 0, line ); } else { return RV <string>( -2, "" ); } } // in use int main() { RV <string> r = ReadFirstLine( "stuff.txt" ); if ( r.Status() == 0 ) { cout << "Read: " << r.Value() << endl; } else { cout << "Error: " << r.Status() << endl; } }


Las devoluciones múltiples no son exclusivas de Go y no sustituyen a las excepciones. En términos de C (o C ++), son un sustituto conciso y fácil de usar para devolver una estructura (objeto) que contiene múltiples valores.

Proporcionan un medio conveniente para indicar errores, si eso es lo que quieres decir.

¿Por qué se "afirma" se consideran alternativas?

Las afirmaciones son inicialmente para la depuración. Detienen el programa en situaciones en las que se encuentra en un estado "imposible", uno que el diseño dice que no debería suceder, pero que de todos modos ha ocurrido. Devolver un error es poco probable que ayude mucho. Obviamente, la base de código todavía no funciona, así que, ¿cómo puede recuperarse con éxito? ¿Por qué querrías hacerlo cuando hay un error que necesita atención?

El uso de aserciones en el código de producción es un tema diferente: obviamente hay problemas de rendimiento y tamaño del código, por lo que el enfoque habitual es eliminarlos una vez que el análisis y las pruebas del código lo hayan convencido de que las situaciones "imposibles" son realmente imposibles. Pero, si está ejecutando código en este nivel de paranoia, que se está auditando a sí mismo, entonces probablemente también esté paranoico de que, si deja que continúe ejecutándose en un estado "imposible", entonces podría hacer algo peligrosamente roto: corromper datos valiosos, que superan una asignación de pila y quizás crean vulnerabilidades de seguridad. Así que, una vez más, solo desea cerrar lo antes posible.

Las cosas para las que utiliza aserciones no son lo mismo que las excepciones para las que usa: cuando los lenguajes de programación como C ++ y Java proporcionan excepciones para situaciones "imposibles" ( ArrayOutOfBoundsException , ArrayOutOfBoundsException ), involuntariamente alientan a algunos programadores a pensar que sus programas Debería intentar recuperarse de situaciones en las que realmente están fuera de control. Algunas veces eso es apropiado, pero el consejo de Java de no atrapar las RuntimeExceptions está ahí por una buena razón. Muy de vez en cuando es una buena idea coger uno, por lo que existen. Casi siempre no es una buena idea atraparlos, lo que significa que equivalen a detener el programa (o al menos el hilo) de todos modos.


No es Go, pero en Lua, el retorno múltiple es un lenguaje extremadamente común para manejar excepciones.

Si tuvieras una función como

function divide(top,bottom) if bottom == 0 then error("cannot divide by zero") else return top/bottom end end

Luego, cuando la bottom era 0, se generaría una excepción y la ejecución del programa se detendría, a menos que envolviera la divide la función en una pcall (o llamada protegida) .

pcall siempre devuelve dos valores: el primero es el resultado es un valor booleano que indica si la función se devolvió correctamente y el segundo es el valor de retorno o el mensaje de error.

El siguiente fragmento de Lua (creado) muestra esto en uso:

local top, bottom = get_numbers_from_user() local status, retval = pcall(divide, top, bottom) if not status then show_message(retval) else show_message(top .. " divided by " .. bottom .. " is " .. retval) end

Por supuesto, no tiene que usar pcall , si la función a la que llama ya regresa en forma de status, value_or_error .

El retorno múltiple ha sido lo suficientemente bueno para Lua durante varios años, por lo que si bien eso no garantiza que sea lo suficientemente bueno para Go, apoya la idea.


Sí, los valores de retorno de error son buenos, pero no captan el verdadero significado del manejo de Excepciones ... es decir, la capacidad y la administración de casos excepcionales en los que normalmente no se pretende.

El diseño de Java (es decir) considera que las excepciones IMO son escenarios de flujo de trabajo válidos y tienen un punto sobre la complejidad de las interfaces y las bibliotecas que tienen que declarar y versionar estas excepciones lanzadas, pero, por desgracia, las excepciones desempeñan un papel importante en la pila de dominó.

Piense en el caso alternativo en el que se manejan condicionalmente códigos de retorno excepcionales en varias docenas de llamadas de método en profundidad. ¿Cómo se verían las huellas de la pila en términos de dónde está el número de la línea ofensiva?


Si necesita la forma en C ++ de hacer un objeto "anulable", use boost :: opcional <T>. Lo prueba como booleano y si se evalúa como verdadero, entonces lo desreferirá a una T. válida.