Before I dive into the matter at hand...
Motivation
I am perfectly aware that there are lots of articles and official documentations on these topics (and I will link to some of them), but I would like to have my own go at it, with a one stop place for everything I consider interesting in the evolution of the C# language I ended up using and loving so much. Well, at least in regards to the latest major version and its corresponding minor versions, 7 (at the time of writing), but also a sneak peek into what might come with version 8 which is still in preview for now.
Also, since I'm at the beginning of the technical blogging road, I need to inject this discipline of writing into my life and mindset. Practice makes perfect, or as close to perfect as it can get, but it will always be a desired target. And by this ideal, I will never want to treat subjects trivially or to not bring something extra to the table, like more examples, thoughts, experiences, gotchas, tips & tricks, or even compiling and structuring existing content in a new way that makes sense for me and probably for you too. So from the start I will not want to waste either my time, the writer, or yours, the reader with meaningless information.
And last but not least, I will want to write an accompanying article for some of the presentations / workshops / courses I deliver in various circumstances. Either because there are some details that would not fit in the allocated time slot, or for the people that wanted to attend but couldn't, or for the ones that did see it live and needed to refresh their memories, either way everyone is welcome.
This particular post is related to a series of short, periodical technical presentations, here at Accesa, where I currently work. I eagerly took the responsibility for these events one year ago when the idea was suggested to me and it's high time for a revival, since shamefully I have to admit that the last months of 2018 were quite quiet from this point of view. It seems like every client needs something with a high priority label attached to it as the year is close to the end. But by no means am I blaming them. And I'm sure that we can learn how to manage our short windows of free time better in order to keep on sharing the experience and illuminate more minds. But in the end the operational time takes #1 priority, so no surprises there.
You can find more posts related to presentations here: https://emilcraciun.net/tag/presentation/
Contents
Now that the introduction (tl;dr;) part is over, here's what you'll find next in this article:
- A very short history of the C# language from 1.0 to 6.0
- New features in C# 7.0, 7.1, 7.2, 7.3
- New features in C# 8.0 preview
You can find all the demos in this article in the following repository: https://github.com/ecraciun/CSharp7and8NewFeatures
First, a short rewind
C# 1.0
Released on January 2002 | .NET Framework 1.0 | Visual Studio .NET 2002
Well everything has to start somewhere, so 1.0 got the basics down:
C# 2.0
Released on November 2005 | .NET Framework 2.0 | Visual Studio 2005
- Generics
- Partial types
- Anonymous methods
- Nullable types
- Iterators
- Covariance and contravariance
- Getter/setter separate accessibility
- Method group conversions (delegates)
- Static classes
- Delegate inference
C# 3.0
Released on November 2007 | .NET Framework 2.0, 3.0, 3.5 | Visual Studio 2008, 2010
- Auto-implemented properties
- Anonymous types
- Query expressions
- Lambda expressions
- Expression trees
- Extension methods
- Implicitly typed local variables
- Partial methods
- Object and collection initializers
C# 4.0
Released on April 2010 | .NET Framework 4.0 | Visual Studio 2010
C# 5.0
Released on August 2012 | .NET Framework 4.5 | Visual Studio 2012, 2013
C# 6.0
Released on July 2015| .NET Framework 4.6 | Visual Studio 2015
- Static imports
- Exception filters
- Auto-property initializers
- Expression bodied members
- Null propagator
- String interpolation
- nameof operator
- Index initializers
- Await in catch/finally blocks
- Default values for getter-only properties
For more details about C# 6.0 go here: https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-6
Well, that's it for this short enumeration of what happened in every major version of the C# language up until version 6.
For more info about C# history: https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history
C# 7.x new features
So here we get to the present times, or actually, since 8.0 is around the corner, 7.x will soon be in the previous versions list also. But until then, let's take a deeper dive into what changes have been brought to the fingertips of eager and tech-savvy developers.
C# 7.0
Released on March 2017 | .NET Framework 4.6.2 | Visual Studio 2017
The major release came along with these changes:
out variables
Isn't it just cumbersome and more work to declare a variable before calling a function that has an out parameter? Well now you can declare the out variable (which can be also implicitly typed) in the parameter list of the method call you want to make, and you don't even need to initialize it, making your great code even better.
var input = Console.ReadLine();
if (int.TryParse(input, out var value))
{
var message = value % 2 == 0 ? "even" : "odd";
Console.WriteLine($"The number is {message}");
}
Tuples
Firstly, to use this feature you will need to download and install the System.ValueTuple nuget package in the project you're working on. It's not like tuples weren't available until 7.0, but they had very limited support, and you could only access its members using the default property names: Item1, Item2, ... ItemN.
Tuples now are basically lightweight data structures that contain multiple custom named fields, which are not validated, and which represent the data members. No methods can be defined on them.
So let's see how can we create tuples:
// No member names, defaults to old tuple behavior
var unnamed = ("Joe", "has", 3, "apples");
unnamed.Item3 = 6;
// Left-hand side assignment tuple member naming
(string FirstName, string LastName) fullName = ("John", "Doe");
Console.WriteLine($"Hi, I'm {fullName.FirstName} {fullName.LastName}");
// Right-hand side assignment tuple member naming
var car = (Model: "i3", Make: "BMW");
car.Model = "i8";
// And an example where you want to return multiple values from a method
private static (int Result, int Remainder) DivideNumbers(int first, int second)
{
var result = first / second;
var remainder = first - (second * result);
return (result, remainder);
}
// and you can have a call like this
var result = DivideNumbers(5, 2);
Console.WriteLine($"{result.Result} {result.Remainder}");
This one I really loved from the start, because at the time when it came out, I was working on a project that had some areas in which tuples were used, so I went about and refactored everything to make the code more readable. No need to create structs or classes, you just type as you go and have your instant box of whatever-you-want!
There is still one little thing left to add here and that's the deconstruction functionality. Say you want a separate variable for each member of the tuple:
(int c, int r) = DivideNumbers(17, 5);
But you can also add this deconstruction to any type in .NET, by creating a Deconstruct method as a member of the type, that has an out parameter for each property you want to extract.
class Rectangle
{
public int L { get; }
public int H { get; }
{...}
public void Deconstruct(out int l, out int h)
{
h = H;
l = L;
}
}
// In the calling code
var rectangle = new Rectangle(2, 4);
(int length, int height) = rectangle;
Discards
Now that I made an introduction to the Deconstruction feature just above, there might be times when you don't want or need all the values returned. But you still have to provide a variable for those values, right? Wrong!
_ to the rescue! (yes, it's just an underscore character) This is the discard variable with its predefined name: _. This is a write only variable and you can use it as the recycle bin for any values and value types you simply don't care about.
Here are the supported scenarios where you can use the discards:
// 1. When deconstructing
var r = new Rectangle(2, 4);
(int length, _) = r;
// 2. When calling methods with out parameters
r.GetRectangleInfo2(out _, out _, out var a);
// 3. In pattern matching with is or switch statements
if(obj is Rectangle _) // yes, you could ommit the discard alltogether
{ ... }
// 4. As a standalone identifier
_ = Task.Run(() =>
{
Console.WriteLine("Hello from a task");
});
If you want more details, see Discards.
Pattern Matching
This is another useful feature, which allows you to implement method dispatch on properties other than the type of an object and for types of data that are not in a hierarchical relationship. You can achieve this through is and switch expressions, also combining them with the when keyword if you want to describe extra rules to the pattern.
Here is an example of the is pattern expression, which extends the is operator with query capabilities beyond an object's type. Oh yeah, and you can also directly initialize variables with it:
foreach (var value in values)
{
if (value is int number)
{
sum += number;
}
}
And extending the match expression of the switch statement, we have the following example:
switch (item)
{
case 0: // constant
break;
case int val: // integer and variable initialized
sum += val;
break;
case IEnumerable<object> subList when subList.Any(): // sub list containing elements
sum += MoreSumInts(subList);
break;
case IEnumerable<object> subList: // empty sub list
break;
case null: // a null object
break;
default: // a type we do not recognize in this example
Console.WriteLine("unknown item type");
break;
}
More details on Pattern Matching here.
ref locals and returns
You can use this feature when you need to return references to variables defined somewhere else. Let's assume a very naïve example of a function returning the first odd number found in an array of integers:
private ref int FindFirstEvenValueInArrayAsRef(int[] array)
{
for (int i = 0; i < array.Length; i++)
{
if (array[i] % 2 == 0)
{
return ref array[i];
}
}
throw new InvalidOperationException("Not found");
}
ref var first = ref FindFirstEvenValueInArrayAsRef(array);
first = 99; // this actually changes the value in the original array
var first2 = FindFirstEvenValueInArrayAsRef(array);
first2 = 99; // this does not change the value in the array, since it wasn't specified as a ref
Some things you should know:
- you must initialize the ref variable when it's declared
- you cannot assign a standard method return value to a ref local variable
- you cannot return a ref to a variable that does not have its lifetime extended beyond the execution of the method
- ref locals can't be used in async methods
Local Functions
There are times when you need to extract a method from another method, all part of normal refactoring practices and keeping the code to a high degree of quality and readability. But sometimes these extracted methods are used only in one specific context. So instead of adding another (private) method to a class, you can add the method inside the method that uses it.
public int DoSomething()
{
// some code
var result = DoSubSomething(x);
return result;
int DoSubSomething(int x)
{
// some extracted code
}
}
More expression-bodied members
Building on C# 6.0's expression-bodied members, 7.0 adds implementations for: constructors, finalizers, get and set property/indexers accessors.
internal class ExpressionMembersExample
{
// Expression-bodied constructor
public ExpressionMembersExample(string label) => this.Label = label;
// Expression-bodied finalizer
~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!");
private string label;
// Expression-bodied get / set accessors.
public string Label
{
get => label;
set => this.label = value ?? "Default label";
}
}
throw Expressions
This one I have been using a lot since it was released. Since the throw has always been a statement, its use was a bit limited. Now that it can be used as an expression, you will be able to write them in a conditional expression for example:
public class Foo
{
private readonly IDependency _dep;
public Foo(IDependency dep)
{
_dep = dep ?? throw new ArgumentNullException(nameof(dep));
}
}
Numeric literal syntax improvements
Well this is something that I would call sugar coating, and I don't mean this in a bad way. It's a small improvement that helps when writing and reading...well numbers.
So now we have binary literals:
public const int One = 0b0001;
public const int Sixteen = 0b0001_0000;
And a new digit separator, the _ underscore that we can use how we want:
public const long BillionsAndBillions = 100_000_000_000;
public const double Pi = 3.141_592_653_589;
More details and a complete list of new features: https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-7
C# 7.1
Released on August 2017 | .NET Framework 4.7 | Visual Studio 2017 version 15.3
async Main method
Well it was about time I'd say! All of those demo console projects which have somewhere within them some async methods can now be full class async citizens with the introduction of the async Main method. Of course, this helps real world scenarios also, not just demos :P.
static async Task Main(string[] args)
{
await DoSomeAsyncStuff();
}
// or
static async Task<int> Main(string[] args)
{
return await DoSomeAsyncStuff();
}
default literal expressions
You can now omit the type of the right-hand side default value expression:
Func<string, bool> whereClause = default; // not default(Func<string,bool>);
You can find out more about default value expressions here.
Inferred tuple element names
Building on the new tuples from C# 7.0, you can now create tuples from variables, and that tuple's member names will be inferred from the original variable names.
var newPair = (count, label); // element names are "count" and "label"
newPair.count = 10;
More details and a complete list of new features: https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-7-1
C# 7.2
Released on November 2017 | .NET Framework 4.7.1 | Visual Studio 2017 version 15.5
Leading underscores in numeric literals
Well this is another small change, which lets you start a binary or hex number with an underscore. So this would be perfectly ok:
int binaryValue = 0b_110_000;
private protected access modifier
This is a new compound access modifier that indicates that a member may be accessed by containing class or derived classes that are declared in the same assembly.
private protected class Test
{
private protected int a;
}
// same assembly
private protected class SimpleTest : Test
{
public void DoSomething()
{
a = 10; // allowed
}
}
Here are some more details on access modifiers.
Conditional ref expressions
In case of a conditional expression producing a ref result and not a value result, you can assign the returned value to a ref variable.
ref var r = ref (arr != null ? ref arr[0] : ref otherArr[0]);
More details and a complete list of new features: https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-7-2
C# 7.3
Released on May 2018 | .NET Framework 4.7.2 | Visual Studio 2017 version 15.7
ref local variables may be reassigned
A pretty straight forward change with this addition: now you can later reassign an initialized ref variable to another instance.
ref int first = ref GetFirstNumber(array: someArray, isEven: true);
first = ref GetFirstNumber(array: someArray, isEven: false);
Enhanced generic constraints
System.Enum and System.Delegate can now be used as base class constraints for a type parameter. Also, there is another new type of constraint called unmanaged which specifies that the type parameter is not a reference type and it does not contain any reference types at any level of nesting.
public class MySpecialClass<A, B, C, D, E, F, G, H, I>
where A : class
where B : struct
where C : System.Enum
where D : System.Delegate
where E : unmanaged
where F : new()
where G : AnotherClass
where H : ISomeInterface
where I : A
{ }
Tuples support == and !=
Another nice upgrade to the tuples is the support for the == and != operators.
var left = (a: 5, b: 10);
var right = (a: 5, b: 10);
Console.WriteLine(left == right); // displays 'true'
left = (a: 5, b: 10);
right = (a: 5, b: 10);
(int a, int b)? nullableTuple = right;
Console.WriteLine(left == nullableTuple); // Also true
// lifted conversions
left = (a: 5, b: 10);
(int? a, int? b) nullableMembers = (5, 10);
Console.WriteLine(left == nullableMembers); // Also true
// converted type of left is (long, long)
(long a, long b) longTuple = (5, 10);
Console.WriteLine(left == longTuple); // Also true
// comparisons performed on (long, long) tuples
(long a, int b) longFirst = (5, 10);
(int a, long b) longSecond = (5, 10);
Console.WriteLine(longFirst == longSecond); // Also true
(int a, string b) pair = (1, "Hello");
(int z, string y) another = (1, "Hello");
Console.WriteLine(pair == another); // true. Member names don't participate.
Console.WriteLine(pair == (z: 1, y: "Hello")); // warning: literal contains different member names
(int, (int, int)) nestedTuple = (1, (2, 3));
Console.WriteLine(nestedTuple == (1, (2, 3)));
More on tuples equality here.
in method overload resolution tiebreaker
When you had a situation like the following you would cause an ambiguity, but not anymore thanks to this version's bug fix:
public void Foo(Bar x);
public void Foo(in Bar x);
Just as a small note here, the in keyword causes the parameters to be passed in by reference, but they cannot be modified by the called method, unlike ref that may be modified, or out that must be modified. More details about the in parameter modifier here.
More details and a complete list of new features: https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-7-3
C# 8.0 preview new features
Probably will be released at Build 2019 | .NET Framework 4.8 | Visual Studio 2019 preview
And we finally get to the goodies part, the ones that may get included in the next major version of the language. So let's see what we have in store.
Nullable reference types
This is going to be a full game changer in time. What do they mean by nullable reference types? Aren't reference types nullable by design, since the beginning of time? Well, the answer is yes, but it's time to shake the foundations a bit. Because who likes those pesky null reference exceptions?
Since this is such a big change, it won't be enabled by default, you will have to opt-in at the following levels:
- project - just add the following XML element in the .csproj file in the <PropertyGroup> element that contains the target framework setting
<NullableContextOptions>enable</NullableContextOptions>
- file - with the new pragma: #nullable enable which sets the nullable context to enabled, and #nullable disable which of course disables the nullable context
So once we have the nullable context enabled one way or another:
string s = null; // this line would cause a warning stating that 'Converting null literal or possible null value to non-nullable type'
string? s2 = null; // but this would be totally fine
There are a lot of discussions and explanations for the reasoning behind this big change. I won't reproduce them here since it would bring no extra value yet (since I have to get dirty myself with this in order to formulate a more founded opinion), but of course I will link to some of them. But from a bird's eye view, showing your intent that a type is nullable or not, will probably make your code less error-prone. Indeed the null value is not an object and should not be treated or used as such. So now we have a way to specify what can and cannot be null.
You can find a more detailed example here. And more information here, or here.
Async streams
Asynchronous streams bring async and enumerables together. A new type, IAsyncEnumerable<T> has been added, which is exactly what it says it is, an asynchronous IEnumerable<T>, which can be iterated asynchronously by placing an await before the foreach statement.
async IAsyncEnumerable<int> GetEvenNumbersAsync()
{
await foreach (var number in GetNumbersAsync())
{
if (number % 2 == 0) yield return number;
}
}
Ranges and indices
Firstly, there is a new Index type that can be used for, well... obviously indexing. You can create an index from an integer, and specify if it's either form the beginning or the end like this:
Index i1 = 2; // number 2 from the beginning
Index i2 = ^3; // number 3 from the end
int[] a = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Console.WriteLine($"{a[i1]}, {a[i2]}"); // "2, 7"
Secondly, another new type has been added, Range which contains two indexes, one for the start, and one for the end of course of the range we want to define. This range can also be written as start..end range expression. Here are some examples:
Range range = 1..4;
foreach (var name in names[range])
{...}
// or
foreach (var name in names[1..^1]) // from index 1 to 1 from the end
{...}
// or
foreach (var name in names[..^2]) // equivalent with [0..^2]
{...}
// or
foreach (var name in names[1..]) // equivalent with [1..^0]
{...}
// or
foreach (var name in names[0..^0]) // basically the whole array, from start to end
{...}
Default implementations of interface members
Modifying an interface later on in the lifetime of a project is quite tricky, especially if you want to add a new method, and even more, if there are a lot of dependencies on that interface. Now you can add a new method and give it a default implementation, so any class down the road is not obligated to do the same if it doesn't need to do so. But you won't be able to test this out yourself just yet, because the feature is still proposed (see details here).
// but it might end up looking something like this
interface ILogger
{
void Log(LogLevel level, string message);
void Log(Exception ex) => Log(LogLevel.Error, ex.ToString()); // New overload
}
class ConsoleLogger : ILogger
{
public void Log(LogLevel level, string message) { ... }
// Log(Exception) gets default implementation
}
Recursive patterns
A pattern can contain another pattern in itself. In the following example the pattern checks if the object is of type Student firstly, then goes deeper and gets only the ones that have not yet graduated and have a non null name value, which is returned.
foreach (var p in people)
{
if (p is Student { Graduated: false, Name: string name }) yield return name;
}
Switch expressions
Bringing the switch statement some much deserved updates, you will now be able to write code that looks like this:
var area = figure switch
{
Line _ => 0,
Rectangle r => r.Width * r.Height,
Circle c => Math.PI * c.Radius * c.Radius,
_ => -1
};
Or even add more refiners to the cases like:
static string Display(object o) => o switch
{
Point p when p.X == 0 && p.Y == 0 => "origin",
Point p => $"({p.X}, {p.Y})",
_ => "unknown"
};
There are several changes:
- the switch keyword is now between the value that's being tested and the {} code block that contains the cases
- the case and : pair have been changed to lambda arrow => expressions
- the default case has been replaced with the _ discard pattern
- the bodies are expressions, and their bodies become the result now of the switch case
Target-typed new-expressions
You will not be able to test this out in VS2019 yet, but now you don't need to specify the type twice (when declaring a variable and instantiating it), because the type can be inferred from the context. So you would be able to do something like this if the feature passes the prototype phase (see details here).
Point[] points = { new (1, 2), new (2,2) };
Dictionary<string, List<int>> field = new () {
{ "item1", new() { 1, 2, 3 } }
};
Property patterns
When matching a value, you can now dig into deeper properties of the object and create more complex patterns.
static string Display4(object o) => o switch
{
Point { X: 0, Y: 0 } => "origin",
Point { X: var x, Y: var y } => $"({x}, {y})",
{ } => o.ToString(), // not null
null => "null"
};
Positional patterns
We can take advantage of the deconstructors and tuples, if a matched type has or is one of these, so that we can write patterns more compactly.
static string Display5(object o) => o switch
{
MyPoint (0, 0) => "origin",
MyPoint (var x, var y) => $"({x}, {y})",
_ => "unknown"
};
Tuple patterns
Continuing on the positional patterns from above, another case where these can be applied is with tuples, where you can test multiple inputs at the same time.
static State ChangeState(State current, Transition transition, bool hasKey) =>
(current, transition, hasKey) switch
{
(State.Opened, Transition.Close, _) => State.Closed,
(State.Closed, Transition.Open, _) => State.Opened,
(State.Closed, Transition.Lock, true) => State.Locked,
(State.Locked, Transition.Unlock, true) => State.Closed,
_ => throw new InvalidOperationException($"Invalid transition")
};
Using declarations
In case you dislike extra code nesting, there is one thing you can do now to avoid at least one situation in which the nesting would increase, and I'm talking about the using statements. You have the option to declare a variable that you want to be disposed at the end of the current statement block with the using keyword in front.
public void WriteHelloWorld()
{
using var sw = new StreamWriter(File.Open("example.txt"));
sw.WriteLine("Hello world");
} // sw will be disposed here
More details:
- https://blogs.msdn.microsoft.com/dotnet/2018/11/12/building-c-8-0/
- Updates: https://blogs.msdn.microsoft.com/dotnet/2019/01/24/do-more-with-patterns-in-c-8-0/
- Try out: https://blogs.msdn.microsoft.com/dotnet/2018/12/05/take-c-8-0-for-a-spin/
Conclusion
Wow, this was a long one indeed. I guess I started out in full force. And I achieved what I wanted with this article, getting almost every change in C# 7 and C# 8 in one place with examples, explanations, comments and references to further resources.
I hope you enjoyed the read and if you have any constructive suggestions or comments feel free to voice your thoughts below.
Even more resources
- General info & history: https://en.wikipedia.org/wiki/C_Sharp_(programming_language)
- The official repository of the C# language design: https://github.com/dotnet/csharplang