¿Mejores prácticas para el manejo de excepciones en una aplicación de Windows Forms?


118

Actualmente estoy en el proceso de escribir mi primera aplicación de Windows Forms. He leído algunos libros de C # ahora, así que tengo una comprensión relativamente buena de qué características del lenguaje C # tiene que lidiar con las excepciones. Sin embargo, todos son bastante teóricos, por lo que todavía no tengo una idea de cómo traducir los conceptos básicos en un buen modelo de manejo de excepciones en mi aplicación.

¿Alguien quisiera compartir alguna perla de sabiduría sobre el tema? Publique cualquier error común que haya visto cometer a novatos como yo, y cualquier consejo general sobre el manejo de excepciones de una manera que haga que mi aplicación sea más estable y sólida.

Las principales cosas que estoy tratando de resolver actualmente son:

  • ¿Cuándo debería volver a lanzar una excepción?
  • ¿Debería intentar tener un mecanismo central de manejo de errores de algún tipo?
  • ¿El manejo de las excepciones que podrían producirse tiene un impacto en el rendimiento en comparación con las pruebas preventivas como si existe un archivo en el disco?
  • ¿Debería incluirse todo el código ejecutable en bloques try-catch-finalmente?
  • ¿Hay ocasiones en las que un bloque de captura vacío podría ser aceptable?

¡Todos los consejos recibidos con gratitud!

Respuestas:


79

Algunos bits más ...

Debe tener una política centralizada de manejo de excepciones. Esto puede ser tan simple como envolver Main()en un intento / captura, fallando rápidamente con un mensaje de error elegante para el usuario. Este es el controlador de excepciones de "último recurso".

Los controles preventivos siempre son correctos si es posible, pero no siempre son perfectos. Por ejemplo, entre el código donde verifica la existencia de un archivo y la siguiente línea donde lo abre, el archivo podría haber sido eliminado o algún otro problema podría impedir su acceso. Todavía necesitas probar / atrapar / finalmente en ese mundo. Utilice tanto el control preventivo como el try / catch / finalmente según corresponda.

Nunca se "trague" una excepción, excepto en los casos mejor documentados, cuando esté absolutamente seguro de que la excepción que se lanza es aceptable. Este casi nunca será el caso. (Y si es así, asegúrese de tragar solo la clase de excepción específica , nunca trague System.Exception).

Al crear bibliotecas (utilizadas por su aplicación), no se trague las excepciones y no tenga miedo de dejar que las excepciones aparezcan. No vuelvas a lanzar a menos que tengas algo útil que agregar. Nunca (en C #) haga esto:

throw ex;

Como borrará la pila de llamadas. Si debe volver a lanzar (lo que ocasionalmente es necesario, como cuando se usa el Bloque de manejo de excepciones de la Biblioteca empresarial), use lo siguiente:

throw;

Al final del día, la gran mayoría de las excepciones generadas por una aplicación en ejecución deberían exponerse en algún lugar. No deben exponerse a los usuarios finales (ya que a menudo contienen datos de propiedad o valiosos), sino que generalmente se registran, y los administradores notifican la excepción. Se le puede presentar al usuario un cuadro de diálogo genérico, tal vez con un número de referencia, para simplificar las cosas.

El manejo de excepciones en .NET es más un arte que una ciencia. Todos tendrán sus favoritos para compartir aquí. Estos son solo algunos de los consejos que he aprendido usando .NET desde el primer día, técnicas que me han salvado el tocino en más de una ocasión. Su experiencia puede ser diferente.


1
Si uno deja que las excepciones aparezcan, ¿cómo se supone que la persona que llama debe saber si la excepción indica que una operación falló pero el sistema está básicamente bien (por ejemplo, un usuario intentó abrir un archivo de documento corrupto; el archivo no se carga, pero todo de lo contrario debería estar bien), o si indica que la CPU está en llamas y uno debe dirigirse a las salidas lo más rápido posible. Algo como ArgumentException podría indicar cualquiera, dependiendo de las circunstancias en las que se lanza.
supercat

2
@supercat Al escribir subclases específicas de ApplicationExceptionpara aquellos casos de falla que la aplicación debería poder diferenciar y manejar razonablemente.
Matt Enright

@Matt Enright: Ciertamente, es posible detectar las propias excepciones, pero no conozco nada que se parezca remotamente a una convención por la cual los módulos que arrojan excepciones indican si indican la corrupción de cualquier estado fuera de lo que implica la falla. Idealmente, un método como SuperDocument.CreateFromFile () tendría éxito, lanzaría una excepción CleanFailure o lanzaría una SomethingReallyBadHappenedException. Desafortunadamente, a menos que uno envuelva todo lo que podría lanzar una excepción dentro de su propio bloque de captura, no hay forma de saber si una InvalidOperationException ...
supercat

@Matt Enright: ... debería estar envuelto en una CleanFailureException o una SomethingReallyBadHappenedException. Y envolver todo en bloques individuales de try-catch anularía todo el propósito de tener excepciones en primer lugar.
supercat

2
Creo que es un diseño de biblioteca deficiente, para ocultar modos de falla con un tipo de excepción inexacto. No arroje una FileNotFoundException si lo que realmente quiere decir es una IOException o una InvalidDataException, ya que la aplicación debe responder de manera diferente a cada caso. Cosas como StackOverflowException u OutOfMemoryException no pueden ser manejadas razonablemente por una aplicación, así que déjelas burbujear, ya que una aplicación que se comporta bien tiene el controlador centralizado de "último recurso" en su lugar de todos modos.
Matt Enright

63

Hay un excelente artículo de CodeProject sobre código aquí . Aquí hay un par de aspectos destacados:

  • Planifica para lo peor *
  • Compruébalo temprano
  • No confíes en los datos externos
  • Los únicos dispositivos confiables son: el video, el mouse y el teclado.
  • Las escrituras también pueden fallar
  • Codifique de forma segura
  • No arrojes una nueva excepción ()
  • No coloque información importante sobre excepciones en el campo Mensaje
  • Ponga una sola captura (Exception ex) por hilo
  • Las excepciones genéricas detectadas deben publicarse
  • Log Exception.ToString (); nunca registre solo Exception.Message!
  • No atrapes (excepción) más de una vez por hilo
  • Nunca trague excepciones
  • El código de limpieza debe colocarse finalmente en bloques
  • Usa "usar" en todas partes
  • No devuelva valores especiales en condiciones de error
  • No use excepciones para indicar la ausencia de un recurso
  • No utilice el manejo de excepciones como medio para devolver información de un método
  • Utilice excepciones para errores que no deben ignorarse
  • No borre el seguimiento de la pila al volver a lanzar una excepción
  • Evite cambiar las excepciones sin agregar valor semántico
  • Las excepciones deben estar marcadas como [Serializable]
  • En caso de duda, no afirme, arroje una excepción
  • Cada clase de excepción debe tener al menos los tres constructores originales
  • Tenga cuidado al usar el evento AppDomain.UnhandledException
  • No reinventes la rueda
  • No use el manejo de errores no estructurado (VB.Net)

3
¿Te importaría explicarme un poco más este punto? "No use excepciones para indicar la ausencia de un recurso" No estoy muy seguro de entender la razón detrás de esto. También solo un comentario: esta respuesta no explica el "por qué" en absoluto. Sé que esto tiene 5 años, pero todavía me molesta un poco
Rémi

El enlace al artículo de referencia ha caducado. Code Project sugiere que el artículo puede haberse movido aquí .
DavidRR

15

Tenga en cuenta que Windows Forms tiene su propio mecanismo de manejo de excepciones. Si se hace clic en un botón en el formulario y su controlador genera una excepción que no se detecta en el controlador, Windows Forms mostrará su propio cuadro de diálogo de excepción no controlada.

Para evitar que se muestre el cuadro de diálogo de excepción no controlada y detectar tales excepciones para el registro y / o para proporcionar su propio cuadro de diálogo de error, puede adjuntar el evento Application.ThreadException antes de la llamada a Application.Run () en su método Main ().


Gracias por esta sugerencia, lo aprendí demasiado tarde, lo acabo de verificar en linqpad, funciona como se esperaba, el delegado es: void Form1_UIThreadException (remitente del objeto, ThreadExceptionEventArgs t) Otra buena fuente sobre este tema es richnewman.wordpress.com/2007/ 04/08 /… Para el manejo general de excepciones no controladas: el evento AppDomain.UnhandledException es para excepciones no controladas lanzadas desde un subproceso de UI no principal.
zhaorufei

14

Todos los consejos publicados aquí hasta ahora son buenos y vale la pena seguirlos.

Una cosa que me gustaría ampliar es su pregunta "¿El manejo de las excepciones que pueden producirse tiene un impacto en el rendimiento en comparación con las pruebas preventivas de cosas como si existe un archivo en el disco?"

La ingenua regla general es que "los bloques de prueba / captura son caros". Eso no es realmente cierto. Probar no es caro. Es la captura, donde el sistema tiene que crear un objeto de excepción y cargarlo con el seguimiento de la pila, eso es caro. Hay muchos casos en los que la excepción es, bueno, lo suficientemente excepcional como para que esté perfectamente bien envolver el código en un bloque try / catch.

Por ejemplo, si está rellenando un Diccionario, esto:

try
{
   dict.Add(key, value);
}
catch(KeyException)
{
}

suele ser más rápido que hacer esto:

if (!dict.ContainsKey(key))
{
   dict.Add(key, value);
}

para cada elemento que está agregando, porque la excepción solo se lanza cuando agrega una clave duplicada. (Las consultas agregadas de LINQ hacen esto).

En el ejemplo que diste, usaría try / catch casi sin pensar. Primero, el hecho de que el archivo exista cuando lo verifica no significa que va a existir cuando lo abra, por lo que realmente debería manejar la excepción de todos modos.

En segundo lugar, y creo que lo más importante, a menos que a) su proceso esté abriendo miles de archivos yb) las probabilidades de que un archivo que está tratando de abrir no exista no sean trivialmente bajas, el impacto en el rendimiento de crear la excepción no es algo que usted ' alguna vez lo vas a notar. En términos generales, cuando su programa intenta abrir un archivo, solo intenta abrir un archivo. Ese es un caso en el que escribir un código más seguro es casi seguro que será mejor que escribir el código más rápido posible.


3
en el caso de dict, simplemente puede hacer: dict[key] = valueque debería ser tan rápido si no más rápido ..
nawfal

9

Aquí hay algunas pautas que sigo

  1. Fail-Fast: esta es más una pauta de generación de excepciones.Por cada suposición que haga y cada parámetro que ingrese en una función, haga una verificación para asegurarse de que está comenzando con los datos correctos y que las suposiciones están haciendo son correctos. Las verificaciones típicas incluyen, argumento no nulo, argumento en el rango esperado, etc.

  2. Al volver a lanzar preservar el seguimiento de pila: esto simplemente se traduce en usar throw al volver a lanzar en lugar de lanzar una nueva Exception (). Alternativamente, si cree que puede agregar más información, envuelva la excepción original como una excepción interna. Pero si está detectando una excepción solo para registrarla, definitivamente use throw;

  3. No detecte excepciones que no pueda manejar, así que no se preocupe por cosas como OutOfMemoryException porque si ocurren, no podrá hacer mucho de todos modos.

  4. Enganche a los manejadores de excepciones globales y asegúrese de registrar tanta información como sea posible. Para winforms, enganche tanto el dominio de aplicación como los eventos de excepción no controlados del hilo.

  5. El rendimiento solo debe tenerse en cuenta cuando haya analizado el código y haya visto que está causando un cuello de botella en el rendimiento; de forma predeterminada, optimice la legibilidad y el diseño. Entonces, sobre su pregunta original sobre la verificación de existencia del archivo, diría que depende, si puede hacer algo para que el archivo no esté allí, entonces sí, haga esa verificación de lo contrario si todo lo que va a hacer es lanzar una excepción si el archivo está no ahí, entonces no veo el punto.

  6. Definitivamente hay ocasiones en las que se requieren bloques de captura vacíos, creo que las personas que dicen lo contrario no han trabajado en bases de código que han evolucionado en varias versiones. Pero deben comentarse y revisarse para asegurarse de que sean realmente necesarios. El ejemplo más típico es que los desarrolladores usan try / catch para convertir cadenas en números enteros en lugar de usar ParseInt ().

  7. Si espera que la persona que llama a su código pueda manejar las condiciones de error, cree excepciones personalizadas que detallen cuál es la situación inesperada y brinden información relevante. De lo contrario, limítese a los tipos de excepción incorporados tanto como sea posible.


¿De qué sirve enlazar tanto el dominio de aplicación como los controladores de excepciones no controlados de subprocesos? Si solo usa Appdomain.UnhandledException, ¿no es el más genérico que captura todo?
Quagmire

¿Quiso decir manejar el Application.ThreadExceptionevento cuando hizo referencia al evento "excepción de subproceso no controlado"?
Jeff B

4

Me gusta la filosofía de no captar nada que no tenga la intención de manejar, sea lo que sea el manejo en mi contexto particular.

Odio cuando veo un código como:

try
{
   // some stuff is done here
}
catch
{
}

He visto esto de vez en cuando y es bastante difícil encontrar problemas cuando alguien 'come' las excepciones. Un compañero de trabajo que tuve hace esto y tiende a terminar contribuyendo a un flujo constante de problemas.

Vuelvo a lanzar si hay algo que mi clase en particular necesita hacer en respuesta a una excepción, pero el problema debe resolverse sin embargo, se debe llamar al método donde sucedió.

Creo que el código debe escribirse de forma proactiva y que las excepciones deben ser para situaciones excepcionales, no para evitar las pruebas de condiciones.


@Kiquenet Lo siento, estaba un poco confuso. Lo que quise decir: no haga capturas vacías, en su lugar agregue un controlador a Dispatcher.UnhandledException , y agregue al menos un registro para que no se coma sin migas de pan :) Pero a menos que tenga el requisito de un componente silencioso, siempre debe incluir excepciones en su diseño. Tírelos cuando necesiten ser lanzados, haga contratos claros para usted Interfaces / API / cualquier Clase.
LuckyLikey

4

Estoy a punto de salir, pero le daré un breve resumen sobre dónde usar el manejo de excepciones. Intentaré abordar sus otros puntos cuando regrese :)

  1. Verifique explícitamente todas las condiciones de error conocidas *
  2. Agregue un try / catch alrededor del código si no está seguro de haber podido manejar todos los casos
  3. Agregue un try / catch alrededor del código si la interfaz .NET a la que está llamando arroja una excepción
  4. Agregue un intento / captura alrededor del código si cruza un umbral de complejidad para usted
  5. Agregue un intento / captura alrededor del código si desea una verificación de cordura: está afirmando que ESTO NUNCA DEBERÍA SUCEDER
  6. Como regla general, no utilizo excepciones como reemplazo de los códigos de devolución. Esto está bien para .NET, pero no para mí. Sin embargo, tengo excepciones (jeje) a esta regla, también depende de la arquitectura de la aplicación en la que esté trabajando.

*Dentro de lo razonable. No es necesario verificar si, digamos, un rayo cósmico golpeó sus datos y provocó que un par de bits se voltearan. Comprender lo que es "razonable" es una habilidad que adquiere un ingeniero. Es difícil de cuantificar, pero fácil de intuir. Es decir, puedo explicar fácilmente por qué utilizo un try / catch en cualquier caso particular, pero me cuesta imbuir a otro con este mismo conocimiento.

Por mi parte, tiendo a alejarme de las arquitecturas basadas en gran medida en excepciones. try / catch no tiene un impacto de rendimiento como tal, el hit se produce cuando se lanza la excepción y es posible que el código tenga que subir varios niveles de la pila de llamadas antes de que algo lo maneje.


4

La regla de oro que han tratado de cumplir es manejar la excepción lo más cerca posible de la fuente.

Si debe volver a lanzar una excepción, intente agregarla, volver a lanzar una FileNotFoundException no ayuda mucho, pero lanzar una ConfigurationFileNotFoundException permitirá que se capture y se actúe en algún lugar de la cadena.

Otra regla que trato de seguir es no usar try / catch como una forma de flujo de programa, así que verifico archivos / conexiones, me aseguro de que los objetos se hayan iniciado, etc., antes de usarlos. Try / catch debería ser para Excepciones, cosas que no puede controlar.

En cuanto a un bloque catch vacío, si está haciendo algo de importancia en el código que generó la excepción, debe volver a lanzar la excepción como mínimo. Si no hay consecuencias de que el código que lanzó la excepción no se ejecute, ¿por qué lo escribió en primer lugar?


Esta "regla de oro" es, en el mejor de los casos, arbitraria.
Evan Harper

3

puede capturar el evento ThreadException.

  1. Seleccione un proyecto de aplicación de Windows en el Explorador de soluciones.

  2. Abra el archivo Program.cs generado haciendo doble clic en él.

  3. Agregue la siguiente línea de código en la parte superior del archivo de código:

    using System.Threading;
  4. En el método Main (), agregue lo siguiente como primera línea del método:

    Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException);
  5. Agregue lo siguiente debajo del método Main ():

    static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
    {
        // Do logging or whatever here
        Application.Exit();
    }
  6. Agregue código para manejar la excepción no controlada dentro del controlador de eventos. Cualquier excepción que no se maneje en ningún otro lugar de la aplicación se maneja mediante el código anterior. Por lo general, este código debe registrar el error y mostrar un mensaje al usuario.

referencia: https://blogs.msmvps.com/deborahk/global-exception-handler-winforms/


@Kiquenet lo siento, no sé sobre VB
Omid-RH

2

Las excepciones son caras pero necesarias. No necesita envolver todo en una captura de prueba, pero debe asegurarse de que las excepciones siempre se detecten eventualmente. Gran parte dependerá de su diseño.

No vuelvas a lanzar si dejar que la excepción se eleve funcionará igual de bien. Nunca dejes que los errores pasen desapercibidos.

ejemplo:

void Main()
{
  try {
    DoStuff();
  }
  catch(Exception ex) {
    LogStuff(ex.ToString());
  }

void DoStuff() {
... Stuff ...
}

Si DoStuff sale mal, querrás que salga bajo fianza de todos modos. La excepción se lanzará a main y verá el tren de eventos en el seguimiento de pila de ex.


1

¿Cuándo debería volver a lanzar una excepción?

En todas partes, pero con métodos de usuario final ... como controladores de clic de botón

¿Debería intentar tener un mecanismo central de manejo de errores de algún tipo?

Escribo un archivo de registro ... bastante fácil para una aplicación WinForm

¿El manejo de las excepciones que podrían producirse tiene un impacto en el rendimiento en comparación con las pruebas preventivas como si existe un archivo en el disco?

No estoy seguro de esto, pero creo que es una buena práctica hacer excepciones ... Quiero decir, puede preguntar si existe un archivo y si no arroja una FileNotFoundException

¿Debería incluirse todo el código ejecutable en bloques try-catch-finalmente?

yeap

¿Hay ocasiones en las que un bloque de captura vacío podría ser aceptable?

Sí, digamos que quiere mostrar una fecha, pero no tiene idea de cómo se almacenó esa fecha (dd / mm / aaaa, mm / dd / aaaa, etc.), intente analizarla, pero si falla, continúe. . si es irrelevante para usted ... yo diría que sí, hay


1

Lo único que aprendí muy rápidamente fue encerrar absolutamente cada fragmento de código que interactúa con cualquier cosa fuera del flujo de mi programa (es decir, sistema de archivos, llamadas a bases de datos, entrada de usuario) con bloques try-catch. Try-catch puede generar un impacto en el rendimiento, pero generalmente en estos lugares de su código no se notará y se amortizará con seguridad.

He usado catch-blocks vacíos en lugares donde el usuario podría hacer algo que no es realmente "incorrecto", pero puede generar una excepción ... un ejemplo que me viene a la mente es en GridView si el usuario hace doble clic en el marcador de posición gris celda en la parte superior izquierda activará el evento CellDoubleClick, pero la celda no pertenece a una fila. En ese caso, no Realmente necesito publicar un mensaje, pero si no lo capta, arrojará un error de excepción no controlado al usuario.


1

Al volver a lanzar una excepción, la palabra clave se lanza sola. Esto lanzará la excepción detectada y aún podrá usar el seguimiento de la pila para ver de dónde vino.

Try
{
int a = 10 / 0;
}
catch(exception e){
//error logging
throw;
}

hacer esto hará que el seguimiento de la pila termine en la declaración de captura. (evita esto)

catch(Exception e)
// logging
throw e;
}

¿Esto todavía se aplica a las versiones más recientes de .NET Framework y .NET Core?
LuckyLikey

1

En mi experiencia, he considerado apropiado detectar excepciones cuando sé que las voy a crear. Para los casos en que estoy en una aplicación web y estoy haciendo un Response.Redirect, sé que obtendré un System.ThreadAbortException. Como es intencional, solo tengo una captura para el tipo específico y me lo trago.

try
{
/*Doing stuff that may cause an exception*/
Response.Redirect("http:\\www.somewhereelse.com");
}
catch (ThreadAbortException tex){/*Ignore*/}
catch (Exception ex){/*HandleException*/}

1

Estoy profundamente de acuerdo con la regla de:

  • Nunca dejes que los errores pasen desapercibidos.

La razón es que:

  • Cuando escriba el código por primera vez, lo más probable es que no tenga el conocimiento completo del código de terceros, la biblioteca .NET FCL o las últimas contribuciones de sus compañeros de trabajo. En realidad, no puede negarse a escribir código hasta que conozca bien todas las posibilidades de excepción. Entonces
  • Constantemente encuentro que uso try / catch (Exception ex) solo porque quiero protegerme de cosas desconocidas y, como notó, atrapo Exception, no las más específicas como OutOfMemoryException, etc. Y, siempre hago la excepción que se me muestre (o el QA) por ForceAssert.AlwaysAssert (false, ex.ToString ());

ForceAssert.AlwaysAssert es mi forma personal de Trace.Assert independientemente de si la macro DEBUG / TRACE está definida.

El ciclo de desarrollo tal vez: noté el feo diálogo de Assert o alguien más se quejó de él, luego vuelvo al código y descubro la razón para generar la excepción y decidir cómo procesarla.

De esta manera puedo anotar MI código en poco tiempo y protegerme de dominios desconocidos, pero siempre notando si sucedían cosas anormales, de esta manera el sistema se volvió seguro y más seguro.

Sé que muchos de ustedes no estarán de acuerdo conmigo porque un desarrollador debería conocer cada detalle de su código, francamente, también soy un purista en los viejos tiempos. Pero hoy en día aprendí que la política anterior es más pragmática.

Para el código de WinForms, una regla de oro que siempre obedezco es:

  • Siempre intente / capture (excepción) su código de controlador de eventos

esto protegerá su interfaz de usuario siempre que se pueda utilizar.

Para el impacto en el rendimiento, la penalización del rendimiento solo ocurre cuando el código alcanza la captura, la ejecución del código de prueba sin que se produzca la excepción real no tiene un efecto significativo.

La excepción debe ocurrir con pocas posibilidades, de lo contrario, no son excepciones.


-1

Tienes que pensar en el usuario. El bloqueo de la aplicación es el últimocosa que el usuario quiere. Por lo tanto, cualquier operación que pueda fallar debe tener un bloque try catch en el nivel de la interfaz de usuario. No es necesario usar try catch en todos los métodos, pero cada vez que el usuario hace algo, debe poder manejar excepciones genéricas. Eso de ninguna manera te libera de revisar todo para evitar excepciones en el primer caso, pero no existe una aplicación compleja sin errores y el sistema operativo puede agregar fácilmente problemas inesperados, por lo que debes anticipar lo inesperado y asegurarte si un usuario quiere usar uno. operación no habrá pérdida de datos porque la aplicación se bloquea. No hay necesidad de dejar que su aplicación se bloquee, si detecta excepciones, nunca estará en un estado indeterminado y el usuario SIEMPRE se verá afectado por un bloqueo. Incluso si la excepción está en el nivel más alto, no fallar significa que el usuario puede reproducir rápidamente la excepción o al menos registrar el mensaje de error y, por lo tanto, ayudarlo enormemente a solucionar el problema. Ciertamente, mucho más que recibir un simple mensaje de error y luego ver solo el cuadro de diálogo de error de Windows o algo así.

Es por eso que NUNCA debe ser engreído y pensar que su aplicación no tiene errores, eso no está garantizado. Y es un esfuerzo muy pequeño envolver algunos bloques try catch sobre el código apropiado y mostrar un mensaje de error / registrar el error.

Como usuario, ciertamente me cabreo seriamente cada vez que una aplicación de Office o un navegador falla. Si la excepción es tan alta que la aplicación no puede continuar, es mejor mostrar ese mensaje y decirle al usuario qué hacer (reiniciar, corregir algunas configuraciones del sistema operativo, informar el error, etc.) que simplemente fallar y eso es todo.


¿Puedes intentar escribir respuestas menos como una perorata y, en cambio, tratar de ser más específico y probar tu punto? También eche un vistazo a Cómo responder .
LuckyLikey
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.