0

i was reading about the covariance & contravariance from this blog and the

covariance on Array got me confused

now, if i have this

object[] obj= new string[5];
obj[0]=4;

why am i getting error during run time? Theoretically obj is a variable of type Object and Object can store any type, as all the types are inherited from the Object class. Now when i run this code i am not getting any run time error, can anyone explain me why

class baseclass
    {

    }
    class client
    {
        static void Main()
        {
            object obj = new baseclass();
            obj = 4;

            Console.Read();
        }
    }
Lijin Durairaj
  • 4,910
  • 15
  • 52
  • 85
  • 4
    Why do you think it's safe to store an integer in a string array? – Servy Nov 01 '18 at 17:48
  • 2
    Because `obj` is an array of strings, so cannot contain an `int` like 4. Your second example doesn't contain any variance at all. – Lee Nov 01 '18 at 17:48
  • 2
    your `object obj` is just a placeholder for a reference to anything – Daniel A. White Nov 01 '18 at 17:49
  • 1
    Please see https://blogs.msdn.microsoft.com/ericlippert/2007/10/17/covariance-and-contravariance-in-c-part-two-array-covariance/ – GSerg Nov 01 '18 at 17:49
  • 1
    Your second code block is an example of how inheritance works. Everything inherits from `System.Object` so the assignment of `4` (an int that also inherits indirectly from System.Object) is nothing more than using the base type as the reference pointer. This has nothing to do with covariance. – Igor Nov 01 '18 at 17:51
  • @Lee obj is a variable of Object[], and Object can contain any type, right? – Lijin Durairaj Nov 01 '18 at 18:10
  • @Igor, this is exactly what covariance is, assinging the most derived type to the most base type. – Lijin Durairaj Nov 01 '18 at 18:12
  • 2
    **That is not at all what covariance is**. That is *assignment compatibility*. – Eric Lippert Nov 01 '18 at 18:14
  • 2
    "Covariance" is a property of a mapping from one type to another that *preserves* assignment compatibility. For example: `Tiger` is assignment compatible with `Animal`. Also, `IEnumerable` is assignment compatible with `IEnumerable`. Therefore `IEnumerable` is *covariant with respect to T*. Make sure you understand this. **Covariance is not assignment compatibility**. Covariance is *the preservation of assignment compatibility when a type is transformed to a related type*. – Eric Lippert Nov 01 '18 at 18:16
  • 1
    We say that arrays are covariant because **string being assignment compatible with object implies that `string[]` is assignment compatible with `object[]`.** The *make it an array* mapping on types *preserves* assignment compatibility, so it is *covariant*. – Eric Lippert Nov 01 '18 at 18:18
  • 1
    It is no wonder you are confused by array covariance, since before now you did not know what the word "covariance" meant. Now that you know, is it more clear? – Eric Lippert Nov 01 '18 at 18:19
  • 2
    Basically, if your logic is "A relates to B, therefore F(A) relates to F(B)", then the transformation "F" is *covariant*. If your logic is "A relates to B, therefore F(B) relates to F(A)" then F is *contravariant*. If you logic is "A relates to B, therefore this subset of A relates to B", that's not variance. **Variance is a property of the transformation**. – Eric Lippert Nov 01 '18 at 18:28
  • @LijinDurairaj - The type of `obj` is `object[]` which is why `obj[0] = 4;` compiles, but at runtime it refers to a `string[]`. Attempting to assign an int into a `string[]` is why an exception is thrown at runtime. The type system allowing an operation which will be rejected at runtime is what is meant by array covariance being 'unsafe'. – Lee Nov 01 '18 at 20:20

2 Answers2

10

It is in fact confusing.

When you say object[] objs = new string[4] {}; then objs is actually an array of strings. Unsafe array covariance is unsafe because the type system is lying to you. That's why it is unsafe. You think that your array can hold a boxed integer, but it is really an array of strings and it cannot actually hold anything but strings.

Your question is "why is this not safe", and then you give an example of why it is not safe. It is not safe because it crashes at runtime when you do something that looks like it should be safe. It's a violation of the most basic rule of the type system: that a variable actually contains a value of the type of the variable.

For a variable of type object, that's not a lie. You can store any object in that variable, so it's safe. But a variable of type object[] is a lie. You can store things in that variable that are not object[].

This is in my opinion the worst feature of C# and the CLR. C# has this feature because the CLR has it. The CLR has it because Java has it, and the CLR designers wanted to be able to implement Java-like languages in the CLR. I do not know why Java has it; it's a terrible idea and they should not have done it.

Is that now clear?

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • well, when u can assign this object obj = 4; then, why not u are able to assign this IEnumerable obj1 = new List { 1, 2, 3 }; – Lijin Durairaj Nov 01 '18 at 18:27
  • 1
    Covariant conversions in C# require that the type arguments be all reference types. The reason is because *reference type conversions do not change the representation in memory of the data*. `int` to `object` is a boxing conversion which *allocates new memory*, and there is nowhere in your example where the boxing can happen! You can say `obj1 = (new List{1, 2, 3}).Cast();` though because the cast operator does the boxing conversion. – Eric Lippert Nov 01 '18 at 18:31
  • 1
    thank you, that answers my question and i am pretty clear now. – Lijin Durairaj Nov 01 '18 at 18:34
  • @LijinDurairaj: You're welcome! I designed and implemented this feature and I have written extensively on it. See the links to my blog in the article you referenced. – Eric Lippert Nov 01 '18 at 18:36
  • i am very grateful for getting an opportunity to interact with u :) – Lijin Durairaj Nov 01 '18 at 18:41
  • Pay special attention to Eric lippert's point, "reference type conversions do not change the representation in memory of the data." A key reason why it is legal to assign `IEnumerable obj1 = new List { 1, 2, 3 }` is that `IEnumerable` allows you to pull members out, but not put them in. The underlying enumerable is still `List`, but there is no risk that you will inadvertently call `obj1[1] = "String"`. Hence, it would not be legal to say `List obj1 = new List { 1, 2, 3 }`, since that would risk you attempting to call `obj1[1] = "string";`. – Brian Nov 05 '18 at 14:44
  • 1
    @Brian: But that conversion is *not* legal since int is a value type and not a reference type. We'd have to change the representation of the ints by boxing them, so the conversion is not legal. But the conversion from `List` to `IEnumerable` would be legal because the reference to a string is also a reference to an object. – Eric Lippert Nov 05 '18 at 14:47
  • 1
    What do you think of the explanation of why this feature was implemented in Java as reported in [this answer](https://stackoverflow.com/a/18666878/1846281)? "However, if array types were treated as invariant, it would only be possible to call these functions on an array of exactly the type Object[]. One could not, for example, shuffle an array of strings." – Luca Cremonesi Nov 05 '18 at 15:25
  • @EricLippert can you elaborate why the CLR needs to specify this feature aswell? Why can the a specific language built in top of it not just produce the corresponding IL and decide itself how to behave? – S. John Fagone Aug 14 '21 at 13:34
  • @S.JohnFagone: I'm sorry but I don't understand your question. A "why not" question can be quite difficult to answer because it presupposes that the world ought to be different than it is, and that we need to supply a reason explaining why the world is different than it ought to be. This is a Q&A site; maybe your comment would be better expressed by posting a new question that is more clear. – Eric Lippert Aug 14 '21 at 22:50
  • 1
    @S.JohnFagone: It's unreasonable that array covariance is legal at compile time but not at runtime. However, we can't fix this by making it legal at runtime. If `string[] a = new[]{"a"};object[] b = a; b[0] = 5;` was legal at runtime, then `string[] a = new[]{"a"};object[] b = a;b[0] = 5;Console.WriteLine(a[0].Substring(0,1));` would fail at runtime, since `5` is not a string. So, it would have been better if this was illegal at compile time. – Brian Aug 16 '21 at 20:36
  • @Brian Thank you for you "reverse"-example that makes completely sense. But my confusion came more from the fact because I was thinking "why would the CLR care at all about this sort of typesystem stuff. If I let the language in top of it decide, the CLR can achieve different behavior for different languages if the language e.g. just uses different clr-instructions". But like this the language wouldn't be language agnostict, meaning one compiled bytecode from one language couldn't be used in another language build in top of it.https://docs.microsoft.com/en-us/dotnet/standard/common-type-system – S. John Fagone Aug 17 '21 at 10:39
  • @S.JohnFagone: If we don't mind breaking existing references (as shown my example above), we *can* do a constant-time transmute (i.e., an in-place, constant-time cast that corrupts any existing references) of an array, on condition that we don't change the size (e.g., using reference types since the references themselves are always the same size). See https://blog.tchatzigiannakis.com/changing-an-objects-type-at-runtime-in-c-sharp/ for the horrifying implementation . Please don't use that in production code; that's more intended to illustrate what's possible. – Brian Aug 18 '21 at 13:16
0

An object of array type T[] has three notable abilities:

  1. Any value read from the array may be stored in a container of type T.

  2. Any value read from the array may be stored back into the same array.

  3. Any value that fits in a container of type T may be stored into the array.

A non-null reference of type T[] will be capable of holding a reference to any object of type U[], where U derives from T. For any possible type U derived from T, any value read from a U[] may be stored into a container of type T, and may also be stored back into the same array. If a container of type T holds an reference to an object which is derived from T, but is not of type U nor any type derived from U, then a U[] would be incapable of holding that reference.

It would be awkward to allow code to read an item from one array and write it back to the same array, without also allowing it to ask for an item be read from one array and written into another. Rather than trying to limit such operations via compile-time constraints, C# opts instead to say that if code tries to store a value held in a T into an array identified via T[], such an operation will succeed if the T is null or identifies an object of a type not derived from the element type of the actual array identified by the T[].

supercat
  • 77,689
  • 9
  • 166
  • 211
  • Point 2 is a direct consequence of point 1, being the original array `T[]` a container of type `T`. – Luca Cremonesi Nov 06 '18 at 14:48
  • @LucaCremonesi: While point 2 would follow from #1 and #3, applying them requires knowing an actual type `T` that can be used for for the intermediate container. Although it doesn't do so, it would be possible for `System.Array` to include methods like `Swap(int index1, int index2)` which could be used on any array objects without having to know or care about their types. – supercat Nov 06 '18 at 15:42