At the end of his entry on the concatenation operator, Bill observes some strange behavior around types that have a user-defined conversion to String but can’t be used with the concatenation operator. This is actually a design bug in our operator overloading resolution logic that we’re planning on fixing, since it won’t impact compatibility at all. To quote almost verbatim from the preliminary spec I wrote on it:
Consider the following program:
Dim CI As New C()
‘ Error: ‘&’ is not defined for ‘String’ and ‘C’
Console.WriteLine(CI & “world”)
Dim S As String = CI
Console.WriteLine(S & “world”)
Public Class C
Public Shared Widening Operator CType(ByVal x As C) As String
The first expression fails because of a problem in the way that the operators that participate in operator resolution are chosen. Given an operation on two types, X and Y, the operator that is used is resolved as follows:
- If X and Y are both intrinsic types, the operator to use is determined by the operator tables in the language specification.
- Otherwise, the user-defined operators in X and the user-defined operators in Y are collected together and overload resolution is used to determine what the best fit, if any, is.
When you mix user-defined and intrinsic types, however, the algorithm can fail unexpectedly. When interpreting the expression C & String, the second bullet point is applied since both types aren’t intrinsic. Since neither type has a user-defined & operator, a compile-time error is given.
(C# doesn’t have this problem because they don’t distinguish between user-defined operators and intrinsic operators: they collect all operators together and throw them into overload resolution. We can’t do this because our intrinsic operator resolution rules do not conform to standard overload resolution rules. According to standard overload resolution, 1 / 2 should result in a Decimal value, but we change the rules to make this result in a Double value. This means we can’t just throw the standard operators into overload resolution into the second bullet point above, because then something like X / Integer would result in Decimal while CInt(X) / Integer would result in Double.)
To solve this problem, operator resolution will pre-select a single intrinsic operator to participate in overload resolution. Thus, the rules for operator resolution will be expanded to:
- If X and Y are both intrinsic types, look up the result type in our operator tables and use that.
- If X is an intrinsic type, then
- Collect all of the intrinsic types that Y converts to.
- Choose the most encompassed type, T, from the list. If there is no single most encompassed type, then we don’t consider an intrinsic operator.
- Lookup up the intrinsic operator for X and T, and call it O. If there is no intrinsic operator defined for those two types, then we don’t consider an intrinsic operator.
- The set of operators to be considered is all the user-defined operators in Y, plus O, if it exists.
- If Y is an intrinsic type, then perform the same steps as for X (obviously, both can’t be intrinsic types at this point).
- Do overload resolution on the set of operators to be considered.
This algorithm would appear to give us what we want. It’s the same basic steps we go through today, with the addition of the “narrowest” intrinsic operator possible if one of the types is intrinsic. It should also solve Bill’s problem.
shouldn’t concatenation be treated as a special case, as you don’t need to look up the operators table ?
Since concatenation applies to so few types, I don’t think we actually look anything up in tables, but we do still do the "intrinsic vs. user-defined" thing, which is why you’ve still got the problem…