Anytime I get the chance to write about C#, I'm eager to do so. This time was no System.Exception!
As of this writing, C# has been around for over 17 years now, and it is safe to say it's not going anywhere. The C# language team is constantly working on new features and improving the developer experience.
Are you keeping up with new developer technologies? Advance your IT career with our Free Developer magazines covering C#, Patterns, .NET Core, MVC, Azure, Angular, React, and more. Subscribe to the DotNetCurry (DNC) Magazine for FREE and download all previous, current and upcoming editions.
In this article, join me as I walk through the various versions of C# and share my favorite features from each release. I'll demonstrate the benefits while emphasizing on practicality.
C# Favorite Features – V1 to V7
C# Version 1.0
Version 1.0 (ISO-1) of C# was really barebones, there was nothing terribly exciting and it lacked many of the things that developers love about the language today. There is one particular feature that does come to mind however, that I would consider my favorite - Implicit and Explicit Interface Implementations.
Interfaces are used all the time and are still prevalent in modern C# today. Consider the following IDateProvider interface for example.
public interface IDateProvider
{
DateTime GetDate();
}
Nothing special, now imagine two implementations – the first of which is implicitly implemented as follows:
public class DefaultDateProvider : IDateProvider
{
public DateTime GetDate()
{
return DateTime.Now;
}
}
The second implementation is explicitly implemented as such:
public class MinDateProvider : IDateProvider
{
DateTime IDateProvider.GetDate()
{
return DateTime.MinValue;
}
}
Notice how the explicit implementation omits an access modifier. Also, the method name is written as IDateProvider.GetDate(), which prefixes the interface name as a qualifier.
These two things make the implementation explicit.
One of the neat things about explicit interface implementation is that it enforces consumers to rely on the interface. An instance object of a class that explicitly implements an interface does not have the interface members available to it – instead the interface itself must be used.
However, when you declare it as the interface or pass this implementation as an argument that is expecting the interface – the members are available as expected.
This is particularly useful as it enforces the use of the interface. By working directly with the interface, you are not coupling your code to the underlying implementation. Likewise, explicit interface implementations handle naming or method signature ambiguity – and make it possible for a single class to implement multiple interfaces that have the same members.
Jeffery Richter warns us about explicit interface implementations in his book CLR via C#. The two primary two concerns are that value type instances are boxed when cast to an interface and methods that are explicitly implemented, cannot be called by a derived type.
Keep in mind that boxing and unboxing carry computational expenses. As with all things programming, you should evaluate the use case to determine the right tool for the job.
C# Version 2.0
For reference I'll list all the features of C# 2.0 (ISO-2).
- Anonymous methods
- Covariance and contravariance
- Generics
- Iterators
- Nullable types
- Partial types
My favorite feature was a tossup between Generics and Iterators, I landed on Generics and here is why.
This was an extremely difficult choice for me and I ended up deciding on Generics because I believe that I use them more often than writing iterators. A lot of the SOLID programming principles are empowered by the adoption of generics in C#, likewise it helps keep code DRY. Don't get me wrong, I do write my fair share of iterators and that is a feature worth adopting in your C# today!
Let's look at Generics in more detail.
Editorial Note: Learn how to use Generics in C# to Improve Application Maintainability
Generics introduce to the .NET Framework the concept of type parameters, which make it possible to design classes and methods that defer the specification of one or more types until the class or method is declared and instantiated by client code.
Let's imagine that we have a class named DataBag that serves as, well… a bag of data. It might look like this:
public class DataBag
{
public void Add(object data)
{
// omitted for brevity...
}
}
At first look this seems like an awesome idea, because you can add anything in an instance of this bag of data object. But when you really think about what this means, it might be rather alarming.
Everything that is added is implicitly upcast to System.Object. Furthermore, if a value-type is added – boxing occurs. These are performance considerations that you should be mindful of.
Generics solve all this while also adding type-safety. Let's modify the previous example to include a type parameter T on the class and notice how the method signature changes too.
public class DataBag<T>
{
public void Add(T data)
{
// omitted for brevity...
}
}
Now for example, a DataBag<DateTime> instance will only allow the consumer to add DateTime instances. Type-safety, no casting or boxing…and the world is a better place.
Generic type parameters can also be constrained. Generic constraints are powerful and allow for a limited range of available type parameters, as they must adhere to the corresponding constraint. There are several ways to write generic type parameter constraints, consider the following syntax:
public class DataBag<T> where T : struct { /* T is value-type */ }
public class DataBag<T> where T : class { /* T is class, interface, delegate or array */ }
public class DataBag<T> where T : new() { /* T has parameter-less .ctor() */ }
public class DataBag<T> where T : IPerson { /* T inherits IPerson */ }
public class DataBag<T> where T : BaseClass { /* T derives from BaseClass */ }
public class DataBag<T> where T : U { /* T inherits U, U is also generic type parameter */ }
Multiple constraints are permitted and are comma delimited. Type parameter constraints are enforced immediately, i.e. compilation errors prevent programmer error. Consider the following constraint on our DataBag<T>.
public class DataBag<T> where T : class
{
public void Add(T value)
{
// omitted for brevity...
}
}
Now, if I were to attempt to instantiate a DataBag<DateTime>, the C# compiler will let me know that I have done something wrong. More specifically it states:
The type 'DateTime' must be a reference type in order to use it as parameter 'T' in the generic type or method 'Program.DataBag<T>'
C# Version 3.0
Here is a listing of the major features of C# 3.0.
- Anonymous types
- Auto implemented properties
- Expression trees
- Extension methods
- Lambda expression
- Query expressions
I was teetering on the edge of choosing Extension Methods over Lambda Expressions. However, when I think about the C# that I write today – I literally use the lambda operator more than any other C# operator in existence.
I love writing expressive C#, I cannot get enough of it.
In C# there are many opportunities to leverage lambda expressions and the lambda operator. The => lambda operator is used to separate the input on the left from the lambda body to the right.
Some developers like to think of lambda expressions as really being a less verbose way of expressing delegation invocation. The types Action, Action<in T, …>, Func<out TResult>, Func<in T, …, out TResult> are just pre-defined generic delegates in the System namespace.
Let's start with a problem that we are trying to solve and apply lambda expressions to help us write some expressive and terse C# code, yeah?!
Imagine that we have a large number of records that represent trending weather information. We may want to perform some various operations on this data, and instead of iterating through it in a typical loop, for, foreach or while – we can approach this differently.
public class WeatherData
{
public DateTime TimeStampUtc { get; set; }
public decimal Temperature { get; set; }
}
private IEnumerable<WeatherData> GetWeatherByZipCode(string zipCode) { /* ... */ }
Being that the invocation of GetWeatherByZipCode returns an IEnumerable<WeatherData> it might seem like you'd want to iterate this collection in a loop. Imagine we have a method to calculate the average temperature, and it does this work.
private static decimal CalculateAverageTemperature(
IEnumerable<WeatherData> weather,
DateTime startUtc,
DateTime endUtc)
{
var sumTemp = 0m;
var total = 0;
foreach (var weatherData in weather)
{
if (weatherData.TimeStampUtc > startUtc &&
weatherData.TimeStampUtc < endUtc)
{
++ total;
sumTemp += weatherData.Temperature;
}
}
return sumTemp / total;
}
We declare some local variable to store the sum of all the temperatures that fall within our filtered date range and a total of them, to later calculate an average. Within the iteration is a logical if block which checks if the weather data is within a specific date range. This could be re-written as follows:
private static decimal CalculateAverageTempatureLambda(
IEnumerable<WeatherData> weather,
DateTime startUtc,
DateTime endUtc)
{
return weather.Where(w => w.TimeStampUtc > startUtc &&
w.TimeStampUtc w.Temperature)
.Average();
}
As you can see, this is dramatically simplified. The logical if block was really just a predicate, if the weather date was within range we'd continue into some additional processing – like a filter. Then we were summing the temperature, so we really just needed to project (or select) this out. We ended up with a filtered list of temperatures IEnumerable<Decimal> that we can now simply invoke Average on.
The lambda expressions are used as arguments to the Where and Select extension methods on the generic IEnumerable<T> interface. Where takes a Func<T, bool> and Select takes a Func<T, TResult>.
C# Version 4.0
C# 4.0 was a smaller in terms of the number of major features from its previous versions releases.
- Dynamic binding
- Embedded interop types
- Generic covariant and contravariant
- Named/optional arguments
All of these features were and still are very useful. But for me it came down to named and optional arguments over covariance and contravariance in generics. Between these two, I debated which feature I use most often and which one has truly benefited me the most as a C# developer through the years.
I believe that feature to be named and optional arguments. It is such a simple feature, but it scores many points for being practical. I mean, who has not written a method with an overload or an optional parameter?
When you write an optional parameter, you must provide a default value for it. If your parameter is a value-type this must be a literal or constant value, or you can use the default keyword. Likewise, you could declare the value-type as Nullable and assign it null. Let us imagine that we have a Repository, and there is a GetData method.
public class Repository
{
public DataTable GetData(
string storedProcedure,
DateTime start = default(DateTime),
DateTime? end = null,
int? rows = 50,
int? offSet = null)
{
// omitted for brevity...
}
}
As we can see, this method's parameter list is rather long – but there are several assignments. This indicates that these values are optional. As such, the caller can omit them and the default value will be used. As you might assume, we can invoke this by only providing the storedProcedure name.
var repo = new Repository();
var sales = repo.GetData("sp_GetHistoricalSales");
Now that we have familiarized ourselves with the optional arguments feature and how those work, let's use some named arguments here. Take our example from above, and imagine that we only want our data table to return 100 rows instead of the default 50. We could change our invocation to include a named argument and pass the desired overridden value.
var repo = new Repository();
var sales = repo.GetData("sp_GetHistoricalSales", rows: 100);
C# Version 5.0
Like C# version 4.0, there were not a lot of features packed into C# version 5.0 – but of the two features one of them was massive.
- Async / Await
- Caller info attributes
When C# 5.0 shipped, it literally altered the way that C# developers wrote asynchronous code. Still today there is much confusion about it and I'm here to reassure you that it's much simpler than most might think. This was a major leap forward for C# – and it introduced a language-level asynchronous model which greatly empowers developers to write "async" code that looks and feels synchronous (or at the very least serial).
Asynchronous programming is very powerful when dealing with I/O bound workloads such as interacting with a database, network, file system, etc. Asynchronous programming helps with throughput by utilizing a non-blocking approach. This approach instead uses suspension points and corresponding continuations in a transparent async state machine.
Likewise, if you have heavy workloads for CPU bound computations, you might want to consider doing this work asynchronously. This will help with the user experience as the UI thread will not be blocked and is instead free to respond to other UI interactions.
Editorial Note: Here’s a good tut on some Best Practices on Asynchronous Programming in C# using Async Await www.dotnetcurry.com/csharp/1307/async-await-asynchronous-programming-examples.
With C# 5.0, asynchronous programming was simplified when the language added two new keywords, async and await. These keywords worked on Task and Task types. The table below will serve as a point of reference:
Task and Task classes represent asynchronous operations. The operations can either return a value via Task or return void via Task. When you modify a Task returning method with the async keyword, it enables the method body to use the await keyword. When the await keyword is evaluated, the control flow is returned back to the caller – and execution is suspended at that point in the method. When the awaited operation completes, execution is then resumed at that same point. Time for some code!
class IOBoundAsyncExample
{
// Yes, this is the internet Chuck Norris Database of jokes!
private const string Url = "http://api.icndb.com/jokes/random?limitTo=[nerdy]";
internal async Task<string> GetJokeAsync()
{
using (var client = new HttpClient())
{
var response = await client.GetStringAsync(Url);
var result = JsonConvert.DeserializeObject(response);
return result.Value.Joke;
}
}
}
public class Result
{
[JsonProperty("type")] public string Type { get; set; }
[JsonProperty("value")] public Value Value { get; set; }
}
public class Value
{
[JsonProperty("id")] public int Id { get; set; }
[JsonProperty("joke")] public string Joke { get; set; }
}
We define a simple class with a single method in it named GetJokeAsync, it is at this point we discover that laughter is imminent. The method is Task<string> returning and this signifies to the reader that our GetJokeAsync method will eventually give you a string – or possibly error out.
The method is modified with the async keyword, which enables the use of the await keyword. We instantiate and use an HttpClient object. We then invoke the GetStringAsync function, which takes a string url and returns a Task<string>. We await the Task<string> returned from the GetStringAsync invocation.
When the response is ready a continuation occurs and control resumes from where we were once suspended. We then deserialize the JSON into our Result class instance and return the Joke property.
A Few of My Favorite Outputs
- Chuck Norris can unit test entire applications with a single assert.
- Chuck Norris can compile syntax errors.
- Project managers never ask Chuck Norris for estimations... ever.
Hilarity ensues! And we learned about C# 5's amazing asynchronous programming model.
C# Version 6.0
There were lots of great advancements with the introduction of C# 6.0 and it was hard to choose my favorite feature.
- Dictionary initializer
- Exception filters
- Expression bodied members
- nameof operator
- Null propagator
- Property initializers
- Static imports
- String interpolation
I narrowed it down to three standout features: String interpolation, Null propagator and nameof operator.
While the nameof operator is awesome and I literally use it nearly every single time I'm writing code, the other two features are more impactful. That left me to decide between string interpolation and null propagation, which was rather difficult. I decided I liked string interpolation the best and here is why.
Null propagation is great and it allows me to write less verbose code, but it doesn't necessarily prevent bugs in my code. However, with string interpolation, runtime bugs can be prevented – and that is a win in my book.
String interpolation syntax in C# is enabled when you start a string literal with the $ symbol. This instructs the C# compiler that you intend to interpolate this string with various C# variables, logic or expressions. This is a major upgrade from manual string concatenation and even the string.Format method. Consider the following:
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public override string ToString()
=> string.Format("{0} {1}", FirstName);
}
We have a simple Person class with two name properties, for their first and last name. We override the ToString method and use string.Format. The issue is that while this compiles, it is error prone as the developer clearly intended to have the last name also part of the resulting string – as evident with the "{0} {1}" argument. But they failed to pass in the LastName. Likewise, the developer could have just as easily swapped the names or supplied both name arguments correctly but messed up the format literal to only include the first index, etc… now consider this with string interpolation.
class Person
{
public string FirstName { get; set; } = "David";
public string LastName { get; set; } = "Pine";
public DateTime DateOfBirth { get; set; } = new DateTime(1984, 7, 7);
public override string ToString()
=> $"{FirstName} {LastName} (Born {DateOfBirth:MMMM dd, yyyy})";
}
I took the liberty of adding a DateOfBirth property and some default property values. Additionally, we are now using string interpolation in our override of the ToString method. As a developer it would be much more difficult to make any of the aforementioned mistakes. Finally, I can also do formatting within the interpolating expressions themselves. Take notice of the third interpolation, the DateOfBirth is a DateTime – as such we can use all the standard formatting that you're accustomed to already. Simply use the : operator to separate the variable and the format.
Example Output
· David Pine (Born July 7, 1984)
Editorial Note: For a detailed tut on the new features of C# 6.0, read www.dotnetcurry.com/csharp/1042/csharp-6-new-features
C# Version 7.0
From all of the features packed into C# 7.0.
- Expanded expression bodied members
- Local functions
- Out variables
- Pattern matching
- Ref locals and returns
- Tuples and deconstruction
I ended up debating between Pattern Matching, Tuples and out Variables. I ended up choosing out Variables and here's why.
Pattern Matching is great but I really don't find myself using it that often, at least not yet. Maybe I'll use it more in the future, but for all of the C# code that I've written thus far, there aren't too many places where I'd leverage this. Again, it's an amazing feature and I do see a place for it – just not my favorite of C# 7.0.
Tuples are a great addition as well. Tuples serve such an important part of the language and becoming a first class citizen is awesome. I would say that "gone are the days of .Item1, .Item2, .Item3, etc… but that isn't necessarily true. Deserialization loses the tuple literal names making this less public API worthy.
I'm also not a fan of the fact that the ValueTuple type is mutable. I just do not understand that design decision. I hope that someone can explain it to me, but it feels kind of like an oversight. Thus, I landed on the out Variables feature.
The try-parse pattern has been around since C# version 1.0 on various value-types. The pattern is as follows:
public boolean TryParse(string value, out DateTime date)
{
// omitted for brevity...
}
The function returned a boolean, indicating whether the given string value was able to be parsed or not. When true the parsed value was assigned to the resulting out parameter date. It was consumed as follows:
DateTime date;
if (DateTime.TryParse(someDateString, out date))
{
// date is now the parsed value
}
else
{
// date is DateTime.MinValue, the default value
}
This pattern is useful, however somewhat a nuisance. Sometimes developers take the same course of action regardless of whether the parse was successful or not. Sometimes it's fine to use a default value. The out variable in C# 7.0 makes this a lot more compound and in my opinion less complex.
Consider the following:
if (DateTime.TryParse(someDateString, out var date))
{
// date is now the parsed value
}
else
{
// date is DateTime.MinValue, the default value
}
Now we removed the outer declaration atop the if block and we in lined the declaration as part of the argument itself. It is legal to use var as the type is already known. Finally, the scope of the date variable hasn't changed. It leaks from its inline declaration back out to the top of the if block.
You might be asking yourself, "why would this be one of his favorite features?"…It kind of feels like not a whole lot has really changed.
But this changes everything!
It allows our C# to be more expressive. Everyone loves extension methods, right – consider the following:
public static class StringExtensions
{
private delegate bool TryParseDelegate<T>(string s, out T result);
private static T To<T>(string value, TryParseDelegate<T> parse)
=> parse(value, out T result) ? result : default;
public static int ToInt32(this string value)
=> To<int>(value, int.TryParse);
public static DateTime ToDateTime(this string value)
=> To<DateTime>(value, DateTime.TryParse);
public static IPAddress ToIPAddress(this string value)
=> To<IPAddress>(value, IPAddress.TryParse);
public static TimeSpan ToTimeSpan(this string value)
=> To<TimeSpan>(value, TimeSpan.TryParse);
}
This extension method class is terse, expressive and powerful. After defining a private delegate that follows the try-parse pattern we can write a generic compound <T> function, that takes a generic type argument, the string value to parse and the TryParseDelegate<T>. Now we can safely rely on these extension methods, consider the following:
public class Program
{
public static void Main(string[] args)
{
var str =
string.Join(
"",
new[] { "James", "Bond", " +7 " }.Select(s => s.ToInt32()));
Console.WriteLine(str); // prints "007"
}
}
Editorial Note: To get an overview of all the new features of C# 7, check this tutorial www.dotnetcurry.com/csharp/1286/csharp-7-new-expected-features
Conclusion
This article was rather challenging for me personally. I love so many features of C# that it was really difficult to narrow down just one favorite for each release.
Each newer version of C# is packed full of powerful and impactful features. The C# language team has been innovating in countless ways – one of which is the introduction of point releases. At the time of writing, C# 7.1 and 7.2 have officially shipped. As C# developers we are living in an exciting time for the language!
Sorting through all these features was rather insightful for me however; as it helped to shed some light on what is practical and most impactful with my day-to-day development. As always, strive to be a pragmatic developer! Not every feature that is available in a language is necessary for the task at hand, but it is important to know what is available to you.
As we look forward to the proposals and prototypes of C# 8, I'm excited for the future of C#. It seems promising indeed and the language is actively attempting to alleviate "the billion dollar mistake".
Resources
This article was technically reviewed by Yacoub Massad.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
David Pine is a Technical Evangelist and
Microsoft MVP working at
Centare in Wisconsin. David loves knowledge sharing with the technical community and
speaks regionally at meetups, user groups, and technical conferences. David is passionate about sharing his thoughts through writing as well and actively maintains a blog at
davidpine.net. David's posts have been featured on
ASP.NET,
MSDN Web-Dev,
MSDN.NET and
DotNetCurry.com.David loves contributing to
open - source projects and
stackoverflow.com as another means of giving back to the community.David sat on the technical board and served as one of the primary organizers of MKE DOT NET for three years. When David isn’t interacting with a keyboard, you can find him spending time with his wife and their three sons, Lyric, Londyn and Lennyx. Follow David on Twitter at
@davidpine7 .