Use the C# Language to its Fullest Potential
If Visual Studio is your samurai sword, then the C# programming language along with patterns and practices of structured software development are your fighting techniques, principles and disciplines. In this post, I'm going to talk about some of the features of C#, versions 6, 7, and 8. I'm going to assume that you are familiar with C# at this point and at least know about the major language features of versions 1 through 4. If you don't, then a good place to start is by reading C# in Depth, by the legendary developer, Jon Skeet. As a matter of fact, I would recommend this book anyway, since the latest edition includes C# 6 and 7, and gives a brief glimpse of C# 8.
I've chosen to focus on these three versions because not only are they the most recent (obviously) but also because the style of language version releases started to change with version 6, focusing on a more frequent tempo and more granular features. Even more importantly, the syntax and feel of the language itself seemed to turn a corner with C# 6, marking what in my opinion is an evolving paradigm shift toward functional programming. Being an F# developer since 2013, this is very obvious to me, as I watch C# "borrow" more and more features from F# that have been idiomatic in that language since virtually the beginning, the result being that C# is fast becoming a hybrid language (I refer to this as the "F#-ification" of C#). There's nothing inherently wrong with this, however it can make slightly daunting the task of learning the language for newer developers, as the reason behind certain features and syntax may not be readily apparent. To that end, I'll comment on such features and give my opinion on some of the newest additions to the language.
There's another force at play as well, which you should be aware of. With the introduction of .NET Core (which was fittingly referred to as a "reboot" of .NET by Eric Sink at the latest CNUG) the language and underlying runtime have become more performant. In fact, this is one of the three "pillars" of the .NET Core framework, which are:
- Open source
- Better performance
In order to achieve this better performance, the CLR and language designers have added in more low-level features, a trend which I refer to as the "C-ification" of C#, in that these features of the language seem to me to be reminiscent of the classic, stalwart programming language C, a language which is the great, great granddaddy of C#, but whose intrinsically low-level focus might appear to be at odds with a modern garbage-collected language.
So there it is: two directions the language is taking which at first glance might be orthogonal, but which make sense in this modern era of terse, succinct syntax, and cross-platform high-performance which cloud computing/containerization require. There's something else I'd like to say at this point: being a multi-paradigm C#/F# software developer with a focus on web development, my bias in reviewing the features should be very apparent. This is not to say that the low-level C-ification features aren't as important—they obviously are, especially for software developers who are building libraries and performance-intensive software (think IOT). They just aren't my core focus at this time.
Without further ceremony, here are the features and some of my commentary on each. I've put an exclamation mark next to features which I think are especially important, and F#/C indicators alongside features which I think exemplify the F#-ification or C-ification of the language.
Read-only auto-properties (auto getter/no setter) [!, F#]
- Prior to this, we had to declare properties with a private set clause and/or explicitly create a backing variable. Now the compiler does this automatically if you just declare a getter.
Auto-property initializers [!, F#]
- Auto properties that are declared with only a getter can be initialized as part of the property declaration.
Expression-bodied function members [!, F#]
- Methods and properties that are a single line can be declared using a lambda (arrow) syntax. This eliminates braces and other syntactic clutter.
- This and the above two features are reminiscent of F# in that they resemble the succinct, terse syntax and heavy compiler inference which is the hallmark of that language.
using static [F#]
- Allows you to import the static methods of a single class (similar to opening a module in F#). Extension methods are only in scope using extension method invocation syntax.
Null-conditional operators [!]
- Allows you to invoke/de-reference members of an instance which might be null, without worrying about throwing a null reference exception. Be aware that using this may cause some boolean expressions to evaluate as bool? (Nullable<bool>) which could complicate some logic.
String interpolation [!]
- Once of the most important features of C#6. This allows you to use an intuitive interpolation syntax to format values inside a string, rather than the old way of using String.Format(). The type produced from a string interpolation is System.FormattableString, which allows you to format it against different cultures.
Exception filters [F#]
- Allows you to catch exceptions that fit specific criteria (basically 'when' guards for C#, much like what has existed for years in F#). Useful for certain exception types which may contain more context-specific information about the error.
The nameof expression [!]
- Evaluates to the simple name of a symbol (namespace is not included). This is a great way to add in diagnostic code, implement INotifyPropertyChanged, manually map properties, etc without worrying about refactorings messing up your code. The compiler has your back.
Await in Catch and Finally blocks [!]
- You can now use await in catch/finally blocks, and it behaves the way you would expect it to. This is really important.
Initialize associative collections using indexers
- Initializing key/value based types now has an index-based syntax, which makes it easier to initialize collections where Add() accepts more than one argument.
Extension Add methods in collection initializers
- Collection initializers work, even if the Add() method is an extension method.
Improved overload resolution
- The compiler is a little bit smarter about picking the right overload in method calls involving lambdas.
New compiler options
- "fake sign" or "OSS sign" - causes the compiler to apply a public key and apply a bit which specifies that the assembly is signed, but does not actually sign the assembly. Used for compatibility with open-source projects.
- Tells the compiler to produce identical binary outputs across builds, as long as the source files are the same.
- You can now declare out parameters in the argument list of a method call. This saves a line of code declaring the variable beforehand.
Tuples [!, F#]
- Tuples are now idiomatic in C# and can be declared using a simple tuple syntax with parentheses. This is a very conspicuous example of the F#-ification of C#, as tuples are a fundamental feature of functional programming languages, including F#.
- You can now use the underscore character as a throwaway placeholder when deconstructing tuples or user-defined types, when calling methods with out parameters, in pattern matching with is and switch, and as a standalone identifier when you want to throw away the value of an assignment. This is another feature directly borrowed from F#.
Pattern Matching [!, F#]
- Pattern matching (much like in F#) is supported in is/switch expressions, along with when guards.
- This is a pretty big change. See here: https://docs.microsoft.com/en-us/dotnet/csharp/pattern-matching
- This is IMO the biggest exemplar of the F#-ification of C#, as the entire F# language is basically a pattern-matching engine.
ref locals and returns [C]
- You can now return a reference to variables. The language designers have very specific rules for this feature so that the intent of code is clear. The intent of this feature is to make the language more performant by prevent copying of values, dereferencing multiple times, etc. This is a good example of the C-ification of the C# language in newer versions.
Local Functions [F#]
- You can now declare methods inside of other methods. Prior to this you could accomplish this using lambda expressions. Microsoft does a decent job explaining the differences here. This feature didn't come as a big surprise to me, as it fits well within the theme of the F#-ification of the C# language (nested functions are idiomatic in the functional programming paradigm).
More expression-bodied members
- Expression-bodied (lambda syntax) members has been extended to constructors, finalizers, and get/set accessors. This is a nice syntactic sugar for the language, but not a huge game-changer.
throw Expressions [!, F#]
- throw has been changed into an expression rather than a statement, so you can now call it more conveniently (like inside null coalescing expressions, expression-bodied members, etc). I've personally found this to be useful inside guard clauses and the like.
- The move toward converting statements into expressions is characteristic of the shift toward the functional paradigm, as in functional programming pretty much everything is an expression.
Generalized async return types [!]
- This is another feature that allows library writers to create more performant code. Returned types must implement the GetAwaiter() method and it must be accessible, otherwise async code can be used without Task or Task<T>. They even introduced a new type, ValueTask<T> which is supposed to be more performant than Task<T>.
Numeric literal syntax improvements
- They introduced binary literals and digit separators (underscore) for easier declaration of numeric types. Syntactic sugar.
async Main method [!]
- You can now mark a Main() method as async, which allows you to use await inside your Main() method instead of having to do MyAsyncMethod().GetAwaiter().GetResult(). This is very handy.
default literal expressions
- Syntactic sugar. Instead of specifying default(MyType) you can use default.
Inferred tuple element names
- More syntactic sugar. You can declare tuples without specifying the name of the elements if you're initializing them with variables instead of literals. Similar to how you can do this with anonymous type properties.
Pattern matching on generic type parameters
- is/switch pattern matches can have the type of a generic type parameter.
Techniques for writing safe efficient code [!, C]
readonly struct / ref readonly / readonly ref struct / in modifier
- These are all performance enhancements when working with value types. They mainly apply if you are writing performance-sensitive libraries.
- See: https://docs.microsoft.com/en-us/dotnet/api/system.span-1?view=netstandard-2.1
- Probably one of the most interesting and powerful features added to .NET Core / .NET Standard. This lets you manipulate arbitrary regions of memory using array-like semantics. They very explicitly added this to support the performance of .NET Core.
Non-trailing named arguments
- Method calls can have named arguments before positional arguments, as long as they are in the right order. Not really a game changer.
Leading underscores in numeric literals
- You can have the underscore decimal separator at the beginning of binary literals. Also not a game changer.
private protected access modifier
- The protected internal access modifier means "derived types OR in the same assembly"—I.e. protected OR internal. However, the CLR has always supported protected AND internal, even though the C# language did not. Finally, they closed this gap with the private protected access modifier, which essentially means "derived types AND in the same assembly"—I.e. protected AND internal. Note that you can still extend access to "friend" assemblies by using the InternalsVisibleTo attribute.
Conditional ref expressions
- The conditional operator (ternary operator) can now produce a ref result.
- Most features are performance/safe code features.
- See: https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-7-3
Indexing fixed fields does not require pinning [C]
- You can access fixed fields without pinning another variable.
ref local variables may be reassigned [C]
- Seems almost like getting closer to using pointers while still writing safe code.
stackalloc arrays support initializers [C]
- I personally don't do a lot with stackalloc but this is nice.
More types support the fixed statement [C]
- Once again, I don't do a lot of "low-level" C# programming, but this feature seems to be huge, especially in the context of the ongoing C-ification of C#. As stated in the docs, it means that "fixed can be used with System.Span<T> and related types." Could be a game-changer.
Enhanced generic constraints [!, F#]
- You can specify System.Enum and System.Delegate as base class constraints for generic parameters.
- There is now an unmanaged constraint.
- I imagine that there may be some pretty sophisticated solutions now possible because of this, therefore I think it's an important feature. For instance, it may allow for more elegant LINQ extension methods which pull C# even more toward the functional programming paradigm (think reduce/fold, etc).
Tuples support == and != [!, F#]
- A pretty important addition to the language, as it facilitates more F#-like syntax.
Attach attributes to the backing fields for auto-implemented properties
- You can attach attributes to the backing field of auto-properties. This is a nice-to-have. Before you had to explicitly declare the backing field.
in method overload resolution tiebreaker
- More or less a bug fix for the language. They just made the compiler a little bit smarter in dealing with readonly-reference parameters.
Extend expression variables in initializers
- You can now use out variables in field initializers, property initializers, constructor initializers and query clauses.
- Not sure how I feel about this one. Just looking at their example code, it seems like this could be used to abuse polymorphism. Might make Uncle Bob upset.
Improved overload candidates
- They made the compiler smarter in overload resolution. The changes are pretty intuitive. My honest opinion is that if you are creating methods which have the possibility of being ambiguous in the eyes of the compiler, you are probably "drawing outside the lines" a little too much in your architecture.
New compiler options
- They listed publicsign again even though it seems to have been introduced with C#6. Their comment is this: "The -publicsign compiler option instructs the compiler to sign the assembly using a public key. The assembly is marked as signed, but the signature is taken from the public key. This option enables you to build signed assemblies from open-source projects using a public key."
- pathmap: lets you replace source file paths with those that you specify. This seems to be used for PDB files and CallerFilePathAttribute. So basically, a debugging feature, as I don't see how you would want to use this in production builds.
Readonly members [F#]
- You can apply the readonly modifier to individual members of a struct. This is both a performance feature and moves C# closer toward being a functional programming language, as immutability is one of the pillars of functional programming.
Default interface implementations [!]
- You can now add default implementations to interfaces, which will be inherited by existing implementations of those interfaces.
- Methods added using this approach will not break source or binary compatibility.
- The reason behind this feature was so that C# can interoperate with APIs that target Android/Swift.
- I'm not too keen on this, as the very definition of an interface is a "code contract" (although some might disagree) and/or polymorphic surface.
Pattern matching enhancements [!, F#]
- See: https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/pattern-matching
- C#8 now allows recursive patterns
- Switch expressions have been introduced. The syntax looks oddly a lot like F#'s match expression. The variable comes before the switch keyword and lambdas are used for the cases. The default case is handled with an underscore discard (_) just like in F#. It also supports when guards. Enough said.
- Property patterns have been introduced. Using curly braces, you can now match on properties of an object. This is nearly identical to matching on record types in F#. The syntax is basically the same.
- You can match on tuple patterns. Using the parentheses tuple syntax, you can match on tuples. Once again, this is nearly identical to tuple matching in F#.
- Positional patterns: when you provide a Deconstruct() method to a type, you can use positional patterns to match against that class. The syntax is similar to tuple deconstruction/pattern matching.
- This is a very impactful feature, as it has fundamentally changed the syntax of certain key constructs in the C# language, possibly causing future programs written in C# to look like a functional language hybrid, rather than the more imperative C/C++ syntax of the original language.
Using declarations [F#]
- This is another feature "borrowed" from F#, basically operating the same way as the F# use keyword.
- It allows you to declare an instance of a type which implements IDisposable, which will disposed at the end of the enclosing scope.
Static local functions
- By prefixing local functions with the static keyword, you are instructing the compiler to prevent that function from capturing any values from the enclosing scope.
- I'm really not sure what value this adds since you could declare a static method instead, which is unit-testable.
Disposable ref structs
- ref structs and readonly ref structs can be made disposable by implementing an accessible void Dispose() method.
Nullable reference types [!]
- See: https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references
- This is a major change by the C# team in an effort to stamp out one of the most common and hated classes of errors: null reference exceptions.
- The compiler now allows you to set different nullable contexts, which may be your entire program or just a specific region of code.
- There are four "nullabilities," listed in order from most strict to most lenient:
- Within an enabled nullable annotation context:
- Reference types are non-nullable by default and can be dereferenced safely.
- Nullable reference types can be declared using the ? operator (just like old-school nullable [value] types)
- You can using the ! null-forgiving operator to explicitly tell the compiler that an expression is not null.
- My thoughts: this is a powerful addition, as it now eliminates the need for things like guard clauses and various other forms of defensive programming within nullable annotation contexts.
Asynchronous streams [!]
- See: https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/generate-consume-asynchronous-stream
- You can create and consume asynchronous streams, which are basically async enumerables.
- Asynchronous stream methods are declared with the async modifier and return IAsyncEnumerable<T>. From there on out it basically resembles a cross between a regular async method and old-school yield/return state machines from C#2.
- You enumerate the sequence by using await foreach.
Indices and ranges [!, F#]
- Yet another F#-ification feature.
- You can basically use sequence expressions in C# now. What's telling is even the use of the term "sequence" in the official docs. The syntax is nearly identical to sequence expressions in F#.
- For those of you who have not used F#, "sequence" is basically just functional-speak for an IEnumerable<T>.
- They've introduced System.Index, System.Range, the index from end operator ^, and the range operator ..
- You can use these sequence expressions with arrays, strings, Span<T>, and ReadOnlySpan<T>.
- This is more or less a sugary feature.
- You can use the null-coalescing assignment operator ??= to assign a value to the left-hand side of an expression only if the left-hand operand is null.
Unmanaged constructed types [C]
- This is a performance enhancement.
- Constructed value types are now unmanaged if it only contains fields of unmanaged types.
- This means that you can do things like allocate instances on the stack, etc.
Stackalloc in nested expressions [C]
- You can now use stackalloc inside nested expressions, if the ultimate result of the greater expression is of type System.Span<T> or System.ReadOnlySpan<T>.
- Honestly, I don't see this as a big game-changer in everyday web programming.
Enhancement of interpolated verbatim strings
- A minor enhancement. Prior to this, if you used interpolation in verbatim strings ([email protected]"my string…") the dollar sign always had to come first. Now you can have the @ come before the $.
There you have it—a quick run down on the language features of C#, versions 6 through 8. As stated in my comments, some features are more or less syntactic sugar or nice-to-haves, while other features are sweeping, paradigm-shifting changes to the language (and possibly even the CLR) that fundamentally alter the way you write code. I’ve identified two separate trends that I see happening with the language—the “C-ification” and “F#-ification” of C#. This is neither good nor bad. It’s just interesting. Take some time to read the official docs (which I’ve linked to above) to get a more in-depth understanding of these features. Then, go out there, fire up Visual Studio, and start writing some amazing code! Let me know how it goes.
This is entry #3 in the Foundational Concepts Series
If you want to view or submit comments you must accept the cookie consent.