June 3rd, 2021

Conversation about the .NET type system

Richard Lander
Product Manager

The .NET or Common Language Runtime (CLR) type system is the foundation of the .NET programming model. We often talk about System.Object being the base of the type system, but it’s really the base of all (reference) types. The type system is (at least) one step lower than that. It defines that both reference and value types exist, that strings are immutable, that single-inheritence is allowed and multiple-inheritence is not, and that generics are a runtime concept. On the other hand, it doesn’t define concepts like IEnumerable<T>, IDisposable, or Task<T>, but it enables those Ts to exist. You can quickly see that the type system is one of the most pervasive concepts in .NET.

We’re using the conversation format again, this time with runtime engineers who work on the type system and related topics.

What is the purpose of a type system and why do they vary so much across languages?

David: The purpose of a type system is to define and control the way in which data and code are arranged within an application. They vary so much across languages as there are many different ways to arrange code and data that make sense, and are useful.

Jared: The type system is the building blocks and rules on which the language features are built. They vary so much across languages because language differentiation is often achieved by starting from a different set of building blocks and rules on which it operates.

What are the aspects of the CLR type system that you think are the most apparent to .NET developers?

David: I see two different aspects of the type system as most apparent to .NET developers.

  1. Code and data are held in constructs like value types and classes, which encourages developers to build object oriented systems.
  2. The CLR type system’s smallest redistributable unit is the assembly, or .dll. This has profound implications on how applications are structured, and developed.

Jared: In general I think the split between struct and class is the aspect that becomes the most apparent to .NET developers. It’s essentially two ways of defining very similar objects with similar capabilities but the “modifier” on the type significantly changes how the type is used by consumers. It’s not common in other languages and runtimes

The line between CLR and C# concepts can be murky. How do you think of the new record types? They are a class that behaves more like a struct. Is that good?

David: “Is that good” probably isn’t the right question here. A better question would more like “Is that useful?” And I believe they are useful, as it makes it simple for one developer to communicate to others that some objects aren’t an object with behaviors, but more just pure data. Also, the new record types are much less effort to use than building the equivalent logic would have been in past versions of C#.

One thing to remember about the line between CLR and C# concepts is that CLR concepts provide the possibility to make some logic work, and C# concepts provide an interface for actual developers to work with. The C# concepts are an opinionated view on the possible programs that can be written using CLR concepts, and over time, the developers of the C# language have found ways for programmers to more clearly and succinctly represent intent on a fairly regular cadence, while the fundamental capabilities provided by CLR concepts are typically much more slow to evolve.

Jared: I often divide types into whether they primarily provide data or behavior. In the case they provide data I often want a number of features to come along with it: immutability, equality, deconstruction, etc … Essentially I want the data objects to fit into all of the C# features that allow me to explore data. Records are a declarative syntax for letting me define data objects that get all of these features for free.

Having classes that behave like values has always been possible in C# and there are many types in the framework that already do this. Generally though these classes fall into the category of “data” style objects, Tuple<> for example. It’s not good or bad to do this, it’s instead an exercise in evaluating trade offs: heap vs. stack, cost of passing / returning, etc …

In the case of records we wanted to explore classes first because that is what most of the customers who valued records were already using. In future versions of the language we will allow for them to be declared as structs as well though to help customers who need to make different trade off decisions.

Value types and structs. Same thing, right?

David: In how I work with the type systems, yes. Value type is the CLR term for what is exposed in C# as a struct.

Jared: Yes they’re the same thing. Except in the case of ValueType which is a fairly special value type. It’s the base type of all value types even though value types can’t inherit from other types.

How about structs vs classes?

Jared: When discussing the difference between struct and class most people tend to focus on how structs default to allocated on the stack and classes are allocated on the heap. I tend to think about them a bit differently. A struct is in many ways a loose grouping of fields while a class is a firm container around a group of fields. When you assign structs together it essentially comes down to a field by field assignment whereas assigning classes together is always a single pointer assignment. Understanding this gives you a better sense of the trade offs between the two types: whether or not assignments are atomic, how they are laid out in memory, what level of control the type has over it’s contents, etc …

There have been several low-level type system changes across CLR and C# in recent years, like ref structs and Span<T>, ref returns, covariant return types, default interface methods, and static interface methods. On the one hand, those are great because they are targeted at performance and other needs. On the other, they are not something that most developers will use. When is the next type system feature coming that the average developer uses?

Jared: I think it will be static virtual interface methods. That is a feature we will be previewing in C# 10 that allows customers to use static methods on type parameters inside generic methods. On the face this may seem like an advanced concept which wouldn’t have broad user reach, however it opens the door to us defining generic math methods. Essentially allowing us to express mathemetical algorithms in terms of any numeric type vs. today where we have to limit to a specific type like double, int, etc … This capability is present and popular in a number of other languages and I think we will see similar usages in .NET once the feature is available.

David: I don’t know. One possibility is the static virtual interface methods feature we are working on in preview. Another possibility that I have been experimenting with is a form of specialized generic code, but we haven’t seen much need for that in our community yet.

Some of these new features press the bounds of safety. Is that OK?

Jared: They press the bounds of safety but in a way that doesn’t push the burden to customers. The rules around Span<T> are quite involved and took many months to refine, verify and make workable with common coding patterns. The burden here though was primarily on the .NET team to stretch the boundaries here and see what we could achieve.

The result is the customer can consume Span<T> and get the performance without worrying about the safety issues: the language simply prevents you from doing unsafe operations as it also does for other features. The customer needs to learn a bit about the new rules but they don’t have to worry about the safety.

David: Yes. The CLR and .NET ecosystem have always embraced code following a spectrum of safety. We strive to make normal code safe, and potentially unsafe code is something that generally has to be opted in using some mechanism, (P/Invoke, Unsafe APIs, Marshal APIs, unsafe code blocks, etc.) In the spectrum of type system provided safety rules .NET/C# exists in a fairly pragmatic place where unsafe code is possible, but we strive to make it difficult to accidentally invoke potentially unsafe behavior.

.NET is often compared to Java, with the biggest differences being value types and reified/runtime generics. Why are those two type system features valuable? Would you repeat those choices if you could re-design .NET?

David: These two type system features are valuable as they allow the way a program is executed to be expressed in code instead of merely expressing the semantic meaning of the code. This is an interesting philosophical design difference between the .NET and Java type systems, where the Java type system less often expresses details on exactly how program semantics should be implemented. This has certain benefits and drawbacks, but I believe I would likely repeat those decisions if I could re-design .NET although I would probably change a few details.

Jared: The advantage of value types is avoiding heap allocations and by extension reducing the pressure on the garbage collector. That is an invaluable tool for high performance code which often needs to ensure a GC will not happen on a given code path. I would definitely want to keep them if we were starting .NET from scratch but I would likely invest in changing how they were expressed. Rather than expressing them at the declaration, essentially having a differentiation between classes and structs, I’d explore if we could express it at usage time. Essentially have a single kind of type and at the use case decide between heap or stack allocation. That is a really difficult problem to solve without pushing a lot of complextiy to the customer. Enough that I think such a redesign may end up where we are today because it’s a very pragmatic trade off.

Closing

The type system is both the result of intentional design decisions made by the original architects of .NET and the outcome of years of organic change based on the needs of .NET users. You can likely see that the type system will continue to evolve as new scenerios and requirements present themselves. Only time will tell what those new type system capabilities will be.

Thanks again to Jared and David for sharing their insights on the type system.

Topics
.NET

Author

Richard Lander
Product Manager

Richard Lander is a Program Manager on the .NET team. He works on making .NET work great in memory-limited Docker containers, on Arm hardware like the Raspberry Pi, and enabling GPIO programming and IoT scenarios. He is part of the design team that defines new .NET runtime capabilities and features. Favourite fantasy: Dune and Doctor Who. He grew up in Canada and New Zealand.

36 comments

Discussion is closed. Login to edit/delete existing comments.

  • Morteza Talebi

    These conversation series are of the most valuable post on this blog.
    they are great to understand the basic building block under the hood for this framework.
    good job Richard.

    • Richard LanderMicrosoft employee Author

      Thanks Morteza! That's very kind. I've just written the questions for the next half-dozen posts this past weekend and am scheduling them with the various engineers I work with now. Should be awesome. The next set of posts will be much more varied. I started with runtime topics since I'm most familiar with them. I have to spend more time thinking of good questions for other parts of the product. We'll see if I succeeded.

      Read more
  • Huo Yaoyuan

    The biggest problem I think in CTS is delegate. Delegate was designed for events. They are not very suitable for functional programming. It's behaviors have causing problems for a long time:
    - It's multicast by default, but stops when any handler throws. A system graceful to exceptions must extract each singlecast.
    - It allocates. This is requiring preallocated s_XXXDelegate instances.
    - It requires explicit definition, unlike function pointers. And since spans can't be used as...

    Read more
  • Jason Lee

    I really hope that C# could have sum type (which is called discriminated union in F# and TypeScript). It’s an extremely underrated language feature. Check the blogs and the book of Scott Wlaschin from F# community.

    • Jason Lee

      Also, with sum types, we can build Result<T, E> to handle expected error, which makes applying this ASP.NET guideline much easier.

  • Paulo Pinto

    Nice overview, it is interesting to see how .NET goes, catching up with what could have been version 1.0, if the learnings of Modula-3, Eiffel, Delphi were taken into account, producing a more friendly version of "managed" C++.

    I guess better be happy that this path is being taken now instead of never.

    Which in a way also relates to Java, while the ongoing comparisasions still give the edge to .NET as per Java 16, they are...

    Read more
    • Richard LanderMicrosoft employee Author

      There is indeed a long history of programming languages to learn and be inspired from.

      catching up with what could have been version 1.0

      I understand this sentiment, but it isn't really fair or reflective of the reality of shipping a v1.0 platform. I'm only addressing this because it comes up a fair bit. If I think back to the 1.0 release, it barely got out the door. I wasn't on the team, but was...

      Read more
      • Paulo Pinto

        I get that, to make it more explicit for me, given the background of .NET architects I was expecting something that was AOT compiled, similar to C++ Builder, Delphi, VB 6 in capabilties, where the class system would just be sugar for COM, basically what .NET Native tried out to be, or from what is public information, System C# in Midori.

        For a while it seemed that it was strange to some Microsoft teams that we...

        Read more
      • anonymous

        this comment has been deleted.

      • Gonçalo Lopes

        I have a question about the hard-line on language backwards compatibility. If there was a provably equivalent procedure of converting, let’s say, a C# 1.0 program into a future breaking lang version, e.g. C# 11, would the team ever consider introducing such a breaking change to fix some of these foundational issues?

        In the past, .NET 4.0 even ran separate runtimes side-by-side for binary compatibility with .NET 2.0. Seems like this could have been a great...

        Read more
      • Richard LanderMicrosoft employee Author

        Unlikely. There have been some minor breaking changes over the years. I don’t have any examples, but that’s what the team tells me. Certainly, if there was a high-value change that had an algorithmic path from source A to source B, that could be interesting. It almost never works out that way.

        Do you have any specific changes you’d like to see?

  • Ayrat Hudaygulov

    C# type system is different from CTS – Common Type System – used in CLR
    F# type system is different from CTS and different from C# type system

    What is .NET Type system?

    • Richard LanderMicrosoft employee Author

      I meant “CLR type system” as called out in the first sentence. This is the same as the “Common Type System” you called out. We don’t use the CTS term.

  • Igor Shigaev

    My feeling is nobody in MS can CLEARLY(!) explain why they introduce “records”. And this is “bad sign” you barely know why you do it.
    We have (reference) classes. We have (value) structs. Does anybody feel tied in these types?? Let’s talk about THAT.
    I read a whole article and found nothing helpful, despite I work on .NET since 2002. Hey, be more useful at conversation – it’s not a beer party!

    • Andy GockeMicrosoft employee

      Records allow you to easily create simple “data holder” types (especially read-only ones). C# 9 supported reference types, C# 10 will support value types as well.

      It’s that simple — data holders are common and fundamental to programming and it was laborious to create them before.

      • Richard LanderMicrosoft employee Author

        That’s a much simpler answer than I gave. I like it and agree.

    • Richard LanderMicrosoft employee Author

      Here are two (partial) answers about records from the post that speak to me:

      it makes it simple for one developer to communicate to others that some objects aren’t an object with behaviors, but more just pure data. Also, the new record types are much less effort to use than building the equivalent logic would have been in past versions of C#.

      and

      I often divide types into whether they primarily provide data or...

      Read more
  • Muhammad Miftah

    I’d love to see C# or the CLR incorporate more ideas and concepts from TypeScript. So many myriad things can be modelled using TS’s type system, and it astounds me it all compiles down to JavaScript and just works! I know there are certain things built into TS due to JavaScript vulgarities, but seeing C# adopt a TS-approach would be a dream come true.

    • Richard LanderMicrosoft employee Author

      I’m not a TS user. Are there a couple concepts that you have in mind?

      • Savo Pejović · Edited

        Inline type definitions
        Creating altered versions of types is so easy you can do it inline:

        <code>

        Here, without an explicit class definition, I have defined the return type as an AttachedDoc class with an extra "comment" field. This is called an "intersection".

        Another great example is LINQ's join statement. When joining two tables, instead of having to specify TResult of the resulting object and having to map the values into that object, you could have another method...

        Read more
  • Martin Sedlmair

    Yes, the type system is really good with a few mistakes made in history (the void as explained above). Also metdata and reflection capabilities are really good. What I'm missing though is a deeper documentation of this topic. There is pretty much from the older days, but for example for the MetadataLoadContext you can barely find any good documentation. There's Type, RuntimeTime, RuntimeTypeHandle, TypeHandle, TypeInfo, TypeCode, etc... it would be really be good to have...

    Read more
      • Martin Sedlmair

        Thanks Rich. Looking at the list a lot of things have already been done so I hope it will come soon 😉

  • MgSam

    Who wrote these questions?

    There has been several low-level type system changes across CLR and C# in recent years, like ref structs and Span, ref returns, covariant return types, default interface methods, and static interface methods. On the one hand, those are great because they are targeted at performance and other needs. On the other, they are not something that most developers will use. When is the next type system feature coming that the...

    Read more
    • Richard LanderMicrosoft employee Author

      I've written all of these "conversation" questions. Thanks for the plural/singular grammar catch.

      The point is that the many of the type system features are niche and in some cases hard to use. Clearly, they were all built for a reason and have a target audience. Sure, some of these features may have broader adoption than I expect. I doubt ref structs, for example, are showing up in many code bases. I've used them and can...

      Read more
      • Martin Sedlmair

        Just because you mention the ref structs. Because I’m doing a lot of performance measurements. Are there any good benchmarks that show the real benefit?

      • Richard LanderMicrosoft employee Author · Edited

        I could be wrong, but I don't see ref structs as have any specific performance benefit over regular structs. However, there is significant benefit for methods and types that can be fully oriented (not having to resort to using . That's what ref structs are for. I see them as an extension of the programming model. Here's an extension of that which I asked for: https://github.com/dotnet/csharplang/discussions/2582.

        Read more
      • Martin Sedlmair

        That makes sense. Thanks.

  • Ian Marteens

    Eiffel had embedded classes. You could turn any reference type into a value type on the fly.

      • Chun Er ON

        What Jared wants sounds like c++?