We've all seen the big "macro" features in .NET, this presentation is to give praise to the "Little Wonders" of .NET -- those little items in the framework that make life as a developer that much easier!
What are “Little Wonders”? The .NET Framework is full of “macro-sized” goodness that can help make our coding lives easier by automating common tasks. But, the .NET Framework also has a lot of smaller “micro- sized” tips and tricks that can improve code. Many developers know of most of these, but it is often surprising how many times newer developers don’t. This presentation picks up where the first “Little Wonders” presentation leaves off…
How do they help?Basically, by employing these small items at the righttime, you can increase application: Readability – some of the wonders make code much more concise and easy to read. Maintainability – often goes hand and hand with readability, by removing ambiguity of the code, it is easier to maintain without introducing errors. Performance – a few of the little wonders can even help increase the performance of your code (depending on usage).
Optional ArgumentsOptional arguments allow you to specify a defaultvalue to be used if argument isn’t provided.Default values must be compile-time constants: Numeric constant expressions String constant expressions nullCan be used to replace redundant overloads whenthe only purpose is to default an argument.
Optional ArgumentsThe default is literally compiled in at call, so this:Is, in effect, compiled into this:
Named ArgumentsDefaults are positional, substituted from left to right:If you want to “skip”, used named arguments:
Default ParametersThere are a couple of potential pitfalls to be aware of: If used across assemblies, can lead to subtle errors. If a default parameter is defined in one assembly, and used in another, must make sure both get recompiled if value changes. Default parameters are not inherited. Defaults established in an interface or base-class do not directly apply (and can be different from) the sub-class. Default depends on reference type, not the object type.
Chained ConstructorsConstructors can call each other directly, which canreduce the need for redundant code in overloads.You could use optional arguments or initializers toreign in overloads, but each have their limitations: Optional arguments must be set to compile-time constant expressions. Initializers aren’t suitable for immutable classes, readonly fields, get-only properties, or items whose construction may be “heavy”.
Generic ConstraintsGenerics allow you to make very powerful types andalgorithms, but can be a two-edged sword.An unconstrained generic type parameter makes noassumptions, giving you the lowest commondenominator of all types.Supplying a constraint reduces the types that can beused to realize the generic, but expands the thingsthat can be done inside the generic.
Generic ConstraintsFor example, how would unconstrained T handle null?Unconstrained T can be compared to null, but this isalways false for value types (except Nullable<T>).Unconstrained T cannot be assigned to null, thoughcan be assigned to default(T).
Generic ConstraintsOr constrain T to be a value type (for example, sinceNullable<T> can only support value types…
Generic ConstraintsOr what if we want to create a new unconstrained Tinstance, or access any of T’s members?Can’t, no way to know if unconstrained T supportsparameter-less construction or other members.
Generic ConstraintsSupplying a constraint constrains the matching types, butalso expands what you can do with the generic: where T : struct – T must be a value type, must be first. Allows T to be used in other value generics (Nullable<T>, etc.). where T : class – T must be a reference type, must be first. Allows T to be assigned to null in generic. where T : U – T must be, inherit, or implement U Allows T to access any public members of U in generic. where T : new() – T must have a public parameter-less constructor, must be last Allows T to be constructed in generic.
Generic ConstraintsConstraining T to things that implement IDictionaryand have parameter-less constructor:
Generic ConstraintsConstraining T only value types that implementIConvertible:
Anonymous TypesWe can construct and initialize any type on the flyusing object initialization syntax:
Anonymous TypesWhat if we only need a type for a very localizedpurpose? Why create the boilerplate code for a class that will only be used in a small, localized section? Most of these classes are simple POCOs, is it worth a full definition? If you need to use a type as a key (equality, hashing), it can get heavy. Must implement Equals() and GetHashCode() correctly.
Anonymous TypesAnonymous types create throw-away types for you: Creates type based on name, type, order of properties. Creates suitable Equals() and GetHashCode(). Syntax is like object initialization, just omit type name.
Anonymous TypesNow, much simpler… Don’t need to define UserDay or UserDayTotal Don’t need to maintain Equals() and GetHashCode(). Doesn’t clutter up project with throw-away types.
Anonymous TypesA few things to be aware of: Type is generated at compile time based on properties’ names, types, order – any deviation is a different type: Difficult to use outside of defined scope (which shouldn’t do anyway, defeats purpose).
The Enum ClassEnum is not only the base of enums, it also hasseveral useful static methods: IsDefined() – Determines if an enum constant exists with the given value. HasFlag() – Determines if a given set of bits is set, much easier to read than the typical bitwise AND. TryParse() – Parse a string value into enum value, much lighter than Parse().
The Enum ClassUse IsDefined() to see if a value existsAnd HasFlag() simplifies flag testing:
The Enum ClassThe TryParse() is much lighter than Parse(), especiallyif you think you may not have a valid value:
The Nullable<T> StructYou can indicate an optional value easily with null forreference types, but value types always exist.Sometimes, appropriate sentinel values don’t exist.The Nullable<T> generic struct simulates optionalvalues for value types.Nullable<T> has two key properties: HasValue – True if Value has been set to a value. Value – The value if set, throws if not.
The Nullable<T> StructUse when you have a value type which may or maynot contain a valid value.For example, the DateTime struct’s default value isn’tvery meaningful, use DateTime? Instead.
The Nullable<T> StructWe then must test to make sure it has a value byusing HasValue property.Once we know the value exists, access Value toretrieve it.
The Nullable<T> StructNullable<T> allows some syntactical shortcuts: Nullable<T> can be abreviated T? Nullable<T> can be assigned to null Nullable<T> can be compared with null
The Nullable<T> StructSome things to watch out for: Nullable<T> doesn’t save space if value not specified, still stores a Value, it’s just not usable. Accessing Value when HasValue is not true is an error and will throw an exception. Math or logical operations between Nullable<T> wrapped numeric types (int, double, etc.) has caveats: Math with null yields null. Logical ordered comparison with null yields false.
The Lazy<T> ClassSome classes may be expensive to create, especially ifthey are not always needed, in these cases lazyinstances are often used.No need to create your own lazy-initialization logicanymore, Lazy<T> does it all for you.Can either call default constructor, or a generator.Various thread-safety modes so you can choose thelevel of performance and safety you require.
Lazy<T>Constructor not called until Value accessed.
Lazy<T>If a constructor isn’t accessible, or desired, you canalways use a factory method:
Lazy<T>By default, Lazy<T> is thread-safe, but can choose:
The TuplesSometimes, you need to just throw together severalvalues to treat as a set of values.Tuples allow you to do this in a generic way.Similar to anonymous types, yet different: Both are immutable and have Equals(), GetHashCode() Anonymous types have named properties, but not type. Tuples have generic property names, but named type.Tuple has static factory methods to easy creation.
The TuplesSimple tuples are a single “layer” and have propertiesnumbered Item1…Item7:
TuplesGet longer Tuples by using octuple, which has TRest:
InterlockedLocking to safely increment a count is heavy:
InterlockedAllows thread-safe, highly performant manipulationof numeric values.Much lighter than full locks for simple operations like: Increment – adds 1 to the interlocked value Decrement – subtracts 1 from the interlocked value Exchange – swaps two values Add – adds a value to the interlocked value
InterlockedUsing interlocked, we can do the same thing, withoutheavy locks:
Strings of Repeated CharHave you ever seen someone construct an array ofrepeated char like this?Instead, quickly create a string of a repeatedcharacter using one of string’s constructors:
Joining StringsMany times people write code to do this:When they can simply do a Join():
Joining StringsThe string.Join() has a lot of power Can Join any array or IEnumerable<T>: Can join a variable argument list, including mixed types:
Generic DelegatesOne of the oldest patterns in the OO playbook is tocreate a class that is 99% complete except for oneanonymous “work” method, which is typicallyabstract…
Generic DelegatesInheriting from a class simply to provide a missingmethod per use is often overkill and poor coupling.Can’t easily change behavior at runtime.Bloats projects with classes that just override.
Generic DelegatesWe can instead provide behavior through a delegate. Behavior can be added with no extra subclasses needed. There is very limited coupling between the delegate and the class/method using it. Behavior could be changed out at runtime.But defining a new delegate each time gets ugly:Generic delegates can be used for most delegateneeds.
The Action DelegateAction is a generic family of delegates that take up to16 arguments and return nothing: Action – takes zero arguments, returns nothing Action<T> - takes one argument, returns nothing. Action<T1, T2> - takes two arguments, returns nothing. … Action<T1…T16> - takes 16 arguments, returns nothing.Action cannot be used for ref or out arguments.
The Func DelegateSimilar to Action, but Func returns a value: Func<TResult> – takes zero args, returns a TResult. Func<T, TResult> - takes one arg, returns a TResult. Func<T1, T2, TResult> - takes two args, returns a TResult. … Func<T1…T16, TResult> - takes 16 args, returns a TResult.Func cannot be used for ref or out arguments.Func<T, bool> is generally preferable to Predicate<T>.
Empty EnumerablesHow many times have you created an empty array orList<T> to represent an empty sequence?
Empty EnumerableEnumerable has a static method Empty<T>() thatgenerates a singleton empty IEnumerable<T>.
Enumerable RepeatAllows you to create a sequence of the same item agiven number of times.Doesn’t recreate the item each time, takes whateverthe parameter is and creates a sequence.For example, to create 10 random numbers:
Collection GeneratorsOften times you’ll have a sequence and want to storein a collection to: Avoid re-generating the sequence during multiple iterations. Convert the sequence from one sequence type to another. Pull the sequence into a collection to avoid deferred execution issues.
Collection GeneratorsThere are several LINQ extension methods togenerate different collections from sequences: ToArray() – stores sequence in a T. ToList() – stores sequence in a List<T>. ToDictionary() – transforms sequence to a Dictionary<TKey, TValue> given selectors for TKey and TValue. ToLookup() – transforms sequence to a Lookup<TKey, TValue> - similar to dictionary but can have repeated keys.
ToArray and ToListToArray() and ToList() are useful for: Remove effects of deferred execution. Convert a sequence to a particular kind.
ToDictionaryToDictionary() converts a linear sequence to aDictionary that maps a single key to a single value. Keys can only exist once, multiple key instances throw. Must provide key selector, value selector optional.
ToLookupToLookup() is similar to ToDictionary(), but it maps akey to a set of values (essentially a multi-map). Again must provide key selector, value optional.