9

I wonder why IEnumerable<int> can't be assigned to a IEnumerable<object>. After all IEnumerable is one of the few interfaces that supports covariance...

  • The subtype relation and covariance stuff works with reference types
  • int seems to be a proper subtype of object

The combination of both features doesn't work however...

class A
{
}

class B : A
{
}

class Program
{
    static void Main(string[] args)
    {
        bool b;
        b = typeof(IEnumerable<A>).IsAssignableFrom(typeof(List<B>));
        Console.WriteLine("ienumerable of ref types is covariant: " + b); //true

        b = typeof(IEnumerable<object>).IsAssignableFrom(typeof(List<int>));
        Console.WriteLine("ienumerable of value tpyes is covariant: " + b); //false

        b = typeof(object).IsAssignableFrom(typeof(int));
        Console.WriteLine("int is a subtype of object: " + b); //true
    }
}

thanks for your help! sebastian

Dan Tao
  • 125,917
  • 54
  • 300
  • 447
Sebastian Gregor
  • 345
  • 2
  • 11

4 Answers4

8

Value types aren't LSP-subtypes of object until they're boxed.

Variance doesn't work with value types. At all.


Demonstration that int is not a proper subtype (subtype in the LSP sense) of object:

Works:

object x = new object();
lock (x) { ... }

Does not work (substitutability violated):

int y = new int();
lock (y) { ... }

Returns true:

object x = new object();
object a = x;
object b = x;
return ReferenceEquals(a, b);

Returns false (substitutability violated):

int y = new int();
object a = y;
object b = y;
return ReferenceEquals(a, b);

Of course, the topic of the question (interface variance) is a third demonstration.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • 1
    I wouldn't go as far as your first sentence. How is `int` not a subtype of `object`? It overrides the virtual methods `GetHashCode` and `ToString` and inherits `GetType`... and can be passed to any method that accepts an `object` parameter... – Dan Tao Mar 24 '11 at 01:51
  • @Dan: You don't think passing an `int` to a method with a `object` parameter causes boxing? No, an unboxed `int` is not a subtype of `object`. It does not have a v-table unless and until it is boxed. – Ben Voigt Mar 24 '11 at 01:53
  • @Ben Voigt: I didn't say anything about boxing. I said `int` is a subtype of `object`. Suppose I have this method: `string Format(T arg) { return arg.ToString(); }` In this case, does `Format(x)` cause `x` to be boxed? No. I think you are treating these concepts as more closely related than they are. – Dan Tao Mar 24 '11 at 01:57
  • @Dan: That won't cause boxing, only because generics are specialized for value types. There's no polymorphism here, the JIT will generate machine code specially for `Format` that calls `int.ToString(&arg)`. On the other hand, consider `void Cleanup(T arg) { arg.Finalize(); }`. Now `Cleanup(x)` will cause `x` to be boxed. – Ben Voigt Mar 24 '11 at 02:00
  • @Dan, int is not a subtype of object; look at the MSDN documentation: http://msdn.microsoft.com/en-us/library/system.int32.aspx Int32 is a struct. Generics are written in such a way that they handle value types and reference types separately, and so you are able to use a generic method to get around the boxing that used to happen when you passed a value type to a method expecting a reference type. – Chris Shaffer Mar 24 '11 at 02:01
  • 1
    @Chris: MSDN doesn't show `System.Object` as a base type even of types that inherit from it directly, so that doesn't prove much. – Ben Voigt Mar 24 '11 at 02:06
  • @Ben, @Chris: Dan is correct. All types inherit from `object`. The language spec is absolutely clear about this: *"C# provides a "unified type system". All types -- including value types -- derive from the type `object`. It is possible to call object methods on any value, even values of "primitive" types such as `int`."* ([from section 8.2.4 of the ECMA C# Specification](http://www.ecma-international.org/publications/standards/Ecma-334.htm)) – LukeH Mar 24 '11 at 02:29
  • @Ben: I shouldn't have even given that example; my real point is that *boxing* is a completely separate issue from inheritance. What does it mean to be a subtype, really? It means you inherit public and protected methods, and you can be passed to any function that expects your base type. Consider this: define your own `struct` and include this method: `public object Clone() { return MemberwiseClone(); }` How do you suppose you have access to that *protected* member of `object`? It's because your `struct` is a subtype! – Dan Tao Mar 24 '11 at 02:30
  • @LukeH, @Dan: A boxed `int` is a subtype of object, as I said in my answer. Only boxed `int`s can be passed to functions that accept `object` (or stored in any variable of type `object`, whether it's a parameter or not). And no, code reuse does not imply subtyping. There's also composition. Protected accessibility IS related to inheritance, but please note: if you call `MemberwiseClone()` inside a value type, boxing occurs. Any instance of a value type must first be boxed before it can be treated as type `object`. – Ben Voigt Mar 24 '11 at 02:34
  • @Dan: May I point out that a proper subtype must contain a subobject of the base type? An `int` does not contain the fields that an instance of `object` does. A boxed `int`, on the other hand, does. – Ben Voigt Mar 24 '11 at 02:39
  • 1
    @Ben: I don't dispute your description of the underlying mechanisms, but there's no getting away from the fact that an `int`, boxed or unboxed, derives from `object`. That's the whole point of having a "unified type system", isn't it? – LukeH Mar 24 '11 at 02:41
  • 2
    @Ben Voigt: *Where* are you getting this notion that "In order to access certain members of `X`, type `Y` must be boxed" somehow means "`Y` is therefore not a subtype of `X`"? Also, what fields of `object`? There are none. – Dan Tao Mar 24 '11 at 02:44
  • @LukeH: It is a derived type for the purposes of access to protected members. It is not a derived type in any other sense. Doesn't contain an `object` subobject. Can't be used in a `lock` statement, even though `object` can. Violates the LSP more ways than I can count. – Ben Voigt Mar 24 '11 at 02:45
  • @Dan: Yes `object` has fields. The vtable. A monitor. Just because they aren't exposed to C# doesn't mean they aren't there. An `int` takes up less space than an `object`, therefore an `int` clearly cannot BE an `object` in the sense that subtyping requires. Once boxed, however, it does contain a subobject of type `object`. – Ben Voigt Mar 24 '11 at 02:47
  • 1
    @Ben Voigt: Ultimately I think the problem with what you are saying is that you're talking about *instances* of types as if they *are* types. To say "A boxed `int` is a subtype of `object`" isn't right; "A boxed `int`" isn't a *type* at all; it's an *object*. There's no such thing as a type that is *sometimes* a subtype of another type. It either *is* or it isn't. *All* types in .NET are subtypes of `object`. – Dan Tao Mar 24 '11 at 02:48
  • @Ben: Boxing and unboxing are the mechanisms that enable a "unified type system"; the existence of those mechanisms doesn't somehow prove that value types aren't objects. The spec is, again, absolutely clear about this: *"Every type in C# directly or indirectly derives from the `object` class type, and `object` is the ultimate base class of all types. Values of reference types are treated as objects simply by viewing the values as type `object`. Values of value types are treated as objects by performing boxing and unboxing operations."* (from section 11 of the ECMA spec) – LukeH Mar 24 '11 at 02:53
  • @Dan: A boxed `int` has a different memory layout from an unboxed `int`. They have different types. The `box` and `unbox` instructions are required to convert between them (except for special cases within generics). You are right that an instance is not a type, I should have said "a boxed `int`'s *type* is a subtype of `object`" The C# compiler confuses the issue by generating the `box` and `unbox` instructions silently. – Ben Voigt Mar 24 '11 at 02:53
  • @LukeH: That supports my claim quite nicely. Values of values types become objects when they are boxed. They aren't otherwise. – Ben Voigt Mar 24 '11 at 02:55
  • 1
    @Ben: The quote doesn't say that. This whole argument is nitpicking about semantics. You're talking about the implementation mechanisms; Dan and I are talking about the conceptual overview. The quote explicitly says that *"`object` is the ultimate base class of **all** types"* [emphasis mine]. – LukeH Mar 24 '11 at 03:01
  • @LukeH: So? In the .NET world, when value types get involved (they are a special case throughout the runtime), derivation does not mean subtyping. The LSP is a very simple test for subtyping, and value types fail miserably. – Ben Voigt Mar 24 '11 at 03:06
  • 2
    @Ben, @LukeH: I must say, I think we are all (Ben and I especially) just talking past each other. No one is saying anything the others didn't already know. We're just operating on different definitions of what it means to be a subtype. Personally, I favor Luke's and my position because it agrees with the definition utilized by the spec. Ben is basically disagreeing with the spec based on a different way of defining the term. So, Ben, I dislike your definition; but at least I understand what you're saying (and I think you understand what I'm saying). – Dan Tao Mar 24 '11 at 03:12
  • @Ben: Yes, value types are undeniably special-cased. Boxing and unboxing are mechanisms that (mostly) enable LSP with value types. @Dan: Absolutely agree. – LukeH Mar 24 '11 at 03:17
  • 1
    @Dan: Here, I've made a slight change to my answer to make it clear that I'm not talking about the "C# spec says `object` is a base class" sense of the term *subtype*. Thing is, the spec-based "subtype" is useless in this context. Being a derived class according to the spec isn't what makes interface variance work. Being an LSP-subtype is what makes interface variance work. – Ben Voigt Mar 24 '11 at 03:18
  • Ok, now I why to know why someone changed their vote to a downvote. I can demonstrate that `int` is not an LSP-subtype of `object` quite easily. – Ben Voigt Mar 24 '11 at 03:19
  • The LSP is *not* a simple test for subtyping. If you think it is, then by all means post a short algorithm in C# for determining whether any two types satisfy it. – Gabe Mar 24 '11 at 04:43
  • @Gabe: Disproof by counter-example is simple. The question is one counter-example, my answer contains two more. – Ben Voigt Mar 24 '11 at 05:21
  • @Gabe: You can't argue this point to Ben, because he is using LSP **in his definition** of what the word *subtype* means. @Ben: Gabe did not make a claim you can disprove. He (along with me and Luke) is saying subtyping is not defined by LSP. Your "counter-example" is based on the opposite premise. – Dan Tao Mar 24 '11 at 17:29
  • @Dan: You're right, but I didn't mean that using the LSP *isn't* a test. I meant that the LSP isn't *simple*. It's both undecidable and subjective, so it doesn't fall under my definition of "simple". Maybe Ben's definition is different, though. – Gabe Mar 24 '11 at 17:32
  • @Gabe: Right, that's a good point. @Ben: It seems to me if you take your criterion for subtyping to the extreme, you could "disprove" that *any* type is a subtype of another. Write a trivial method that accepts an `object` and throws an exception if the object passed in has a more specific type. Now your program's behavior is different depending on whether you pass in an `object` or a `string`; and LSP is violated. How is this different from your examples? – Dan Tao Mar 24 '11 at 17:44
  • @Gabe: Proving that an LSP-subtyping relation holds is not simple. Proving that it does not hold, is easy. One counter-example suffices. This is typical of scientific hypotheses. @Dan: You make a good point that `GetType` in any subtype will have different behavior. Shall we agree that the contract for LSP purposes shall be "`GetType()` returns a non-`null` instance of `System.Type`" rather than "`GetType()` returns `typeof (object)`? But that's explicitly trying to detect different types, whereas I think my examples cover legitimately different behavior, they are not straw men. – Ben Voigt Mar 24 '11 at 19:11
  • None of your examples are proper violations because you haven't stated them properly. The first should compare `object x = new object(); lock(x) { ... }` versus `object x = new int(); lock(x) { ... }`. – jason Apr 11 '11 at 17:08
  • @Jason: The fact that `object x = new int();` supports operations that `int x = new int();` doesn't proves that `int` and "boxed `int`" are different types. Which is my point. – Ben Voigt Apr 11 '11 at 17:11
  • @Ben Voigt: You are missing the point of my statement which is effectively that you are misunderstanding the statement of LSP. – jason Apr 11 '11 at 17:14
  • @Jason: I understand that you're saying that. But you're wrong. You can't use a boxed int to test substitutability of `int` itself. The question isn't about a `List` which contains boxed ints, the question is about `List`. Your proposed LSP test reveals nothing about `List`. – Ben Voigt Apr 11 '11 at 17:18
  • I would suggest rewriting your example to use generic type parameters: `bool test(T param) {Object v1=param, v2=param; return v1==v2;}` That should make abundantly clear that the type of an `int` storage location is not a proper LSP subtype of `Object`, even though a boxed `int` is. – supercat Oct 18 '12 at 23:22
6

The problem is that object is a reference type, not a value type. The only reason you can assign an int to a variable of type object is boxing.

In order to assign List<int> to IEnumerable<object> you'd have to box each element of the list. You can't do that just by assigning the reference to the list and calling it a different type.

Andrew Cooper
  • 32,176
  • 5
  • 81
  • 116
  • thanks for that easy explanation. so we have object and int variables that can have the runtime type int32. however i think there is some design flaw in c#, when implicit boxing is done in one case and not in the other... one time int behaves like a subtype, next time it doesn't. so it is no subtype. thanks... – Sebastian Gregor Mar 24 '11 at 15:23
  • @SebastianGregor: The simplest way to think of things is to recognize that `System.Int32` describes a heap object type which derives from `ValueType` which in turn derives from `Object`, and also describes a storage location type which is implicitly convertible to the heap-object type, and explicitly convertible from it. – supercat Dec 19 '12 at 03:05
6

The simplistic answer is that this is just one of the quirks in the way that variance is implemented in C# and the CLR.

From "Covariance and Contravariance in Generics":

Variance applies only to reference types; if you specify a value type for a variant type parameter, that type parameter is invariant for the resulting constructed type.

LukeH
  • 263,068
  • 57
  • 365
  • 409
1

Every value type in .net has a corresponding ("boxed") object type. Non-boxed value types are effectively outside the object type hierarchy, but the compiler will perform a widening from the value type to the boxed class type. It would be helpful to have a "class" Boxed<T> which would support a widening conversions to and from T, but which would be a class type. Internally, I think that's what the compiler's doing implicitly, but I don't know any way to do it explicitly. For any particular type like "integer", there would be no difficulty defining a class which would behave as a Boxed<Integer> should, but I don't know any way of doing such a thing in generic fashion.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • yes. i also think that such a type would help a lot. int i = 4; object x = i; Console.WriteLine(x.GetType()); should then return Boxed... – Sebastian Gregor Mar 24 '11 at 15:15
  • @Sebastian Gregor: I'm not sure the best semantics for GetType, but there have been some occasions when I've found myself creating a class holding a single value type when it would have been more convenient to have a Boxed type that could have been used implicitly as the value type. For one's own types, it's sometimes useful to declare ISelf with a read-only Self property that returns T. Note that if T is a value type, an ISelf will be a boxed version. A function with a generic parameter that's constrained to ISelf will accept... – supercat Mar 24 '11 at 17:45
  • ...either a boxed or unboxed Foo as a parameter. Curiously, it's possible to access explicit interface members of a struct without boxing or other heap allocations, but I can't figure any really clean way. – supercat Mar 24 '11 at 19:00