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

3

9

(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.

Slideaction answered 26/8, 2014 at 14:39 Comment(4)
Can you clarify, do you mean "is it boxed when passing the value type to the generic method" or "is it boxed inside the generic method when calling the interface method on the value type"?Emmeram
The boxing incurred by constrained does not occur outside the method, passing the value type to the method will not be boxed. However, the constrained callvirt inside the method might box the value type depending on circumstances. What exactly is your question here?Emmeram
@LasseV.Karlsen: I read an essential part of the question as "If an enum is passed as an IComparable-constrained generic, will calling Compare on it cause the constrained generic to be boxed"? I would guess that in the case of the IComparable interface implemented by System.Enum the answer would be that such a call will not only involve boxing, but other horribly slow Refection-based nastiness as well, but types with "user-defined" interface implementations boxing will be avoided.Jim
@LasseV.Karlsen Either one. I was concerned that passing (e.g.) int to a generic method f with where T : IComparable might actually cast the int to an IComparable at the call site and pass it that way. And if it didn't, I was also worried that perhaps the T would first be cast to IComparable within f in order to call CompareTo on it. My experiment showed that neither happens in the specific case I examined.Slideaction
M
8

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.

Mccool answered 26/8, 2014 at 14:49 Comment(6)
The question is not about passing the struct to the generic method, but the calling of the method on the required interface, the part using the constrained and callvirt instructions. The documentation of constrained indicates that the value type will be boxed if the type of the value type does not directly implement the method called on the interface.Emmeram
@LasseV.Karlsen Still answer is same. It will not be boxed. Otherwise why do you need generics?Mccool
It will not be boxed when you pass the value to the method. It might be boxed when you call the interface method, inside the method, and that's what the question is about.Emmeram
Sorry, I think this question is unclear, re-reading it twice gives me the indication that you're right as well.Emmeram
@LasseV.Karlsen Still not clear for me, so edited my answer to include that caveat you mentioned.Mccool
The caveat sounds scary, but it is not actually much of an issue. If the struct implements an interface (even using explicitly interface implementation), it does implement that interface and the call can be made directly without boxing. As the caveat mentions, only things on System.Object, System.Enum, and System.ValueType will result in this boxing. So that means you will get boxing if you call .ToString(), .GetHashCode(), or .Equals() and have not overridden those in the struct. And if the struct calls the base implementation, it will box itself then, heh. Interesting.Preclinical
N
1

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.

Nilsson answered 26/8, 2014 at 15:0 Comment(1)
This is a good and (in retrospect) obvious way to test for boxing. I probably should have done something like this before diving into the IL!Slideaction
M
0

I took as a base the answer that Servy gave and I believe that my answer is more explanatory and it proves the claimed behavior.

The code creates a struct and a class that implement an interface method. This method tries to mutate them. The code calls that interface method from a generic method for the struct, the struct cast to the interface, and then for the class. The output is quite self-explanatory and shows that the passed struct is not boxed till not cast to the interface. Also, I added some IL code to see when boxing happens.

using System;

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

    public struct EvilStruct: IMutable
    {
        public int value;

        public void Mutate()
        {
            value++;
        }

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

    public class EvilClass : IMutable
    {
        public int value;

        public void Mutate()
        {
            value++;
        }

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

    class Program
    {
        public static void Foo<T>(T mutable)
            where T: IMutable
        {
            mutable.Mutate();
        }

        static void Main(string[] args)
        {
            EvilStruct Struct = new EvilStruct() { value = 1 };
            Foo(Struct);
            //Shows 1 after calling Mutate on value type 
            Console.WriteLine(Struct.Value);

            IMutable YetAnotherStruct = new EvilStruct() { value = 1 };
            Foo(YetAnotherStruct);
            //Shows 2 after calling Mutate on value type
            Console.WriteLine(YetAnotherStruct.Value);

            EvilClass Class = new EvilClass() { value = 1 };
            Foo(Class);
            //Shows 2 after calling Mutate on ref type 
            Console.WriteLine(Class.Value);

            Console.ReadLine();
        }
    }
}  

The output: 1 2 2

Here is the IL code for the Main method. You can see at IL_0038 that boxing happens:

Program.Main:
IL_0000:  nop         
IL_0001:  ldloca.s    03 
IL_0003:  initobj     UserQuery.EvilStruct
IL_0009:  ldloca.s    03 
IL_000B:  ldc.i4.1    
IL_000C:  stfld       UserQuery+EvilStruct.value
IL_0011:  ldloc.3     
IL_0012:  stloc.0     // Struct
IL_0013:  ldloc.0     // Struct
IL_0014:  call        UserQuery+Program.Foo<EvilStruct>
IL_0019:  nop         
IL_001A:  ldloca.s    00 // Struct
IL_001C:  call        UserQuery+EvilStruct.get_Value
IL_0021:  call        System.Console.WriteLine
IL_0026:  nop         
IL_0027:  ldloca.s    03 
IL_0029:  initobj     UserQuery.EvilStruct
IL_002F:  ldloca.s    03 
IL_0031:  ldc.i4.1    
IL_0032:  stfld       UserQuery+EvilStruct.value
IL_0037:  ldloc.3     
IL_0038:  box         UserQuery.EvilStruct
IL_003D:  stloc.1     // YetAnotherStruct
IL_003E:  ldloc.1     // YetAnotherStruct
IL_003F:  call        UserQuery+Program.Foo<IMutable>
IL_0044:  nop         
IL_0045:  ldloc.1     // YetAnotherStruct
IL_0046:  callvirt    UserQuery+IMutable.get_Value
IL_004B:  call        System.Console.WriteLine
IL_0050:  nop         
IL_0051:  newobj      UserQuery+EvilClass..ctor
IL_0056:  dup         
IL_0057:  ldc.i4.1    
IL_0058:  stfld       UserQuery+EvilClass.value
IL_005D:  stloc.2     // Class
IL_005E:  ldloc.2     // Class
IL_005F:  call        UserQuery+Program.Foo<EvilClass>
IL_0064:  nop         
IL_0065:  ldloc.2     // Class
IL_0066:  callvirt    UserQuery+EvilClass.get_Value
IL_006B:  call        System.Console.WriteLine
IL_0070:  nop         
IL_0071:  call        System.Console.ReadLine
IL_0076:  pop         
Mesosphere answered 4/6, 2020 at 18:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.