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

(As a result of the study, in order to answer this question, I (I think I have!) Determined that the answer is β€œno.” However, I had to look in several different places to understand this, so I think that the question remains, 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 implements box triggers:

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

What I don’t know is that passing a value type as a general parameter with an interface constraint is tantamount to casting - the language "where T is IComparable" suggests casting, but just turns T into IComparable it seems that it will defeat the whole purpose be generic!

To clarify, I would like to make sure that none of these events happen in the code above:

  • When g calls f(4) , 4 converted to IComparable , since there is an IComparable restriction on the parameter type f .
  • Assuming that (1) does not occur, inside f , val.CompareTo(null) does not pass val from Int32 to IComparable to call CompareTo .

But I would like to understand the general case; not just what happens with int and IComparable s.

Now, if I put the code below in 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 is clear that boxing occurs as expected for an explicit cast, but not a single box is obvious in f itself * or on its calling site in Main . This is good news. However, this is also one example with one type. This lack of boxing, what can be assumed for all cases?


* This MSDN article discusses the constrained prefix and indicates that its use in combination with callvirt will not start boxing for value types while the called method is implemented in the type itself (unlike the base class). I'm not sure if a type will always remain a value type when we are here.

+6
generics c # clr boxing type-constraints
source share
2 answers

As you already found out, when a struct is passed to the general method, it will not be put into the box.

Runtime creates a new method for each "type argument". When you call a generic method with a value of a type, you are actually calling the dedicated method created for the corresponding value type. Therefore, there is no need for boxing.

When calling an interface method that is not directly implemented in your structure type, boxing will occur. Spec calls this here:

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

This last case can occur only when the method has been defined in Object, ValueType or Enum and is not overridden by this type. In this case, the box calls up a copy of the original object. However, because none of the Object, ValueType, and Enum methods change the state of an object, this fact cannot be detected.

So, while you explicitly [1] implement a member of the interface in your structure, boxing will not be executed.

How, when and where are common methods made concrete?

1. Not to be confused with the implementation of the Java interface. This means that your interface method should be implemented in the structure itself, and not in its basic type.

+6
source share

A simple test is to simply create a mutable structure using an interface method that mutates it. Call this interface method from the general method and see if there was a mutation in the original structure.

 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, which means that the actual variable was mutated, not a copy, so the struct did not fit in the box.

+1
source share

All Articles