Collections, Classes, Clocks

Here’s a talk I did at a local tech meet-up event. This blog post is, unashamedly, NET 8 for the Nerds.

I was meant to be speaking about the new Microsoft .NET 8 release, but decided to be rebellious and talk lots about Python instead. Partly, my excuse was that tech meet-ups are more fun if there are different technologies.

But, struggling for a better excuse, I decided to talk about new language features that are moving C# closer towards the convenience of Python. Read on to be unconvinced.

0. Tech we’ll be using

  • .NET 8 and C# 12: new language and runtime features
  • Python: hugely convenient, can C# learn from it?
  • Jupyter Notebooks: markdown and code OK the talk used Jupyter but I can’t do that in a blog post

I’ve grouped the talk into 3 parts: Collections, Classes, Clocks.

1. Collections

Collections are the mainstay of most applications. After all, what’s ever been achieved by processing a single datapoint on its own?

Everyone has their favourite. IEnumerable<T> or IList<T> or just plain arrays. They’re a bit faffy to initialise though, aren’t they? It all depends on the method you’re calling, and what type that expects:

1
2
3
4
ProcessData(new int[] { 1, 2, 3 }); // If you're lucky.
ProcessData(new int[] { 1, 2, 3 }.ToList()); // If you're unlucky.
ProcessData(Enumerable.Empty<int>()); // Or even if you even just want to do nothing.
ProcessData(Array.Empty<int>()); // ...unless it needs an Array, in which case it's this.

This is even more of a pain if you change the method signature to a more derived type. Even your empty array calls don’t compile any more!

Before we start, let’s talk about Python!

Python rightly has a reputation for being quick to develop code. The syntax is really simple and doesn’t have any of that unnecessary faff:

1
ProcessData([1,2,3])

Can we get close to Python in .NET 8 with C# 12?

1
2
3
4
5
6
// Let's try...
ProcessData([1,2,3]);

// Or even...
int[] others = [9,8];
ProcessData([1, 2, ..others, 3]);

So yes we can! If you compare the Python example to that first C# 12 block above, you’ll see they’re literally identical.

(in fact, when I gave this talk live, I used a Jupyter notebook and ran that “Python” code sample inside a C# .NET interpreter block, because the syntax was identical. Except I hid a semicolon on the next line. I know right? What a trickster!)

So what’s going on?

A new language feature: collection expressions^1. Lots of magic under the hood. Some neat features:

  • If the target type changes (e.g. a method used to take IEnumerable but now takes Array) the compiler finds the most appropriate initialiser.
  • All sorts of performance optimisations internally; better than most people would hand-roll. Example^2

2. Classes

Let’s create a class that processes some data and returns a calculated result.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using MathNet.Numerics.Statistics;

public class Stats
{
public Stats(IEnumerable<double> data)
{
_data = data;
}

private readonly IEnumerable<double> _data;

public (double Average, double Deviation) Calculate()
{
return (_data.Average(), _data.StandardDeviation());
}
}


var result = new Stats([1,2,3]).Calculate();

Console.WriteLine($"{result.Average}, {result.Deviation}");

You’ll see that this takes some data, does two calculations on it (average and stdev) and returns the result.

(NB I have taken liberties here and enumerated the same IEnumerable more than once. This is usually a Bad Thing (TM) and I’ve done it for artistic license. Don’t do it in your production systems!)

Can we learn much from Python?

1
2
3
4
5
6
7
8
9
10
import statistics

class Stats:
def __init__(self, data):
self._data = data

def calculate(self):
return (statistics.mean(self._data), statistics.stdev(self._data))

avg, dev = Stats([1, 2, 3]).calculate()

So yes, Python has simplified this in a few ways:

  • Eliminated the explicit declaration of the private readonly variable for _data
  • Enables de-structuring the result directly into two variables

Let’s see what we can do with C# 12 and .NET 8

1
2
3
4
5
6
7
8
9
10
using MathNet.Numerics.Statistics;

public class Stats(IEnumerable<double> data)
{
public (double Average, double Deviation) Calculate() => (data.Average(), data.StandardDeviation());
}

var (avg, dev) = new Stats([1,2,3]).Calculate();

Console.WriteLine($"{avg}, {dev}");

Hang on. That’s shorter than the Python equivalent!

What’s going on?

Some magic syntax coming from the new C# 12 specification. We used a few recent-ish features from prior C# specifications too:

  • C# 12: Primary Constructors^3. A bit like we see in the recent record class definitions we gained in C# 10. (although record classes expose the field publicly, whereas classes with primary constructors don’t.)
  • C# 10: Destructuring of objects into variables (since C# 7 with some restrictions)
  • C# 6: Expression-bodied members

3. Clocks

To be fair I’ve dropped the Python rebellion pretence now, but I do want to talk about the most significant new feature to appear in .NET for many a long year.

Let’s talk time.

So you’re not afraid of working with time?

Time is one of the most difficult concepts in programming. Need persuading? If you live somewhere with a summertime timezone offset, just start with that. Can you think of any unsafe assumptions people typically make?

TL;DR: If you’re not Unit Testing your time calcs, your code isn’t production-ready.

What happens when we try to Unit Test our time calculations, usually? Let’s create a class that does some basic time calcs.

1
2
3
4
5
public class UntestableTimeCode()
{
private readonly DateTimeOffset _createdOn = DateTimeOffset.Now.Date;
public bool IsItTomorrowYet() => DateTimeOffset.Now.Date > _createdOn.Date;
}

Completely fictional obviously, but this code would store today’s date when you initialise it, and then you could repeatedly ask it “Is it tomorrow yet”. Until it is.

The clue’s in the name here: it’s not really testable. But if we really really had to test that class, could we do it in a hacky creative way?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using Xunit;

public class TimeTest1
{
[Fact]
public async Task TestIf_IsItTomorrowYet_ReturnsTrueTomorrow()
{
var untestable = new UntestableTimeCode();

// Umm...?
await Task.Delay(TimeSpan.FromHours(24));

Assert.True(untestable.IsItTomorrowYet());
}
}

…yes. But you can see the problem here. Your unit test would take 24 hours to run. FAIL.

.NET 8 secret sauce

So. We need some way to modify the system clock. Or rather, de-couple our code from the system clock, and use an abstraction that we can control in a test.

1
2
3
4
5
6
7
8
public class TestableTimeCode(TimeProvider timeProvider)
{
private readonly DateTimeOffset _createdOn = timeProvider.GetUtcNow().Date;
public bool IsItTomorrowYet() => timeProvider.GetUtcNow().Date > _createdOn.Date;
}

// In normal use, we pass the current System time provider into the constructor:
var myTimeCode = new TestableTimeCode(TimeProvider.System);

OK, so we have now de-coupled from the System clock by adding a dependency. How does this make Unit Tests possible?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using Microsoft.Extensions.Time.Testing;

public class TimeTest2
{
[Fact]
public void TestIf_IsItTomorrowYet_ReturnsTrueTomorrow()
{
var fakeTime = new FakeTimeProvider(startDateTime: DateTimeOffset.UtcNow.Date);
var untestable = new TestableTimeCode(fakeTime);

// await Task.Delay(TimeSpan.FromHours(24)); --not any more!
fakeTime.Advance(TimeSpan.FromHours(24));

Assert.True(untestable.IsItTomorrowYet());
}
}

Amazing! We can artificially advance the system clock to suit our whim.

TimeProvider is our new best friend!

Remember, take a TimeProvider dependency into all your time-based code.

You can then use FakeTimeProvider in lots of powerful ways:

  • Advance time immediately (like above)
  • Timezone changes e.g. summertime
  • Other countries’ timezones
dark
sans