Another year, another C# update. This time we’re up for already C# 8. I want to dive into the most likely new C# 8 features, what they look like and why they’re useful.
Disclaimer: The information in this blog post was written well before the release of C# 8 and all information in this post is subject to change. This applies to possible inclusion of features in C# 8 as well as the described syntax.
C# 8: Nullable reference types
This feature is the most anticipated feature of C# 8 because it brings a lot of value and is known from other languages such as F#. For backward compatibility reasons however, it will work slightly different in C#.
What?
Hold on! Aren’t reference types already nullable? Yes, they are indeed. What this features brings is a slight shift: from C# 8, all reference types will be considered to be non-nullable by default. When you want a nullable reference type you will have to express that explicitly.
Why?
The dreaded NullReferenceException! We’ve all run into this exception plenty of times and they are mistakes that could have easily been caught at compile time if only we had the means to express it. Nullable reference types don’t solve this problem, but they do allow you to express your intent much better.
Apart from that, there’s an inconsistency between reference and value types: Value types are by default non-nullable, when adding the ?-modifier to the type we can make them nullable. Reference types on the other hand are currently nullable and there’s no way to express a non-nullable reference type.
How?
Similar to nullable value types, in C# 8 you will be able to declare that a variable is non-nullable by simply using the type. In case you want a nullable reference type, you will have to explicitly mention that by appending the ?-modifier at the end of the type. The first syntax enhancement is the ability to express a nullable reference type:
string? someText = null;
The above denotes the syntax for non-nullable reference types. Nullable reference types will then get the following syntax:
string someText = "this is some string";
someText = null; // WARNING!
As you can see in the above example, the current syntax will start to have a slightly different meaning: all reference types are considered to be non-nullable. That is a breaking change though. Since C# is so widely used, it’s quite impossible to make such a breaking change, so that’s the reason why the second line says “Warning”, rather than “Error”. If this would be considered an error it would promptly break thousands of code bases. Furthermore, even if this doesn’t break code, it’s still a breaking change because you now get a warning where you didn’t get one before. Therefore, nullable reference types will be considered an opt-in feature.
Static flow analysis
Apart from the rather obvious example above, there will be more benefits through the application of static flow analysis. Consider the following examples:
string someText = null; // Warning: Cannot convert null to non-nullable reference
string? otherText = null; // OK, assign null to nullable
string nonnull = otherText; // Warning: possible null assignment
On line 3, the analyzer can detect that we are assigning a value that is potentially null to a non-nullable variable and will correctly raise a warning.
string? text = null;
var length = text.Length; // Warning: Possible dereference of a null reference
The same applies to dereferencing a nullable variable. The compiler knows that we are potentially dealing with a null reference and will raise a warning. There are two ways to get around this issue: either we declare the original variable as a non-nullable type or we do a null check. Through static analysis, it can detect that the variable cannot possible be null:
string? text = null;
if(text != null)
{
var length = text.Length; // OK, you checked
}
Limitations
There are however some limitations on what static analysis flow can provide:
- Methods that return a non-nullable type will be interpreted as safe. They can however still return a null reference. (For example a library that has not been updated)
- It won’t always recognize whether you have done a proper null check. When you call string.IsNullOrEmpty for example, the analyzer won’t go into that method call to check this.
The first item cannot be solved unfortunately. Because of historical reasons, there’s no way to fit strict non-nullability into C#. This feature will reduce problems with null references, but will not eliminate it.
The second issue is more of an annoyance, as you would have to do an explicit null check even if you know that the item is not null. This would lead to this sort of code:
public void DoSomething(string? text)
{
if(!string.IsNullOrEmpty(text) && text != null)
Console.WriteLine(text.Length);
}
The above code is obviously redundant and we need a terser way of describing that. Therefore, the !-modifier is introduced. It allows you to specifically get rid of the warning:
public void DoSomething(string? text)
{
if(!string.IsNullOrEmpty(text))
Console.WriteLine(text!.Length);
}
By adding the !-operator when dereferencing the string, we tell the compiler “trust me, I know what I’m doing!”. The same can be applied when assigning a nullable reference to a non-nullable reference:
string? text = "some text";
string someText = text; // Warning!
string otherText = text!; // OK, I trust you
C# 8: Asynchronous Streams
What?
The ability to use async/await inside an iterator.
Why?
When you implement an iterator, currently you are limited to doing it synchronously. There are cases where you might need to await a call on every iteration to fetch the next item.
How?
To support this feature, a couple of things need to be implemented:
- New types: the async equivalents of IEnumerable
and IEnumerator - Methods with a yield return don’t currently support async/await.
- The ability to specify an await on an iterator construct such as foreach
The new types that will be introduced are IAsyncEnumerable
public interface IAsyncEnumerator<out T> : IAsyncDisposable
{
Task<bool> MoveNextAsync(); T Current { get; }
}
This allows you to build an iterator that can await each call to the MoveNextAsync method.
The second thing that will be added is the support for async/await in methods that yield results:
static async IAsyncEnumerable<int> GetNumbers()
{
for (int i = 0; i < 100; i++)
{
await Task.Delay(1000);
yield return i;
}
}
Some notable things:
- We have an async keyword, but the return type is not a Task<>. This is the support that will be built-in and specifically enabled when you return an IAsyncEnumerable
- We can await any calls inside the method
The last bit that will be added is the consumption side. When we iterate over an IAsyncEnumerable we need to make sure that the calling method also awaits the call, so from the caller we also await the call:
foreach await (var number in GetNumbers())
{
Console.WriteLine(number);
}
C# 8: Default interface implementations
What?
The ability to provide an implementation on an interface method. This makes it optional for implementers to override the method or not.
Why?
It allows for (a limited type of) multiple inheritance in C#
How?
Very similar to abstract classes you will be able to express method bodies inside an interface declaration:
interface ILogger
{
void Write(string text)
{
Console.WriteLine(text);
}
}
This then allows you to implement the interface without implementing the Write-method explicitly:
class Logger : ILogger { }
ILogger l = new Logger();
l.Write("text");
Note, however that the class does not inherit the method of the interface and the following will still give you a compilation error:
Logger l = new Logger();
l.Write("text"); // Error: Logger does not contain a method "Write"
This is also the reason why we can't really call it multiple inheritance. Consider the following implementation:
interface IA
{
void Write(string text)
{
Console.WriteLine(text);
}
}
interface IB
{
void Something()
{
// ...
}
}
class C : IA, IB {}
The following won't work, because you need to dereference it through the interface in order to use the default implementations:
var obj = new C();
obj.Write("test"); // Error
obj.Something(); // Error
To circumvent this issue, we need to dereference the methods through the interfaces:
(obj as IA).Write("OK"); // OK
(obj as IB).Something(); // OK
This feature has quite a few edge cases, which you can read about in the proposal here: Default interface methods proposal
Note also that for this feature, it will be necessary to modify the runtime, which is one of the reasons why it is unsure as whether this feature will make it into C# 8 or potentially a future version.
C# 8: Extension everything
What?
The ability to create extension properties, fields and operators.
Why?
When extension methods were introduced in C# 3 it was a supporting feature to enable LINQ. Once published it became clear that there’s a lot of value in it outside of LINQ. The ability to create extension properties, fields and operators would greatly increase this value.
How?
Currently extension methods are declared as static method which accept a special first parameter: the instance which the method is extending. In essence, it’s syntactical sugar over calling a static method where you pass in the instance as the first parameter. With extension everything, this syntax will change. There are a few competing design proposals and it’s unclear which will be the ultimate design, but for illustration’s sake I will use one of them. It’s highly likely this will change though:
public extension class ListExtensions<T> : List<T>
{
// instance extensions
private int _sum = 0;
public int GetSum() => _sum;
public int Sum => _sum;
public List<T> this[int[] indices] => ...;
// static extensions
static Random _rnd = new Random();
public static List<T> GetRandomLengthList() =>
new T[_rnd.Next].ToList();
public static implicit operator int(List<T> self) => self.GetSum();
public static implicit List<T> operator +(List<T> left, List<T> right) =>
left.Concat(right);
}
Conclusion
All in all, I think the planned additions will be a significant improvement in making C# more robust and terse. None of the above is set in stone, but it’s very likely these features will appear in C# 8 and if not they will surely come to the next version of C#. All of the discussion are these new features are open and available on GitHub. I encourage you to have a look at them and have a play with it once they are ready for trial.