C#/.NET Little Pitfalls

Preview:

DESCRIPTION

C# is a great programming language for modern development. Like any language, however, there are parts of the language and BCL that can trip you up if you have invalid assumptions as to what is going on behind the scenes. This presentation discusses a few of these pitfalls and how to avoid them.

Citation preview

James Michael Hare2011 Visual C# MVPApplication Architect

ScottradeAugust 5th, 2011

Blog: http://www.BlackRabbitCoder.netTwitter: @BlkRabbitCoder

The joys of managed codeModern programming languages are extremely

advanced and give us great frameworks that allow us to develop software more easily:Higher levels of abstractionManaged Memory / Garbage CollectionBetter management of resourcesStronger type safetyProcessor / Platform independenceShorter learning curveFaster time to market

But, there is a catch…Managed languages do a lot of work for the

developer which may cause issues if invalid assumptions or accidental misuse occur.

Knowing your programming language well is the best way to guard against pitfalls:Know what the language tries to do for you

behind the scenes.Know the default actions of your language if no

other direction given.Know what is compile-time and run-time

behavior in your language.

Today’s Little Pitfalls:Converting boxed values.Creating unnecessary strings.Nullable<T> arithmetic and comparison operators.Mutable readonly values.Compile-time const value substitutions.Operator overloads are not overrides.Hiding is the default, not overriding.Default parameters are compile-time substitutions.Differences between struct and class.Order of initialization differences in .NET languages.Premature polymorphism in construction.Unconstrained generic type parameters are compiled like

object.

Converting boxed valuesWe all know we can cast a double to an int, right?

But what if the double were boxed?

The former works, latter throws InvalidCastException, since no conversion from object -> int it’s a cast.

Remember when unboxing must use correct type (important when reading from IDataReader, etc)

Creating unnecessary stringsIf building string over multiple statements, don’t

do:

But do:

Latter is 100x faster (literally) and creates less garbage.

Creating unnecessary stringsWhen comparing strings insensitively, use

insensitive IComparer, or form of Compare() which takes case-insensitive parameter, do not convert string!

Not:

But:

Latter is faster and doesn’t create temp upper-case string that needs to be garbage collected.

Creating unnecessary stringsWhen you need to join two strings, by all means use

concatenation, it’s the fastest and most concise.But, when you are building a string over multiple

statements (like a loop), concatenating creates a lot of intermediate strings that need to be garbage collected.

This allocation and collection can cause some overhead if done too often, and is much slower.

Thus, Prefer to use StringBuilder when building a string over multiple statements.

Prefer to use insensitive variants of Compare() and Equals() instead of ToUpper()/ToLower() compares.

Nullable<T> arithmeticValue types cannot truly be null, always has a value.We can “simulate” a nullable value type using the

System.Nullable<T> wrapper (can use T? shorthand).This wrapper is a struct which has two key members:

HasValue – property that states whether null or not.Value – property that returns the wrapped value if

HasValue is true and throws InvalidOperationException if not.

In addition, we can compare and assign nullable value types to null, though this is not same as null reference.

Nullable<T> arithmeticTo make nullable value types more usable, the

arithmetic operators supported by the wrapped type are supported on the nullable-wrapped type as well.

That is you can directly add two int? since int supports operator+.

This is true of the primitive and BCL value types as well as your own custom value types.

The problem happens when you attempt to use one of these operators on a nullable type with “null” value.

Nullable<T> arithmeticFor example, let’s say you have:

Nullable<T> arithmeticAnd you perform:

What is the result?Does it compile?Does it throw at runtime?Does it succeed?

Nullable<T> arithmeticIt does indeed “succeed”, but the value will be a

“null” Fraction? That is, one with HasValue == false.

Arithmetic operations on nullable types where either operand is “null” returns a “null” instance.

You can see a hint of this if you hover over h:

Since h was a Fraction? we have a pretty good hint result will be “null” if either operand is “null”.

Nullable<T> arithmeticIf we chose a result type of Fraction instead

of Fraction?, we’d get a compiler error which would force us to use the Value property to resolve:

Nullable<T> comparisonsA similar thing occurs between nullable-

wrapped value types and some of the comparison operators.

The == and != operators work exactly as you’d expect.

The <=, <, >=, and > operators, however, will all return false if either operand is “null”.

This is true even if both are “null” on >= and <= :

Nullable<T> comparisonsNote that this means you can not rely on

logic ladders like this for Nullable<T> wrapped types:

Nullable<T> comparisonsRemember that arithmetic operators on a “null”

instance of a Nullable<T> wrapped value type yield a “null” result.To prevent, make result of operation non-nullable

and compiler will force you to use Value property.Remember that the <=, >=, <, and > operators

on a “null” instance of a Nullable<T> wrapped value type yield a false result.No way to directly prevent this, just have to watch

out and avoid or understand the underlying logic.

Mutable readonly valuesreadonly defines a run-time, read-only value:

This lets you assign a value once to the field in an initializer or constructor, and then it can’t be changed.

For value types, this works much as you’d expect.

Mutable readonly valuesThis means the readonly field is immutable…

right?Strictly speaking, yes it does. Practically speaking, it’s a bit more

complicated.Consider the following snippet of code where

we attempt to define a readonly List<T> of options:

Mutable readonly valuesIs the object _availableOptions refers to

immutable? No, readonly modifies the reference being

declared.Means once assigned, it can’t be reassigned

new object.However, this does not mean that the object

referred to by _availableOptions cannot be modified.

Mutable readonly valuesMust remember that readonly refers to the

identifier, not the value it labels.For value types, this is the actual value and will

be immutable.For reference types, this is the object reference

which will itself be immutable, but the object referred to is not.

If you want to make a readonly reference type immutable, must be an immutable type:StringReadOnlyCollection

const values are compile-timereadonly is a “constant” resolved at run-time.However, const is resolved at compile-time.This has a few interesting ramifications:

const values must be compile-time definitions.

const values are substituted at compile-time.const identifiers are already static by definition.

const values are compile-timeConsider this class in a class library

Order.DLL:

Now, we use this in a program, OrderServer.EXE:

const values are compile-timeIf we run this, we get the results we expect:

Max parts per order is: 2Max open orders per customer is: 5

What if we double both to 4/10 in Orders.dll, build Orders.dll but deploy only the changed assembly?Max parts per order is: 2Max open orders per customer is: 10

The const was substituted originally at compile of OrderServer.exe, since we only built the changed Orders.dll, OrderServer.exe never saw the change.

const values are compile-timeIf you have a “constant” that is likely to

change, consider making it static readonly instead, in this way it is a run-time value.

Only use const in cases where you are not likely to have changes across compilation units:When the const is very unlikely to change, for

example a const for the number of days in a week.

When the const is private and thus not visible to outside classes or other compilation units.

Operators are overloadedOperators can be overloaded in .NET languages.Allows you to use operators to perform actions

between objects in a meaningful way.For instance, DateTime and operator - :

Works great when operator meaningful to the type.

Operators are overloadedThe pitfall comes when people think

operators are overridden, instead of overloaded.

For example, what if we defined a class for Fraction with a Numerator and Denominator and == overload:

Operators are overloadedWhat happens if we attempt to use the following

snippet of code:

This does not call Fraction’s operator == overload because oneHalf and anotherOneHalf are references to object, not Fraction.

Operator definitions are overloaded for specific types.

Operators are overloadedRemember that operators are overloaded, not

overridden.The operator implementation used is based on

the type of the references, not the objects they refer to.

Be especially wary of == and != overloads since these are defined for object and will not give you a compiler error if used incorrectly.

When using generics, remember that a non-narrowed generic type parameter always assumes object (coming up in a later pitfall).

Hiding is the defaultWhen you want to “replace” the behavior of a

method (or property) from a base class with one from a sub class, you have two basic choices:Overriding:

Base class member must be abstract, virtual, or override. Member resolved at run-time based on type of object. No direct way to get to original member from outside.

Hiding: Base class member can be anything but abstract. Member resolved at compile-time based on type of

reference. Can directly get to the original member using cast, etc.

Hiding is the defaultThe default behavior is to hide, not override

even if base-class member declared virtual or override:

Hiding is the defaultThis means that which method is invoked depends

solely on the reference:

The object referenced in each case is irrelevant, method is non-virtual, thus resolved at compile-time.

Hiding is the defaultWatch your compiler warnings! Implicit

hiding is not an error, but it will give a compiler warning.

Consider treating warnings as errors in your projects.

Whenever you do intentionally hide, make the hide explicit by using the new keyword:

Default parametersCan specify a default for a parameter whose

argument is not specified in a method or constructor call.

Can reduce need for multiple overloads.Arguments are defaulted left to right if not

specified (or can be arbitrary if using named arguments)

Default parametersThe problem is, default parameter values are

substituted at compile-time, not run-time.Default value substitutions are based on the

reference and not the object itself.Default parameter values are not inherited from

base classes or interfaces.Default parameter values are not required to be

consistent between interfaces, classes, sub classes, etc.

Subject to same compile-time change issues across compilation units as const values.

Default parametersConsider:

Default parametersWhat happens:

Because resolved at compile-time, we will get:SubTagBaseTagITag

Default parametersAvoid specifying default parameters in

interfaces.Avoid specifying default parameters in non-

sealed classes that are likely to be sub-classed.

Prefer to only use default parameters in sealed methods or classes.

If parameter values refer to public properties that will be passed in during construction, consider object initialization syntax instead.

A struct is not a classIn C++, structures and classes are nearly identical

with the exception of default accessibility level.This is not true in .NET since they have completely

different semantics.The most notable difference between the two is

that classes are reference types, and structures are value types.

This means whenever you are passing or return a structure to or from a method, you are returning a copy of the value and not just a reference to the original.

A struct is not a classIf you aren’t aware of the value type semantics of a

struct, you may write code that won’t function correctly, fails to compile, or is less performant:Passing a struct as a return value or parameter is a

copy.Assigning a struct creates a new copy.Subscribing to event in copy has no effect on original.Default constructors of a struct cannot be suppressed.Cannot change default value of struct members.Cannot inherit from a struct (implicitly sealed).Defining struct > ~16 bytes can degrade performance.

A struct is not a classAssuming:

What happens:

A struct is not a classA struct passed or returned by value is a copy,

modifying the copy does not affect original.Important because struct returned as a property

can’t be modified (compiler error) since you’d be directly modifying a temp copy and not original property.

Also creates issues when attempting to subscribe to an event in a copy when you intended to subscribe to original.

If struct is too large, these copies can be detrimental to performance, recommended size is < ~16 bytes.

A struct is not a classIf you subscribe to a copy you do not

subscribe to original.

A struct is not a classThis will have no output, because subscribed

to copy of event in AddEventHandler() and not original event in Main():

A struct is not a classLike class, the default constructor of struct

is implicit.Unlike class, you cannot specify

implementation.Unlike class, defining other constructors

doesn’t suppress default, nor can you make it non-visible.

A struct is not a classOnly create a struct when the internal

representation is very small (~16 bytes or less).Typically, we prefer value types to behave like

primitives in that they can be treated as a single value.

The best uses of struct tend to be in creating small immutable values, due to value type copy semantics.

Avoid event definitions in struct.Do not attempt to hide default constructor for a

struct, you can’t.

Initialization OrderWhen you create a class hierarchy and then

construct a sub-class, in what order are the objects constructed?

The answer? It depends on your language…Given this little class that simply logs a message on

construction, which we’ll use to show initialization:

Let’s look at an example.

Initialization OrderConsider this base class for an abstract Shape:

Note that we are initializing a field (LogMe() will write a message to console).

Initialization OrderNow consider this sub-class for a concrete

Rectangle:

Initialization OrderWhat happens if we construct a

Rectangle(3,5) in C#?

But if we translate the same code to VB.NET?

Initialization OrderYou should know the differences in initialization

order, but also strive not to write code dependent on it:C#:

Sub-class initialization Base-class initialization Base-class constructor Sub-class constructor

VB.NET: Base-class initialization Base-class constructor Sub-class initialization Sub-class constructor

Premature polymorphismConsider again the Shape and Rectangle

puzzle from the previous pitfall. Let’s alter the Shape class to call a

polymorphic (virtual or abstract) method from the constructor:

Premature polymorphismRemembering that Rectangle looks like this:

Premature polymorphismSo what happens if we construct a new

Rectangle(3,5), remembering that now the Shape default constructor will perform a polymorphic draw?

This will compile successfully, but what will the result be?

Premature polymorphismYou might expect an explosion, because

Draw() is abstract in the base, but even though Rectangle hasn’t been constructed yet, object still considers itself a Rectangle.

This means it will call the Rectangle.Draw() method.

But there’s a problem, Rectangle’s constructor has not run yet, thus _length and _width are unassigned:

Premature polymorphismPolymorphic calls in base class constructors

are problematic since the sub-class constructor has not yet been executed, which may have class in inconsistent state.

Also remember that due to the order of initialization differences between VB.NET/C#.NET, the sub-class initializers will not have run yet for VB.NET as well.

Safer to just avoid polymorphic calls in a base class constructor altogether.

Unconstrained genericsWhen you create a generic, you specify a

generic type parameter.You can constrain the generic type parameter

to specify it must be of a certain type.Narrowing allows you to treat variables of

the generic type to act like the narrowed type, thus exposing their members.

Unconstrained genericsLet’s constraint a generic that it must be a

reference type (class) but not constrain it’s type:

What does that mean if we do this?

What’s the output given T is type string?

Unconstrained genericsThe result was false!This is because the == comparison between the

unconstrained type T assumes object.Thus even though T is type string in our usage, when

the generic is compiled, it is compiled assuming object.

This means that object’s == is invoked and not string’s.

Polymorphic overrides work as you’d expect, but for hides and operator overloads, the resolution depends on the type constraint (object if unconstrained).

Unconstrained genericsRemember that a generic type parameter

that is not constrained to a specific base-type or interface is considered to be constrained to object implicitly.

Whatever the generic type parameter is constrained to, any operator overloads or method hides will resolve to that constrained type if available and not the actual generic type argument used in the call.

Conclusion.NET is an excellent development platform offering:

Great execution performance.Ease of development.Quick time to market.Fewer bugs and memory leaks.

There are a few interesting features of the language that if not understood can lead to pitfalls.

Knowing those pitfalls, why they occur, and how to avoid them are key strengths for .NET developers.

Questions?

Recommended