Closed class hierarchies (Exploring the .NET 11 preview – Part 4)

rellem1 pts0 comments

Closed class hierarchies Sponsored by Dometrain Courses —Get 30% off Dometrain Pro with code ANDREW30 and access the best courses for .NET Developers In this post I look at the implementation of closed class hierarchies that is available in .NET 11 preview 5. I'll describe what a close closed hierarchy is, how to create one, and discuss why you might want to.<br>What is a "closed class hierarchy"?<br>A closed class hierarchy is a class hierarchy that can only be defined within a single assembly. Attempting to derived from a closed class from a different assembly is a compilation error. This is easiest to see in action.<br>Imagine you have the following classes, all in the same assembly:<br>// Create a closed base class<br>public closed class Animal { }

// Each class derives from the closed Animal class<br>public class Dog : Animal { }<br>public class Cat : Animal { }<br>public class Horse : Animal { }

The Dog, Cat, and Horse classes all derive from Animal. This is all C# 101; the closed keyword isn't really changing anything about that.<br>The difference is if we create a new assembly, and try to derive from Animal in a different assembly:<br>// Assembly 2<br>public class Cow : Animal { }

then this won't compile! Instead you'll get an error like the following:<br>error CS9382: 'Cow': cannot use a closed type 'Animal' from another assembly as a base type.

So from this example, you can see that this approach allows you to have a public base type, but ensure that no one other than you can define types derived from it. This can be very useful for modelling certain domains, and can simplify the logic in your own code significantly. However, that's not all they're for.<br>Couldn't we do this already with private constructors?<br>Being able to prevent derived types using closed is useful, but there have been other ways to achieve this in the past. By providing a private protected or internal default constructor on the base class, it's practically impossible to create a derived type.<br>For example, if we update the Animal definition to:<br>// Assembly 1<br>// remove 'closed', make abstract, and add constructor<br>public abstract class Animal<br>private protected Animal()

then you still can't define Cow in another assembly:<br>// Assembly 2<br>public class Cow : Animal { } // Won't compile

because you get one of two different errors:<br>error CS0122: 'Animal.Animal()' is inaccessible due to its protection level<br>error CS1729: 'Animal' does not contain a constructor that takes 0 arguments

Using closed is clearly semantically nicer than the private constructor approach, but it doesn't seem like it's actually providing new functionality.<br>However, there is a difference. With the private constructor approach, the compiler didn't really "know" that you couldn't create any derived types, because theoretically you could, even if you couldn't practically.<br>With the closed keyword, when the compiler builds an assembly, it knows exactly what all the derived types of a closed class are. The main benefit of that is that the compiler can apply exhaustiveness checking to switch expressions.<br>Exhaustiveness checking in switch expressions<br>Let's say we have a "normal" class hierarchy, similar to the one shown above, but this time just using abstract instead of closed:<br>// Abstract base type<br>public abstract class Animal { }

// The same derived types as before<br>public class Dog : Animal { }<br>public class Cat : Animal { }<br>public class Horse : Animal { }

We then also define the following method, which takes an Animal instance, and performs a switch:<br>static string Speak(Animal animal) => animal switch<br>Dog => "Woof",<br>Cat => "Meow",<br>Horse => "Neigh",<br>};

If you compile this code, you'll get a warning:<br>warning CS8509: The switch expression does not handle all possible<br>values of its input type (it is not exhaustive). For example,<br>the pattern '_' is not covered.

Even though you're handling all the types that could be passed to Speak(), the compiler doesn't know that. As far as it's concerned, you could have created a type derived from Animal in a different assembly, and passed it to the method.<br>Note that even if we add the private protected constructor here, so that practically a different assembly can't derive from the type, that doesn't change the warning. The compiler can't infer the fact that there will only ever be these three implementations.

Using closed means that the compiler does know that these are all of the implementations, and there can't be any more. That means the compiler can correctly apply exhaustiveness checking to switch expressions which used closed types.<br>This might seem like a small thing, but it's actually quite a big deal for writing correct code.<br>For example, imagine you have code similar to the previous that you were using with .NET 10. In order to quiet the warnings, you add a catch-all handler to your switch expressions<br>static string Speak(Animal animal) => animal switch<br>Dog => "Woof",<br>Cat => "Meow",<br>Horse => "Neigh",<br>_ => throw new InvalidOperationException(); // Can't be...

animal class closed assembly public from

Related Articles