¡Hola a todos! Hay mucha información sobre las diferentes funciones de C#. Sobre varios trucos y mejores prácticas en este idioma.
Quiero contarte algunos consejos igualmente útiles, pero menos populares, para trabajar con este lenguaje.
Devolver nulo desde un método Task/Task<T> no asíncrono provocará una NullReferenceException en tiempo de ejecución. Este problema se puede evitar devolviendo Task.FromResult<T>(null) en su lugar.
Mal ejemplo:
public Task< object > GetFooAsync ( ) { return null ; // Noncompliant }
Buen ejemplo:
public Task< object > GetFooAsync ( ) { return Task.FromResult< object >( null ); }
StringBuilder es más eficiente que la concatenación de cadenas, especialmente cuando el operador se repite una y otra vez como en bucles.
Mal ejemplo:
string str = "" ; for ( int i = 0 ; i < arrayOfStrings.Length ; ++i) { str = str + arrayOfStrings[i]; }
Buen ejemplo:
StringBuilder bld = new StringBuilder(); for ( int i = 0 ; i < arrayOfStrings.Length; ++i) { bld.Append(arrayOfStrings[i]); } string str = bld.ToString();
La búsqueda de una subcadena determinada a partir de un desplazamiento específico se puede lograr mediante dicho código: str.Substring(startIndex).IndexOf(char1). Esto funciona bien, pero crea una nueva cadena para cada llamada al método Substring. Cuando esto se hace en un bucle, se crean muchas cadenas de forma gratuita, lo que puede provocar problemas de rendimiento si str es grande.
Para evitar problemas de rendimiento, string.Substring(startIndex) no debe encadenarse con los siguientes métodos:
Para cada uno de estos métodos, hay disponible otro método con un parámetro adicional para especificar un desplazamiento.
El uso de estos métodos da el mismo resultado y evita la creación de instancias de String adicionales.
Mal ejemplo:
str.Substring(StartIndex).IndexOf(char1); // Noncompliant; a new string is going to be created by "Substring"
Buen ejemplo:
str.IndexOf(char1, startIndex);
Pasar una colección como argumento al propio método de la colección es un error (se pretendía algún otro argumento) o simplemente un código sin sentido.
Además, dado que algunos métodos requieren que el argumento no se modifique durante la ejecución, pasar una colección a sí misma puede provocar un comportamiento inesperado.
Malos ejemplos:
var list = new List< int >(); list.AddRange(list); // Noncompliant list.Concat(list); // Noncompliant list.Union(list); // Noncompliant; always returns list list.Except(list); // Noncompliant; always empty list.Intersect(list); // Noncompliant; always list list.SequenceEqual(list); // Noncompliant; always true var set = new HashSet< int >(); set .UnionWith( set ); // Noncompliant; no changes set .ExceptWith( set ); // Noncompliant; always empty set .IntersectWith( set ); // Noncompliant; no changes set .IsProperSubsetOf( set ); // Noncompliant; always false set .IsProperSupersetOf( set ); // Noncompliant; always false set .IsSubsetOf( set ); // Noncompliant; always true set .IsSupersetOf( set ); // Noncompliant; always true set .Overlaps( set ); // Noncompliant; always true set .SetEquals( set ); // Noncompliant; always true set .SymmetricExceptWith( set ); // Noncompliant; always empty
Devolver nulo en lugar de una matriz o colección real obliga a las personas que llaman al método a probar explícitamente la nulidad, haciéndolos más complejos y menos legibles.
Además, en muchos casos, null se usa como sinónimo de vacío.
Malos ejemplos:
public Result[] GetResults ( ) { return null ; // Noncompliant } public IEnumerable<Result> GetResults ( ) { return null ; // Noncompliant }
Buenos ejemplos:
public Result[] GetResults ( ) { return new Result[ 0 ]; } public IEnumerable<Result> GetResults ( ) { return Enumerable.Empty<Result>(); }
Cuando la división se realiza en enteros, el resultado siempre será un entero. Puede asignar ese resultado a un doble, flotante o decimal con conversión de tipo automática, pero al comenzar como un int, es probable que el resultado no sea el esperado. Si el resultado de la división int se asigna a una variable de coma flotante, se habrá perdido la precisión antes de la asignación. En su lugar, al menos un operando debe convertirse o promoverse al tipo final antes de que se lleve a cabo la operación.
Ejemplos:
decimal dec = 3 / 2 ; // Noncompliant decimal dec = ( decimal ) 3 / 2 ;
Los recursos compartidos no deben usarse para el bloqueo, ya que aumenta la posibilidad de interbloqueos. Cualquier otro subproceso podría adquirir (o intentar adquirir) el mismo bloqueo para otro propósito no relacionado.
En su lugar, se debe usar una instancia de objeto dedicada para cada recurso compartido, para evitar interbloqueos o bloqueos de contención.
Los siguientes objetos se consideran recursos compartidos:
Un subproceso que adquiere un bloqueo en un objeto al que se puede acceder a través de los límites del dominio de la aplicación corre el riesgo de ser bloqueado por otro subproceso en un dominio de aplicación diferente. Se dice que los objetos a los que se puede acceder a través de los límites del dominio de la aplicación tienen una identidad débil. Los tipos con identidad débil son:
Thread.Suspend y Thread.Resume pueden dar resultados impredecibles, y ambos métodos han quedado obsoletos. De hecho, si Thread.Suspend no se usa con mucho cuidado, un subproceso puede suspenderse mientras se mantiene un candado, lo que lleva a un interbloqueo. Se deben utilizar otros mecanismos de sincronización más seguros, como Monitor, Mutex y Semaphore.
Al volver a lanzar una excepción, debe hacerlo simplemente llamando a throw; y no throw exc;, porque el seguimiento de la pila se restablece con la segunda sintaxis, lo que dificulta mucho la depuración.
Ejemplos:
try {} catch (ExceptionType1 exc) { Console.WriteLine(exc); throw exc; // Noncompliant; stacktrace is reset } catch (ExceptionType2 exc) { Console.WriteLine(exc); throw ; // Compliant } catch (ExceptionType3 exc) { throw new Exception( "My custom message" , exc); // Compliant; stack trace preserved }
Se espera que algunos métodos se llamen con precaución, pero se espera que otros, como ToString, "simplemente funcionen". Lanzar una excepción de dicho método es probable que rompa el código de las personas que llaman de forma inesperada.
El problema ocurre cuando se lanza una excepción de cualquiera de los siguientes:
Mal ejemplo:
public override string ToString ( ) { if ( string .IsNullOrEmpty(Name)) { throw new ArgumentException( "..." ); // Noncompliant } }
Lanzar excepciones generales como Exception, SystemException, ApplicationException, IndexOutOfRangeException, NullReferenceException, OutOfMemoryException y ExecutionEngineException evita que los métodos de llamada manejen excepciones verdaderas generadas por el sistema de manera diferente a los errores generados por la aplicación.
Lanzar una excepción desde dentro de un bloque finalmente enmascarará cualquier excepción que se haya lanzado previamente en el bloque try o catch, y el mensaje de excepción enmascarado y el seguimiento de la pila se perderán.
El objetivo de tener tipos de excepción personalizados es transmitir más información de la que está disponible en los tipos estándar. Pero los tipos de excepción personalizados deben ser públicos para que funcionen.
Si un método arroja una excepción no pública, lo mejor que puede hacer en el lado de la persona que llama es capturar la base pública más cercana de la clase. Es decir, pierde toda la información personalizada que creó para pasar el tipo de excepción.
Si Finalize o una anulación de Finalize genera una excepción, y el tiempo de ejecución no está alojado en una aplicación que anula la política predeterminada, el tiempo de ejecución finaliza el proceso inmediatamente sin una limpieza correcta (finalmente, los bloques y los finalizadores no se ejecutan). Este comportamiento garantiza la integridad del proceso si el finalizador no puede liberar o destruir recursos.
Mal ejemplo:
class MyClass { ~MyClass() { throw new NotImplementedException(); // Noncompliant } }
Por lo general, querrá usar using para crear una variable IDisposable local; activará la eliminación del objeto cuando el control pase fuera del alcance del bloque. La excepción a esta regla es cuando su método devuelve ese IDisposable. En ese caso, el uso de desecha el objeto antes de que la persona que llama pueda usarlo, lo que probablemente provoque excepciones en el tiempo de ejecución. Por lo tanto, debe eliminar el uso o evitar devolver el IDisposable.
Mal ejemplo:
public FileStream WriteToFile ( string path, string text ) { using ( var fs = File.Create(path)) // Noncompliant { var bytes = Encoding.UTF8.GetBytes(text); fs.Write(bytes, 0 , bytes.Length); return fs; } }
Se espera que el uso de == para comparar objetos haga una comparación de referencia. Es decir, se espera que devuelva verdadero si y solo si son la misma instancia de objeto. Sobrecargar al operador para que haga cualquier otra cosa conducirá inevitablemente a la introducción de errores por parte de las personas que llaman. Por otro lado, sobrecargarlo para hacer exactamente eso no tiene sentido; eso es lo que == hace por defecto.
Existe un contrato entre Equals(objeto) y GetHashCode(): si dos objetos son iguales según el método Equals(objeto), llamar a GetHashCode() en cada uno de ellos debe arrojar el mismo resultado. Si este no es el caso, muchas colecciones no manejarán correctamente las instancias de clase.
Para cumplir con el contrato, Equals(object) y GetHashCode() deben heredarse o anularse.
GetHashCode se utiliza para archivar un objeto en un diccionario o tabla Hash. Si GetHashCode usa campos que no son de solo lectura y esos campos cambian después de almacenar el objeto, el objeto se archiva de forma incorrecta en Hashtable de inmediato. Cualquier prueba posterior para ver si el objeto está en Hashtable arrojará un falso negativo.
Mal ejemplo:
public int age; public string name; public override int GetHashCode ( ) { int hash = 12 ; hash += this .age.GetHashCode(); // Noncompliant hash += this .name.GetHashCode(); // Noncompliant return hash; }
Buen ejemplo:
public readonly DateTime birthday; public string name; public override int GetHashCode ( ) { int hash = 12 ; hash += this .birthday.GetHashCode(); return hash; }
Dado que las clases abstractas no se pueden instanciar, no tiene sentido que tengan constructores públicos o internos. Si hay una lógica de inicialización básica que debe ejecutarse cuando se crea una instancia de clase extendida, puede ponerla en un constructor, pero hacer que ese constructor sea privado o protegido.
La recursión es aceptable en los métodos, en los que puede salir de ella. Pero con los tipos de clase, terminas con un código que se compilará pero no se ejecutará si intentas instanciar la clase.
Mal ejemplo:
class C1 < T > { } class C2 < T > : C1 < C2 < C2 < T >>> // Noncompliant { } var c2 = new C2< int >();
Cuando se usa la sintaxis new Guid() (es decir, creación de instancias sin parámetros), debe ser que se desea una de estas tres cosas:
Llamar a GC.Collect rara vez es necesario y puede afectar significativamente el rendimiento de la aplicación. Esto se debe a que activa una operación de bloqueo que examina todos los objetos en la memoria para limpiarlos. Además, no tiene control sobre cuándo se ejecutará realmente esta limpieza de bloqueo.
Como regla general, las consecuencias de llamar a este método superan con creces los beneficios, a menos que tal vez haya desencadenado algún evento único en la ejecución de su programa que causó la muerte de muchos objetos de larga duración.
Los programadores no deben comentar el código, ya que infla los programas y reduce la legibilidad.
El código no utilizado debe eliminarse y puede recuperarse del historial de control de código fuente si es necesario.
goto es una instrucción de flujo de control no estructurada. Hace que el código sea menos legible y mantenible. En su lugar, se deben utilizar declaraciones de flujo de control estructurado como if, for, while, continue o break.
PD ¡Gracias por leer! ¡Más consejos próximamente! Un agradecimiento especial a SonarQube y sus reglas - https://www.sonarqube.org/