views:

318

answers:

3

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.

+5  A: 

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.
Daniel Schaffer
Obviously best answer. I'll add that some refactoring on the Row class might be useful (isn't there way too much interface implementation ?)
Sylvestre Equy
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.
Daniel Schaffer
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.
Seth Petry-Johnson
Seth, she hasn't implemented any generic interface that accepts that type. That's why it's only looking for a `KeyValuePair<string, string>`
Daniel Schaffer
It's also worth noting that `IEnumerable` doesn't *have* an `Add` method - and neither does `ICollection`. `ICollection<T>` does.
Daniel Schaffer
I think you're on the right track, but implementing `ICollection<KeyValuePair<MyEnum, string>>` on `SpecificRow` didn't fix the `Add` issue.
Sarah Vessels
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)
Ruben
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`.
Daniel Schaffer
@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`.
Lee
@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?
Seth Petry-Johnson
@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.
Ruben
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?
Daniel Schaffer
@Ruben 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!!
Seth Petry-Johnson
@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.
Ruben
@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.
Daniel Schaffer
@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.
Ruben
@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.
Lee
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.
Daniel Schaffer
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)
Ruben
FWIW, this is exactly why I loooove SO. Excellent discussion!
Daniel Schaffer
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?
Daniel Schaffer
Take a look at this article by Mads Torgersen for some interesting background info and the rationale behind the `IEnumerable`/`Add` method requirement: http://blogs.msdn.com/madst/archive/2006/10/10/What-is-a-collection_3F00_.aspx
LukeH
@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.
Daniel Schaffer
+7  A: 

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.

Ruben
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.
Sarah Vessels
+1  A: 

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, ""})
};
Seth Petry-Johnson