7

The following combination of object and collection initializers does not give compilation error, but it is fundamentally wrong (https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers#examples), because the Add method will be used in the initialization:

public class Foo
{
    public List<string> Bar { get; set; }
}

static void Main()
{
    var foo = new Foo
    {
        Bar =
        {
            "one",
            "two"
        }
    };
}

So you'll get NullReferenceException. What is the reason for making such an unsafe decision while developing the syntax of the language? Why not to use initialization of a new collection for example?

smolchanovsky
  • 1,775
  • 2
  • 15
  • 29
  • Fundamentally wrong? Unsafe? Do you mind to elaborate? – Rui Jarimba Oct 20 '18 at 14:51
  • 4
    This code will throw a NullReferenceException, which is not obvious at first glance (at least, it took me a while to notice it) – Kevin Gosse Oct 20 '18 at 14:54
  • @KevinGosse, you are absolutely right. I will add this to the question – smolchanovsky Oct 20 '18 at 14:56
  • 2
    To be honest, I don't know why the `new XXX` is not mandatory before a collection initializer. I've never ever used one without `new`, so I didn't realize this was possible – Kevin Gosse Oct 20 '18 at 14:56
  • 1
    You're clearly not the first to bump into it: https://github.com/dotnet/roslyn/issues/25508 But I doubt we'll learn the reason of why this is legal in first place. I suppose this is to cover the edge case where the property has a getter but no setter. Still, it looks very dangerous for little benefit – Kevin Gosse Oct 20 '18 at 15:01
  • Only _javascript / json_ kids would write that code...:O) – jsanalytics Oct 24 '18 at 17:13
  • 1
    Because `Bar = { }` in C# is just syntactic sugar for calling an "Add" method on Bar, not for creating a new instance (try with string[] instead and see the error). I agree with @jsanalytics :-) – Simon Mourier Oct 26 '18 at 07:29
  • @SimonMourier Thanks, I know it. The question is what was the reason for the creation of such a strange and dangerous syntactic sugar. – smolchanovsky Oct 26 '18 at 07:39
  • 2
    This is not wrong and the design was carefully considered to give a good balance of representational power and safety. Like any feature, if you use it incorrectly, it will break, so use it correctly! – Eric Lippert Oct 28 '18 at 01:26
  • You suggest initializing a new collection. **You do so with the `new` keyword**. This should make sense: you want a new object, you say `new`. – Eric Lippert Oct 28 '18 at 01:26
  • @EricLippert I can give an example where the `new` keyword is not needed to create a new object: `string[] array = { "A" , "B" };` – smolchanovsky Nov 09 '18 at 20:58
  • @StanislavMolchanovsky: So can I: `object x = 1;` There is not a requirement that creating a new object has `new` in it, but it is a pretty good bet that if you want a new object, and you're not getting one, then getting `new` in there somewhere could help. – Eric Lippert Nov 09 '18 at 21:40

4 Answers4

4

First, it's not only for combination of object and collection initializers. What you are referring here is called nested collection initializers, and the same rule (or issue by your opinion) applies to nested object initializers. So if you have the following classes:

public class Foo
{
    public Bar Bar { get; set; }
}

public class Bar
{
    public string Baz { get; set; }
}

and you use the following code

var foo = new Foo
{
    Bar = { Baz = "one" }
};

you'll get the same NRE at runtime because no new Bar will be created, but attempt to set Baz property of the Foo.Bar.

In general the syntax for object/collection initializer is

target = source

where the source could be an expression, object initializer or collection initializer. Note that new List<Bar> { … } is not a collection initializer - it's an object creation expression (after all, everything is an object, including collection) combined with collection initializer. And here is the difference - the idea is not to omit the new, but give you a choice to either use creation expression + object/collection initializer or only initializers.

Unfortunately the C# documentation does not explain that concept, but C# specification does that in the Object Initializers section:

A member initializer that specifies an object initializer after the equals sign is a nested object initializer, i.e. an initialization of an embedded object. Instead of assigning a new value to the field or property, the assignments in the nested object initializer are treated as assignments to members of the field or property. Nested object initializers cannot be applied to properties with a value type, or to read-only fields with a value type.

and

A member initializer that specifies a collection initializer after the equals sign is an initialization of an embedded collection. Instead of assigning a new collection to the target field, property or indexer, the elements given in the initializer are added to the collection referenced by the target.


So why is that? First, because it clearly does exactly what you are telling it to do. If you need new, then use new, otherwise it works as assignment (or add for collections).

Other reasons are - the target property could not be settable (already mentioned in other answers). But also it could be non creatable type (e.g. interface, abstract class), and even when it is a concrete class, except it is a struct, how it will decide that it should use new List<Bar> (or new Bar in my example) instead of new MyBarList, if we have

class MyBarList : List<Bar> { }

or new MyBar if we have

class MyBar : Bar { }

As you can see, the compiler cannot make such assumptions, so IMO the language feature is designed to work in the quite clear and logical way. The only confusing part probably is the usage of the = operator for something else, but I guess that was a tradeoff decision - use the same operator = and add new after that if needed.

Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
2

Take a look at this code and the output of it due to the Debug.WriteLine():

public class Foo
{
    public ObservableCollection<string> _bar = new ObservableCollection<string>();

    public ObservableCollection<string> Bar
    {
        get
        {
            Debug.WriteLine("Bar property getter called");
            return _bar;
        }

        set
        {
            Debug.WriteLine("Bar allocated");
            _bar = value;
        }
    }

    public Foo()
    {
        _bar.CollectionChanged += _bar_CollectionChanged;
    }

    private void _bar_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        Debug.WriteLine("Item added");
    }
}

public MainWindow()
{
    Debug.WriteLine("Starting..");

    var foo = new Foo
    {
        Bar =
        {
            "one",
            "two"
        }
    };

    Debug.WriteLine("Ending..");
}

The output is:

Starting..
Bar property getter called
Item added
Bar property getter called
Item added
Ending..

For you questions: What is the reason for making such an unsafe decision while developing the syntax of the language? Why not to use initialization of a new collection for example?

Answer: As you can see the intention of the designer of that feature was not to reallocate the collection but rather to help you add items to it more easily considering that you manage your collection allocation by yourself.

Hope this clear things out ;)

Eibi
  • 402
  • 4
  • 17
0

Consider the following code:

class Program
{
    static void Main()
    {
        var foo = new Foo
        {
            Bar =
            {
                "one",
                "two"
            }
        };
    }
}

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

The compiler does not know whether you already created a new list instance within the class constructor (or in another method).

Recall that collection initializer is a series of calls to Add method on an existing collection!

See also: Custom Collection Initializers

Also note that this initializer applies to a collection that was exposed as a property. Hence the collection initializer is possible as part of the outer object initializer (the Foo object in your example).

However, if it was a simple variable, the compiler would not let you to intialize the collection this way. Here is an example:

List<string> list = 
{
    "one",
    "two"
};

This will throws a compilation error.

As last example, the output of the following code will be: "one, two, three, four, ". I think that now you understand why. Pay attention to the list static instance, as well as to the private modifier in the "set" of the Bar property, which does not matters because the initializer just calls the Add method, which is accessible even when the Bar "set" is private.

class Program
{
    static void Main()
    {
        var foo1 = new Foo
        {
            Bar =
            {
                "one",
                "two"
            }
        };

        var foo2 = new Foo
        {
            Bar =
            {
                "three",
                "four"
            }
        };

        PrintList(foo1.Bar);
    }

    public static void PrintList(List<string> list)
    {
        foreach (var item in list)
        {
            Console.Write(item + ", ");
        }
        Console.WriteLine();
    }

}

public class Foo
{
    private static readonly List<string> _bar = new List<string>();
    public List<string> Bar { get; private set; } = _bar;
}
Ilan
  • 624
  • 6
  • 11
0

I believe the key thing to understand here is that there are two syntactic sugar flavors at play (or at least, there should be):

  1. Object Initialization
  2. Collection Initialization

Take away the List for a moment and look at the field as an object:

public class Foo
{
    public object Bar { get; set; }
}

When using Object Initialization, you assign an object (or null):

var foo = new Foo()
{
    Bar = new object(); //or Bar = null
}

Now, let's go back to your original example and slap Collection Initialization on top of this. This time around, the compiler realizes this property implements IEnumerable and the array you have provided is of the right type, so it attempts to call the Add method of the interface. It must first go looking for the object, which in your case is null because you haven't initialized it internally. If you debug through this, you will find that the getter gets called and returns null, hence the error.

The correct way of mixing both features then would be for you to assign a new object that you initialize with your values:

var foo = new Foo()
{
    Bar = new List<string>(){ "one", "two" }
};

If you debug this version, you will find that the setter is called instead, with the new instance you initialized.

Alternatively, you can initialize your property internally:

public List<string> Bar { get; set; } = new List<string>();

If you debug this version, you will find that the property is first initialized with a value and your version of the code then executes without error (by calling the getter first):

var foo = new Foo()
{    
    Bar = {"one", "two"}
};

To illustrate the syntactic sugar aspect, Collection Initialization only works within the confines of a constructor calling statement:

List<string> bar = {"one", "two" }; //ERROR: Can only use array initializer expressions to assign to array types. Try using a new expression instead.

List<string> bar = new[] { "one", "two" }; //ERROR: Cannot implicitly convert type 'string[]' to 'System.Collections.Generic.List<string>'

List<string> bar = new List<string>() { "one", "two" }; //This works!

If you wish to allow initialization like in your original example, then the expectation is that the variable will be set to an instance before the Add method can be called. This is true whether you use syntactic sugar or not. I could just as well run into the same error by doing this:

var foo = new Foo();
foo.Bar.Add("one");

So you may want to initialize the variable in order to cover all bases, unless of course a null value has a semantic meaning in your application.

JuanR
  • 7,405
  • 1
  • 19
  • 30