0

The following is a question I've seen on a test which raised some questions:

Given the following code:

public delegate B func(B b);

public class A
{
    public func x;
    public int i = 2;
    public A(int i) { this.i = i; x += f; }
    public virtual C f(A a) { Console.WriteLine("C A.f(A)"); return new C(1); }
    public virtual A f(C c) { Console.WriteLine("A A.f(C)"); return new A(2); }
}

public class B: A{
    public B(int i) : base(i) { x += f; }
    public virtual B f(B b) { Console.WriteLine("B B.f(B)"); return new B(3); }
    public new C f(A a) { Console.WriteLine("C B.f(A)"); return new C(4); } 
}

public class C: B{
    public C(int i) : base(i) { x += f; }
    public new B f(B b) { Console.WriteLine("B C.f(B)"); return new C(5); }
    public override A f(C c) { Console.WriteLine("A C.f(C)"); return new A(6); }
}

with the following main:

static void Main(string[] args)
{
    C c = new C(12);
    A a = c.x(c);
    Console.WriteLine(a.i);
}
  1. What is the output?
  2. What happens if we change the delegate definition to: public delegate C func(B b);
  3. What happens when we change the delegate definition to public delegate T func<T,U>(U u); and change x in A to public func<B,B> x;?

The official answers were:

To #1:

C A.f(A)
B B.f(B)
B C.f(B)
5

To #2:

It won't compile because the method that are added to x in class B and C are chosen based on the static type of the argument. So in class B B B.f(B) needs to be added but the return type doesn't match, and in class C B C.f(B) needs to be added but the return type doesn't match.

To #3:

It prints the same answer as #1 since both definitions are equivalent.

Here is what I don't understand:

  1. How are the methods which are added to x in the line x+=f; chosen out of all the overloaded options? Is the overloaded method with the "closest" argument type to the dynamic type of "this" the one chosen?
  2. Aren't generic delegates non variant unless declared with in and out? How does the answer to #3 make sense?
matanc1
  • 6,525
  • 6
  • 37
  • 57

1 Answers1

2

Is the overloaded method with the "closest" argument type to the dynamic type of "this" the one chosen?

No. If that were true, then all constructors would choose the same method (B C.f(B)), because it matches exactly and is in the most derived type. Instead, the static type of this is used.

This means that:

  1. In A, C A.f(A) is chosen, because it's the only method on A that matches func.
  2. In B, B B.f(B) is chosen, because it's the only method on B that matches perfectly.
  3. In C, B C.f(B) is chosen, because it matches perfectly and is declared in a more derived type than B B.f(B).

Aren't generic delegates non variant unless declared with in and out?

Yes, such delegates are invariant, which means that you can't convert func<B, B> to func<A, B>, func<B, C> or func<A, C>. But it doesn't change the rules of converting from method groups to delegates, which allow similar conversions, no matter whether the delegate type is generic or not.

svick
  • 236,525
  • 50
  • 385
  • 514
  • So which methods are added to the delegate? If I add to class A `public virtual C f(B b) { Console.WriteLine("C A.f(B)"); return new C(2); }` then it'll be added instead of `C A.f(A)`. Notice that it has 2 methods which are of the delegate type now and that the new one doesn't match the static type. Why is that? – matanc1 Nov 23 '13 at 19:23
  • @Shookie Because it's a better match than `C f(A a)`. From `C f(A a)` to `B func(B b)`, you have to make two changes. From `C f(B b)`, there's only one change. – svick Nov 23 '13 at 19:26
  • It may be worth noting that even delegates which are declared using `in` and `out` decorations are unable to support covariance or contravariance in scenarios involving `Delegate.Combine`. – supercat Nov 23 '13 at 21:30
  • @supercat What do you mean? The following code works fine for me: `Func fs1 = () => "a"; Func fs2 = () => "b"; Func fo = fs1 + fs2; fo().Dump();`. Isn't this what you meant? – svick Nov 23 '13 at 23:18
  • 1
    He means stuff like this: `Action a1 = x => { }; Action a2 = x => { }; Action b1 = a1; Action b2 = a2; var combination = b1 + b2;`. I used an example where the delegate return type is `void` because one rarely combines delegates that return a value. – Jeppe Stig Nielsen Nov 24 '13 at 00:44
  • @JeppeStigNielsen: I wonder if there would have been (or, for that matter, would be) any particular problem with having each delegate class define a static `Combine` method overload for its own type? Given `Action a1; Action a2;` a method `Action>.Combine()` would be able to produce a delegate which combined them in such a fashion that `Action>.Remove()` would be able to remove them [even if the former had to wrap the delegates, the latter could recognize and handle such wrapping]. – supercat Nov 25 '13 at 16:36
  • @JeppeStigNielsen: I think the only real fundamental problem with `Delegate.Combine` and contravariance/covariance is that the static non-generic function of `Delegate` has no way of specifying the result type. It's possible to have two pairs of unrelated delegate types X,Y and A,B such that X:A,B and Y:AB, even though X is unrelated to Y and A is unrelated to B. A combining method could combine delegates of type X and Y into one of type A or type B, but there's no way a non-generic `Delegate.Combine` could infer which type to produce. – supercat Nov 25 '13 at 16:48
  • @supercat Absolutely. In my example above, `ICloneable` and `IConvertible` are unrelated, and clearly very many types (in many assemblies) could inherit or implement both `ICloneable` and `IConvertible`, so a `Combine` method needs a clue for which type (here `System.String`) to use. This is the case of contravariance which will probably be most relevant in practice. With covariance, of course, what is needed is a clue for which one of the types in the set of base classes and interfaces common to both the types we are combining. – Jeppe Stig Nielsen Nov 25 '13 at 17:21
  • @JeppeStigNielsen: I wonder if there would have been any difficulty having subtypes of `Delegate` each include their own `Combine` method, or what the consequences would be of adding such a thing now? Even before generics, it would have improved the type safety of combining delegates while eliminating a typecast. – supercat Nov 26 '13 at 08:22