Buggy C# Code: Los 10 errores más comunes en la programación de C#
Publicado: 2022-03-11Acerca de C Sharp
C# es uno de varios lenguajes que apuntan a Microsoft Common Language Runtime (CLR). Los lenguajes que apuntan a CLR se benefician de características como la integración entre lenguajes y el manejo de excepciones, seguridad mejorada, un modelo simplificado para la interacción de componentes y servicios de depuración y generación de perfiles. De los lenguajes CLR actuales, C# es el más utilizado para proyectos de desarrollo profesional y complejos destinados a entornos de escritorio, móviles o servidores de Windows.
C# es un lenguaje fuertemente tipado orientado a objetos. La estricta verificación de tipos en C#, tanto en tiempo de compilación como de ejecución, da como resultado que la mayoría de los errores típicos de programación de C# se notifiquen lo antes posible y que sus ubicaciones se identifiquen con bastante precisión. Esto puede ahorrar mucho tiempo en la programación de C Sharp, en comparación con rastrear la causa de los errores desconcertantes que pueden ocurrir mucho después de que la operación infractora tenga lugar en lenguajes que son más liberales con la aplicación de la seguridad de tipo. Sin embargo, muchos codificadores de C#, sin saberlo (o por descuido), desechan los beneficios de esta detección, lo que lleva a algunos de los problemas discutidos en este tutorial de C#.
Acerca de este tutorial de programación de C Sharp
Este tutorial describe 10 de los errores de programación de C# más comunes que cometen los programadores de C#, o los problemas que deben evitar, y les brinda ayuda.
Si bien la mayoría de los errores discutidos en este artículo son específicos de C#, algunos también son relevantes para otros lenguajes que tienen como objetivo CLR o hacen uso de la biblioteca de clases de Framework (FCL).
Error común de programación en C# n.º 1: Usar una referencia como un valor o viceversa
Los programadores de C++ y muchos otros lenguajes están acostumbrados a controlar si los valores que asignan a las variables son simplemente valores o son referencias a objetos existentes. En la programación de C Sharp, sin embargo, esa decisión la toma el programador que escribió el objeto, no el programador que instancia el objeto y lo asigna a una variable. Este es un problema común para aquellos que intentan aprender a programar en C#.
Si no sabe si el objeto que está utilizando es un tipo de valor o un tipo de referencia, podría encontrarse con algunas sorpresas. Por ejemplo:
Point point1 = new Point(20, 30); Point point2 = point1; point2.X = 50; Console.WriteLine(point1.X); // 20 (does this surprise you?) Console.WriteLine(point2.X); // 50 Pen pen1 = new Pen(Color.Black); Pen pen2 = pen1; pen2.Color = Color.Blue; Console.WriteLine(pen1.Color); // Blue (or does this surprise you?) Console.WriteLine(pen2.Color); // Blue
Como puede ver, los objetos Point
y Pen
se crearon exactamente de la misma manera, pero el valor de point1
permaneció sin cambios cuando se asignó un nuevo valor de coordenada X
a point2
, mientras que el valor de pen1
se modificó cuando se asignó un nuevo color a pen2
. Por lo tanto, podemos deducir que point1
y point2
contienen cada uno su propia copia de un objeto Point
, mientras que pen1
y pen2
contienen referencias al mismo objeto Pen
. Pero, ¿cómo podemos saber eso sin hacer este experimento?
La respuesta es mirar las definiciones de los tipos de objeto (que puede hacer fácilmente en Visual Studio colocando el cursor sobre el nombre del tipo de objeto y presionando F12):
public struct Point { ... } // defines a “value” type public class Pen { ... } // defines a “reference” type
Como se muestra arriba, en la programación de C#, la palabra clave struct
se usa para definir un tipo de valor, mientras que la palabra clave class
se usa para definir un tipo de referencia. Para aquellos con experiencia en C++, que se sintieron engañados por una falsa sensación de seguridad por las muchas similitudes entre las palabras clave de C++ y C#, este comportamiento probablemente sea una sorpresa que puede hacer que solicite ayuda de un tutorial de C#.
Si va a depender de algún comportamiento que difiera entre los tipos de valor y referencia, como la capacidad de pasar un objeto como un parámetro de método y hacer que ese método cambie el estado del objeto, asegúrese de que está tratando con el tipo correcto de objeto para evitar problemas de programación en C#.
Error común de programación en C# n.º 2: no entender los valores predeterminados de las variables no inicializadas
En C#, los tipos de valor no pueden ser nulos. Por definición, los tipos de valor tienen un valor, e incluso las variables no inicializadas de tipos de valor deben tener un valor. Esto se llama el valor predeterminado para ese tipo. Esto conduce al siguiente resultado, generalmente inesperado, al verificar si una variable no está inicializada:
class Program { static Point point1; static Pen pen1; static void Main(string[] args) { Console.WriteLine(pen1 == null); // True Console.WriteLine(point1 == null); // False (huh?) } }
¿Por qué point1
no es nulo? La respuesta es que Point
es un tipo de valor y el valor predeterminado para Point
es (0,0), no nulo. No reconocer esto es un error muy fácil (y común) de cometer en C#.
Muchos tipos de valor (pero no todos) tienen una propiedad IsEmpty
que puede verificar para ver si es igual a su valor predeterminado:
Console.WriteLine(point1.IsEmpty); // True
Cuando verifique si una variable se ha inicializado o no, asegúrese de saber qué valor tendrá una variable no inicializada de ese tipo de forma predeterminada y no confíe en que sea nula.
Error común de programación en C# n.º 3: uso de métodos de comparación de cadenas inadecuados o no especificados
Hay muchas formas diferentes de comparar cadenas en C#.
Aunque muchos programadores usan el operador ==
para la comparación de cadenas, en realidad es uno de los métodos menos deseables, principalmente porque no especifica explícitamente en el código qué tipo de comparación se desea.
Más bien, la forma preferida de probar la igualdad de cadenas en la programación de C# es con el método Equals
:
public bool Equals(string value); public bool Equals(string value, StringComparison comparisonType);
La firma del primer método (es decir, sin el parámetro tipo de comparisonType
) es en realidad lo mismo que usar el operador ==
, pero tiene la ventaja de que se aplica explícitamente a las cadenas. Realiza una comparación ordinal de las cadenas, que es básicamente una comparación byte por byte. En muchos casos, este es exactamente el tipo de comparación que desea, especialmente cuando compara cadenas cuyos valores se establecen mediante programación, como nombres de archivo, variables de entorno, atributos, etc. En estos casos, siempre que una comparación ordinal sea del tipo correcto de comparación para esa situación, el único inconveniente de usar el método Equals
sin un tipo de comparisonType
es que alguien que lea el código puede no saber qué tipo de comparación está haciendo.
Sin embargo, el uso de la firma del método Equals
que incluye un tipo de comparisonType
cada vez que compara cadenas no solo hará que su código sea más claro, sino que le hará pensar explícitamente qué tipo de comparación necesita hacer. Esto es algo que vale la pena hacer, porque incluso si el inglés no proporciona muchas diferencias entre las comparaciones ordinales y sensibles a la cultura, otros idiomas proporcionan muchas, e ignorar la posibilidad de otros idiomas te abre a un gran potencial para errores en el camino. Por ejemplo:
string s = "strasse"; // outputs False: Console.WriteLine(s == "straße"); Console.WriteLine(s.Equals("straße")); Console.WriteLine(s.Equals("straße", StringComparison.Ordinal)); Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCulture)); Console.WriteLine(s.Equals("straße", StringComparison.OrdinalIgnoreCase)); // outputs True: Console.WriteLine(s.Equals("straße", StringComparison.CurrentCulture)); Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCultureIgnoreCase));
La práctica más segura es proporcionar siempre un parámetro de tipo de comparisonType
al método Equals
. Aquí hay algunas pautas básicas:
- Al comparar cadenas que ingresó el usuario o que se mostrarán al usuario, use una comparación que tenga en cuenta la cultura (
CurrentCulture
oCurrentCultureIgnoreCase
). - Al comparar cadenas programáticas, utilice la comparación ordinal (
Ordinal
uOrdinalIgnoreCase
). -
InvariantCulture
eInvariantCultureIgnoreCase
generalmente no se deben usar, excepto en circunstancias muy limitadas, porque las comparaciones ordinales son más eficientes. Si es necesaria una comparación consciente de la cultura, por lo general se debe realizar con la cultura actual u otra cultura específica.
Además del método Equals
, las cadenas también proporcionan el método Compare
, que brinda información sobre el orden relativo de las cadenas en lugar de solo una prueba de igualdad. Este método es preferible a los operadores <
, <=
, >
y >=
, por las mismas razones que se mencionaron anteriormente: para evitar problemas de C#.
Error común de programación en C# n.° 4: usar declaraciones iterativas (en lugar de declarativas) para manipular colecciones
En C# 3.0, la adición de Language-Integrated Query (LINQ) al lenguaje cambió para siempre la forma en que se consultan y manipulan las colecciones. Desde entonces, si usa declaraciones iterativas para manipular colecciones, no usó LINQ cuando probablemente debería haberlo hecho.
Algunos programadores de C# ni siquiera conocen la existencia de LINQ, pero afortunadamente ese número es cada vez más pequeño. Muchos todavía piensan, sin embargo, que debido a la similitud entre las palabras clave de LINQ y las declaraciones de SQL, su único uso es en código que consulta bases de datos.
Si bien la consulta de bases de datos es un uso muy frecuente de las instrucciones LINQ, en realidad funcionan sobre cualquier colección enumerable (es decir, cualquier objeto que implemente la interfaz IEnumerable). Entonces, por ejemplo, si tuviera una matriz de Cuentas, en lugar de escribir una Lista de C# para cada una:
decimal total = 0; foreach (Account account in myAccounts) { if (account.Status == "active") { total += account.Balance; } }
podrías simplemente escribir:
decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();
Si bien este es un ejemplo bastante simple de cómo evitar este problema común de programación en C#, hay casos en los que una sola declaración LINQ puede reemplazar fácilmente docenas de declaraciones en un bucle iterativo (o bucles anidados) en su código. Y menos código general significa menos oportunidades para que se introduzcan errores. Sin embargo, tenga en cuenta que puede haber una compensación en términos de rendimiento. En escenarios críticos para el rendimiento, especialmente donde su código iterativo puede hacer suposiciones sobre su colección que LINQ no puede, asegúrese de hacer una comparación de rendimiento entre los dos métodos.
Error común de programación en C# n.º 5: no tener en cuenta los objetos subyacentes en una instrucción LINQ
LINQ es excelente para abstraer la tarea de manipular colecciones, ya sean objetos en memoria, tablas de bases de datos o documentos XML. En un mundo perfecto, no necesitarías saber cuáles son los objetos subyacentes. Pero el error aquí es suponer que vivimos en un mundo perfecto. De hecho, las declaraciones LINQ idénticas pueden devolver resultados diferentes cuando se ejecutan exactamente en los mismos datos, si esos datos están en un formato diferente.
Por ejemplo, considere la siguiente declaración:
decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();
¿Qué sucede si uno de los estados de la account.Status
del objeto es igual a “Activo” (tenga en cuenta la A mayúscula)? Bueno, si myAccounts
fuera un objeto DbSet
(que se configuró con la configuración predeterminada que no distingue entre mayúsculas y minúsculas), la expresión where
aún coincidiría con ese elemento. Sin embargo, si myAccounts
estuviera en una matriz en memoria, no coincidiría y, por lo tanto, arrojaría un resultado diferente para el total.
Pero espera un minuto. Cuando hablamos sobre la comparación de cadenas anteriormente, vimos que el operador ==
realizaba una comparación ordinal de cadenas. Entonces, ¿por qué en este caso el operador ==
realiza una comparación que no distingue entre mayúsculas y minúsculas?
La respuesta es que cuando los objetos subyacentes en una declaración LINQ son referencias a los datos de la tabla SQL (como es el caso del objeto Entity Framework DbSet en este ejemplo), la declaración se convierte en una declaración T-SQL. Luego, los operadores siguen las reglas de programación de T-SQL, no las reglas de programación de C#, por lo que la comparación en el caso anterior no distingue entre mayúsculas y minúsculas.
En general, aunque LINQ es una forma útil y coherente de consultar colecciones de objetos, en realidad todavía necesita saber si su declaración se traducirá o no a algo que no sea C# para asegurarse de que el comportamiento de su código ser como se esperaba en tiempo de ejecución.
Error común de programación en C# n.º 6: confundirse o falsificarse con los métodos de extensión
Como se mencionó anteriormente, las instrucciones LINQ funcionan en cualquier objeto que implemente IEnumerable. Por ejemplo, la siguiente función simple sumará los saldos de cualquier colección de cuentas:
public decimal SumAccounts(IEnumerable<Account> myAccounts) { return myAccounts.Sum(a => a.Balance); }
En el código anterior, el tipo del parámetro myAccounts se declara como IEnumerable<Account>
. Dado que myAccounts
hace referencia a un método Sum
(C# usa la conocida "notación de puntos" para hacer referencia a un método en una clase o interfaz), esperaríamos ver un método llamado Sum()
en la definición de la IEnumerable<T>
. Sin embargo, la definición de IEnumerable<T>
no hace referencia a ningún método Sum
y simplemente se ve así:
public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); }
Entonces, ¿dónde está definido el método Sum()
? C# está fuertemente tipado, por lo que si la referencia al método Sum
no es válida, el compilador de C# seguramente lo marcará como un error. Por lo tanto, sabemos que debe existir, pero ¿dónde? Además, ¿dónde están las definiciones de todos los demás métodos que proporciona LINQ para consultar o agregar estas colecciones?
La respuesta es que Sum()
no es un método definido en la interfaz IEnumerable
. Más bien, es un método estático (llamado "método de extensión") que se define en la clase System.Linq.Enumerable
:
namespace System.Linq { public static class Enumerable { ... // the reference here to “this IEnumerable<TSource> source” is // the magic sauce that provides access to the extension method Sum public static decimal Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal> selector); ... } }
Entonces, ¿qué hace que un método de extensión sea diferente de cualquier otro método estático y qué nos permite acceder a él en otras clases?
La característica distintiva de un método de extensión es el modificador this
en su primer parámetro. Esta es la "magia" que lo identifica ante el compilador como un método de extensión. El tipo del parámetro que modifica (en este caso IEnumerable<TSource>
) denota la clase o interfaz que aparecerá para implementar este método.
(Como punto adicional, no hay nada mágico en la similitud entre el nombre de la interfaz IEnumerable
y el nombre de la clase Enumerable
en la que se define el método de extensión. Esta similitud es solo una elección estilística arbitraria).
Con este entendimiento, también podemos ver que la función sumAccounts
que presentamos anteriormente podría haberse implementado de la siguiente manera:

public decimal SumAccounts(IEnumerable<Account> myAccounts) { return Enumerable.Sum(myAccounts, a => a.Balance); }
El hecho de que pudiéramos haberlo implementado de esta manera plantea la pregunta de ¿por qué tener métodos de extensión? Los métodos de extensión son esencialmente una conveniencia del lenguaje de programación C# que le permite "agregar" métodos a los tipos existentes sin crear un nuevo tipo derivado, volver a compilar o modificar el tipo original.
Los métodos de extensión se incorporan al ámbito al incluir un using [namespace];
declaración en la parte superior del archivo. Debe saber qué espacio de nombres de C# incluye los métodos de extensión que está buscando, pero eso es bastante fácil de determinar una vez que sabe qué es lo que está buscando.
Cuando el compilador de C# encuentra una llamada de método en una instancia de un objeto y no encuentra ese método definido en la clase de objeto a la que se hace referencia, busca todos los métodos de extensión que están dentro del alcance para intentar encontrar uno que coincida con el método requerido. firma y clase. Si encuentra uno, pasará la referencia de la instancia como primer argumento a ese método de extensión, luego el resto de los argumentos, si los hay, se pasarán como argumentos subsiguientes al método de extensión. (Si el compilador de C# no encuentra ningún método de extensión correspondiente dentro del alcance, generará un error).
Los métodos de extensión son un ejemplo de "azúcar sintáctico" por parte del compilador de C#, que nos permite escribir código que (generalmente) es más claro y fácil de mantener. Más claro, es decir, si está al tanto de su uso. De lo contrario, puede ser un poco confuso, especialmente al principio.
Aunque ciertamente hay ventajas en el uso de métodos de extensión, pueden causar problemas y un grito de ayuda de programación C# para aquellos desarrolladores que no los conocen o no los entienden correctamente. Esto es especialmente cierto cuando se buscan ejemplos de código en línea o cualquier otro código preescrito. Cuando dicho código produce errores de compilación (porque invoca métodos que claramente no están definidos en las clases en las que se invocan), la tendencia es pensar que el código se aplica a una versión diferente de la biblioteca, o a una biblioteca completamente diferente. Se puede pasar mucho tiempo buscando una nueva versión, o una “biblioteca faltante” fantasma, que no existe.
Incluso los desarrolladores que están familiarizados con los métodos de extensión todavía quedan atrapados ocasionalmente, cuando hay un método con el mismo nombre en el objeto, pero la firma del método difiere sutilmente de la del método de extensión. Se puede perder mucho tiempo buscando un error tipográfico o un error que simplemente no está ahí.
El uso de métodos de extensión en las bibliotecas de C# es cada vez más frecuente. Además de LINQ, Unity Application Block y Web API framework son ejemplos de dos bibliotecas modernas muy utilizadas por Microsoft que también utilizan métodos de extensión, y hay muchos otros. Cuanto más moderno sea el marco, más probable es que incorpore métodos de extensión.
Por supuesto, también puede escribir sus propios métodos de extensión. Tenga en cuenta, sin embargo, que si bien los métodos de extensión parecen invocarse como métodos de instancia regulares, en realidad esto es solo una ilusión. En particular, sus métodos de extensión no pueden hacer referencia a miembros privados o protegidos de la clase que están extendiendo y, por lo tanto, no pueden servir como un reemplazo completo para la herencia de clases más tradicional.
Error común de programación en C# n.º 7: usar el tipo de colección incorrecto para la tarea en cuestión
C# proporciona una gran variedad de objetos de colección, siendo la siguiente solo una lista parcial:
Array
, ArrayList
, BitArray
, BitVector32
, Dictionary<K,V>
, HashTable
, HybridDictionary
, List<T>
, NameValueCollection
, OrderedDictionary
, Queue, Queue<T>
, SortedList
, Stack, Stack<T>
, StringCollection
, StringDictionary
.
Si bien puede haber casos en los que demasiadas opciones sean tan malas como no tener suficientes opciones, ese no es el caso con los objetos de colección. La cantidad de opciones disponibles definitivamente puede funcionar a su favor. Tómese un poco más de tiempo por adelantado para investigar y elegir el tipo de colección óptimo para su propósito. Es probable que resulte en un mejor rendimiento y menos margen de error.
Si hay un tipo de colección dirigido específicamente al tipo de elemento que tiene (como una cadena o un bit), prepárese para usar ese primero. La implementación es generalmente más eficiente cuando está dirigida a un tipo específico de elemento.
Para aprovechar la seguridad de tipos de C#, normalmente debería preferir una interfaz genérica a una no genérica. Los elementos de una interfaz genérica son del tipo que especificas cuando declaras tu objeto, mientras que los elementos de las interfaces no genéricas son del tipo objeto. Cuando se usa una interfaz no genérica, el compilador de C# no puede verificar el tipo de su código. Además, cuando se trata de colecciones de tipos de valores primitivos, el uso de una colección no genérica dará como resultado el empaquetado/desempaquetado repetido de esos tipos, lo que puede tener un impacto negativo significativo en el rendimiento en comparación con una colección genérica del tipo apropiado.
Otro problema común de C# es escribir su propio objeto de colección. Eso no quiere decir que nunca sea apropiado, pero con una selección tan completa como la que ofrece .NET, probablemente pueda ahorrar mucho tiempo usando o ampliando uno que ya existe, en lugar de reinventar la rueda. En particular, la biblioteca de colecciones genéricas de C5 para C# y CLI ofrece una amplia gama de colecciones adicionales listas para usar, como estructuras de datos de árbol persistentes, colas de prioridad basadas en pilas, listas de matrices indexadas hash, listas vinculadas y mucho más.
Error común de programación en C# n.° 8: Descuidar la liberación de recursos
El entorno CLR emplea un recolector de elementos no utilizados, por lo que no es necesario liberar explícitamente la memoria creada para ningún objeto. De hecho, no puedes. No existe un equivalente del operador de delete
de C++ o la función free()
en C . Pero eso no significa que pueda olvidarse de todos los objetos una vez que haya terminado de usarlos. Muchos tipos de objetos encapsulan algún otro tipo de recurso del sistema (por ejemplo, un archivo de disco, una conexión a una base de datos, un socket de red, etc.). Dejar estos recursos abiertos puede agotar rápidamente la cantidad total de recursos del sistema, degradando el rendimiento y, en última instancia, provocando fallas en el programa.
Si bien se puede definir un método destructor en cualquier clase de C#, el problema con los destructores (también llamados finalizadores en C#) es que no se puede saber con seguridad cuándo se llamarán. Son llamados por el recolector de basura (en un subproceso separado, lo que puede causar complicaciones adicionales) en un momento indeterminado en el futuro. Intentar eludir estas limitaciones forzando la recolección de elementos no utilizados con GC.Collect()
no es una práctica recomendada de C#, ya que bloqueará el subproceso durante un período de tiempo desconocido mientras recopila todos los objetos elegibles para la recolección.
Esto no quiere decir que no haya buenos usos para los finalizadores, pero liberar recursos de forma determinista no es uno de ellos. Más bien, cuando está operando en una conexión de archivo, red o base de datos, desea liberar explícitamente el recurso subyacente tan pronto como termine con él.
Las fugas de recursos son una preocupación en casi cualquier entorno. Sin embargo, C# proporciona un mecanismo sólido y fácil de usar que, si se utiliza, puede hacer que las fugas ocurran con menos frecuencia. El marco .NET define la interfaz IDisposable
, que consta únicamente del método Dispose()
. Cualquier objeto que implemente IDisposable
espera que se llame a ese método cada vez que el consumidor del objeto termine de manipularlo. Esto da como resultado una liberación de recursos explícita y determinista.
Si está creando y desechando un objeto dentro del contexto de un solo bloque de código, es básicamente imperdonable olvidarse de llamar a Dispose()
, porque C# proporciona una declaración de using
que garantizará que se llame a Dispose( Dispose()
sin importar cómo bloquee el código. se sale (ya sea una excepción, una declaración de retorno o simplemente el cierre del bloque). Y sí, esa es la misma declaración de using
mencionada anteriormente que se usa para incluir espacios de nombres de C# en la parte superior de su archivo. Tiene un segundo propósito, completamente ajeno, que muchos desarrolladores de C# desconocen; es decir, para garantizar que se llame a Dispose()
en un objeto cuando se sale del bloque de código:
using (FileStream myFile = File.OpenRead("foo.txt")) { myFile.Read(buffer, 0, 100); }
Al crear un bloque de using
en el ejemplo anterior, sabe con certeza que se llamará a myFile.Dispose()
tan pronto como termine con el archivo, ya sea que Read()
arroje una excepción o no.
Error común de programación en C# n.º 9: rehuir las excepciones
C# continúa aplicando la seguridad de tipos en tiempo de ejecución. Esto le permite identificar muchos tipos de errores en C# mucho más rápido que en lenguajes como C++, donde las conversiones de tipos defectuosas pueden dar como resultado que se asignen valores arbitrarios a los campos de un objeto. Sin embargo, una vez más, los programadores pueden desperdiciar esta gran función, lo que genera problemas en C#. Caen en esta trampa porque C# proporciona dos formas diferentes de hacer las cosas, una que puede generar una excepción y otra que no. Algunos se alejarán de la ruta de excepción, pensando que no tener que escribir un bloque try/catch les ahorra algo de codificación.
Por ejemplo, aquí hay dos formas diferentes de realizar una conversión de tipos explícita en C#:
// METHOD 1: // Throws an exception if account can't be cast to SavingsAccount SavingsAccount savingsAccount = (SavingsAccount)account; // METHOD 2: // Does NOT throw an exception if account can't be cast to // SavingsAccount; will just set savingsAccount to null instead SavingsAccount savingsAccount = account as SavingsAccount;
El error más obvio que podría ocurrir con el uso del Método 2 sería no verificar el valor de retorno. Eso probablemente daría como resultado una NullReferenceException eventual, que posiblemente podría surgir mucho más tarde, lo que dificultaría mucho rastrear el origen del problema. Por el contrario, el Método 1 habría arrojado inmediatamente una InvalidCastException
, lo que haría que el origen del problema fuera mucho más obvio de inmediato.
Además, incluso si recuerda verificar el valor de retorno en el Método 2, ¿qué hará si encuentra que es nulo? ¿El método que está escribiendo es un lugar apropiado para informar un error? ¿Hay algo más que puedas probar si ese lanzamiento falla? De lo contrario, lanzar una excepción es lo correcto, por lo que también puede dejar que ocurra lo más cerca posible de la fuente del problema.
Aquí hay un par de ejemplos de otros pares de métodos comunes donde uno lanza una excepción y el otro no:
int.Parse(); // throws exception if argument can't be parsed int.TryParse(); // returns a bool to denote whether parse succeeded IEnumerable.First(); // throws exception if sequence is empty IEnumerable.FirstOrDefault(); // returns null/default value if sequence is empty
Algunos desarrolladores de C# son tan "adversos a las excepciones" que asumen automáticamente que el método que no arroja una excepción es superior. Si bien hay ciertos casos selectos en los que esto puede ser cierto, no es del todo correcto como generalización.
Como ejemplo específico, en un caso en el que tenga una acción alternativa legítima (por ejemplo, predeterminada) para tomar si se hubiera generado una excepción, entonces el enfoque de no excepción podría ser una opción legítima. En tal caso, puede ser mejor escribir algo como esto:
if (int.TryParse(myString, out myInt)) { // use myInt } else { // use default value }
en lugar de:
try { myInt = int.Parse(myString); // use myInt } catch (FormatException) { // use default value }
Sin embargo, es incorrecto suponer que TryParse
es necesariamente el método "mejor". A veces ese es el caso, a veces no lo es. Por eso hay dos formas de hacerlo. Utilice el correcto para el contexto en el que se encuentra, recordando que las excepciones sin duda pueden ser su amigo como desarrollador.
Error común de programación en C# n.º 10: Permitir que se acumulen advertencias del compilador
Si bien este problema definitivamente no es específico de C#, es particularmente grave en la programación de C#, ya que abandona los beneficios de la estricta verificación de tipos que ofrece el compilador de C#.
Las advertencias se generan por una razón. Si bien todos los errores del compilador de C# significan un defecto en su código, muchas advertencias también lo hacen. Lo que diferencia a los dos es que, en el caso de una advertencia, el compilador no tiene problemas para emitir las instrucciones que representa su código. Aun así, encuentra su código un poco sospechoso y existe una probabilidad razonable de que su código no refleje con precisión su intención.
Un ejemplo simple y común por el bien de este tutorial de programación en C# es cuando modifica su algoritmo para eliminar el uso de una variable que estaba usando, pero olvida eliminar la declaración de la variable. El programa funcionará perfectamente, pero el compilador marcará la declaración de variable inútil. El hecho de que el programa funcione perfectamente hace que los programadores descuiden corregir la causa de la advertencia. Además, los codificadores aprovechan una función de Visual Studio que les facilita ocultar las advertencias en la ventana "Lista de errores" para que puedan concentrarse solo en los errores. No pasa mucho tiempo hasta que hay docenas de advertencias, todas ellas felizmente ignoradas (o peor aún, ocultas).
Pero si ignora este tipo de advertencia, tarde o temprano, es muy posible que algo como esto llegue a su código:
class Account { int myId; int Id; // compiler warned you about this, but you didn't listen! // Constructor Account(int id) { this.myId = Id; // OOPS! } }
Y a la velocidad que Intellisense nos permite escribir código, este error no es tan improbable como parece.
Ahora tiene un error grave en su programa (aunque el compilador solo lo ha marcado como una advertencia, por las razones ya explicadas), y dependiendo de cuán complejo sea su programa, podría perder mucho tiempo localizándolo. Si hubiera prestado atención a esta advertencia en primer lugar, habría evitado este problema con una simple solución de cinco segundos.
Recuerde, el compilador de C Sharp le brinda mucha información útil sobre la solidez de su código... si está escuchando. No ignore las advertencias. Por lo general, solo demoran unos segundos en repararse, y reparar los nuevos cuando suceden puede ahorrarle horas. Prepárese para esperar que la ventana "Lista de errores" de Visual Studio muestre "0 errores, 0 advertencias", de modo que cualquier advertencia lo haga sentir lo suficientemente incómodo como para abordarla de inmediato.
Por supuesto, hay excepciones para cada regla. En consecuencia, puede haber ocasiones en las que el código parezca un poco sospechoso para el compilador, aunque sea exactamente como lo pretendías. En esos casos muy raros, use #pragma warning disable [warning id]
solo alrededor del código que activa la advertencia, y solo para la ID de advertencia que activa. Esto suprimirá esa advertencia, y solo esa advertencia, para que aún pueda estar alerta a las nuevas.
Envolver
C# es un lenguaje poderoso y flexible con muchos mecanismos y paradigmas que pueden mejorar enormemente la productividad. Sin embargo, al igual que con cualquier herramienta de software o lenguaje, tener una comprensión o apreciación limitada de sus capacidades a veces puede ser más un impedimento que un beneficio, lo que lo deja a uno en el estado proverbial de "saber lo suficiente como para ser peligroso".
El uso de un tutorial de C Sharp como este para familiarizarse con los matices clave de C#, como (pero de ninguna manera limitados a) los problemas planteados en este artículo, ayudará en la optimización de C# y evitará algunos de los errores más comunes de la idioma.
Lecturas adicionales en el blog de ingeniería de Toptal:
- Preguntas esenciales de la entrevista de C#
- C# frente a C++: ¿Qué hay en el núcleo?