C# Updates for the Absent C# Developer (C# 6.0 and newer overview)
This post is part of C# Advent Calendar 2020.
Update 6. December 2020: Some good small discussion on Reddit.com
It has been a while since I actively developed in C#. I mostly worked with C# and .NET during the 3.0 to 4.5 days and I did async/await work very early on, so I skip over that as well. After a job change, I didn’t touch C# for actual work. I mostly just watched the development from the sidelines via news. Today, I take a short look at some features. I will skip a lot and just add some of my highlights tour.
.NET / C# runs on Everywhere
Ok, it is the first time I tried out dotNet core on Linux. I do love dotNet core: It behaves like most language-SDKs. You download it, unpack it and of you go. No installation and bundling into Windows \o/
I downloaded .NET SDK 5.0, unzipped it and set up the environment variables. I installed IntelliJ Rider, since I’m familiar with the Jetbrains ecosystem and used it previously. It worked out of the box.
String Interpolation
Yeah, string interpolation. I think there isn’t a day in programming where I don’t have to concatenate strings together. So, either you had to use the plus operator or string.Format. String.Format is brittle and hard to read, because you need to do the line up the placeholders with the arguments. String interpolation makes everything crystal clear
Console.WriteLine("The number is "+aNumber+" and the string is "+aString+" with more "+moreStuff + " " + evenMore +" stuff");
// I hope you got the order right =)
Console.WriteLine("The number is {0} and the string is {1} with more {2} {3} stuff", aNumber, aString, moreStuff, evenMore);
// Woop, woop, cristal clear
Console.WriteLine($"The number is {aNumber} and the string is {aString} with more {moreStuff} {evenMore} stuff");
Static Imports (C# 6.0)
This is something I missed from Java. The ability to import static methods. It allows you to use methods like their free-standing methods. Especially useful for things like importing Asserts. Previously you had to always add the class, which was most of the time noise:
Assert.AreEqual("Expected", actualValue);
Assert.IsNotEmpty(otherValue);
Assert.IsEmpty(yetAnotherValue);
// Use a static import of the Assert class
using static NUnit.Framework.Assert;
// Then you can use the static methods on it directly
AreEqual("Expected", actualValue);
IsNotEmpty(otherValue);
IsEmpty(yetAnotherValue);
Better Auto Property Support (C# 6.0)
It allows you to create an auto property and directly initialize it. Previously you only could initialize fields directly, but auto-properties had to be initialized in the constructor. This levels the playing field.
class SomeValues
{
public int Answer = 42; // directly initialized
public string StringAnswer { get; set; }; // had to be initialized in the constructor =(
public SomeValues()
{
StringAnswer = "42";
}
}
class SomeValues
{
public int Answer = 42; // directly initialized
public string StringAnswer { get; set; } = "42"; // Yeah
}
Also read only auto properties are better supported now. Before, you couldn’t have read-only auto-properties. You either had to create a read-only backing field yourself, or have an implicit understanding that a property is read-only:
class ReadOnlyValues
{
// Choose your poison: Boiler plate code
private readonly int _intAnswer = 42;
public int IntAnswer
{
get { return _intAnswer; }
}
// Or 'trust' that everyone understand this is read only
public string StringAnswer { get; private set; }
public ReadOnlyValues(int answer)
{
_intAnswer = answer;
StringAnswer = answer.ToString();
}
}
class ReadOnlyValues
{
public int IntAnswer { get; }
public string StringAnswer { get; }
public ReadOnlyValues(int answer)
{
IntAnswer = answer;
StringAnswer = answer.ToString();
}
}
Conditional Null (C# 6.0)
The new ?.
operator allows dereferencing a field or property,
but return null if the reference itself is null.
var answerOrNull = someValues == null ? null : someValues.StringAnswer;
var answerOrNull = someValues?.StringAnswer;
Expression-Bodied Members
This is probably the change that feels most 'non-C#' to me. You can put a lamda expression where a method body would be. It removes some boilerplate. However, it seems to me that a more compact code formatting would do half of the trick. It’s just that the C# community likes 'well spaced out' formatting.
public override string ToString()
{
return $"{IntAnswer} -> {StringAnswer}";
}
public void PrintToConsole(string prefix)
{
Console.WriteLine($"{prefix}: {IntAnswer} -> {StringAnswer}");
}
public override string ToString() => $"{IntAnswer} -> {StringAnswer}";
public void PrintToConsole(string prefix) => Console.WriteLine($"{prefix}: {IntAnswer} -> {StringAnswer}");
public override string ToString() { return $"{IntAnswer} -> {StringAnswer}"; }
public void PrintToConsole(string prefix) { Console.WriteLine($"{prefix}: {IntAnswer} -> {StringAnswer}"); }
Pattern Matching (C# 7.0)
I’m very familiar with pattern matching from my work in Scala. So, nice to have. It allows you to write compact matching rules instead of a long if-else chain.
interface Shape
{
}
class Circle : Shape
{
public int Radius { get; }
}
class Square : Shape
{
public int Side { get; }
}
public static double ComputeArea(Shape shape)
{
var s = shape as Square;
var c = shape as Circle;
if (s != null)
{
if (s.Side == 0)
return 0;
else
return s.Side * s.Side;
}
else if(c != null)
{
if (c.Radius == 0)
return 0;
else
return c.Radius * c.Radius * Math.PI;
}
else
{
throw new ArgumentException(
message: "shape is not a recognized shape",
paramName: nameof(shape));
}
}
public static double ComputeArea(Shape shape)
{
switch (shape)
{
case Square s when s.Side == 0:
case Circle c when c.Radius == 0:
return 0;
case Square s:
return s.Side * s.Side;
case Circle c:
return c.Radius * c.Radius * Math.PI;
default:
throw new ArgumentException(
message: "shape is not a recognized shape",
paramName: nameof(shape));
}
}
Digit Separator (C# 7.0)
Such a small thing I was missing from Java and still missing in some languages. It allows you to use an underscore to write down large number literals
// How large is this number? Count the zeros, don't screw it up
public const long LargeNumber = 10000000000;
// How large is this number? Count the zeros, don't screw it up
public const long LargeNumber = 10_000_000_000;
Default Interface Members (C# 8.0)
So people would extend interfaces with extension methods. However, sometimes you want to extend the interface with some reasonable method, but allow implementations to override that method. Default methods allows adding a method with implementation to an interface. This looks like the same mechanism as Java has to me.
// Original Interface
public interface IOrder
{
DateTime Purchased { get; }
decimal Cost { get; }
}
// Added Later
public static class OrderExtensions
{
// Works, but implementations cannot override this static method.
public static string AsCsvLine(this IOrder order)
{
return $"Order;{order.Purchased};{order.Cost}";
}
}
public interface IOrder
{
DateTime Purchased { get; }
decimal Cost { get; }
public string AsCVSLine()
{
return $"Order;{Purchased};{Cost}";
}
}
// Unlike a Extension method, Default methods can be overridden
public class SpecialOrder : IOrder
{
// ...
public string AsCVSLine()
{
return $"Super-Special;{Purchased};{Cost}";
}
}
Nullable References (C# 8.0)
You now can declare references as nullable. What? you say, references are already nullable. Yes, but as reader, it helps me a lot to know that something is nullable or not. My own conversion in my code usually is: References are not nullable, and if they are, they have an ugly name like 'nameNullable'. But with nullable references, I can easily declare it without ugly names.
Even better, the compiler can warn about nullable issues. I expect that over time the warnings screws get tightened bye default, until putting a null into a regular reference is an error in future versions of C#. So, it will transition to a Kotlin like behavior.
public class Person
{
// Required
public string DisplayName { get; set; } = "";
// Nullable
public string NickName { get;set; }
}
public static void SomeOperations()
{
var person = SomePerson();
// Oups, maybe assigns null to a field not expecting null
// Nothing hints at the issue. NullPointer exception maybe way later on
// Or worse, invalid state got persisted to database/disk
person.DisplayName = person.NickName;
}
// Enable nullable warnings by the compiler
#nullable enable
public class Person
{
public string DisplayName { get; set; } = "";
public string? NickName { get;set; }
}
public static void SomeOperations()
{
var person = SomePerson();
// Compiler warns: Program.cs(46, 34): [CS8601] Possible null reference assignment.
person.DisplayName = person.NickName;
}
Records (C# 9.0)
Records, yeah =). Very familiar with it from Scala (called case classes in Scala) I’m happy to see this appear in C# and Java. It allows you to write down a pure data class and I have tons of these usually. It removes the noise no-one is interested in. In languages with records, its way easier to just pass data around where you don’t need more. Yes, you can do it without records, but it gets drawn down in the noise and it’s easy to break the rules: Like change the fields and forget to update the equality operations. Furthermore, records 'scream' at the reader: I’m just holding this data, nothing to see here.
Without Records:
public class Person
{
public string DisplayName { get; set; } = "";
public string? NickName { get;set; }
public string? Email { get;set; }
public Person(string displayName, string? nickName, string? email)
{
DisplayName = displayName;
NickName = nickName;
Email = email;
}
protected bool Equals(Person other) // Boiler plate
public override bool Equals(object? obj) // Boiler plate
public override int GetHashCode() // Boiler plate
}
With Records:
public record Person
{
public string DisplayName { get; init; } = "";
public string? NickName { get;init; }
public string? Email { get;init; }
// Done. Constructor, Equality etc is done for us
}
Even better, it also includes improvements to work with the
immutable records. There is a with
operator to create a new record
with your changes applied.
var person = SomePerson();
var alternateEgo = person with { DisplayName = "Batman" };