5 C# Traps to Avoid
C# Corner columnist Patrick Steele offers a heads up on five gotchas that can trip up even veteran C# programmers.
Make no mistake, C# is an impressive and powerful programming language that serves both hobbyist and enterprise developers alike. But a tool as powerful as C# doesn't come without its sharp edges. In this installment of C# Corner, I want to look at the five most common and most dangerous traps that developers will want to avoid when programming in C#. Some of these gotchas have been around since C# 1.0 (and still manage catch developers) while others are native to newer versions of C#, including C# 4.
Trap #1: Mistaking "var" as Late-bound
C# 3.0 introduced the "var" keyword as a way to let the compiler infer the type of variable based on the expression used to initialize the variable. Here's a simple example:
var counter = 15;
In this example, the C# compiler will create an int variable called counter and initialize it to 15. Where var really shines is in situations like this (C# 2.0):
IEnumerable<Customer> customers = GetCustomers();
With var, we can simplify this to:
var customers = GetCustomers();
The C# compiler sees that GetCustomers returns an IEnumerable<Customer> so it defines "customers" as that type. The "var" keyword also removes some of the redundancy we used to have:
List<String> names = new List<string>();
If the assignment is creating a List<String>, why do we need to define the variable as a List<String>? The var keyword infers this from the expression:
var names = new List<string>();
int age = 29;
var age = 29;
Produce the exact same IL when compiled. Don't fear the "var" keyword. It can save you time and typing.
Trap #2: Be careful with optional parameters
C# 4.0 introduces "optional" parameters. An optional parameter defines a default value to be used if the caller doesn't supply a value. In the past, the concept of default values for parameters was realized by overloading:
public decimal GetTemperature()
public decimal GetTemperature(bool inCelsius)
With optional parameters, you write one method and define a default value:
public decimal GetTemperatue(bool inCelsius = false)
While functionally equivalent, there's a big difference in how they are implemented.
With overloading, a call to GetTemperature() will execute a call to GetTemperature(false). Suppose this method is in a utility library. You decide the new default should be to return temperatures in Celsius. You change the code in GetTemperature to "return GetTemperature(true);", recompile and drop it in your directory with the consuming application. Everything is good to go and temperatures are now returned in Celsius by default.
When using optional parameters, the default "false" value was baked in to the calling assembly at compile time. Consumers of your utility library won't get the new default value unless they are recompiled. Be careful when creating public API's with optional parameters!
Trap #3: Deferred Execution in LINQ
LINQ makes querying of data so much easier than for/each loops with nested if blocks and other conditional logic. But remember that all of that fancy LINQ stuff is simply syntactic sugar over method calls.
Consider this simple example of a method using LINQ-to-SQL to get a list of customers:
public IEnumerable<Customer> GetCustomers()
using(var context = new DBContext())
return from c in context.Customers
where c.Balance > 2000
Seems pretty harmless -- until you get an "ObjectDisposedException" when you try and enumerate the collection. Why? Because LINQ doesn't actually perform the query until you try and enumerate the results. The DBContext class (which exposes the Customers collection) is disposed of when this call exits. Once you try and enumerate through the collection, the DBContext.Customers class is referenced and you get the exception.
A simpler example:
decimal minimumBalance = 500;
var customersOver500 = from c in customers
where c.Balance > minimumBalance select c;
minimumBalance = 200;
var customersOver200 = from c in customers
where c.Balance > minimumBalance select c;
int count1 = customersOver500.Count();
int count2 = customersOver200.Count();
Suppose we have four customers with the following balances: 100, 300, 400 and 600. What will count1 and count2 be? They'll both be 3. The "customersOver500" references the "minimumBalance" variable, but the value isn't obtained until the query results are iterated over (through a for/each loop, a ToList() call or even a "Count()" call as shown above). At the time the value is used to process the query, the value for minimumBalance has already changed to 200, so both LINQ queries produce identical results (customers with a balance over 200).
If you have cases where you want to ensure your LINQ queries are evaluated right away, convert them to a List via ToList(), or to an array via ToArray().
Trap #4: Overloading Methods in a base class
This is an interesting one that can trip you up if you're not aware of how .NET resolves overloads. Suppose you have the following classes:
public class MyBase
public void DoSomething(int a)
public class MyDerived : MyBase
public void DoSomething(long a)
What gets printed with the following code:
MyDerived d = new MyDerived();
Most people assume the DoSomething(int) overload on the base class is a better match than the DoSomething(long) overload. However, since the variable is typed as a "MyDerived", that version of DoSomething will be called. The .NET runtime always favors the most-derived compile-time type.
Can you force a call to the base class DoSomething with a cast?
MyDerived d = new MyDerived();
Nope. Remember what we just said: The .NET runtime always favors the most-derived compile-time type. In this case, .NET will still call DoSomething(long). In general, you should avoid overloading methods defined in base classes.
Trap #5: Forgetting to unsubscribe from events
This has been around since the first version of C#, but still causes bugs from some people.
As you're aware, the .NET runtime employs a very smart garbage collector to clean up objects in memory that are no longer used. It determines "used" by keeping track of what objects reference other objects (which reference other objects and so on). Once an object is found to not have any other objects referencing it, it's available for garbage collection and the memory can be released.
Let's consider a simple scenario. You have a main application form. This form opens a child form to do some editing. The edits need to update some status panel on the main form. To achieve the updates, your child form exposes an event that the main form can subscribe to. You handle the creation of the child form like this:
using(var child = new ChildForm())
child.StatusUpdated += new EventHandler(child_StatusUpdated);
You've wrapped your child form in a "using" block to make sure the Dispose method is always called. That's fine -- the Dispose call will release unmanaged resources (the window handle, child controls, etc.), but your main form is still subscribed to the StatusUpdated event of your child form. That means the garbage collector can't free up the memory used by the child form. Each time this code is hit, a new child form is created and its memory will never be released until the main form is no longer referenced.
Always make sure you unsubscribe from events you've subscribed to. It allows your unused objects to be collected sooner and puts less pressure on the garbage collector.
Quick Word of Thanks
I'd like to thank Bill Wagner, Dennis Burton and Chris Marinos for their assistance with this article. Hopefully their able assistance will help you sidestep some of these common C# traps in the future.
Do you have a C# trap you'd like to share? Email me at [email protected].
Patrick Steele is a senior .NET developer with Billhighway in Troy, Mich. A recognized expert on the Microsoft .NET Framework, he’s a former Microsoft MVP award winner and a presenter at conferences and user group meetings.