C# 6 in action
Now that the Visual Studio 2015 Preview is available and the C# 6 feature set is a bit more stable, I figured it was time to start updating the Noda Time 2.0 source code to C# 6. The target framework is still .NET 3.5 (although that might change; I gather very few developers are actually going to be hampered by a change to target 4.0 if that would make things easier) but we can still take advantage of all the goodies C# 6 has in store.
I've checked all the changes into a dedicated branch which will only contain changes relevant to C# 6 (although a couple of tiny other changes have snuck in). When I've got round to updating my continuous integration server, I'll merge onto the default branch, but I'm in no rush. (I'll need to work out what to do about Mono at that point, too - there are various options there.)
In this post, I'll go through the various C# 6 features, and show how useful (or otherwise) they are in Noda Time.
Read-only automatically implemented properties ("autoprops")Finally! I've been waiting for these for ages. You can specify a property with just a blank getter, and then assign it a value from either the declaration statement, or within a constructor/static constructor.
So for example, in DateTimeZone, this:
private static readonly DateTimeZone UtcZone = new FixedDateTimeZone(Offset.Zero);public static DateTimeZone Utc { get { return UtcZone; } }
becomes
public static DateTimeZone Utc { get; } = new FixedDateTimeZone(Offset.Zero);
and
private readonly string id;public string Id { get { return id; } }protected DateTimeZone(string id, ...){ this.id = id; ...}
becomes
public string Id { get; }protected DateTimeZone(string id, ...){ this.Id = id; ...}
As I mentioned before, I've been asking for this feature for a very long time - so I'm slightly surprised to find myself not entirely positive about the results. The problem it introduces isn't really new - it's just one that I'm not used to, as I haven't used automatically-implemented properties much in a large code base. The issue is consistency.
With separate fields and properties, if you knew you didn't need any special behaviour due to the properties when you accessed the value within the same type, you could always use the fields. With automatically-implemented properties, the incidental fact that a field is also exposed as a property changes the code - because now the whole class refers to it as a property instead of as a field.
I'm sure I'll get used to this - it's just a matter of time.
Initial values for automatically-implemented propertiesThe ability to specify an initial value for automatically-implemented properties applies to writable properties as well. I haven't used that in the main body of Noda Time (almost all Noda Time types are immutable), but here's an example from PatternTestData, which is used to provide data for text parsing/formatting tests. The code before:
internal CultureInfo Culture { get; set; }public PatternTestData(...){ ... Culture = CultureInfo.InvariantCulture;}
And after:
internal CultureInfo Culture { get; set; } = CultureInfo.InvariantCulture;public PatternTestData(...){ ...}
It's worth noting that just like with a field initializer, you can't call instance members within the same class when initializing an automatically-implemented property.
Expression-bodied membersI'd expected to like this feature" but I hadn't expected to fall in love with it quite as much as I've done. It's really simple to describe - if you've got a read-only property, or a method or operator, and the body is just a single return statement (or any other simple statement for a void method), you can use => to express that instead of putting the body in braces. It's worth noting that this is not a lambda expression, nor is the compiler converting anything to delegates - it's just a different way of expressing the same thing. Three examples of this from LocalDateTime (one property, one operator, one method - they're not next to each other in the original source code, but it makes it simpler for this post):
public int Year { get { return date.Year; } }public static LocalDateTime operator +(LocalDateTime localDateTime, Period period){ return localDateTime.Plus(period);}public static LocalDateTime Add(LocalDateTime localDateTime, Period period){ return localDateTime.Plus(period);}
becomes
public int Year => date.Year;public static LocalDateTime operator +(LocalDateTime localDateTime, Period period) => localDateTime.Plus(period);public static LocalDateTime Add(LocalDateTime localDateTime, Period period) => localDateTime.Plus(period);
In my actual code, the operator and method each only take up a single (pretty long) line. For some other methods - particularly ones where the body has a comment - I've split it into multiple lines. How you format your code is up to you, of course :)
So what's the benefit of this? Why do I like it? It makes the code feel more functional. It makes it really clear which methods are just shorthand for some other expression, and which really do involve a series of steps. It's far too early to claim that this improves the quality of the code or the API, but it definitely feels nice. One interesting data point - using this has removed about half of the return statements across the whole of the NodaTime production assembly. Yup, we've got a lot of properties which just delegate to something else - particularly in the core types like LocalDate and LocalTime.
The nameof operatorThis was a no-brainer in Noda Time. We have a lot of code like this:
public void Foo([NotNull] string x){ Preconditions.CheckNotNull(x, "x"); ...}
This trivially becomes:
public void Foo([NotNull] string x){ Preconditions.CheckNotNull(x, nameof(x)); ...}
Checking that every call to Preconditions.CheckNotNull (and CheckArgument etc) uses nameof and that the name is indeed the name of a parameter is going to be one of the first code diagnostics I write in Roslyn, when I finally get round to it. (That will hopefully be very soon - I'm talking about it at CodeMash in a month!)
Dictionary initializersWe've been able to use collection initializers with dictionaries since C# 3, using the Add method. C# 6 adds the ability to use the indexer too, which leads to code which looks more like normal dictionary access. As an example, I've changed a "field enum value to delegate" dictionary in TzdbStreamData from
private static readonly Dictionary<TzdbStreamFieldId, Action<Builder, TzdbStreamField>> FieldHanders = new Dictionary<TzdbStreamFieldId, Action<Builder, TzdbStreamField>>{ { TzdbStreamFieldId.StringPool, (builder, field) => builder.HandleStringPoolField(field) }, { TzdbStreamFieldId.TimeZone, (builder, field) => builder.HandleZoneField(field) }, { TzdbStreamFieldId.TzdbIdMap, (builder, field) => builder.HandleTzdbIdMapField(field) }, ...}
to:
private static readonly Dictionary<TzdbStreamFieldId, Action<Builder, TzdbStreamField>> FieldHanders = new Dictionary<TzdbStreamFieldId, Action<Builder, TzdbStreamField>>{ [TzdbStreamFieldId.StringPool] = (builder, field) => builder.HandleStringPoolField(field), [TzdbStreamFieldId.TimeZone] = (builder, field) => builder.HandleZoneField(field), [TzdbStreamFieldId.TzdbIdMap] = (builder, field) => builder.HandleTzdbIdMapField(field),...}
One downside of this is that the initializer will now not throw an exception if the same key is specified twice. So whereas the bug in this code is obvious immediately:
var dictionary = new Dictionary<string, string>{ { "a", "b" }, { "a", "c" }};
if you convert it to similar code using the indexer:
var dictionary = new Dictionary<string, string{ ["a"] = "b", ["a"] = "c",};
" you end up with a dictionary which only has a single value.
To be honest, I'm now pretty used to the syntax which uses Add - so even though there are some other dictionaries initialized with collection initializers in Noda Time, I don't think I'll be changing them.
Using static membersFor a while I didn't think I was going to use this much - and then I remembered NodaConstants. The majority of the constants here are things like MillisecondsPerHour, and they're used a lot in some of the core types like Duration. The ability to add a using directive for a whole type, which imports all the members of that type, allows code like this:
public int Seconds => unchecked((int) ((NanosecondOfDay / NodaConstants.NanosecondsPerSecond) % NodaConstants.SecondsPerMinute));
to become:
using NodaTime.NodaConstants;...public int Seconds => unchecked((int) ((NanosecondOfDay / NanosecondsPerSecond) % SecondsPerMinute));
Expect to see this to be used a lot in trigonometry code, making all those calls to Math.Cos, Math.Sin etc a lot more readable.
Another benefit of this syntax is to allow extension methods to be imported just from an individual type instead of from a whole namespace. In Noda Time 2.0, I'm introducing a NodaTime.Extensions namespace with extensions to various BCL types (to allow more fluent conversions such as DateTimeOffset.ToOffsetDateTime()) - I suspect that not everyone will want all of these extensions to be available all the time, so the ability to import selectively is very welcome.
String interpolationWe don't use the system default culture much in Noda Time, which the string interpolation feature always does, so string interpolation isn't terribly useful - but there are a few cases where it's handy.
For example, consider this code:
throw new KeyNotFoundException( string.Format("No calendar system for ID {0} exists", id));
With C# 6 in the VS2015 preview, this has become
throw new KeyNotFoundException("No calendar system for ID \{id} exists");
Note that the syntax of this feature is not finalized yet - I expect to have to change this for the final release to:
throw new KeyNotFoundException($"No calendar system for ID {id} exists");
It's always worth considering places where a feature could be used, but probably shouldn't. ZoneInterval is one such place. Its ToString() feature looks like this:
public override string ToString() => string.Format("{0}: [{1}, {2}) {3} ({4})", Name, HasStart ? Start.ToString() : "StartOfTime", HasEnd ? End.ToString() : "EndOfTime", WallOffset, Savings);
I tried using string interpolation here, but it ended up being pretty horrible:
- String literals within string literals look very odd
- An interpolated string literal has to be on a single line, which ended up being very long
- The fact that two of the arguments use the conditional operator makes them harder to read as part of interpolation
Basically, I can see string interpolation being great for "simple" interpolation with no significant logic, but less helpful for code like this.
Null propagationAmazingly, I've only found a single place to use null propagation in Noda Time so far. As a lot of the types are value types, we don't do a lot of null checking - and when we do, it's typically to throw an exception if the value is null. However, this is the one place I've found so far, in BclDateTimeZone.ForSystemDefault. The original code is:
if (currentSystemDefault == null || currentSystemDefault.OriginalZone != local)
With null propagation we can handle "we don't have a cached system default" and "the cached system default is for the wrong time zone" with a single expression:
if (currentSystemDefault?.OriginalZone != local)
(Note that local will never be null, or this transformation wouldn't be valid.)
There may well be a few other places this could be used, and I'm certain it'll be really useful in a lot of other code - it's just that the Noda Time codebase doesn't contain much of the kind of code this feature is targeted at.
ConclusionWhat a lot of features! C# 6 is definitely a "lots of little features" release rather than the "single big feature" releases we've seen with C# 4 (dynamic) and C# 5 (async). Even C# 3 had a lot of little features which all served the common purpose of LINQ. If you had to put a single theme around C# 6, it would probably be making existing code more concise - it's the sort of feature set that really lends itself to this "refactor the whole codebase to tidy it up" approach.
I've been very pleasantly surprised by how much I like expression-bodied members, and read-only automatically implemented properties are a win too (even though I need to get used to it a bit). Other features such as static imports are definitely welcome to remove some of the drudgery of constants and provide finer-grained extension method discovery.
Altogether, I'm really pleased with how C# 6 has come together - I'm looking forward to merging the C# 6 branch into the main Noda Time code base as soon as I've got my continuous integration server ready for it"