Recently, I made a surprising discovery. I had downloaded a preview of Visual Studio 2019 and was experimenting with some of the new language features in C# 8, when I discovered something:
int can sometimes move type checks from compile-time to runtime.
This shocked me. The experimental evidence (which I’ll get to later) was irrefutable, but it was very much not what I had expected.
Anyone who has been explaining C# in public for long enough will remember the uphill struggle when C# 3.0 introduced the
var keyword. Its obvious lexical resemblance to
dynamic keyword is for.) We all spent a lot of effort patiently explaining that no matter what
var means in other languages, a C#
var declaration is statically typed. Consequently, this new behaviour in C# 8 feels almost like a joke.
Adding to the surprise was the fact that in the (very particular) scenario I was exploring,
var gets the stronger compile-time checking. Stating the expected type explicitly had the side effect of deferring the relevant type checks until runtime!
This made me slightly sad. I’m aware that I’m very much in a minority here, but I don’t like the widespread practice of using
var wherever possible in C#. I spend more time reading code than writing it, and the big problem with
var is that it imposes significant cognitive load during code review. It requires you to remember method return types, and to perform type inference in your head, both of which distract from the task at hand. Only yesterday I was looking at someone else’s code, trying to work out whether a particular
var was an
IEnumerable<string> or a
List<string>. The distinction mattered a great deal, but the code concealed it. (And I couldn’t simply mouse-over the
var to find out, because the code was on someone else’s computer. You can’t do that when reviewing PRs in the web browser either.) I find explicitly typed variable declarations make it much easier to comprehend code, but for reasons I’ve never really understood most people seem to prefer
Explicitly typed variable declarations are a second class citizen in tooling. E.g., https://github.com/dotnet/roslyn/issues/29657 describes how the code analysis settings for preferring not to use
var seem to have been implemented by people who really prefer
var, and don’t understand where us
var-dislikers are coming from. But my recent discovery shows that things have escalated: a slight preference for
var is now baked into the language itself. It’s one thing for a code analysis tool to have suboptimal behaviour—I can and have implemented my own analyser that gets it right. But it’s quite another matter when a decision not to use
var downgrades the compile-time type checking. Forking C# is not practical.
As I dug into the example that revealed this, I realised it was a symptom of a larger issue about how C# is evolving. And this has led me to write this series of blogs:
- C# 8 surprising patterns (i.e., this blog)
- C# faux amis 1: discards and underscores
- C# faux amis 2: tuple deconstruction and positional patterns
- C# faux amis 3: variable declarations and type patterns
As I’ll show, the underlying issue is not new in C# 8. It has been growing for a while. The unfortunate downgrading of explicitly typed declarations is, I think, an inevitable (and quite possibly accidental) consequence. In this first, introductory blog I’ll outline the problem, and the articles that follow will show various examples, ending with the one that led to my recent discovery.
The (increasing) loneliness of the (increasingly) long-distance runtime
One of C# 1.0’s main attractions was its very straightforward connection to the .NET runtime. C#’s most direct competitor at the time, VB.NET, suffered from trying to retain substantial baggage from its predecessor Visual Basic 6. VB.NET was different enough from VB6 to prevent easy migration of existing projects, but so burdened by its historical oddities that it was a hard sell as a new language. To understand it thoroughly meant grappling with two competing sets of abstractions: the underlying .NET runtime, and a thick layer of VB’s peculiarities.
C# looked crystal clear by comparison. To understand the runtime was to understand the language. In cases where C# 1.0 added anything on top of the runtime it was pretty thin, and easily understood.
Fast forward nearly two decades to today’s previews of C# 8, and it’s not quite so thin. C# has grown numerous features, and only a handful of these correspond to equivalent advances in the CLR (the Common Language Runtime, as .NET’s runtime is formally known). Of the four most notable additions to the language—generics, LINQ, the
dynamic keyword, and
await—only one corresponded to a new runtime feature. (Generics in C# 2.0 were inseparable from their support in the CLR.) LINQ and async make use of .NET Framework class library features, but did not require anything new from the CLR, so with each of these features, C# became more like a traditional compiled language, in which the language builds its own abstractions on the underlying platform capabilities.
C# remains very much a native of the .NET world: although it now offers a substantial layer of functionality on top of .NET’s basics, it does not attempt to conceal the underlying realities of the CLR. Nor does it introduce any concepts that conflict with how the runtime works. An understanding of the CLR continues to go hand in hand with an understanding of C#.
However, a language cannot evolve for 20 years without picking up a few oddities. While trying to understand how C# had ended up producing the situation described at the start, I was reminded of the linguistic idea of faux amis.
Faux amis in C#
I was taught about faux amis when learning French at school. Literally, the phrase means “false friends” and it refers to linguistic features that seem at first to be connected in ways that will help you to learn the language, but which turn out to lead you astray.
In natural languages, faux amis come in various forms. For example, the French word for “address” sounds an awful lot like the English one, which seems like a helpful shortcut when memorising vocabulary, but this makes it all too easy for a native English speaker to misspell. (The French “adresse” has only one ‘d’, and an extra ‘e’.) But it’s not just about spelling. There are French words that look or sound similar to English ones but which turn out to have completely different meanings. For example, the French word “location” means something entirely different from the identical-looking English word. And the most slippery faux amis are the ones which, because of English and French’s shared roots, have meanings that are related but which have diverged just enough to cause confusion. (E.g., in French, claiming to have no “monnaie” doesn’t mean that you have no money, only that you don’t have the right change.)
The heart of any pair of faux amis is this: a misleading resemblance. C# now has a few of these. If you understand one language feature, its striking resemblance to another feature may fool you into misunderstanding how your code will behave.
Next time, we will examine some faux amis that emerged from the almost entirely successful addition of support for discards in C# 7.0.