Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are value types boxed when passed as generic parameters with an interface constraint?

(As a result of doing the research to answer this question, I (think I have!) determined that the answer is "no." However, I had to look in several different places to figure this out, so I think there is still value to the question. But I won't be devastated if the community votes to close.)

For example:

void f<T>(T val) where T : IComparable
{
   val.CompareTo(null);
}

void g()
{
   f(4);
}

Is 4 boxed? I know that explicitly casting a value type to an interface that it implements triggers boxing:

((IComparable)4).CompareTo(null); // The Int32 "4" is boxed

What I don't know is whether passing a value type as a generic parameter with an interface constraint is tantamount to performing a cast--the language "where T is an IComparable" sort of suggests casting, but simply turning T into IComparable seems like it would defeat the entire purpose of being generic!

To clarify, I would like to be sure neither of these things happens in the code above:

  1. When g calls f(4), the 4 is cast to IComparable since there is an IComparable constraint on f's parameter type.
  2. Assuming (1) does not occur, within f, val.CompareTo(null) does not cast val from Int32 to IComparable in order to call CompareTo.

But I would like to understand the general case; not just what happens with ints and IComparables.

Now, if I put the below code into LinqPad:

void Main()
{
    ((IComparable)4).CompareTo(null);
    f(4);
}

void f<T>(T val) where T : IComparable
{
   val.CompareTo(null);
}

And then examine the generated IL:

IL_0001:  ldc.i4.4    
IL_0002:  box         System.Int32
IL_0007:  ldnull      
IL_0008:  callvirt    System.IComparable.CompareTo
IL_000D:  pop         
IL_000E:  ldarg.0     
IL_000F:  ldc.i4.4    
IL_0010:  call        UserQuery.f

f:
IL_0000:  nop         
IL_0001:  ldarga.s    01 
IL_0003:  ldnull      
IL_0004:  constrained. 01 00 00 1B 
IL_000A:  callvirt    System.IComparable.CompareTo
IL_000F:  pop         
IL_0010:  ret  

It's clear that boxing occurs as expected for the explicit cast, but no boxing is obvious either in f itself* or at its call site in Main. This is good news. However, that's also just one example with one type. Is this lack of boxing something that can be assumed for all cases?


*This MSDN article discusses the constrained prefix and states that using it in conjunction with callvirt will not trigger boxing for value types as long as the called method is implemented on the type itself (as opposed to a base class). What I'm not sure of is whether the type will always still be a value type when we get here.

like image 835
dlf Avatar asked Aug 26 '14 14:08

dlf


People also ask

What does the generic constraint of type interface do?

Interface Type Constraint You can constrain the generic type by interface, thereby allowing only classes that implement that interface or classes that inherit from classes that implement the interface as the type parameter.

Which of the following generic constraints restricts the generic type parameter to an object of the class?

Value type constraint If we declare the generic class using the following code then we will get a compile-time error if we try to substitute a reference type for the type parameter.

How do you specify a constraint for the type to be used in a generic class?

It will give a compile-time error if you try to instantiate a generic type using a type that is not allowed by the specified constraints. You can specify one or more constraints on the generic type using the where clause after the generic type name.

What is the purpose of the class constraint on a type parameter?

Object, you'll apply constraints to the type parameter. For example, the base class constraint tells the compiler that only objects of this type or derived from this type will be used as type arguments. Once the compiler has this guarantee, it can allow methods of that type to be called in the generic class.


2 Answers

As you figured out already, When a struct is passed to generic method, It will not be boxed.

Runtime creates new method for every "Type Argument". When you call a generic method with a value type, you're actually calling a dedicated method created for respective value type. So there is no need of boxing.

When calling the interface method which is not directly implemented in your struct type, then boxing will happen. Spec calls this out here:

If thisType is a value type and thisType does not implement method then ptr is dereferenced, boxed, and passed as the 'this' pointer to the callvirt method instruction.

This last case can occur only when method was defined on Object, ValueType, or Enum and not overridden by thisType. In this case, the boxing causes a copy of the original object to be made. However, because none of the methods of Object, ValueType, and Enum modify the state of the object, this fact cannot be detected.

So, as long as you explicitly[1] implement interface member in your struct itself, boxing will not occur.

How, when and where are generic methods made concrete?

1.Not to be confused with Explicit interface implementation. It is to say that your interface method should be implemented in struct itself rather than its base type.

like image 177
Sriram Sakthivel Avatar answered Oct 07 '22 09:10

Sriram Sakthivel


A simple enough test is to simply create a mutable struct with an interface method that mutates it. Call that interface method from a generic method, and see if the original struct was mutated.

public interface IMutable
{
    void Mutate();
    int Value { get; }
}

public struct Evil : IMutable
{
    public int value;

    public void Mutate()
    {
        value = 9;
    }

    public int Value { get { return value; } }
}

public static void Foo<T>(T mutable)
    where T : IMutable
{
    mutable.Mutate();
    Console.WriteLine(mutable.Value);
}

static void Main(string[] args2)
{
    Evil evil = new Evil() { value = 2 };
    Foo(evil);
}

Here we see 9 printed out, which means the actual variable was mutated, not a copy, so the struct wasn't boxed.

like image 28
Servy Avatar answered Oct 07 '22 09:10

Servy