装箱值类型以将其发送到方法并获取结果
Boxing value type to send it to a method and get the result
我很好奇 C# 在将 value/reference 类型传递给方法时的行为方式。我想将盒装值类型传递给方法“AddThree”。这个想法是在调用函数 (Main) 中获取在“AddThree”.
中执行的操作的结果。
static void Main(string[] args)
{
int age = 3;
object myBox = age;
AddThree(myBox);
// here myBox = 3 but I was expecting to be = 6
}
private static void AddThree(object age2)
{
age2 = (int)age2 + 3;
}
我已经尝试使用像 string 这样的纯引用类型,我得到了相同的结果。如果我 "wrap" 我的 int 在 class 中并且我在这个例子中使用了 "wrap" class 它会像我期望的那样工作,即,我得到 myBox = 6。如果我修改 "AddThree" 方法签名以通过 ref 传递参数,这也是 returns 6。但是, 我不想修改签名或创建包装器 class,我只想将值装箱。
I don't want to modify the signature or create a wrapper class, I
want to just box the value.
那就麻烦了。您的方法传递一个装箱的 int
,然后将其拆箱并将 3 添加到本地 age2
,这将导致另一个装箱操作,然后丢弃该值。事实上,您将 age2
分配给堆上的两个不同对象,它们不指向同一个对象。
如果不修改方法签名,这是不可能的。
如果您查看为 AddThree
生成的 IL,您会清楚地看到:
AddThree:
IL_0000: nop
IL_0001: ldarg.0
IL_0002: unbox.any System.Int32 // unbox age2
IL_0007: ldc.i4.3 // load 3
IL_0008: add // add the two together
IL_0009: box System.Int32 // box the result
IL_000E: starg.s 00
IL_0010: ret
您将值拆箱,加 3,然后再次将值装箱,但您从未 return 它。
为了进一步形象化这种情况,尝试 return 从方法中输入新装箱的值(只是为了测试),并使用 object.ReferenceEquals
比较它们:
static void Main(string[] args)
{
int age = 3;
object myBox = age;
var otherBox = AddThree(myBox);
Console.WriteLine(object.ReferenceEquals(otherBox, myBox)); // False
}
private static object AddThree(object age2)
{
age2 = (int)age2 + 3;
return age2;
}
为未通过 ref
传递的方法参数分配新值不会更改原始引用。
在您的例子中,age2
(方法参数)是 myBox
的副本。它们都引用相同的对象(带框的 age
),但分配给 age2
不会更改 myBox
。它只是让 age2
引用另一个对象。
其实和拳击没有关系。这就是将参数传递给方法的方式。
盒装引用意味着不可变。例如,这不会编译:
((Point)p).X += 3; // CS0445: Cannot modify the result of an unboxing conversion.
正如其他人所说,这一行导致了一对装箱和拆箱操作,最终产生了一个新的引用:
age2 = (int)age2 + 3;
所以即使装箱的 int 实际上是一个引用,上面的行也修改了对象引用,所以调用者仍然会看到相同的内容,除非对象本身是通过引用传递的。
但是,有几种方法可以在不更改引用的情况下取消引用和更改装箱值(不过,推荐其中 none 种方法)。
解决方案一:
最简单的方法是通过反射。这看起来有点傻,因为 Int32.m_value
字段本身就是 int 值,但这允许您直接访问 int。
private static void AddThree(object age2)
{
FieldInfo intValue = typeof(int).GetTypeInfo().GetDeclaredField("m_value");
intValue.SetValue(age2, (int)age2 + 3);
}
方案二:
这是一个更大的 hack,涉及使用主要未记录的 TypedReference
和 __makeref()
运算符,但或多或少这就是第一个解决方案在后台发生的事情:
private static unsafe void AddThree(object age2)
{
// pinning is required to prevent GC reallocating the object during the pointer operations
var objectPinned = GCHandle.Alloc(age2, GCHandleType.Pinned);
try
{
// The __makeref() operator returns a TypedReference.
// It is basically a pair of pointers for the reference value and type.
TypedReference objRef = __makeref(age2);
// Dereference it to access the boxed value like this: objRef.Value->object->boxed content
// For more details see the memory layout of objects: https://blogs.msdn.microsoft.com/seteplia/2017/05/26/managed-object-internals-part-1-layout/
int* rawContent = (int*)*(IntPtr*)*(IntPtr*)&objRef;
// rawContent now points to the type handle (just another pointer to the method table).
// The actual instance fields start after these 4 or 8 bytes depending on the pointer size:
int* boxedInt = rawContent + (IntPtr.Size == 4 ? 1 : 2);
*boxedInt += 3;
}
finally
{
objectPinned.Free();
}
}
⚠️ Caution: Please note that this solution is platform dependent and does not work on Mono because its TypedReference
implementation is different.
更新:解决方案 3:
从 .NET Core 开始,您可以使用更简单且性能更高的技巧:Unsafe.As<T>
方法允许您将任何对象重新解释为另一种引用类型。您只需要一个 class,其第一个字段的类型与您的装箱值相同。实际上,您可以为此目的完美地使用 StrongBox<T>
类型,因为它的单个 Value
字段是 public 并且是可变的:
private static void AddThree(object age2) =>
Unsafe.As<StrongBox<int>>(age2).Value += 3;
我很好奇 C# 在将 value/reference 类型传递给方法时的行为方式。我想将盒装值类型传递给方法“AddThree”。这个想法是在调用函数 (Main) 中获取在“AddThree”.
中执行的操作的结果。static void Main(string[] args)
{
int age = 3;
object myBox = age;
AddThree(myBox);
// here myBox = 3 but I was expecting to be = 6
}
private static void AddThree(object age2)
{
age2 = (int)age2 + 3;
}
我已经尝试使用像 string 这样的纯引用类型,我得到了相同的结果。如果我 "wrap" 我的 int 在 class 中并且我在这个例子中使用了 "wrap" class 它会像我期望的那样工作,即,我得到 myBox = 6。如果我修改 "AddThree" 方法签名以通过 ref 传递参数,这也是 returns 6。但是, 我不想修改签名或创建包装器 class,我只想将值装箱。
I don't want to modify the signature or create a wrapper class, I want to just box the value.
那就麻烦了。您的方法传递一个装箱的 int
,然后将其拆箱并将 3 添加到本地 age2
,这将导致另一个装箱操作,然后丢弃该值。事实上,您将 age2
分配给堆上的两个不同对象,它们不指向同一个对象。
如果不修改方法签名,这是不可能的。
如果您查看为 AddThree
生成的 IL,您会清楚地看到:
AddThree:
IL_0000: nop
IL_0001: ldarg.0
IL_0002: unbox.any System.Int32 // unbox age2
IL_0007: ldc.i4.3 // load 3
IL_0008: add // add the two together
IL_0009: box System.Int32 // box the result
IL_000E: starg.s 00
IL_0010: ret
您将值拆箱,加 3,然后再次将值装箱,但您从未 return 它。
为了进一步形象化这种情况,尝试 return 从方法中输入新装箱的值(只是为了测试),并使用 object.ReferenceEquals
比较它们:
static void Main(string[] args)
{
int age = 3;
object myBox = age;
var otherBox = AddThree(myBox);
Console.WriteLine(object.ReferenceEquals(otherBox, myBox)); // False
}
private static object AddThree(object age2)
{
age2 = (int)age2 + 3;
return age2;
}
为未通过 ref
传递的方法参数分配新值不会更改原始引用。
在您的例子中,age2
(方法参数)是 myBox
的副本。它们都引用相同的对象(带框的 age
),但分配给 age2
不会更改 myBox
。它只是让 age2
引用另一个对象。
其实和拳击没有关系。这就是将参数传递给方法的方式。
盒装引用意味着不可变。例如,这不会编译:
((Point)p).X += 3; // CS0445: Cannot modify the result of an unboxing conversion.
正如其他人所说,这一行导致了一对装箱和拆箱操作,最终产生了一个新的引用:
age2 = (int)age2 + 3;
所以即使装箱的 int 实际上是一个引用,上面的行也修改了对象引用,所以调用者仍然会看到相同的内容,除非对象本身是通过引用传递的。
但是,有几种方法可以在不更改引用的情况下取消引用和更改装箱值(不过,推荐其中 none 种方法)。
解决方案一:
最简单的方法是通过反射。这看起来有点傻,因为 Int32.m_value
字段本身就是 int 值,但这允许您直接访问 int。
private static void AddThree(object age2)
{
FieldInfo intValue = typeof(int).GetTypeInfo().GetDeclaredField("m_value");
intValue.SetValue(age2, (int)age2 + 3);
}
方案二:
这是一个更大的 hack,涉及使用主要未记录的 TypedReference
和 __makeref()
运算符,但或多或少这就是第一个解决方案在后台发生的事情:
private static unsafe void AddThree(object age2)
{
// pinning is required to prevent GC reallocating the object during the pointer operations
var objectPinned = GCHandle.Alloc(age2, GCHandleType.Pinned);
try
{
// The __makeref() operator returns a TypedReference.
// It is basically a pair of pointers for the reference value and type.
TypedReference objRef = __makeref(age2);
// Dereference it to access the boxed value like this: objRef.Value->object->boxed content
// For more details see the memory layout of objects: https://blogs.msdn.microsoft.com/seteplia/2017/05/26/managed-object-internals-part-1-layout/
int* rawContent = (int*)*(IntPtr*)*(IntPtr*)&objRef;
// rawContent now points to the type handle (just another pointer to the method table).
// The actual instance fields start after these 4 or 8 bytes depending on the pointer size:
int* boxedInt = rawContent + (IntPtr.Size == 4 ? 1 : 2);
*boxedInt += 3;
}
finally
{
objectPinned.Free();
}
}
⚠️ Caution: Please note that this solution is platform dependent and does not work on Mono because its
TypedReference
implementation is different.
更新:解决方案 3:
从 .NET Core 开始,您可以使用更简单且性能更高的技巧:Unsafe.As<T>
方法允许您将任何对象重新解释为另一种引用类型。您只需要一个 class,其第一个字段的类型与您的装箱值相同。实际上,您可以为此目的完美地使用 StrongBox<T>
类型,因为它的单个 Value
字段是 public 并且是可变的:
private static void AddThree(object age2) =>
Unsafe.As<StrongBox<int>>(age2).Value += 3;