Search code examples
c#projectionreadonlyobject-initializers

Object initializer for readonly properties in c#


If you have the class:

class Foo {
      Bar Bar { get; } = new Bar();
}

class Bar {
      string Prop {get; set; }
}

You can use a object initialise like:

var foo = new Foo { 
    Bar = { Prop = "Hello World!" }
}

If you have a class

class Foo2 {
      ICollection<Bar> Bars { get; } = new List<Bar>();
}

You can write

var foo = new Foo2 { 
    Bars = { 
        new Bar { Prop = "Hello" }, 
        new Bar { Prop = "World" }
    }
}

but, I would like to write something like

var items = new [] {"Hello", "World"};
var foo = new Foo2 { 
    Bars = { items.Select(s => new Bar { Prop = s }) }
}

However, the code above does not compile with:

cannot assigne IEnumerable to Bar

I cannot write:

var foo = new Foo2 { 
    Bars = items.Select(s => new Bar { Prop = s })
}

Property Bars is readonly.

Can this be archived?


Solution

  • 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.