Qual è il peggior trucco in C # o .NET?

Recentemente ho lavorato con un object DateTime e ho scritto qualcosa del genere:

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

La documentazione di intellisense per AddDays() dice che aggiunge un giorno alla data, che non fa – in realtà restituisce una data con un giorno aggiunto ad essa, quindi devi scriverlo come:

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

Questo mi ha morso un paio di volte prima, quindi ho pensato che sarebbe stato utile catalogare i peggiori trucchi C #.

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

Blammo. La tua app si arresta in modo anomalo senza traccia dello stack. Succede tutte le volte.

(Si noti la maiuscola MyVar posto della myVar minuscola nel getter).

Type.GetType

Quello che ho visto mordere molte persone è Type.GetType(string) . Si chiedono perché funzioni per i tipi nel proprio assembly e per alcuni tipi come System.String , ma non System.Windows.Forms.Form . La risposta è che guarda solo nell’assemblaggio corrente e in mscorlib .


Metodi anonimi

C # 2.0 ha introdotto metodi anonimi, portando a situazioni sgradevoli come questa:

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

Cosa stamperà? Bene, dipende interamente dalla programmazione. Stamperà 10 numeri, ma probabilmente non stamperà 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, che è ciò che potresti aspettarti. Il problema è che è la variabile i che è stata catturata, non il suo valore al momento della creazione del delegato. Questo può essere risolto facilmente con una variabile locale extra dell'ambito corretto:

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

Esecuzione posticipata dei blocchi iteratori

Questo "test dell'unità povero" non passa - perché 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 risposta è che il codice all'interno dell'origine del codice CapitalLetters non viene eseguito finché non viene chiamato prima il metodo MoveNext() CapitalLetters .

Ho altre stranezze nella mia pagina dei rompicapo .

Rilanciare le eccezioni

Un getcha che ottiene molti nuovi sviluppatori, è la semantica dell’eccezione di nuova generazione.

Un sacco di tempo vedo il codice come il seguente

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

Il problema è che cancella la traccia dello stack e rende molto più difficile diagnosticare i problemi, poiché non è ansible tracciare la provenienza dell’eccezione.

Il codice corretto è l’istruzione throw con no args:

 catch(Exception) { throw; } 

O avvolgendo l’eccezione in un altro e usando l’eccezione interna per ottenere la traccia dello stack originale:

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

La finestra dell’orologio di Heisenberg

Questo può morderti male se stai facendo cose che richiedono il carico su richiesta, come questo:

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

Ora supponiamo di avere del codice altrove usando questo:

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

Ora si desidera eseguire il debug del metodo CreateMyObj() . Quindi metti un breakpoint sulla riga 3 sopra, con l’intenzione di entrare nel codice. Solo per buona misura, metti anche un punto di interruzione nella riga sopra che dice _myObj = CreateMyObj(); e persino un punto di interruzione all’interno di CreateMyObj() .

Il codice raggiunge il punto di interruzione sulla linea 3. Si entra nel codice. Ti aspetti di inserire il codice condizionale, perché _myObj è ovviamente nullo, giusto? Uh … quindi … perché ha saltato la condizione e andare dritto a return _myObj ?! Muovi il mouse su _myObj … e in effetti ha un valore! Come è successo?!

La risposta è che il tuo IDE ha causato l’ottenimento di un valore, perché hai una finestra di “sorveglianza” aperta – specialmente la finestra di controllo “Autos”, che visualizza i valori di tutte le variabili / proprietà relative alla linea di esecuzione corrente o precedente. Quando hai raggiunto il punto di interruzione sulla Linea 3, la finestra dell’orologio ha deciso che ti interesserebbe conoscere il valore di MyObj : così dietro le quinte, ignorando qualsiasi punto di interruzione , è andato a calcolare il valore di MyObj per te, inclusa la chiamata to CreateMyObj() che imposta il valore di _myObj!

Ecco perché lo chiamo Heisenberg Watch Window – non puoi osservare il valore senza intaccarlo … 🙂

GOTCHA!


Modifica – Sento che il commento di @ ChristianHayter merita l’inclusione nella risposta principale, perché sembra una soluzione efficace per questo problema. Quindi ogni volta che hai una proprietà pigro-caricata …

Decora la tua proprietà con [DebuggerBrowsable (DebuggerBrowsableState.Never)] o [DebuggerDisplay (“”)]. – Christian Hayter

Ecco un’altra volta che mi viene in aiuto:

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

TimeSpan.Seconds è la parte dei secondi del periodo di tempo (2 minuti e 0 secondi ha un valore di secondi pari a 0).

TimeSpan.TotalSeconds è l’intero intervallo misurato in secondi (2 minuti ha un valore totale di secondi di 120).

Perdita di memoria perché non hai sganciato gli eventi.

Ciò ha anche colto alcuni sviluppatori senior che conosco.

Immagina un modulo WPF con un sacco di cose al suo interno, e da qualche parte lì ti iscrivi ad un evento. Se non si annulla l’iscrizione, l’intero modulo viene tenuto in memoria dopo essere stato chiuso e de-referenziato.

Credo che il problema che ho visto è stato la creazione di un DispatchTimer nel modulo WPF e l’iscrizione all’evento Tick, se non si esegue un – = sul timer il modulo perde memoria!

In questo esempio dovrebbe avere il tuo codice di rimozione

 timer.Tick -= TimerTickEventHandler; 

Questo è particolarmente complicato da quando hai creato l’istanza di DispatchTimer all’interno del modulo WPF, quindi penseresti che sarebbe un riferimento interno gestito dal processo Garbage Collection … purtroppo DispatchTimer utilizza un elenco interno statico di abbonamenti e servizi richieste sul thread dell’interfaccia utente, quindi il riferimento è “di proprietà” della class statica.

Forse non è un trucco perché il comportamento è scritto chiaramente in MSDN, ma mi ha spezzato il collo una volta perché l’ho trovato piuttosto contro-intuitivo:

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

Questo tipo lascia il file "nice.pic" bloccato finché l’immagine non viene eliminata. A quel tempo pensavo che sarebbe stato bello caricare le icone al volo e non mi ero reso conto (inizialmente) di aver trovato decine di file aperti e bloccati! L’immagine tiene traccia di dove ha caricato il file da …

Come risolvere questo? Pensavo che una sola nave avrebbe fatto il lavoro. Mi aspettavo un parametro aggiuntivo per FromFile() , ma non ne avevo, quindi ho scritto questo …

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

Se conti ASP.NET, direi che il ciclo di vita dei webform è un grosso problema per me. Ho passato innumerevoli ore a fare il debug di codice di webforms scritto male, solo perché molti sviluppatori non capiscono davvero quando utilizzare il gestore di eventi (me incluso, purtroppo).

overload == operatori e contenitori non tipizzati (arraylists, dataset, ecc.):

 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 

Soluzioni?

  • utilizzare sempre string.Equals(a, b) quando si confrontano i tipi di stringa

  • usando i generici come List per assicurare che entrambi gli operandi siano stringhe.

DateTime.ToString (“gg / MM / aaaa”) ; Questo in realtà non ti darà sempre dd / MM / yyyy ma invece prenderà in considerazione le impostazioni regionali e sostituirà il separatore di date a seconda di dove ti trovi. Quindi potresti ottenere dd-MM-yyyy o qualcosa di simile.

Il modo giusto per farlo è usare DateTime.ToString (“dd ‘/’ MM ‘/’ yyyy”);


DateTime.ToString (“r”) dovrebbe convertire in RFC1123, che utilizza GMT. GMT si trova a una frazione di secondo da UTC, eppure l’identificatore di formato “r” non viene convertito in UTC , anche se il DateTime in questione è specificato come Locale.

Ciò si traduce nel seguente trucchetto (varia a seconda di quanto è distante l’ora locale da UTC):

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

Ops!

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

Morale della trama: gli inizializzatori di campo non vengono eseguiti durante la deserializzazione di un object

Ho visto questo postato l’altro giorno, e penso che sia piuttosto oscuro e doloroso per quelli che non lo so

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

Come quello restituirà 0 e non 1 come la maggior parte si aspetterebbe

Sono un po ‘in ritardo per questa festa, ma ho due trucchi che mi hanno morso entrambi di recente:

Risoluzione DateTime

La proprietà Ticks misura il tempo in 10 milionesimi di secondo (100 nanosecondi di blocchi), tuttavia la risoluzione non è di 100 nanosecondi, è di circa 15ms.

Questo codice:

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

ti darà un risultato di (per esempio):

 0 0 0 0 0 0 0 156254 156254 156254 

Allo stesso modo, se guardi DateTime.Now.Millisecond, otterrai valori in blocchi arrotondati di 15.625ms: 15, 31, 46, ecc.

Questo particolare comportamento varia da sistema a sistema , ma ci sono altri trucchi relativi alla risoluzione in questa API data / ora.


Path.Combine

Un ottimo modo per combinare percorsi di file, ma non sempre si comporta come ci si aspetterebbe.

Se il secondo parametro inizia con un carattere \ , non ti darà un percorso completo:

Questo codice:

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

Ti dà questo risultato:

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

Quando si avvia un processo (utilizzando System.Diagnostics) che scrive sulla console, ma non si legge mai lo stato di Console.Out, dopo una certa quantità di output la app sembrerà bloccarsi.

Nessun collegamento dell’operatore in Linq-To-Sql

Vedi qui

In breve, all’interno della clausola condizionale di una query Linq-To-Sql, non è ansible utilizzare scorciatoie condizionali come || e && per evitare eccezioni di riferimento null; Linq-To-Sql valuta entrambi i lati dell’operatore OR o AND anche se la prima condizione ovvia alla necessità di valutare la seconda condizione!

Utilizzo di parametri predefiniti con metodi virtuali

 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(); 

Produzione:
base derivata

Valore oggetti in raccolte mutabili

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

non ha alcun effetto

mypoints[i] restituisce una copia di un object valore Point . C # ti consente di modificare felicemente un campo della copia. Silenziosamente non facendo niente


Aggiornamento: sembra corretto in C # 3.0:

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

Forse non il peggiore, ma alcune parti del framework .net usano gradi mentre altri usano radianti (e la documentazione che appare con Intellisense non ti dice mai quale, devi visitare MSDN per scoprirlo)

Tutto ciò avrebbe potuto essere evitato avendo invece una class Angle

Per i programmatori C / C ++, la transizione a C # è naturale. Tuttavia, il trambusto più grande che ho incontrato personalmente (e ho visto con altri che hanno effettuato la stessa transizione) non comprende appieno la differenza tra le classi e le strutture in C #.

In C ++, le classi e le strutture sono identiche; differiscono solo per la visibilità predefinita, in cui le classi vengono impostate come predefinite per la visibilità privata e le strutture predefinite per la visibilità pubblica. In C ++, questa definizione di class

  class A { public: int i; }; 

è funzionalmente equivalente a questa definizione di struttura.

  struct A { int i; }; 

In C #, tuttavia, le classi sono tipi di riferimento mentre le strutture sono tipi di valore. Questo fa una GRANDE differenza nel (1) decidere quando usare l’uno sull’altro, (2) testare l’uguaglianza degli oggetti, (3) le prestazioni (es. Box / unboxing), ecc.

Vi sono tutti i tipi di informazioni sul web relative alle differenze tra i due (ad esempio, qui ). Incoraggerei vivamente chiunque facesse il passaggio a C # per avere almeno una conoscenza pratica delle differenze e delle loro implicazioni.

Garbage Collection e Dispose (). Anche se non devi fare nulla per liberare memoria , devi comunque liberare risorse tramite Dispose (). Questa è una cosa immensamente facile da dimenticare quando si utilizzano WinForms o il tracciamento degli oggetti in qualsiasi modo.

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 malformsd 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!

eventi

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.