C# Object Initialization of Read Only Collection Properties

C# object initialization of read only collection properties

This:

MyClass c = new MyClass
{
StringCollection = { "test2", "test3" }
};

is translated into this:

MyClass tmp = new MyClass();
tmp.StringCollection.Add("test2");
tmp.StringCollection.Add("test3");
MyClass c = tmp;

It's never trying to call a setter - it's just calling Add on the results of calling the getter. Note that it's also not clearing the original collection either.

This is described in more detail in section 7.6.10.3 of the C# 4 spec.

EDIT: Just as a point of interest, I was slightly surprised that it calls the getter twice. I expected it to call the getter once, and then call Add twice... the spec includes an example which demonstrates that.

Readonly field in object initializer

Object Initializer internally uses a temporary object and then assign each value to the properties. Having a readonly field would break that.

Following

TestStruct ts = new TestStruct 
{
TestField = "something";
};

Would translate into

TestStruct ts;
var tmp = new TestStruct();
tmp.TestField = "something"; //this is not possible
ts = tmp;

(Here is the answer from Jon Skeet explaining the usage of temporary object with object initalizer but with a different scenario)

Object initializer for readonly properties in c#

If you read the actual compiler errors (and the docs for collection initializers), you'll find that collection initializers are merly syntactic sugar for Add() calls:

CS1950: The best overloaded collection initalizer method System.Collections.Generic.ICollection<Bar>.Add(Bar) has some invalid arguments

CS1503: Argument #1 cannot convert System.Collections.Generic.IEnumerable<Bar> expression to type Bar

So the syntax SomeCollection = { someItem } will be compiled to SomeCollection.Add(someItem). And you can't add IEnumerable<Bar> to a collection of Bars.

You need to manually add all items:

foreach (bar in items.Select(s => new Bar { Prop = s }))
{
foo.Bars.Add(bar);
}

Or, given shorter code is your goal, do the same in Foo2's constructor:

public class Foo2 
{
public ICollection<Bar> Bars { get; }

public Foo2() : this(Enumerable.Empty<Bar>()) { }

public Foo2(IEnumerable<Bar> bars)
{
Bars = new List<Bar>(bars);
}
}

Then you can initialize Foo2 like this:

var foo = new Foo2(items.Select(...));

For a funny abuse of the collection initializer syntax as supposed by @JeroenMostert, you could use an extension method:

public static class ICollectionExtensions
{
public static void Add<T>(this ICollection<T> collection, IEnumerable<T> items)
{
foreach (var item in items)
{
collection.Add(item);
}
}
}

Which allows this:

public class Foo
{
public ICollection<string> Bar { get; } = new List<string>();
}

var foo = new Foo
{
Bar = { new [] { "foo", "bar", "baz" } }
};


But that's just nasty.

Initialize get-only collection in object initializer from existing collection

No. You can't assign this read-only property from a collection initializer. It is read-only after all.

TheList = { "A", "B" } works since it calls Add on TheList (once for each item added), it doesn't create and assign a new instance, which it is not allowed to.

TheList = { existingList } doesn't work since there is a typing issue (TheList = { existingList[0] } does work).

The best option you have it to create a constructor parameter and drop your idea of using collection initializers for something it isn't fit for.

C# Class without explicit constructor and read only property

This somewhat confusing object initializer syntax retrieves the collection with the get accessor and uses the public Add method on the collection. It works because the compile-time type of the property IList<Person> has (inherits) a public Add method with a compatible signature (takes in a Person argument).

Add to a readonly collection in a constructor?

Try this. It is definitely possible to add elements on construction

var node = new Node
{
Children =
{
new Node(),
new Node()
}
};

C# object initializer will initialize read only properties, but for non-primitive types only

var baz = new Foo { Bar = { Prop = "Hello World!" } }; // legal

This is not an assignment to Bar. It is essentially:

var tmp = new Foo();
tmp.Bar.Prop = "Hello World!";
var baz = tmp;

At no point is .Bar assigned to.

Conversely, however:

var buzz = new Foo { Primitive = 1 };

is:

var tmp = new Foo();
tmp.Primitive = 1;
var buzz = tmp;

which does assign to .Primitive.

Why can I use an anonymous collection initializer with a read-only auto-property while I can't use an object initializer

This is calling the Add method on the object referenced by ClientPermissions:

ClientPermissions = { "a1", "b1" }

It is not assigning a new object to that property, hence why it is allowed.

This, conversely, is invalid, because you cannot assign a new object to that property after construction:

ClientPermissions = new HashSet { "a1", "b1" }

The relevant documentation is here.

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

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.



Related Topics



Leave a reply



Submit