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.
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}";
}
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?
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.
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.
%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.
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.
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.
new SomeType { Property = value }
with
keyword, ie. var copy = original with { Property = newValue }
init
accessor of another property (so one init
accessor can write to other init
accessor properties)[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:
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With