Changing semantics in C# analyser "Collection initialization can be simplified" IDE0028
Asked Answered
H

1

6

In C# 12 there's the "Collection initialization can be simplified" analyser IDE0028.

I've been wondering how the fixer decides when to fix or not fix initialisers such as new(), new List<string>(), new Collection<int>(), Array.Empty<double>(), new string[] { }, etc. It's fix is to replace a qualifying initialiser with [].

For example:

// Collection initialization can be simplified
public List<string> Foo1 { get; } = new List<string>();
public HashSet<string> Foo2 { get; } = new HashSet<string>();
public ICollection<string> Foo3 { get; } = new List<string>();
public Dictionary<int, string> Foo4 = new Dictionary<int, string>();

// Collection initialization can be simplified (may change semantics)
public IEnumerable<string> Bar1 { get; } = new List<string>();
public ICollection<string> Bar2 { get; } = new Collection<string>();

// no code fix offered
public IDictionary<int, string> Baz1 = new Dictionary<int, string>();

I usually choose a collection type that suits a specific purpose. So I don't want to blindly accept the analyser's recommendation without understanding what collection type the compiler will actually use when it sees [].

So:

  • How does the analyser/fixer decide when to suggest a fix, and what exactly does it mean by "may change semantics"
  • What collection type does the compiler/runtime actually use when it encounters []
Hentrich answered 10/9 at 5:13 Comment(4)
I think it's reasonable to deduce that it adds the "may change semantics" if you're not going to end up with exactly the same type you're currently explicitly specifying.Madeline
@Madeline One would think so, but it doesn't mention a "semantic change" for List to ICollection, for example.Hentrich
@Hentrich That's not what "change" means. It is comparing the runtime type that the collection literal will produce, with the runtime type that your current code would produce, not with the declared type of the property.Flume
@Flume That comment was just as valuable as the full answer below. (For me, even more so, as it only "clicked" after I read that comment.)Hentrich
F
9

"May change semantics" just means that using a collection literal here does not produce code equivalent to the code you currently have.

Rules for determining what type to use are specified in the Construction and Collection Literal Translation sections of the feature specification.

For concrete classes and structs (without a create method), the collection literal will create an instance of that class/struct using its parameterless constructor, and then add the elements to.

How things are added to the collection also follows some rules, but they are not relevant here because there are no elements in any of the collection literals. Read the spec if you are interested.

This explains why Foo1, Foo2 and Foo4 cases do not change semantics.

For interfaces, the spec gives relatively more freedom. For a read-only interface like IEnumerable<T> or IReadOnlyList<T>, the compiler is free to use any concrete type that fulfils some some requirements.

Therefore, the Bar1 case says "might change semantics". The collection literal is actually translated to Array.Empty() under the hood.

For mutable interfaces like ICollection<T>, it is specified that the collection literal will always be a List<T>, so the Foo3 case does not say "may change semantics", but Bar2 does.

Lastly, collection literals simply does not support IDictionary<K, V>, so there is no diagnostic for Baz1. Out of all the interfaces, collection literals only work with IEnumerable<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection<T> and IList<T>.

Flume answered 10/9 at 5:53 Comment(1)
Thanks for this comprehensive explanation!Hentrich

© 2022 - 2024 — McMap. All rights reserved.