views:

129

answers:

2

Update!

See my dissection of a portion of the C# spec below; I think I must be missing something, because to me it looks like the behavior I'm describing in this question actually violates the spec.

Update 2!

OK, upon further reflection, and based on some comments, I think I now understand what's going on. The words "source type" in the spec refer to the type being converted from -- i.e., Type2 in my example below -- which simply means that the compiler is able to narrow the candidates down to the two operators defined (since Type2 is the source type for both). However, it cannot narrow the choices any further. So the key words in the spec (as it applies to this question) are "source type", which I previously misinterpreted (I think) to mean "declaring type."


Original Question

Say I have these types defined:

class Type0
{
    public string Value { get; private set; }

    public Type0(string value)
    {
        Value = value;
    }
}

class Type1 : Type0
{
    public Type1(string value) : base(value) { }

    public static implicit operator Type1(Type2 other)
    {
        return new Type1("Converted using Type1's operator.");
    }
}

class Type2 : Type0
{
    public Type2(string value) : base(value) { }

    public static implicit operator Type1(Type2 other)
    {
        return new Type1("Converted using Type2's operator.");
    }
}

Then say I do this:

Type2 t2 = new Type2("B");
Type1 t1 = t2;

Obviously this is ambiguous, as it is not clear which implicit operator should be used. My question is -- since I cannot see any way to resolve this ambiguity (it isn't like I can perform some explicit cast to clarify which version I want), and yet the class definitions above do compile -- why would the compiler allow those matching implicit operators at all?


Dissection

OK, I'm going to step through the excerpt of the C# spec quoted by Hans Passant in an attempt to make sense of this.

Find the set of types, D, from which user-defined conversion operators will be considered. This set consists of S (if S is a class or struct), the base classes of S (if S is a class), and T (if T is a class or struct).

We're converting from Type2 (S) to Type1 (T). So it seems that here D would include all three types in the example: Type0 (because it is a base class of S), Type1 (T) and Type2 (S).

Find the set of applicable user-defined conversion operators, U. This set consists of the user-defined implicit conversion operators declared by the classes or structs in D that convert from a type encompassing S to a type encompassed by T. If U is empty, the conversion is undefined and a compile-time error occurs.

All right, we've got two operators satisfying these conditions. The version declared in Type1 meets the requirements because Type1 is in D and it converts from Type2 (which obviously encompasses S) to Type1 (which is obviously encompassed by T). The version in Type2 also meets the requirements for exactly the same reasons. So U includes both of these operators.

Lastly, with respect to finding the most specific "source type" SX of the operators in U:

If any of the operators in U convert from S, then SX is S.

Now, both operators in U convert from S -- so this tells me that SX is S.

Doesn't this mean that the Type2 version should be used?

But wait! I'm confused!

Couldn't I have only defined Type1's version of the operator, in which case, the only remaining candidate would be Type1's version, and yet according to the spec SX would be Type2? This seems like a possible scenario in which the spec mandates something impossible (namely, that the conversion declared in Type2 should be used when in fact it does not exist).

+1  A: 

Ultimately, it can't be prohibitted with complete success. You and I could publish two assemblies. They we could start using each other's assembles, while updating our own. Then we could each provide implicit casts between types defined in each assembly. Only when we release the next version, could this be caught, rather than at compile time.

There's an advantage in not trying to ban things that can't be banned, as it makes for clarity and consistency (and there's a lesson for legislators in that).

Jon Hanna
+1  A: 

We don't really want it to be a compile-time error just to define conversions which might cause ambiguity. Suppose that we alter Type0 to store a double, and for some reason we want to provide separate conversions to signed integer and unsigned integer.

class Type0
{
    public double Value { get; private set; }

    public Type0(double value)
    {
        Value = value;
    }

    public static implicit operator Int32(Type0 other)
    {
        return (Int32)other.Value;
    }

    public static implicit operator UInt32(Type0 other)
    {
        return (UInt32)Math.Abs(other.Value);
    }

}

This compiles fine, and I can use use both conversions with

Type0 t = new Type0(0.9);
int i = t;
UInt32 u = t;

However, it's a compile error to try float f = t because either of the implicit conversions could be used to get to an integer type which can then be converted to float.

We only want the compiler to complain about these more complex ambiguities when they're actually used, since we'd like the Type0 above to compile. For consistency, the simpler ambiguity should also cause an error at the point you use it rather than when you define it.

EDIT

Since Hans removed his answer which quoted the spec, here's a quick run through the part of the C# spec that determines whether a conversion is ambiguous, having defined U to be the set of all the conversions which could possibly do the job:

  • Find the most specific source type, SX, of the operators in U:
    • If any of the operators in U convert from S, then SX is S.
    • Otherwise, SX is the most encompassed type in the combined set of target types of the operators in U. If no most encompassed type can be found, then the conversion is ambiguous and a compile-time error occurs.

Paraphrased, we prefer a conversion which converts directly from S, otherwise we prefer the type which is "easiest" to convert S to. In both examples, we have two conversions from S available. If there were no conversions from Type2, we'd prefer a conversion from Type0 over one from object. If no one type is obviously the better choice to convert from, we fail here.

  • Find the most specific target type, TX, of the operators in U:
    • If any of the operators in U convert to T, then TX is T.
    • Otherwise, TX is the most encompassing type in the combined set of target types of the operators in U. If no most encompassing type can be found, then the conversion is ambiguous and a compile-time error occurs.

Again, we'd prefer to convert directly to T, but we'll settle for the type that's "easiest" to convert to T. In Dan's example, we have two conversions to T available. In my example, the possible targets are Int32 and UInt32, and neither is a better match than the other, so this is where the conversion fails. The compiler has no way to know whether float f = t means float f = (float)(Int32)t or float f = (float)(UInt32)t.

  • If U contains exactly one user-defined conversion operator that converts from SX to TX, then this is the most specific conversion operator. If no such operator exists, or if more than one such operator exists, then the conversion is ambiguous and a compile-time error occurs.

In Dan's example, we fail here because we have two conversions left from SX to TX. We could have no conversions from SX to TX if we chose different conversions when deciding SX and TX. For example, if we had a Type1a derived from Type1, then we might have conversions from Type2 to Type1a and from Type0 to Type1 These would still give us SX=Type2 and TX=Type1, but we don't actually have any conversion from Type2 to Type1. This is OK, because this really is ambiguous. The compiler doesn't know whether to convert Type2 to Type1a and then cast to Type1, or cast to Type0 first so that it can use that conversion to Type1.

stevemegson
OK, I like this explanation. I also like the comment you provided to Hans's (now deleted) answer -- would you be up for incorporating the content of that comment into this answer? Either way, I feel this is worthy of acceptance.
Dan Tao