I just stumbled upon the following issue:
class Settings
{
// Let's set some default value: { 1 }
public ICollection<int> AllowedIds = new List<int>() { 1 };
}
static void Main(string[] args)
{
var s = new Settings
{
AllowedIds = { 1, 2 }
};
Console.WriteLine(string.Join(", ", s.AllowedIds)); // prints 1, 1, 2
}
I understand why this happens: AllowedIds = { 1, 2 }
is not an assignment but a collection initializer inside an object initializer, i.e., it's an implicit call of AllowedIds.Add(1); AllowedIds.Add(2)
.
Still, for me it was a gotcha, since it looks like an assignment (since it uses =
).
As an API/library developer (let's say I'm the one developing the Settings
class) who wants to adhere to the principle of least surprise, is there anything I can do to prevent the consumers of my library from falling into that trap?
Footnotes:
In that particular case, I could use an
ISet/HashSet<int>
instead ofICollection/List
(since duplicates do not make sense forAllowedIds
), which would yield the expected result of1, 2
. Still, initializingAllowedIds = { 2 }
would yield the counter-intuitive result of1, 2
.I found a related discussion on the C# github repo, which basically concluded that, yes, this syntax is confusing, but it's an old feature (introduced in 2006), and we can't change it without breaking backwards compatibility.
If you are not expecting the user of the
Settings
class to add toAllowedIds
, why expose it as anICollection<int>
(which contains anAdd
method and signifies the intention to be added into)?The reason why
AllowedIds = { 1, 2 }
works in your code is because C# uses duck typing to call theAdd
method on the collection. If you eliminate the possibility of the compiler finding anAdd
method, there will be a compile error on the lineAllowedIds = { 1, 2 }
, thus preventing the trap.You can do something like:
This way you are still allowing the caller to set a new collection using the setter, while preventing the trap you mentioned.