C# object initializer wanting to use wrong Add method
Asked Answered
L

3

6

I have the following class hierarchy:

public class Row : ICloneable, IComparable, IEquatable<Row>,
    IStringIndexable, IDictionary<string, string>,
    ICollection<KeyValuePair<string, string>>,
    IEnumerable<KeyValuePair<string, string>>,
    System.Collections.IEnumerable
{ }

public class SpecificRow : Row, IXmlSerializable,
    System.Collections.IEnumerable
{
    public void Add(KeyValuePair<MyEnum, string> item) { }
}

However, trying to do the following gives an error:

var result = new SpecificRow
    {
        {MyEnum.Value, ""},
        {MyEnum.OtherValue, ""}
    };

I get this error:

The best overloaded Add method 'Row.Add(string, string)' for the collection initializer has some invalid arguments

How can I make it so that using an object initializer on the derived class SpecificRow allows type MyEnum? It seems like it should see the Add method in SpecificRow.

Update: I implemented an extra interface on SpecificRow so it now looks like this:

public class SpecificRow : Row, IXmlSerializable,
    System.Collections.IEnumerable,
    ICollection<KeyValuePair<MyEnum, string>>
{ }

However, I still get the same Add error. I'm going to try implementing IDictionary<MyEnum, string> next.

Lacroix answered 1/2, 2010 at 20:47 Comment(1)
@JonH: incorrect, the error message is stating that the "best overload" that could be found for the given call site is Add(string, string), and this doesn't match the arguments actually being passed in.Circumnavigate
V
8

A collection initializer does not necessarily look at any ICollection.Add(x) method. More specifically, for a collection initializer

new SpecificRow {
    { ? }
}

C# looks at any Add method with signature Add(?); if ? contains comma's, C# looks at an Add method with multiple arguments. The compiler does not have any special handling of KeyValuePair<,> at all. The reason { string, string } works, is because your base class has an overload Add(string, string), and not because it has an overload for Add(KeyValuePair<string, string>).

So to support your syntax for

new SpecificRow {
    { MyEnum.Value, "" }
};

you need an overload of the form

void Add(MyEnum key, string value)

That's all there is to it.

Vas answered 1/2, 2010 at 21:19 Comment(1)
Thank you! I'm so tickled not to have to implement an entire interface just to get object initializer capabilities. Adding your suggested overload to SpecificRow worked.Lacroix
R
5

It looks like it's because you're only implementing IDictionary<string, string>, and all the other interfaces associated with it. Your Add(KeyValuePair<MyEnum, string>) method isn't implementing any interface member, it's just another member of the SpecificRow class, which happens to be named Add, which is why it is getting ignored.

You should be able to do one of the following, depending on what your requirements are:

  1. Implement IDictionary<MyEnum, string> in addition to IDictionary<MyEnum, string>, including the dependent interfaces (ICollection<KeyValuePair<MyEnum, string>>, etc).
  2. Implement IDictionary<MyEnum, string> instead of IDictionary<MyEnum, string>, again including the dependent interfaces.
  3. Change the declaration of Row to Row<T>, and implement IDictionary<T, string>, including the dependent interfaces. SpecificRow would then implement Row<MyEnum> instead of just Row.
Rilke answered 1/2, 2010 at 20:57 Comment(23)
Obviously best answer. I'll add that some refactoring on the Row class might be useful (isn't there way too much interface implementation ?)Octennial
Well a lot of them would be implicitly implemented... IIRC, IDictionary<TKey, TValue> requires ICollection<KeyValuePair<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>` and IEnumerable. For the rest of them, there are plenty of scenarios where implementing all of those interfaces would be reasonable, acceptable, and likely even desirable.Rilke
I'm not sure if this is correct. Section 7.5.10.3 of the C# language specification states that collections must only implement IEnumerable to be initialized this way, and "...for each specified element in order, the collection initializer invokes an Add method on the target object... applying normal overload resolution for each invocation". In other words, if the collection has an Add() method that matches KeyValuePair<MyEnum, string>, I'd expect it to be called, just like the original poster.Circumnavigate
Seth, she hasn't implemented any generic interface that accepts that type. That's why it's only looking for a KeyValuePair<string, string>Rilke
It's also worth noting that IEnumerable doesn't have an Add method - and neither does ICollection. ICollection<T> does.Rilke
I think you're on the right track, but implementing ICollection<KeyValuePair<MyEnum, string>> on SpecificRow didn't fix the Add issue.Lacroix
What section 7.5.10.3 means, is that { x, y } will translate to a method call Add(x, y). C# has no special support for KeyValuePair<,>, but rather the IDictionary<,>.Add(key, value) method is called, not IDictionary<,>.Add(KeyValuePair<,> pair)Vas
Ruben: You're completely right. I had a total duh moment and just edited my answer to reflect that fact that the issue is the IDictionary implementation, not ICollection.Rilke
@Daniel - I think Seth is right that you only need to implement IEnumerable<T> instead of ICollection<T> since the compiler doesn't use the interface to intialise the collection. However the OP has only implemented IEnumerable.Swarm
@Daniel: I understand what you're saying, but my understanding of collection initializers is that each element in the list specifies a set of arguments to pass to an Add() method of the collection type being initialized. Since she's explicitly constructing a SpecificRow, I would expect the overload resolution to say "what overload of SpecificRow.Add() accepts a KVP<enum, string>", and then resolve to the correct method. Collection initializers only require the initialized class implement the non-generic IEnumerable, why should she have to implement a generic interface?Circumnavigate
@Daniel: it's not the IDictionary implementation either. It's any public Add method, whether it's from an IDictionary or just one of your own. Just like foreach calls the public GetEnumerator, whether it implements IEnumerable or not. The only requirement for both is that the class implements IEnumerable somehow, but the implementation itself is mostly ignored in favor of some well named public methods.Vas
Lee: You can't add to an enumerable... an enumerable, as the name might suggest is only for enumerating, and as I alluded to in an earlier comment, neither the non-generic IEnumerable nor the generic IEnumerable<T> interface have an Add method... so how would that work?Rilke
@Vas and Daniel: I just had my own "duh" moment. If the collection initializer was written as { new KeyValuePair<T,V> { Enum.Foo, "bar" } } it probably would have worked, correct? Because then the SpecificRow.Add() method would have been picked, not the dictionary's Add method. I haven't had this much fun in days!!Circumnavigate
@Seth: right. the whole IEnumerable/ICollection/IDictionary stuff is throwing you off. The only thing you need is an Add method with the right number (and type) of arguments, and in this case, and Add with two arguments.Vas
@Ruben... wow, I totally didn't realize that you could implement GetEnumerator and have it work without implementing IEnumerable. That seems a bit messy to me - I'm not sure why it would be desirable to implement GetEnumerator without IEnumerable. So it looks like she just needs an Add(MyEnum, string) method after all.Rilke
@Daniel: the reason foreach looks at a public GetEnumerator, is because you can return a specialized enumerator (pre generics, take at look at IDictionary), or a struct enumerator (List<T>), not a plain IEnumerator(<T>). It also solves the ambiguity when you're implementing multiple IEnumerable<T>'s.Vas
@Daniel - Because as I pointed out, the compiler doesn't use the interface to add to the collection - it simply checks it implements IEnumerable<T> and has an Add(T item) method. It then uses that Add method to add to the collection.Swarm
The pre-generics argument I can understand, besides that, why anyone would do it now is beyond me. For implementing multiple IEnumerable<T>, they'd each have their own signature (that is, if you're returning IEnumerator<T>) so there's no ambiguity or you could implement one or more of them explicitly.Rilke
I'm afraid there is lots of ambiguity, I'm afraid. what would foreach(var e in c) mean? What type would var be for Dictionary<K,V>? KeyValuePair<K,V>, DictionaryEntry or object? (IDictionary<K,V> : IDictionary : IEnumerable)Vas
FWIW, this is exactly why I loooove SO. Excellent discussion!Rilke
IMHO, there are very limited scenarios where implementing multiple generic IEnumerable<T> definitions would make sense. In any of those, it seems to me that when working with an instance in a situation where you need to enumerate using one of those interfaces, the best practice would be to reference the instance cast as an interface, not the concrete type, which would remove any of the ambiguity. Using public instead of explicit implementations, or implementations that aren't part of an interface seems like it would only add to the confusion or ambiguity. Are we saying the same thing?Rilke
Take a look at this article by Mads Torgersen for some interesting background info and the rationale behind the IEnumerable/Add method requirement: blogs.msdn.com/madst/archive/2006/10/10/…Chelyuskin
@Luke: Interesting read, though it only enforces my opinion that not implementing the appropriate generic interfaces is uuugly, and that the ability have this functionality without the interfaces is really just a usability hack intended for pre-generics framework types.Rilke
C
1

Ruben's answer is definitely the best, but if you didn't want to add Add(MyEnum key, string value) then you could also initialize the collection like so:

var result = new SpecificRow
{
    new KeyValuePair<MyEnum, string>(MyEnum.Value, ""}),
    new KeyValuePair<MyEnum, string>(MyEnum.OtherValue, ""})
};
Circumnavigate answered 1/2, 2010 at 22:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.