Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In C#9, how do init-only properties differ from read-only properties?

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?

like image 791
Mark A. Donohoe Avatar asked Nov 09 '20 09:11

Mark A. Donohoe


People also ask

What does |= mean in C?

The bitwise OR assignment operator ( |= ) uses the binary representation of both operands, does a bitwise OR operation on them and assigns the result to the variable.

What is ?: operator in C?

In other words, we can also say that an operator is a symbol that tells the compiler to perform specific mathematical, conditional, or logical functions. It is a symbol that operates on a value or a variable. For example, + and - are the operators to perform addition and subtraction in any C program.

What does %d do in C?

%d is a format specifier, used in C Language. Now a format specifier is indicated by a % (percentage symbol) before the letter describing it. In simple words, a format specifier tells us the type of data to store and print. Now, %d represents the signed decimal integer.

What is -= in C?

This operator is a combination of '-' and '=' operators. This operator first subtracts the value on the right from the current value of the variable on left and then assigns the result to the variable on the left. Example: (a -= b) can be written as (a = a - b) If initially value stored in a is 8.


1 Answers

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.

like image 78
Lasse V. Karlsen Avatar answered Oct 05 '22 23:10

Lasse V. Karlsen