4

I've just realised I'd written code that I'd expect to be invalid (with regard to syntax), but the compiler accepts.

For the sake of brevity I've recreated an example. Given a type:

private class MyType
{
}

I can then compare an instance of MyType to an IEnumerable<MyType>:

var myCollection = new List<MyType> { };
var test = new MyType() == (IEnumerable<MyType>)myCollection;

I'm surprised I can compare two objects of different types. Even more interesting (interesting to me at least), is if I remove the cast to IEnumerable I can't compare it. Why is this the case? A List implements IEnumerable so assuming there's some kind of equality comparer somewhere that I'm missing (may be wrong), why isn't this available to List, too?

Johnny
  • 8,939
  • 2
  • 28
  • 33
  • `I can't compare it.` Tell us what you mean by that. – mjwills Feb 27 '18 at 12:14
  • @mjwills - syntax error. –  Feb 27 '18 at 12:15
  • 1
    You don't compare the (content of the) object/list anyway, you'd only compare the **references**. And there _is no_ overridden `==` operator that takes a `MyType` and a `List` as arguments. The interesting part of this question (for me) is: which operator in which type and with what kind of arguments is taken for the `==`call with a `MyType` and an explicit `IEnumerable`. – René Vogt Feb 27 '18 at 12:16
  • 1
    In the compile time compiler doesn't have enough information to know what type is really behind `IEnumerable` or any other `interface`... But it knows that `List` cannot be compared with `MyType`... Basically you doesn't provide enough information to compiler. – Johnny Feb 27 '18 at 12:17

2 Answers2

7

Compiler is letting you compare objects to interfaces, but it doesn't let you compare them to unrelated concrete classes because it knows the precise type up-front and knows that the two types cannot be compared. Comparison to interface is allowed because a cast from the concrete object will be attempted at run time, and that is a valid operation per se.

What exactly are you going to do with that is a different story, and in fact none of your comparisons will be semantically correct.

In any case, your code does not produce a true comparison. It will return False when run.

var test1 = new MyType() == (IEnumerable<MyType>)myCollection; // False
var test2 = new MyType() == new List<MyType>();                // Compile-time error
var test3 = new MyType() == (IComparable)5;                    // False

Another question is the use of == - which is wrong on another level. You are free to call Equals as well, and seemingly attempt to perform true semantically meaningful comparison. Default implementation will not do anything meaningful, but again - looking from the compiler's point of view, everything is still syntactically correct.

Zoran Horvat
  • 10,924
  • 3
  • 31
  • 43
  • Can you quote some spec for the first paragraph? I mean, how is operator resolution done here? Is this an explicit rule? Or does it follow from there being a default `==` operator in each `CustomType` that has a first argument of `CustomType` and a second argument with..well..how is "interface-only" declared? There is an implicit conversion from `List` to `object`...guess I have to read the specs again... – René Vogt Feb 27 '18 at 12:26
  • 1
    I don't have the precise quote, but on top of my head `==` will turn into a `ReferenceEquals(object, object)` unless there is a custom `==` operator which matches the compile-time types of the left-hand and right-hand object (including implicit conversions). – Zoran Horvat Feb 27 '18 at 12:28
  • 1
    You have right Zoran(e), https://stackoverflow.com/questions/814878/c-sharp-difference-between-and-equals – Johnny Feb 27 '18 at 12:29
  • @RenéVogt relevant spec part is "7.10.6 Reference type equality operators", first couple of paragraphs. There you will find that unless explicit reference conversion exists from one operand to other - binding error (so, compile error) occurs. And in part "6.2.4" you can find that explicit reference conversion exists from any interface to any non-sealed class (for example, `var test = (IEnumerable) new MyType()` compiles fine, even though this type does not implement this interface). – Evk Feb 27 '18 at 12:41
  • @ZoranHorvat - thanks a lot for your answer, will mark as correct when it allows me to. As an aside, though, if I change `MyType` to `string` - there's then a compiler error again, but I'd still be "comparing objects to interfaces". –  Feb 27 '18 at 12:41
  • @JᴀʏMᴇᴇ That might be because there is a custom operator `==(string, string)`, I'm not sure. – Zoran Horvat Feb 27 '18 at 12:43
3

Here is quote from specification about that (section 7.10.6 Reference type equality operators). This section is about predefined object equality operators (== (object x, object y)) which are used in this case:

The predefined reference type equality operators require one of the following:

• Both operands are a value of a type known to be a reference-type or the literal null. Furthermore, an explicit reference conversion (§6.2.4) exists from the type of either operand to the type of the other operand.

• One operand is a value of type T where T is a type-parameter and the other operand is the literal null. Furthermore T does not have the value type constraint.

And from section 6.2.4 we may find that:

The explicit reference conversions are:

• From any class-type S to any interface-type T, provided S is not sealed and provided S does not implement T.

Because of that

var test = new MyType() == (IEnumerable<MyType>)myCollection;

works, because there is explicit reference conversion from MyType to IEnumerable<MyType> as per above. However, if you make MyType sealed - then there will be no such conversion and that will not compile. The same will happen if you use some predefined sealed type, such as string (I mean - it will not compile).

The reason explicit reference conversion is allowed for non-sealed type to any interface is because runtime type of that variable can be any of the child classes, and that child class might implement that interface. Sealed classes cannot be inherited, so compiler can be sure that is not the case.

Evk
  • 98,527
  • 8
  • 141
  • 191