¿Cuál es el peor problema en C # o .NET?

Hace poco estuve trabajando con un objeto DateTime y escribí algo como esto:

 DateTime dt = DateTime.Now; dt.AddDays(1); return dt; // still today's date! WTF? 

La documentación intellisense para AddDays() dice que agrega un día a la fecha, que no lo hace, en realidad devuelve una fecha con un día agregado a ella, por lo que debe escribirla como sigue:

 DateTime dt = DateTime.Now; dt = dt.AddDays(1); return dt; // tomorrow's date 

Este me ha mordido varias veces antes, así que pensé que sería útil catalogar los peores errores de C #.

 private int myVar; public int MyVar { get { return MyVar; } } 

Blammo. Tu aplicación se bloquea sin rastro de stack. Pasa todo el tiempo.

(Observe la letra mayúscula myVar en lugar de myVar en minúscula en el getter).

Type.GetType

El que he visto morder a muchas personas es Type.GetType(string) . Se preguntan por qué funciona para los tipos en su propio ensamblaje, y algunos tipos como System.String , pero no System.Windows.Forms.Form . La respuesta es que solo se ve en el ensamblado actual y en mscorlib .


Métodos anónimos

C # 2.0 introdujo métodos anónimos, dando lugar a situaciones desagradables como esta:

 using System; using System.Threading; class Test { static void Main() { for (int i=0; i < 10; i++) { ThreadStart ts = delegate { Console.WriteLine(i); }; new Thread(ts).Start(); } } } 

¿Qué va a imprimir eso? Bueno, depende completamente de la progtwigción. Imprimirá 10 números, pero probablemente no se imprimirá 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, que es lo que cabría esperar. El problema es que es la variable i que se ha capturado, no su valor en el momento de la creación del delegado. Esto se puede resolver fácilmente con una variable local adicional del scope correcto:

 using System; using System.Threading; class Test { static void Main() { for (int i=0; i < 10; i++) { int copy = i; ThreadStart ts = delegate { Console.WriteLine(copy); }; new Thread(ts).Start(); } } } 

Ejecución diferida de bloques iteradores

Esta "prueba de unidad del pobre" no se aprueba, ¿por qué no?

 using System; using System.Collections.Generic; using System.Diagnostics; class Test { static IEnumerable CapitalLetters(string input) { if (input == null) { throw new ArgumentNullException(input); } foreach (char c in input) { yield return char.ToUpper(c); } } static void Main() { // Test that null input is handled correctly try { CapitalLetters(null); Console.WriteLine("An exception should have been thrown!"); } catch (ArgumentNullException) { // Expected } } } 

La respuesta es que el código dentro de la fuente del código CapitalLetters no se ejecuta hasta que se llama por primera vez al método MoveNext() del iterador.

Tengo algunas otras rarezas en mi página de acertijos .

Volviendo a lanzar excepciones

Un gotcha que recibe muchos desarrolladores nuevos, es la semántica de excepción de re-lanzamiento.

Mucho tiempo veo código como el siguiente

 catch(Exception e) { // Do stuff throw e; } 

El problema es que borra el seguimiento de la stack y hace que diagnosticar problemas sea mucho más difícil, porque no se puede rastrear dónde se originó la excepción.

El código correcto es la instrucción throw sin argumentos:

 catch(Exception) { throw; } 

O envolviendo la excepción en otra, y usando la excepción interna para obtener la traza original de la stack:

 catch(Exception e) { // Do stuff throw new MySpecialException(e); } 

La ventana de observación de Heisenberg

Esto puede morderte de gravedad si estás cargando cosas a pedido, como este:

 private MyClass _myObj; public MyClass MyObj { get { if (_myObj == null) _myObj = CreateMyObj(); // some other code to create my object return _myObj; } } 

Ahora digamos que tienes algún código en otro lugar usando esto:

 // blah // blah MyObj.DoStuff(); // Line 3 // blah 

Ahora quiere depurar su método CreateMyObj() . Así que pones un punto de interrupción en la Línea 3 de arriba, con la intención de entrar en el código. Solo para una buena medida, también pones un punto de interrupción en la línea superior que dice _myObj = CreateMyObj(); e incluso un punto de interrupción dentro de CreateMyObj() .

El código llega a su punto de interrupción en la Línea 3. Ingresa al código. Esperas ingresar el código condicional, porque _myObj es obviamente nulo, ¿verdad? Uh … entonces … ¿por qué se saltó la condición e ir directamente a return _myObj ?! Mueves el mouse sobre _myObj … y de hecho, ¡tiene un valor! ¡¿Cómo ocurrió eso?!

La respuesta es que su IDE hizo que obtuviera un valor, porque tiene una ventana de “vigilancia” abierta, especialmente la ventana de “Autos”, que muestra los valores de todas las variables / propiedades relevantes para la línea de ejecución actual o anterior. Cuando llegue a su punto de interrupción en la Línea 3, la ventana del reloj decidió que le interesaría saber el valor de MyObj , así que detrás de las escenas, ignorando cualquiera de sus puntos de interrupción , fue y calculó el valor de MyObj para usted, incluida la llamada a CreateMyObj() que establece el valor de _myObj!

Es por eso que llamo a esto la ventana de observación Heisenberg: no se puede observar el valor sin afectarlo … 🙂

GOTCHA!


Editar – Siento que el comentario de @ ChristianHayter merece ser incluido en la respuesta principal, ya que parece una solución efectiva para este problema. Entonces, cada vez que tenga una propiedad de carga lenta …

Decora tu propiedad con [DebuggerBrowsable (DebuggerBrowsableState.Never)] o [DebuggerDisplay (“”)]. – Christian Hayter

Aquí hay otro momento en que uno me entiende:

 static void PrintHowLong(DateTime a, DateTime b) { TimeSpan span = a - b; Console.WriteLine(span.Seconds); // WRONG! Console.WriteLine(span.TotalSeconds); // RIGHT! } 

TimeSpan.Seconds es la porción de segundos del intervalo de tiempo (2 minutos y 0 segundos tiene un valor de segundos de 0).

TimeSpan.TotalSeconds es el intervalo de tiempo completo medido en segundos (2 minutos tiene un valor de segundos totales de 120).

Fuga de memoria porque no deshizo los eventos.

Esto incluso atrapó a algunos desarrolladores senior que conozco.

Imagina un formulario WPF con muchas cosas y, en algún lugar, te suscribes a un evento. Si no cancela la suscripción, todo el formulario se guardará en la memoria una vez cerrado y desreferenciado.

Creo que el problema que vi fue crear un DispatchTimer en el formulario de WPF y suscribirse al evento Tick, si no haces un – = en el temporizador, ¡tu formulario pierde memoria!

En este ejemplo, su código de desassembly debería tener

 timer.Tick -= TimerTickEventHandler; 

Este es especialmente complicado ya que creó la instancia de DispatchTimer dentro del formulario WPF, por lo que podría pensar que sería una referencia interna manejada por el proceso de recolección de basura … desafortunadamente el DispatchTimer usa una lista interna estática de suscripciones y servicios solicitudes en el hilo de la interfaz de usuario, por lo que la referencia es ‘propiedad’ de la clase estática.

Tal vez no sea realmente una sorpresa porque el comportamiento está escrito claramente en MSDN, pero me he roto el cuello una vez porque me pareció bastante contrario a la intuición:

 Image image = System.Drawing.Image.FromFile("nice.pic"); 

Este tipo deja el archivo "nice.pic" bloqueado hasta que se elimina la imagen. En el momento en que lo enfrenté, pensé que sería bueno cargar íconos sobre la marcha y no me di cuenta (al principio) de que terminé con docenas de archivos abiertos y bloqueados. La imagen realiza un seguimiento de dónde había cargado el archivo desde …

¿Cómo resolver esto? Pensé que un trazador de líneas haría el trabajo. Esperaba un parámetro extra para FromFile() , pero no tenía ninguno, así que escribí esto …

 using (Stream fs = new FileStream("nice.pic", FileMode.Open, FileAccess.Read)) { image = System.Drawing.Image.FromStream(fs); } 

Si cuentas ASP.NET, diría que el ciclo de vida de las webforms es muy importante para mí. He pasado innumerables horas depurando código de webforms mal escrito, simplemente porque muchos desarrolladores simplemente no entienden cuándo usar qué controlador de eventos (incluido yo, por desgracia).

sobrecarga == operadores y contenedores sin tipo (listas de conjuntos, conjuntos de datos, etc.):

 string my = "my "; Debug.Assert(my+"string" == "my string"); //true var a = new ArrayList(); a.Add(my+"string"); a.Add("my string"); // uses ==(object) instead of ==(string) Debug.Assert(a[1] == "my string"); // true, due to interning magic Debug.Assert(a[0] == "my string"); // false 

Soluciones?

  • siempre use string.Equals(a, b) cuando compara tipos de cadenas

  • usando generics como List para asegurar que ambos operandos sean cadenas.

DateTime.ToString (“dd / MM / aaaa”) ; En realidad, esto no siempre le proporcionará dd / MM / aaaa, sino que tendrá en cuenta la configuración regional y reemplazará su separador de fechas dependiendo de dónde se encuentre. Entonces puedes obtener dd-MM-aaaa o algo por el estilo.

La forma correcta de hacerlo es usar DateTime.ToString (“dd ‘/’ MM ‘/’ yyyy”);


DateTime.ToString (“r”) se supone que se convierte a RFC1123, que usa GMT. GMT está dentro de una fracción de segundo de UTC, y sin embargo el especificador de formato “r” no se convierte a UTC , incluso si el DateTime en cuestión se especifica como Local.

Esto da como resultado la siguiente información (varía según la distancia de UTC a la hora local):

 DateTime.Parse("Tue, 06 Sep 2011 16:35:12 GMT").ToString("r") > "Tue, 06 Sep 2011 17:35:12 GMT" 

¡Ups!

 [Serializable] class Hello { readonly object accountsLock = new object(); } //Do stuff to deserialize Hello with BinaryFormatter //and now... accountsLock == null ;) 

Moraleja de la historia: los inicializadores de campo no se ejecutan al deserializar un objeto

Vi este publicado el otro día, y creo que es bastante oscuro y doloroso para aquellos que no saben

 int x = 0; x = x++; return x; 

Como eso devolverá 0 y no 1 como la mayoría esperaría

Llego un poco tarde a esta fiesta, pero tengo dos trampas que me han picado recientemente:

Resolución de fecha y hora

La propiedad Ticks mide el tiempo en 10 millonésimas de segundo (bloques de 100 nanosegundos), sin embargo, la resolución no es de 100 nanosegundos, es aproximadamente 15 ms.

Este código:

 long now = DateTime.Now.Ticks; for (int i = 0; i < 10; i++) { System.Threading.Thread.Sleep(1); Console.WriteLine(DateTime.Now.Ticks - now); } 

le dará una salida de (por ejemplo):

 0 0 0 0 0 0 0 156254 156254 156254 

Del mismo modo, si mira DateTime.Now.Millisecond, obtendrá valores en fragmentos redondeados de 15.625ms: 15, 31, 46, etc.

Este comportamiento particular varía de un sistema a otro , pero hay otros errores relacionados con la resolución en esta API de fecha / hora.


Path.Combine

Una excelente manera de combinar rutas de archivos, pero no siempre se comporta de la manera que esperaría.

Si el segundo parámetro comienza con un carácter \ , no le dará una ruta completa:

Este código:

 string prefix1 = "C:\\MyFolder\\MySubFolder"; string prefix2 = "C:\\MyFolder\\MySubFolder\\"; string suffix1 = "log\\"; string suffix2 = "\\log\\"; Console.WriteLine(Path.Combine(prefix1, suffix1)); Console.WriteLine(Path.Combine(prefix1, suffix2)); Console.WriteLine(Path.Combine(prefix2, suffix1)); Console.WriteLine(Path.Combine(prefix2, suffix2)); 

Te da esta salida:

 C:\MyFolder\MySubFolder\log\ \log\ C:\MyFolder\MySubFolder\log\ \log\ 

Cuando se inicia un proceso (usando System.Diagnostics) que se escribe en la consola, pero nunca se lee Console.Out stream, después de cierta cantidad de salida, su aplicación parecerá bloquearse.

Sin atajos de operador en Linq-To-Sql

Mira aquí .

En resumen, dentro de la cláusula condicional de una consulta Linq-To-Sql, no puede usar accesos directos condicionales como || y && para evitar excepciones de referencia nulas; ¡Linq-To-Sql evalúa ambos lados del operador OR o AND incluso si la primera condición obvia la necesidad de evaluar la segunda condición!

Usar parámetros predeterminados con métodos virtuales

 abstract class Base { public virtual void foo(string s = "base") { Console.WriteLine("base " + s); } } class Derived : Base { public override void foo(string s = "derived") { Console.WriteLine("derived " + s); } } ... Base b = new Derived(); b.foo(); 

Salida:
base derivada

Objetos de valor en colecciones mutables

 struct Point { ... } List mypoints = ...; mypoints[i].x = 10; 

no tiene efecto.

mypoints[i] devuelve una copia de un objeto de valor Point . C # felizmente le permite modificar un campo de la copia. Silenciosamente sin hacer nada.


Actualización: Esto parece estar corregido en C # 3.0:

 Cannot modify the return value of 'System.Collections.Generic.List.this[int]' because it is not a variable 

Quizás no sea lo peor, pero algunas partes del framework .net usan grados mientras que otras usan radianes (y la documentación que aparece con Intellisense nunca te dice cuál, debes visitar MSDN para averiguarlo)

Todo esto podría haberse evitado al tener una clase de Angle lugar …

Para los progtwigdores de C / C ++, la transición a C # es natural. Sin embargo, la mayor sorpresa con la que me he encontrado personalmente (y he visto a otros hacer la misma transición) no es comprender por completo la diferencia entre las clases y las estructuras en C #.

En C ++, las clases y las estructuras son idénticas; solo difieren en la visibilidad predeterminada, donde las clases tienen por defecto la visibilidad privada y las estructuras predeterminadas para la visibilidad pública. En C ++, esta definición de clase

  class A { public: int i; }; 

es funcionalmente equivalente a esta definición de estructura.

  struct A { int i; }; 

En C #, sin embargo, las clases son tipos de referencia mientras que las estructuras son tipos de valores. Esto hace una GRAN diferencia en (1) decidir cuándo usar uno sobre el otro, (2) probar la igualdad de los objetos, (3) el rendimiento (por ejemplo, boxeo / unboxing), etc.

Hay todo tipo de información en la web relacionada con las diferencias entre los dos (por ejemplo, aquí ). Recomiendo encarecidamente a cualquiera que realice la transición a C # que al menos tenga un conocimiento práctico de las diferencias y sus implicaciones.

Recolección de basura y Dispose (). Aunque no tiene que hacer nada para liberar memoria , aún debe liberar recursos mediante Dispose (). Esto es algo inmensamente fácil de olvidar cuando usa WinForms o rastrea objetos de cualquier forma.

foreach loops variables scope!

 var l = new List>(); var strings = new[] { "Lorem" , "ipsum", "dolor", "sit", "amet" }; foreach (var s in strings) { l.Add(() => s); } foreach (var a in l) Console.WriteLine(a()); 

prints five “amet”, while the following example works fine

 var l = new List>(); var strings = new[] { "Lorem" , "ipsum", "dolor", "sit", "amet" }; foreach (var s in strings) { var t = s; l.Add(() => t); } foreach (var a in l) Console.WriteLine(a()); 

MS SQL Server can’t handle dates before 1753. Significantly, that is out of synch with the .NET DateTime.MinDate constant, which is 1/1/1. So if you try to save a mindate, a malformed date (as recently happened to me in a data import) or simply the birth date of William the Conqueror, you’re gonna be in trouble. There is no built-in workaround for this; if you’re likely to need to work with dates before 1753, you need to write your own workaround.

Arrays implement IList

But don’t implement it. When you call Add, it tells you that it doesn’t work. So why does a class implement an interface when it can’t support it?

Compiles, but doesn’t work:

 IList myList = new int[] { 1, 2, 4 }; myList.Add(5); 

We have this issue a lot, because the serializer (WCF) turns all the ILists into arrays and we get runtime errors.

The contract on Stream.Read is something that I’ve seen trip up a lot of people:

 // Read 8 bytes and turn them into a ulong byte[] data = new byte[8]; stream.Read(data, 0, 8); // <-- WRONG! ulong data = BitConverter.ToUInt64(data); 

The reason this is wrong is that Stream.Read will read at most the specified number of bytes, but is entirely free to read just 1 byte, even if another 7 bytes are available before end of stream.

It doesn't help that this looks so similar to Stream.Write , which is guaranteed to have written all the bytes if it returns with no exception. It also doesn't help that the above code works almost all the time . And of course it doesn't help that there is no ready-made, convenient method for reading exactly N bytes correctly.

So, to plug the hole, and increase awareness of this, here is an example of a correct way to do this:

  ///  /// Attempts to fill the buffer with the specified number of bytes from the /// stream. If there are fewer bytes left in the stream than requested then /// all available bytes will be read into the buffer. ///  /// Stream to read from. /// Buffer to write the bytes to. /// Offset at which to write the first byte read from /// the stream. /// Number of bytes to read from the stream. /// Number of bytes read from the stream into buffer. This may be /// less than requested, but only if the stream ended before the /// required number of bytes were read. public static int FillBuffer(this Stream stream, byte[] buffer, int offset, int length) { int totalRead = 0; while (length > 0) { var read = stream.Read(buffer, offset, length); if (read == 0) return totalRead; offset += read; length -= read; totalRead += read; } return totalRead; } ///  /// Attempts to read the specified number of bytes from the stream. If /// there are fewer bytes left before the end of the stream, a shorter /// (possibly empty) array is returned. ///  /// Stream to read from. /// Number of bytes to read from the stream. public static byte[] Read(this Stream stream, int length) { byte[] buf = new byte[length]; int read = stream.FillBuffer(buf, 0, length); if (read < length) Array.Resize(ref buf, read); return buf; } 

The Nasty Linq Caching Gotcha

See my question that led to this discovery, and the blogger who discovered the problem.

In short, the DataContext keeps a cache of all Linq-to-Sql objects that you have ever loaded. If anyone else makes any changes to a record that you have previously loaded, you will not be able to get the latest data, even if you explicitly reload the record!

This is because of a property called ObjectTrackingEnabled on the DataContext, which by default is true. If you set that property to false, the record will be loaded anew every time… BUT … you can’t persist any changes to that record with SubmitChanges().

GOTCHA!

Eventos

I never understood why events are a language feature. They are complicated to use: you need to check for null before calling, you need to unregister (yourself), you can’t find out who is registered (eg: did I register?). Why isn’t an event just a class in the library? Basically a specialized List ?

Enumerables can be evaluated more than once

It’ll bite you when you have a lazily-enumerated enumerable and you iterate over it twice and get different results. (or you get the same results but it executes twice unnecessarily)

For example, while writing a certain test, I needed a few temp files to test the logic:

 var files = Enumerable.Range(0, 5) .Select(i => Path.GetTempFileName()); foreach (var file in files) File.WriteAllText(file, "HELLO WORLD!"); /* ... many lines of codes later ... */ foreach (var file in files) File.Delete(file); 

Imagine my surprise when File.Delete(file) throws FileNotFound !!

What’s happening here is that the files enumerable got iterated twice (the results from the first iteration are simply not remembered) and on each new iteration you’d be re-calling Path.GetTempFilename() so you’ll get a different set of temp filenames.

The solution is, of course, to eager-enumerate the value by using ToArray() or ToList() :

 var files = Enumerable.Range(0, 5) .Select(i => Path.GetTempFileName()) .ToArray(); 

This is even scarier when you’re doing something multi-threaded, like:

 foreach (var file in files) content = content + File.ReadAllText(file); 

and you find out content.Length is still 0 after all the writes!! You then begin to rigorously checks that you don’t have a race condition when…. after one wasted hour… you figured out it’s just that tiny little Enumerable gotcha thing you forgot….

Just found a weird one that had me stuck in debug for a while:

You can increment null for a nullable int without throwing an excecption and the value stays null.

 int? i = null; i++; // I would have expected an exception but runs fine and stays as null 

Today I fixed a bug that eluded for long time. The bug was in a generic class that was used in multi threaded scenario and a static int field was used to provide lock free synchronisation using Interlocked. The bug was caused because each instantiation of the generic class for a type has its own static. So each thread got its own static field and it wasn’t used a lock as intended.

 class SomeGeneric { public static int i = 0; } class Test { public static void main(string[] args) { SomeGeneric.i = 5; SomeGeneric.i = 10; Console.WriteLine(SomeGeneric.i); Console.WriteLine(SomeGeneric.i); Console.WriteLine(SomeGeneric.i); } } 

This prints 5 10 5

 TextInfo textInfo = Thread.CurrentThread.CurrentCulture.TextInfo; textInfo.ToTitleCase("hello world!"); //Returns "Hello World!" textInfo.ToTitleCase("hElLo WoRld!"); //Returns "Hello World!" textInfo.ToTitleCase("Hello World!"); //Returns "Hello World!" textInfo.ToTitleCase("HELLO WORLD!"); //Returns "HELLO WORLD!" 

Yes, this behavior is documented, but that certainly doesn’t make it right.