11

Usually, treating a struct S as an interface I will trigger autoboxing of the struct, which can have impacts on performance if done often. However, if I write a generic method taking a type parameter T : I and call it with an S, then will the compiler omit the boxing, since it knows the type S and does not have to use the interface?

This code shows my point:

interface I{
    void foo();
}

struct S : I {
    public void foo() { /* do something */ }
}

class Y {

    void doFoo(I i){
        i.foo();
    }
    void doFooGeneric<T>(T t) where T : I {
        t.foo(); // <--- Will an S be boxed here??
    }

    public static void Main(string[] args){
        S x;
        doFoo(x); // x is boxed
        doFooGeneric(x); // x is not boxed, at least not here, right?
    }

}

The doFoo method calls foo() on an object of type I, so once we call it with an S, that S will get boxed. The doFooGeneric method does the same thing. However, once we call it with an S, no autoboxing might be required, since the runtime knows how to call foo() on an S. But will this be done? Or will the runtime blindly box S to an I to call the interface method?

gexicide
  • 38,535
  • 21
  • 92
  • 152
  • 1
    Try adding the `struct` constraint - ie `void doFooGeneric(T t) where T : struct, I {` – Alex Jul 16 '15 at 15:05
  • FWIW, using the `IsBoxed` function from [this answer](http://stackoverflow.com/a/5807132/791010) says that it's not boxed. I don't know the details of why though. – James Thorpe Jul 16 '15 at 15:06
  • Related if not duplicate: http://stackoverflow.com/questions/3032750/structs-interfaces-and-boxing and specially [this answer](http://stackoverflow.com/a/3033357/961113) by Marc Gravell – Habib Jul 16 '15 at 15:07
  • @JamesThorpe As far as I can see you cannot use that method to see if `t` is boxed in the `t.foo()` call. – Jeppe Stig Nielsen Jul 16 '15 at 15:48
  • @JeppeStigNielsen You wouldn't... you'd do it inside `doFooGeneric`? [Like this](http://imgur.com/SwDaYeG)? – James Thorpe Jul 16 '15 at 15:50
  • @JamesThorpe That just shows that `t` is not boxed inside the method `doFooGeneric`. It does not show what happens when `t.foo()` executes. Suppose we wrote `t.ToString()` instead of `t.foo()`. That **would** box because our `S` does not `override` `ToString`. Or if we had `t.GetType()`, that would always box (`GetType()` is non-virtual so never overridden). – Jeppe Stig Nielsen Jul 16 '15 at 15:57
  • @JeppeStigNielsen Hmm... I thought we were only finding out whether or not `t` was boxed in `doFooGeneric`... I think I need to go read some more... – James Thorpe Jul 16 '15 at 16:00

2 Answers2

7
void doFooGeneric<T>(T t) where T : I {
    t.foo(); // <--- Will an S be boxed here??
}

Boxing will be avoided there!

The struct type S is sealed. For value type versions of the type parameter T to your method doFooGeneric above, the C# compiler gives code that calls the relevant struct member directly, without boxing.

Which is cool.

See Sameer's answer for some technical details.


OK, so I came up with an example of this. I will be interested in better examples if anyone has some:

using System;
using System.Collections.Generic;

namespace AvoidBoxing
{
  static class Program
  {
    static void Main()
    {
      var myStruct = new List<int> { 10, 20, 30, }.GetEnumerator();
      myStruct.MoveNext(); // moves to '10' in list

      //
      // UNCOMMENT ONLY *ONE* OF THESE CALLS:
      //

      //UseMyStruct(ref myStruct);
      //UseMyStructAndBox(ref myStruct);

      Console.WriteLine("After call, current is now: " + myStruct.Current); // 10 or 20?
    }

    static void UseMyStruct<T>(ref T myStruct) where T : IEnumerator<int>
    {
      myStruct.MoveNext();
    }

    static void UseMyStructAndBox<T>(ref T myStruct)
    {
      ((IEnumerator<int>)myStruct).MoveNext();
    }
  }
}

Here the type of myStruct is a mutable value-type which holds a reference back to the List<>, and also holds "counter" that remembers what index in the List<> we have reached until now.

I had to use ref, otherwise the value-type would be copied by value when passed into either of the methods!

When I uncomment the call to UseMyStruct (only), this method moves the "counter" inside our value type one position ahead. If it did that in a boxed copy of the value type, we would not see it in the original instance of the struct.

To see what the difference is with boxing, try the call UseMyStructAndBox instead (comment UseMyStruct again). It creates a box at the cast, and the MoveNext happens on a copy. So the output is different!


To those who are unhappy with (or confused by) the ref, just write out Current from within the method instead. Then we can get rid of ref. Example:

static void F<T>(T t) where T : IEnumerator<int>
{
  t.MoveNext(); // OK, not boxed
  Console.WriteLine(t.Current);
}

static void G<T>(T t) where T : IEnumerator<int>
{
  ((IEnumerator<int>)t).MoveNext(); // We said "Box!", it will box; 'Move' happens to a copy
  Console.WriteLine(t.Current);
}
Jeppe Stig Nielsen
  • 60,409
  • 11
  • 110
  • 181
  • If so, then yes, that would be very cool. But are you sure? Habib's answer states exactly the opposite. – gexicide Jul 16 '15 at 15:03
  • @gexicide, no, my answer was wrong with respect to the the `where T: I` otherwise it was correct. – Habib Jul 16 '15 at 15:06
  • This optimization is extremely important for example for Array.Sort. This should work. – usr Jul 16 '15 at 15:09
3

Boxing will be avoided as Constrained Opcodes come to play in the second case.

Sameer
  • 3,124
  • 5
  • 30
  • 57