14

I keep reading up on init-only properties in C#9 but I thought we already had that with read-only properties which can only be set in a constructor. After that, it’s immutable.

For instance, in the class here, both Name and Description can be assigned to in the constructor, but only there, which is exactly how init-only properties are described.

Example Class


class Thingy {
    
    public Thingy(string name, string description){
        Name        = name;
        Description = description;
    }
    
    public string Name        { get; }
    public string Description { get; }
    
    public override string ToString()
        => $"{Name}: {Description}";
}

Test program

using System;

class Program {

    public static void Main (string[] args) {
        
        var thingy = new Thingy("Test", "This is a test object");
        Console.WriteLine(thingy);
        // thingy.Name = “Illegal”; <— Won’t compile this line
    }
}

This outputs the following:

Test: This is a test object

Additionally, if I attempt to modify Name or Description after the constructor runs, it won’t compile.

So what am I missing?

Daniel A. White
  • 187,200
  • 47
  • 362
  • 445
Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • 2
    Since I only have observational knowledge of this I will refrain from posting an answer, someone will undoubtedly come along and post a good answer, but here goes. An `init` accessor is identical to a `set` accessor except that the compiler will prevent you from using it outside of the "allowed contexts", which is from a constructor, an object initializer, or the new `with` keyword. This also means that reflection will be able to set values using it, so for instance deserialization will work. – Lasse V. Karlsen Nov 09 '20 at 09:53
  • The difference is that `init`-only properties can also be used in an object initializer, meaning it's no longer necessary to write giant sets of constructors that initialize everything in every possible combination (which is one of the major pains of non-trivial immutable types today). – Jeroen Mostert Nov 09 '20 at 09:54
  • @LasseV.Karlsen Huh, right you are – canton7 Nov 09 '20 at 09:59
  • This could be helpful [Init-Only](https://www.thomasclaudiushuber.com/2020/08/25/c-9-0-init-only-properties/) (**object initializer**) – Rans Nov 09 '20 at 10:04
  • Man… So many good answers below. It’s gonna be hard to pick which one is the excepted answer. – Mark A. Donohoe Nov 09 '20 at 10:11
  • @canton7 I've never seen this `modreq` modifier, learned something new today, this is one reason I didn't feel qualified to post an actual answer (though I see now that the answers here gloss over technical details anyway) is that I couldn't see what actually made the difference between a `set` accessor and an `init` accessor, I have inspected the property definitions and method definitions using reflection and there was no trace of this `init` difference anywhere, but now I know that there's something down on the IL level that isn't readily available using normal reflection. – Lasse V. Karlsen Nov 09 '20 at 10:13
  • And one thing I've wondered about as well is if it is possible to consume a class library written in C# 9, using the new init keywords, in older C# versions and compilers, and then what would happen, would the `init` accessor be considered a normal `set` accessor, or would it appear to be invisible. I assume this `modreq` thing is going to prevent older compilers from considering the accessor, but now at least I know the mechanism so I guess it is time to test it out, .NET 5 and C# 9 is out this week so I will do some testing :) – Lasse V. Karlsen Nov 09 '20 at 10:15
  • @LasseV.Karlsen Yeah, you can see the `modreq` on the setter [on SharpLab](https://sharplab.io/#v2:EYLgtghgzgLgpgJwDQxASwDZICYgNQA+AAgEwCMAsAFDVEDMABKQwMLUDe1D3TjaAdjAYAxAPaiG7BgHM4MANwMBaBQwC+1DTSoB6HQwCS0/qIRwGAWgsN+cONnsMAZqYYBlABYQEABwAyEMAMABTQSjBQDABuiFBoovwMok4MMB7mCACugmhg5hgQ0pHAcB4C2KnpDADGomA+mIjRsfH8AJTU/BB5UD4Q1eZEZAB0AErZMLlwwyx1DRiIbohRaANQHFw89ExkAGxMJIZQAKIAHvAIXRgG/CqS6ppAA=). – canton7 Nov 09 '20 at 10:22

5 Answers5

19

An init accessor is identical to a set accessor in implementation in almost all areas, except that it is flagged in a certain manner that makes the compiler disallow usage of it outside of a few specific contexts.

By identical I really do mean identical. The name of the hidden method that is created is set_PropertyName, just as with a set accessor, and using reflection you can't even tell them apart, they will appear to be identical (see my note about this below).

The difference is that the compiler, using this flag (more on this below) will only allow you to set a value to the property in C# (more on this below as well) in a few specific contexts.

  • From a constructor of the type, or a derived type
  • From an object initializer, ie. new SomeType { Property = value }
  • From within the construct with the new with keyword, ie. var copy = original with { Property = newValue }
  • From within the init accessor of another property (so one init accessor can write to other init accessor properties)
  • From attribute specifiers, so you can still write [AttributeName(InitProperty = value)]

Outside of these, which basically amounts to normal property assignment, the compiler will prevent you from writing to the property with a compiler error like this:

CS8852 Init-only property or indexer 'Type.Property' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.

So given this type:

public class Test
{
    public int Value { get; init; }
}

you can use it in all these ways:

var test = new Test { Value = 42 };
var copy = test with { Value = 17 };

...

public class Derived : Test
{
    public Derived() { Value = 42; }
}

public class ViaOtherInit : Test
{
    public int OtherValue
    {
        get => Value;
        init => Value = value + 5;
    }
}

but you can not do this:

var test = new Test();
test.Value = 42; // Gives compiler error

So for all intents and purposes this type is immutable, but it now allows you to more easily construct an instance of the type without tripping into this immutability issue.


I said above that reflection doesn't really see this, and note that I learned about the actual mechanism just today so perhaps there is a way to find some reflection code that can actually tell the difference. The important part is that the compiler can see the difference, and here it is.

Given that the type is declared as:

public class Test
{
    public int Value1 { get; set; }
    public int Value2 { get; init; }
}

then the generated IL for those two properties will look like this:

.property instance int32 Value1()
{
    .get instance int32 UserQuery/Test::get_Value1()
    .set instance void UserQuery/Test::set_Value1(int32)
}
.property instance int32 Value2()
{
    .get instance int32 UserQuery/Test::get_Value2()
    .set instance void modreq(System.Runtime.CompilerServices.IsExternalInit) UserQuery/Test::set_Value2(int32)
}

You can see that the Value2 property setter (the init method) has been tagged/flagged (unsure if these are the right words, I did say I learned this today) with the modreq(System.Runtime.CompilerServices.IsExternalInit) type which tells the compiler this method is not your uncle's set accessor.

This is how the compiler will know to treat this accessor method differently than a normal set accessor.

Given @canton7's comments on the question this modreq construct also means that if you try to use a library compiled with the new C# 9 compiler in an older C# compiler it will not consider this method. It also means you won't be able to set the property in an object initializer but that is of course only available in C# 9 and newer compilers anyway.


So what about reflection for setting the value? Well, turns out reflection will be able to call the init accessor just fine, which is nice because this means deserialization, which you could argue is a kind of object initialization, will still work as you would expect.

Observe the following LINQPad program:

void Main()
{
    var test = new Test();
    // test.Value = 42; // Gives compiler error
    typeof(Test).GetProperty("Value").SetValue(test, 42);
    test.Dump();
}

public class Test
{
    public int Value { get; init; }
}

which produces this output:

output of reflection code

and here's a Json.net example:

void Main()
{
    var json = "{ \"Value\": 42 }";
    var test = JsonConvert.DeserializeObject<Test>(json);
    test.Dump();
}

which gives the exact same output as above.

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
  • Ah, sorry about the 4K copy of the LINQPad screenshot :P – Lasse V. Karlsen Nov 09 '20 at 10:30
  • 1
    modreq - as the name suggests - is required. It is part of the signature of the method, and used by the runtime for overload resolution. If you try to call a method with a modreq and you don't specify it in the call, it will not resolve. For example, modred is used for example, for out parameters. I think that should be a `TargetInvocationException`. Edit: Refer to the [ECMA standard on the CLI](https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf). – Theraot Nov 09 '20 at 10:44
  • Nice to know, didn't know about `modreq` until today so I was wondering how the compiler could tell apart a `set` property from a `init` property. LINQPad has this nice Diff-thing it can do so for instance `Util.Dif(p1, p2)` where `p1` and `p2` are `PropertyInfo` for the two types of properties, and they only differed in metadatatokens and property names, but I see now that this is deeper than that. – Lasse V. Karlsen Nov 09 '20 at 10:47
  • 1
    Qudos for diving into the detail! – canton7 Nov 09 '20 at 11:00
5

The difference is that init properties can be set from an object initializer, as well as the constructor:

public class C
{
     public int Foo { get; init; }   
}

// Legal
var c = new C()
{
    Foo = 3,  
};

// Illegal
c.Foo = 4;

See SharpLab.

If you declare a record with init properties, the compiler also lets you set them using a with expression:

public record C
{
    public int Foo { get; init; }
}

var c = new C() { Foo = 3 };
var d = c with { Foo = 4 };

See SharpLab.

They also appear as writable when using reflection. This was a deliberate design decision to allow reflection-based serializers to deserialize into objects with init-only properties, without needing modification.

public class C
{
    public int GetterOnly { get; }
    public int InitOnly { get; init; }
}

typeof(C).GetProperty("GetterOnly").CanWrite); // False
typeof(C).GetProperty("InitOnly").CanWrite); // True

See SharpLab.

canton7
  • 37,633
  • 3
  • 64
  • 77
  • I’m torn between your answer and the answer that talks about Set Methods which I didn’t even consider. That makes it really powerful. Question though, how would this apply to structs which afaik have to support a parameterless constructor? (I know records address at lot of that.) – Mark A. Donohoe Nov 09 '20 at 10:15
  • @MarkA.Donohoe They apply the same to structs as they do to classes. Just like getter-only properties, init-only properties can be set from the constructor, and they'll also be initialized to their default value. You're not forced to set an init-only property. If a struct has an init-only property and you call its parameterless constructor and then don't explicitly set it, the init-only property will just keep its default value. – canton7 Nov 09 '20 at 10:20
4

You can write an init body. Just like a set body. Except it will only work during initialization.

Also init-only properties can be set from object initializers or from the constructor.

Example of init body:

    public string LastName
    {
        get => _lastName;
        init => _lastName = string.IsNullOrWhiteSpace(value)
            ? throw new ArgumentException("Shouldn't be null or whitespace",
                nameof(LastName))
            : value;
    }

Example from the first link below.

See also:

Theraot
  • 31,890
  • 5
  • 57
  • 86
  • *`init` properties can be set from an object initializer, as well as the constructor...* And in case with `set`'less property, they can only be assigned from the initializer? Or only from the constructor? (I tried the link to SharpLab but for some reason, the first one is generating an error and I'm too unclear how it's supposed to work to say why.) – Konrad Viltersten Mar 19 '22 at 08:59
  • @KonradViltersten I think you were trying to comment on [canton7's answer](https://stackoverflow.com/a/64749335/402022). Anyway, as far as I can tell something is up with the main branch (18 Mar 2022) in SharpLab, try changing it to default. Regardless, the first one is supposed to cause an error (see the `//Illegal` comment). And yes, you are right, `init` properties can be set from the object initializers or from the constructor, I'll update my answer (edit: done). A property without `set` cannot be set from object initializers, only from the constructor. – Theraot Mar 19 '22 at 09:18
  • You're perfectly correct in all your statements in the comment above. I have no idea how I managed to post to another answer than I intended. Brain poof, I guess... :) – Konrad Viltersten Mar 19 '22 at 13:32
2

Imagine you would have a parameterless constructor:

class Thingy {
    
    public Thingy(){
    }
    
    public string Name        { get; }
    public string Description { get; }
    
    public override string ToString()
        => $"{Name}: {Description}";
}

Then you couldn't do this:

var test = new Thingy
 {
 Name = "Test",
 Description "Test"
 };

If you write your class with the init keyword:

class Thingy {
    
    public Thingy(){
    }
    
    public string Name        { get; init; }
    public string Description { get; init; }
    
    public override string ToString()
        => $"{Name}: {Description}";
}

Then the above code would be legal.

SomeBody
  • 7,515
  • 2
  • 17
  • 33
0

I think that answer to this question may be found here: Official doc

"The underlying mechanisms for building immutable data in C# haven't changed since 1.0. They remain: Declaring fields as read-only. Declaring properties that contain only a get accessor.

These mechanisms are effective at allowing the construction of immutable data but they do so by adding cost to the boilerplate code of types and opting such types out of features like object and collection initializers. This means developers must choose between the ease of use and immutability."

The documentation goes into detail explaining the differences.

Theraot
  • 31,890
  • 5
  • 57
  • 86
Piotr G
  • 133
  • 8