Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Do C# generics prevent autoboxing of structs in this case?

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?

like image 545
gexicide Avatar asked Jul 16 '15 14:07

gexicide


1 Answers

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);
}
like image 106
Jeppe Stig Nielsen Avatar answered Sep 26 '22 09:09

Jeppe Stig Nielsen