(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:
g
calls f(4)
, the 4
is cast to IComparable
since there is an IComparable
constraint on f
's parameter type.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 int
s and IComparable
s.
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.
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.
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.
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With