[英]Why do we need boxing and unboxing in C#?
為什么我們需要在 C# 中裝箱和拆箱?
我知道裝箱和拆箱是什么,但我無法理解它的真正用途。 為什么以及我應該在哪里使用它?
short s = 25;
object objshort = s; //Boxing
short anothershort = (short)objshort; //Unboxing
為什么
擁有統一的類型系統,並允許值類型對其底層數據的表示與引用類型表示其底層數據的方式完全不同(例如, int
只是一個 32 位的桶,與一個完全不同的引用類型)。
像這樣想。 您有一個object
類型的變量o
。 現在你有一個int
並且你想把它放入o
。 o
是對某處某物的引用,而int
顯然不是對某某某物的引用(畢竟,它只是一個數字)。 因此,您要做的是:創建一個可以存儲int
的新object
,然后將該對象的引用分配給o
。 我們稱這個過程為“拳擊”。
因此,如果您不關心具有統一的類型系統(即,引用類型和值類型具有非常不同的表示形式,並且您不想要一種通用的方式來“表示”兩者),那么您就不需要裝箱。 如果您不關心讓int
表示它們的基礎值(即,也讓int
成為引用類型,並且只存儲對其基礎值的引用),那么您就不需要裝箱。
我應該在哪里使用它。
例如,舊的集合類型ArrayList
只吃object
s。 也就是說,它只存儲對某處某物的引用。 如果沒有裝箱,您就不能將int
放入這樣的集合中。 但是用拳擊,你可以。
現在,在泛型時代,您並不真正需要它,並且通常可以在不考慮問題的情況下愉快地進行。 但是有一些注意事項需要注意:
這是對的:
double e = 2.718281828459045;
int ee = (int)e;
這不是:
double e = 2.718281828459045;
object o = e; // box
int ee = (int)o; // runtime exception
相反,您必須這樣做:
double e = 2.718281828459045;
object o = e; // box
int ee = (int)(double)o;
首先,我們必須顯式地將double
( (double)o
) 拆箱,然后將其轉換為int
。
下面的結果是什么:
double e = 2.718281828459045;
double d = e;
object o1 = d;
object o2 = e;
Console.WriteLine(d == e);
Console.WriteLine(o1 == o2);
在繼續下一個句子之前先想一想。
如果你說的True
和False
太棒了! 等等,什么? 那是因為==
在引用類型上使用引用相等來檢查引用是否相等,而不是基礎值是否相等。 這是一個很容易犯的危險錯誤。 或許更微妙
double e = 2.718281828459045;
object o1 = e;
object o2 = e;
Console.WriteLine(o1 == o2);
也會打印False
!
最好說:
Console.WriteLine(o1.Equals(o2));
幸運的是,它將打印True
。
最后一個微妙之處:
[struct|class] Point {
public int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
Point p = new Point(1, 1);
object o = p;
p.x = 2;
Console.WriteLine(((Point)o).x);
輸出是什么? 這取決於! 如果Point
是struct
則輸出為1
但如果Point
是class
則輸出為2
! 裝箱轉換會復制被裝箱的值,解釋行為的差異。
在.NET 框架中,有兩種類型——值類型和引用類型。 這在 OO 語言中比較常見。
面向對象語言的重要特性之一是能夠以與類型無關的方式處理實例。 這稱為多態性。 由於我們想利用多態性,但我們有兩種不同的類型,因此必須有某種方法將它們組合在一起,以便我們可以以相同的方式處理其中一種。
現在,回到過去(Microsoft.NET 的 1.0),沒有這種新奇的泛型喧囂。 您無法編寫一個具有單個參數的方法,該方法可以為值類型和引用類型提供服務。 這違反了多態性。 因此采用裝箱作為將值類型強制轉換為對象的一種手段。
如果這是不可能的,那么該框架將充斥着方法和類,其唯一目的是接受其他類型的類型。 不僅如此,由於值類型並沒有真正共享一個共同的類型祖先,因此您必須為每個值類型(位、字節、int16、int32 等)使用不同的方法重載。
拳擊阻止了這種情況的發生。 這就是英國人慶祝節禮日的原因。
理解這一點的最佳方法是查看 C# 構建的低級編程語言。
在像 C 這樣的最低級語言中,所有變量都集中在一個地方:堆棧。 每次聲明一個變量時,它都會進入堆棧。 它們只能是原始值,如 bool、字節、32 位 int、32 位 uint 等。堆棧既簡單又快速。 添加變量時,它們只是一個在另一個之上,因此您聲明的第一個位於例如 0x00,下一個位於 0x01,下一個位於 RAM 中的 0x02,等等。此外,變量通常在編譯時預先尋址 -時間,所以他們的地址在你運行程序之前就已經知道了。
在下一個級別,如 C++,引入了稱為堆的第二個內存結構。 您仍然主要生活在堆棧中,但是可以將稱為指針的特殊整數添加到堆棧中,用於存儲對象第一個字節的內存地址,並且該對象位於堆中。 堆有點亂,維護起來有些昂貴,因為與堆棧變量不同,它們不會在程序執行時線性堆積然后堆積。 它們可以沒有特定的順序來來去去,也可以增長和縮小。
處理指針很困難。 它們是內存泄漏、緩沖區溢出和沮喪的原因。 C# 來救援。
在更高級別,C#,您不需要考慮指針 - .Net 框架(用 C++ 編寫)會為您考慮這些並將它們作為對對象的引用呈現給您,並且為了性能,您可以存儲更簡單的值像 bools、bytes 和 ints 作為值類型。 在幕后,對象和實例化類的東西在昂貴的內存管理堆中,而值類型則在與低級 C 相同的堆棧中 - 超快。
為了從編碼人員的角度保持這兩個根本不同的內存概念(和存儲策略)之間的交互簡單,值類型可以隨時裝箱。 裝箱會導致值從堆棧中復制,放入一個對象中,然后放置在堆上- 更昂貴,但與引用世界的交互流暢。 正如其他答案所指出的那樣,例如,當您說:
bool b = false; // Cheap, on Stack
object o = b; // Legal, easy to code, but complex - Boxing!
bool b2 = (bool)o; // Unboxing!
Boxing 優勢的一個有力例證是檢查 null:
if (b == null) // Will not compile - bools can't be null
if (o == null) // Will compile and always return false
我們的對象 o 從技術上講是堆棧中的一個地址,它指向我們的 bool b 的副本,該副本已被復制到堆中。 我們可以檢查 o 是否為 null,因為 bool 已被裝箱並放在那里。
一般來說,除非需要,否則應該避免拳擊,例如將 int/bool/whatever 作為對象傳遞給參數。 .Net 中有一些基本結構仍然需要將值類型作為對象傳遞(因此需要裝箱),但在大多數情況下,您永遠不需要裝箱。
需要裝箱的歷史 C# 結構的非詳盡列表,您應該避免:
事實證明,事件系統在幼稚的使用中存在競爭條件,並且它不支持異步。 添加拳擊問題,它可能應該避免。 (例如,您可以將其替換為使用泛型的異步事件系統。)
舊的 Threading 和 Timer 模型在其參數上強制使用 Box,但已被 async/await 取代,后者更簡潔、更高效。
.Net 1.1 Collections 完全依賴於 Boxing,因為它們出現在泛型之前。 這些仍然在 System.Collections 中出現。 在任何新代碼中,您都應該使用 System.Collections.Generic 中的集合,它除了避免裝箱外,還為您提供更強的類型安全性。
您應該避免將您的值類型聲明或傳遞為對象,除非您必須處理上述強制使用 Boxing 的歷史問題,並且您想避免稍后在您知道它無論如何都會被 Boxed 時對其進行 Boxing 的性能影響。
根據以下 Mikael 的建議:
using System.Collections.Generic;
var employeeCount = 5;
var list = new List<int>(10);
using System.Collections;
Int32 employeeCount = 5;
var list = new ArrayList(10);
這個答案最初建議 Int32、Bool 等導致裝箱,而實際上它們是值類型的簡單別名。 也就是說,.Net 具有 Bool、Int32、String 和 C# 等類型,將它們別名為 bool、int、string,沒有任何功能差異。
裝箱並不是您真正使用的東西 - 它是運行時使用的東西,以便您可以在必要時以相同的方式處理引用和值類型。 例如,如果您使用 ArrayList 來保存整數列表,則整數會被裝箱以適合 ArrayList 中的對象類型槽。
現在使用泛型集合,這幾乎消失了。 如果您創建List<int>
,則不會進行裝箱 - List<int>
可以直接保存整數。
裝箱和拆箱專門用於將值類型對象視為引用類型; 將它們的實際值移動到托管堆並通過引用訪問它們的值。
沒有裝箱和拆箱,你永遠無法通過引用傳遞值類型; 這意味着您不能將值類型作為 Object 的實例傳遞。
我不得不拆箱的最后一個地方是在編寫一些從數據庫中檢索一些數據的代碼時(我沒有使用LINQ to SQL ,只是簡單的舊ADO.NET ):
int myIntValue = (int)reader["MyIntValue"];
基本上,如果您在泛型之前使用較舊的 API,您會遇到拳擊。 除此之外,它並不常見。
裝箱是必需的,當我們有一個需要對象作為參數的函數,但我們有不同的值類型需要傳遞時,在這種情況下,我們需要先將值類型轉換為對象數據類型,然后再將其傳遞給函數。
我不認為這是真的,試試這個:
class Program
{
static void Main(string[] args)
{
int x = 4;
test(x);
}
static void test(object o)
{
Console.WriteLine(o.ToString());
}
}
運行得很好,我沒有使用裝箱/拆箱。 (除非編譯器在幕后這樣做?)
在 .net 中,Object 的每個實例或從其派生的任何類型都包含一個數據結構,其中包含有關其類型的信息。 .net 中的“真實”值類型不包含任何此類信息。 為了允許期望接收從對象派生的類型的例程操作值類型中的數據,系統自動為每個值類型定義具有相同成員和字段的對應類類型。 裝箱創建此類類型的新實例,從值類型實例復制字段。 拆箱將字段從類類型的實例復制到值類型的實例。 所有從值類型創建的類類型都是從具有諷刺意味的命名類 ValueType 派生的(盡管它的名字,它實際上是一個引用類型)。
通常,您通常希望避免裝箱您的值類型。
但是,在極少數情況下這是有用的。 例如,如果您需要針對 1.1 框架,您將無法訪問泛型集合。 .NET 1.1 中集合的任何使用都需要將您的值類型視為 System.Object,這會導致裝箱/拆箱。
這在 .NET 2.0+ 中仍然有用。 任何時候您想利用所有類型(包括值類型)都可以直接視為對象這一事實,您可能需要使用裝箱/拆箱。 這有時很方便,因為它允許您在集合中保存任何類型(通過在泛型集合中使用 object 而不是 T ),但一般來說,最好避免這種情況,因為您會失去類型安全性。 但是,經常發生裝箱的一種情況是當您使用反射時 - 反射中的許多調用在使用值類型時都需要裝箱/拆箱,因為類型是事先未知的。
當一個方法只接受一個引用類型作為參數時(比如一個通過new
約束被約束為類的泛型方法),你將無法將引用類型傳遞給它,而必須將它裝箱。
對於任何將object
作為參數的方法也是如此——這必須是引用類型。
裝箱是將值轉換為引用類型,其中數據位於堆上對象的某個偏移處。
至於拳擊實際上是做什么的。 這里有些例子
單聲道 C++
void* mono_object_unbox (MonoObject *obj)
{
MONO_EXTERNAL_ONLY_GC_UNSAFE (void*, mono_object_unbox_internal (obj));
}
#define MONO_EXTERNAL_ONLY_GC_UNSAFE(t, expr) \
t result; \
MONO_ENTER_GC_UNSAFE; \
result = expr; \
MONO_EXIT_GC_UNSAFE; \
return result;
static inline gpointer
mono_object_unbox_internal (MonoObject *obj)
{
/* add assert for valuetypes? */
g_assert (m_class_is_valuetype (mono_object_class (obj)));
return mono_object_get_data (obj);
}
static inline gpointer
mono_object_get_data (MonoObject *o)
{
return (guint8*)o + MONO_ABI_SIZEOF (MonoObject);
}
#define MONO_ABI_SIZEOF(type) (MONO_STRUCT_SIZE (type))
#define MONO_STRUCT_SIZE(struct) MONO_SIZEOF_ ## struct
#define MONO_SIZEOF_MonoObject (2 * MONO_SIZEOF_gpointer)
typedef struct {
MonoVTable *vtable;
MonoThreadsSync *synchronisation;
} MonoObject;
在 Mono 中對對象進行拆箱是在對象中的 2 個 gpointers 偏移量處(例如 16 字節)投射指針的過程。 gpointer
是一個void*
。 這在查看MonoObject
的定義時MonoObject
因為它顯然只是數據的標題。
C++
要在 C++ 中裝箱一個值,您可以執行以下操作:
#include <iostream>
#define Object void*
template<class T> Object box(T j){
return new T(j);
}
template<class T> T unbox(Object j){
T temp = *(T*)j;
delete j;
return temp;
}
int main() {
int j=2;
Object o = box(j);
int k = unbox<int>(o);
std::cout << k;
}
當將值類型傳遞給具有object
類型的變量或參數時,會發生裝箱。 由於它是自動發生的,問題不在於何時應該使用裝箱,而是何時應該使用類型object
。
僅在絕對必要時才應使用類型object
,因為它繞過了類型安全性,否則類型安全性是 C# 等靜態類型語言的主要優點。 但是在編譯時無法知道值的類型的情況下可能有必要。
例如,當通過 ADO.NET 框架讀取數據庫字段值時。 返回值可以是整數或字符串或其他東西,因此類型必須是object
,並且客戶端代碼必須執行適當的轉換。 為了避免這個問題,像 Linq-to-SQL 或 EF Core 這樣的 ORM 框架使用靜態類型的實體來代替,因此避免使用object
。
在引入泛型之前,像ArrayList
這樣的集合的項目類型為object
。 這意味着您可以將任何內容存儲在列表中,並且您可以將字符串添加到數字列表中,而無需類型系統抱怨。 泛型解決了這個問題,並且在使用值類型的集合時不需要裝箱。
因此很少需要將某些東西輸入為object
,並且您想避免它。 在代碼需要能夠處理值類型和引用類型的情況下,泛型通常是更好的解決方案。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.