2009-12-05 8 views
16

Me parece alternativas de Google para excepciones son¿Es la declaración de devolución multivalor del lenguaje "Go" de Google una alternativa a las excepciones?

  • GO: multi-valor de retorno "retorno val, se equivocan;"
  • GO, C++: cero cheques (pronto regreso)
  • GO, C++: "manejar la maldición de error" (mi término)
  • C++: assert (expresión)

  • GO: aplazar/pánico/recuperar las características del lenguaje son añadidas después de esta pregunta se hizo

está múltiples valor de retorno lo suficientemente útil para actuar como una alternativa? ¿Por qué los "asertos" se consideran alternativas? ¿Google piensa que es O.K. si un programa se detiene si ocurre un error que no se maneja correctamente?

Effective GO: Multiple return values

One of Go's unusual features is that functions and methods can return multiple values. This can be used to improve on a couple of clumsy idioms in C programs: in-band error returns (such as -1 for EOF) and modifying an argument.

In C, a write error is signaled by a negative count with the error code secreted away in a volatile location. In Go, Write can return a count and an error: “Yes, you wrote some bytes but not all of them because you filled the device”. The signature of *File.Write in package os is:

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

and as the documentation says, it returns the number of bytes written and a non-nil Error when n != len(b). This is a common style; see the section on error handling for more examples.

Effective GO: Named result parameters

The return or result "parameters" of a Go function can be given names and used as regular variables, just like the incoming parameters. When named, they are initialized to the zero values for their types when the function begins; if the function executes a return statement with no arguments, the current values of the result parameters are used as the returned values.

The names are not mandatory but they can make code shorter and clearer: they're documentation. If we name the results of nextInt it becomes obvious which returned int is which.

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

Because named results are initialized and tied to an unadorned return, they can simplify as well as clarify. Here's a version of io.ReadFull that uses them well:

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; 
} 

Why does Go not have exceptions?

Exceptions are a similar story. A number of designs for exceptions have been proposed but each adds significant complexity to the language and run-time. By their very nature, exceptions span functions and perhaps even goroutines; they have wide-ranging implications. There is also concern about the effect they would have on the libraries. They are, by definition, exceptional yet experience with other languages that support them show they have profound effect on library and interface specification. It would be nice to find a design that allows them to be truly exceptional without encouraging common errors to turn into special control flow that requires every programmer to compensate.

Like generics, exceptions remain an open issue.

Google C++ Style Guide: Exceptions

Decision:

On their face, the benefits of using exceptions outweigh the costs, especially in new projects. However, for existing code, the introduction of exceptions has implications on all dependent code. If exceptions can be propagated beyond a new project, it also becomes problematic to integrate the new project into existing exception-free code. Because most existing C++ code at Google is not prepared to deal with exceptions, it is comparatively difficult to adopt new code that generates exceptions.

Given that Google's existing code is not exception-tolerant, the costs of using exceptions are somewhat greater than the costs in in a new project. The conversion process would be slow and error-prone. We don't believe that the available alternatives to exceptions, such as error codes and assertions, introduce a significant burden.

Our advice against using exceptions is not predicated on philosophical or moral grounds, but practical ones. Because we'd like to use our open-source projects at Google and it's difficult to do so if those projects use exceptions, we need to advise against exceptions in Google open-source projects as well. Things would probably be different if we had to do it all over again from scratch.

GO: Defer, Panic and Recover

Defer statements allow us to think about closing each file right after opening it, guaranteeing that, regardless of the number of return statements in the function, the files will be closed.

The behavior of defer statements is straightforward and predictable. There are three simple rules:

1. A deferred function's arguments are evaluated when the defer statement is evaluated.

In this example, the expression "i" is evaluated when the Println call is deferred. The deferred call will print "0" after the function returns.

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

2. Deferred function calls are executed in Last In First Out order after the surrounding function returns. This function prints "3210":

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

3. Deferred functions may read and assign to the returning function's named return values.

In this example, a deferred function increments the return value i after the surrounding function returns. Thus, this function returns 2:

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

This is convenient for modifying the error return value of a function; we will see an example of this shortly.

Panic is a built-in function that stops the ordinary flow of control and begins panicking. When the function F calls panic, execution of F stops, any deferred functions in F are executed normally, and then F returns to its caller. To the caller, F then behaves like a call to panic. The process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes. Panics can be initiated by invoking panic directly. They can also be caused by runtime errors, such as out-of-bounds array accesses.

Recover is a built-in function that regains control of a panicking goroutine. Recover is only useful inside deferred functions. During normal execution, a call to recover will return nil and have no other effect. If the current goroutine is panicking, a call to recover will capture the value given to panic and resume normal execution.

Here's an example program that demonstrates the mechanics of panic and defer:

<snip> 

For a real-world example of panic and recover, see the json package from the Go standard library. It decodes JSON-encoded data with a set of recursive functions. When malformed JSON is encountered, the parser calls panic is to unwind the stack to the top-level function call, which recovers from the panic and returns an appropriate error value (see the 'error' and 'unmarshal' functions in decode.go). There is a similar example of this technique in the Compile routine of the regexp package. The convention in the Go libraries is that even when a package uses panic internally, its external API still presents explicit error return values.

Other uses of defer (beyond the file.Close() example given earlier) include releasing a mutex:

mu.Lock() 
defer mu.Unlock 
+11

C++ también puede devolver varios valores; solo tiene que ajustarlos en una estructura. –

+5

para compensar el no uso de excepciones, creo que cambiar el tipo de valor devuelto de una función (y las personas que llaman según sea necesario) con "struct" personalizadas hechas a mano es MUY perturbador para la lógica, no práctico y propenso a errores. – reechard

+2

@Neil: +1 Ni siquiera necesita definir sus propias estructuras: 'std :: pair',' boost :: tuple' (también hay una tupla en C++ 0x) –

Respuesta

14

múltiples retornos no son exclusivos de Ir, y no son un sustituto de excepciones. En términos C (o C++), son un sustituto conciso y fácil de usar para devolver una estructura (objeto) que contiene valores múltiples.

Ellos proporcionan un medio conveniente de errores que indican, si eso es todo lo que quiere decir.

alternativas ¿Por qué son "afirma" en cuenta?

Afirma son inicialmente para la depuración. Detienen el programa en situaciones en las que se encuentra en un estado "imposible", que el diseño dice que no debería suceder, pero que de todos modos tiene. Devolver un error es poco probable que ayude mucho. Evidentemente, la base de código aún no funciona, entonces, ¿cómo diablos puede recuperarse con éxito? ¿Por qué querrías hacerlo, cuando hay un error que necesita atención? Usar afirmaciones en el código de producción es un asunto diferente. Obviamente, hay problemas de rendimiento y tamaño de código, por lo que el enfoque habitual es eliminarlos una vez que el análisis y las pruebas del código son imposiblesPero, si está ejecutando código en este nivel de paranoia, que está auditando a sí mismo, entonces probablemente también esté paranoico de que si deja que siga funcionando en un estado "imposible", entonces podría hacer algo peligrosamente roto: corromper datos valiosos, sobrepasando una asignación de pila y tal vez creando vulnerabilidades de seguridad. Entonces, nuevamente, solo quiere cerrar lo más pronto posible.

El material se utiliza afirma para realmente no es el mismo que el material se utiliza excepciones para: cuando los lenguajes de programación como C++ y Java prever excepciones para las situaciones "imposibles" (logic_error, ArrayOutOfBoundsException), que sin querer animar a algunos programadores piense que sus programas deberían intentar recuperarse de situaciones en las que realmente están fuera de control. A veces eso es apropiado, pero el consejo de Java de no detectar RuntimeExceptions está ahí por una buena razón. Muy ocasionalmente es una buena idea atrapar uno, y es por eso 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.

+0

Sí, eso es lo que quiero decir, una forma de indicar una condición de error (en la pila de llamadas) que no perturba demasiado la lógica existente. – reechard

+0

Depende de qué manera está utilizando el valor de retorno. La mayoría de las veces no necesita el rango completo del tipo de devolución. Si devuelve el número de empleados de su empresa, entonces no creo que (número, es correcto) arroje ninguna diferencia en la lógica del número devuelto, o -1 por error. Hace que a la persona que llama le resulte más difícil ignorar el caso de error, pero su lógica para manejar el error es la misma: si (algo) luego (trate con eso). –

+0

@Steve: no del todo, con excepciones puede tener un bloque de código lógico agrupados y procesar las condiciones de error en un momento posterior ('catch'). Esto hace una diferencia ya que la lógica se agrupa y, por lo tanto, es más fácil de seguir. Recuerdo algunas tareas en la escuela en C donde cada línea de código real contenía un 'si' y un par de líneas de procesamiento de errores que siempre resultaban básicamente iguales: un mensaje para el usuario y la devolución de un error en banda token a la persona que llama, cuando las excepciones se habrían propagado muy bien y podrían procesarse fácilmente donde tenga sentido. –

2

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

El Java (es decir) de diseño considera excepciones OMI ser válidos de flujo de trabajo escenarios y tienen un punto sobre la complejidad de las interfaces y las bibliotecas tienen que declarar y la versión de éstos excepción lanzada, pero por desgracia excepciones juegan un papel importante en la pila de dominó.

Piense en el caso alternativo en el que los códigos de retorno excepcionales se manejan condicionalmente en varias docenas de llamadas a métodos. ¿Cómo se verían los vestigios en términos de dónde está el número de línea ofensivo?

4

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

Sin entrar en un argumento profundo, las excepciones deben arrojarse donde se encuentra y se captura la condición de error donde la condición de error se puede manejar de manera significativa. Los valores de retorno solo se procesan en la primera función en la pila de jerarquía, que podría o no podría procesarse el problema. Un ejemplo sencillo sería un archivo de configuración que se pueden recuperar valores como cadenas, y también es compatible transformación en sentencias de retorno proporcionado:

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 ha encontrado la clave. Si utiliza códigos de retorno (por ejemplo, en el camino correcto), el problema es que get_as debe manejar el código de error get y actuar en consecuencia. Ya que realmente no sabe qué hacer, lo único sensato es manualmente propagar el error aguas arriba:

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 que código se entremezclan con la lógica real del problema. En C++ esto es probablemente más engorroso que en un lenguaje diseñado para asignaciones múltiples (a,b=4,5) pero aún así, si la lógica depende del posible error (aquí llamando al lexical_cast solo debe realizarse si tenemos una cadena real), entonces tendrá que los valores de caché en variables de todos modos.

+0

Estoy familiarizado con las excepciones y las dificultades. "Exceptional C++" de Herb Sutter es un buen libro. Me citan mal: "los valores de retorno no son excepciones". Dicho esto, "config2" parece bastante general para un patrón de "objeto válido y código de éxito frente a objeto predeterminado y código de error". – reechard

+0

Olvidé agregar un catch a la función 'lexical_cast'. Si se descartaran excepciones, 'lexical_cast' (bastante simple) podría implementarse con el mismo patrón de valor/estado, pero el código no sería mucho mejor de lo que es con try/catch, a menos que el valor de retorno del molde modificado era exactamente igual que el método 'get_as' y, por lo tanto, se podía reenviar directamente. –

+1

En cualquier caso, el código se vuelve mucho más complejo en cada etapa nueva, debe agregar código para controlar el flujo y declaraciones de retorno en cualquier punto posible de falla. –

2

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

Pero si tuviera que especular, creo que la razón principal de las excepciones no está incluida en Go es porque complica el compilador y puede llevar a implicaciones no triviales al escribir bibliotecas. Las excepciones son difíciles de corregir, y priorizaron que algo funcionara.

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

Ejemplo de situación de peligro 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; 
}; 

valores de retorno múltiples hace que sea más fácil de poner en práctica la gestión de errores basados ​​valor de retorno sin tener que depender de los argumentos de funciones, pero no cambia nada fundamental.

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

3

No es Go, pero en Lua, el retorno múltiple es una expresión extremadamente común para manejar excepciones.

Si había 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 bottom de 0, una excepción sería levantado y ejecución del programa puedan ser suspendidas, a menos que la función envuelto en una dividepcall (or protected call).

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

La siguiente (artificial) Lua fragmento muestra esta 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, usted no tiene que utilizar pcall, si la función ya está llamando vuelve en forma de status, value_or_error.

El retorno múltiple ha sido suficiente para Lua durante varios años, por lo que si bien no es garantizar que es lo suficientemente bueno para Go, es partidario de la idea.

+0

'pcall' está más cerca de una invocación de 'catch' estilo erlang aquí. el error desenrolla la pila y omitirá los fotogramas que no están realizando la detección de errores a uno que sí lo está. Eso es bastante diferente. – Dustin

1

Aquí hay un ejemplo de cómo pueden funcionar los valores de retorno múltiples en C++. Yo no escribiría este código yo mismo, pero no creo que esté fuera de lugar 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; 
    } 
} 
+0

:) Esto me recuerda un código propietario que leí hace un par de meses, donde utilizaron este tipo de enfoque para garantizar que los valores de retorno siempre se procesaran. La clase mantendría un registro de si se había llamado 'Status()' y ** throw ** y exception (en compilaciones de depuración) durante la destrucción si no hubiera sido así. Las excepciones estaban prohibidas en cualquier otro lugar del código, por lo que no habría 'try/catch' en ninguna parte de la pila de llamadas, la excepción mataría a la aplicación. Me pregunto cómo llegaron a ese código ... –

-2

Si usted necesita la manera de C++ de hacer un "anulable" el uso de objetos impulso :: opcional < T>. Lo prueba como un valor booleano y si se evalúa como verdadero, lo desreferencia a un valor válido.

+1

por favor refiérase a la pregunta. No pregunta nada sobre C++, y no necesito nada "anulable". Si esto es una respuesta a la respuesta anterior, déjela en los comentarios. – reechard

Cuestiones relacionadas