Tại sao chúng ta cần đấm bốc và unboxing trong C #?


323

Tại sao chúng ta cần đấm bốc và unboxing trong C #?

Tôi biết đấm bốc và unboxing là gì, nhưng tôi không thể hiểu được công dụng thực sự của nó. Tại sao và tôi nên sử dụng nó ở đâu?

short s = 25;

object objshort = s;  //Boxing

short anothershort = (short)objshort;  //Unboxing

Câu trả lời:


480

Tại sao

Để có một hệ thống loại thống nhất và cho phép các loại giá trị có cách biểu thị dữ liệu cơ bản hoàn toàn khác với cách các loại tham chiếu thể hiện dữ liệu cơ bản của chúng (ví dụ: int chỉ là một nhóm ba mươi hai bit khác hoàn toàn so với tham chiếu kiểu).

Hãy nghĩ về nó như thế này. Bạn có một biến số ocủa loại object. Và bây giờ bạn có một intvà bạn muốn đặt nó vào o. olà một tham chiếu đến một cái gì đó ở đâu đó, và intrõ ràng không phải là một tham chiếu đến một cái gì đó ở đâu đó (sau tất cả, nó chỉ là một con số). Vì vậy, những gì bạn làm là thế này: bạn tạo một cái mới objectcó thể lưu trữ intvà sau đó bạn gán một tham chiếu cho đối tượng đó o. Chúng tôi gọi quá trình này là "quyền anh."

Vì vậy, nếu bạn không quan tâm đến việc có một hệ thống loại thống nhất (nghĩa là các loại tham chiếu và loại giá trị có các cách biểu diễn rất khác nhau và bạn không muốn có một cách chung để "đại diện" cho cả hai) thì bạn không cần quyền anh. Nếu bạn không quan tâm đến việc intđại diện cho giá trị cơ bản của chúng (nghĩa là thay vào đó cũng intlà các loại tham chiếu và chỉ lưu trữ một tham chiếu đến giá trị cơ bản của chúng) thì bạn không cần quyền anh.

Tôi nên sử dụng nó ở đâu.

Ví dụ, loại bộ sưu tập cũ ArrayListchỉ ăn objects. Đó là, nó chỉ lưu trữ các tài liệu tham khảo đến một số nơi sống ở đâu đó. Không có quyền anh bạn không thể đặt intmột bộ sưu tập như vậy. Nhưng với quyền anh, bạn có thể.

Bây giờ, trong thời đại của thuốc generic, bạn không thực sự cần điều này và thường có thể vui vẻ đi cùng mà không nghĩ về vấn đề này. Nhưng có một vài lưu ý cần lưu ý:

Chính xác:

double e = 2.718281828459045;
int ee = (int)e;

Đây không phải là:

double e = 2.718281828459045;
object o = e; // box
int ee = (int)o; // runtime exception

Thay vào đó bạn phải làm điều này:

double e = 2.718281828459045;
object o = e; // box
int ee = (int)(double)o;

Đầu tiên, chúng ta phải bỏ hộp double( (double)o) và sau đó bỏ nó thành mộtint .

Kết quả của những điều sau đây là gì:

double e = 2.718281828459045;
double d = e;
object o1 = d;
object o2 = e;
Console.WriteLine(d == e);
Console.WriteLine(o1 == o2);

Hãy suy nghĩ về nó trong một giây trước khi đi đến câu tiếp theo.

Nếu bạn nói TrueFalsetuyệt vời! Đợi đã, cái gì? Đó là bởi vì ==trên các kiểu tham chiếu sử dụng đẳng thức tham chiếu để kiểm tra xem các tham chiếu có bằng nhau không, nếu các giá trị cơ bản bằng nhau. Đây là một sai lầm nguy hiểm dễ mắc phải. Có lẽ còn tinh tế hơn

double e = 2.718281828459045;
object o1 = e;
object o2 = e;
Console.WriteLine(o1 == o2);

cũng sẽ in False !

Tốt hơn để nói:

Console.WriteLine(o1.Equals(o2));

Sau đó, may mắn thay, in True .

Một sự tinh tế cuối cùng:

[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);

Đầu ra là gì? Nó phụ thuộc! Nếu Pointlà a structthì đầu ra là 1nhưng nếu Pointlà a classthì đầu ra là 2! Một chuyển đổi quyền anh làm cho một bản sao của giá trị được đóng hộp giải thích sự khác biệt trong hành vi.


@Jason Bạn có muốn nói rằng nếu chúng ta có danh sách nguyên thủy, không có lý do gì để sử dụng bất kỳ quyền anh / unboxing nào không?
Pacerier

Tôi không chắc ý của bạn là "danh sách nguyên thủy".
jason

3
Bạn có thể vui lòng nói chuyện với tác động hiệu suất của boxingunboxing?
Kevin Meredith

@KevinMeredith có một lời giải thích cơ bản về hiệu suất cho các hoạt động đấm bốc và mở hộp trong msdn.microsoft.com/en-us/l
Library / ms173196.aspx

2
Câu trả lời tuyệt vời - tốt hơn hầu hết các giải thích tôi đã đọc trong những cuốn sách được đánh giá cao.
FredM

58

Trong khung .NET, có hai loại loại - loại giá trị và loại tham chiếu. Điều này là tương đối phổ biến trong các ngôn ngữ OO.

Một trong những tính năng quan trọng của các ngôn ngữ hướng đối tượng là khả năng xử lý các trường hợp theo cách thức bất khả tri. Điều này được gọi là đa hình . Vì chúng tôi muốn tận dụng tính đa hình, nhưng chúng tôi có hai loại khác nhau, nên có một số cách để mang chúng lại với nhau để chúng tôi có thể xử lý một hoặc cùng một cách.

Bây giờ, trở lại thời xa xưa (1.0 của Microsoft.NET), không có thế hệ mới này hullabaloo. Bạn không thể viết một phương thức có một đối số duy nhất có thể phục vụ loại giá trị và loại tham chiếu. Đó là một sự vi phạm của đa hình. Vì vậy, quyền anh đã được thông qua như một phương tiện để ép buộc một loại giá trị vào một đối tượng.

Nếu điều này là không thể, khung sẽ được lấp đầy bằng các phương thức và các lớp với mục đích duy nhất là chấp nhận các loại khác. Không chỉ vậy, vì các loại giá trị không thực sự chia sẻ tổ tiên loại chung, bạn phải có quá tải phương thức khác nhau cho từng loại giá trị (bit, byte, int16, int32, v.v.).

Quyền anh ngăn chặn điều này xảy ra. Và đó là lý do tại sao người Anh kỷ niệm Ngày Quyền anh.


1
Trước khi thuốc generic, tự động đấm bốc là cần thiết để làm nhiều việc; Tuy nhiên, do sự tồn tại của thuốc generic, không phải vì nhu cầu duy trì khả năng tương thích với mã cũ, tôi nghĩ .net sẽ tốt hơn nếu không có chuyển đổi quyền anh. Đúc một loại giá trị như List<string>.Enumeratorđể IEnumerator<string>sản lượng một đối tượng mà chủ yếu là hoạt động như một loại lớp, nhưng với một phá vỡ Equalsphương pháp. Một cách tốt hơn để cast List<string>.Enumeratorđể IEnumerator<string>sẽ được gọi một nhà điều hành chuyển đổi tùy chỉnh, nhưng sự tồn tại của một ngăn chặn chuyển đổi ngụ ý đó.
supercat

42

Cách tốt nhất để hiểu điều này là xem xét các ngôn ngữ lập trình cấp thấp hơn mà C # xây dựng.

Trong các ngôn ngữ cấp thấp nhất như C, tất cả các biến đều đi một nơi: Ngăn xếp. Mỗi khi bạn khai báo một biến, nó sẽ xuất hiện trên Stack. Chúng chỉ có thể là các giá trị nguyên thủy, như bool, byte, int 32 bit, uint 32 bit, v.v ... Stack vừa đơn giản vừa nhanh. Khi các biến được thêm vào, chúng chỉ đi lên trên một biến khác, do đó, biến đầu tiên bạn khai báo là 0x00, tiếp theo là 0x01, tiếp theo là 0x02 trong RAM, v.v. Ngoài ra, các biến thường được xử lý trước khi biên dịch- thời gian, vì vậy địa chỉ của họ được biết trước khi bạn chạy chương trình.

Ở cấp độ tiếp theo, như C ++, cấu trúc bộ nhớ thứ hai được gọi là Heap được giới thiệu. Bạn vẫn chủ yếu sống trong Stack, nhưng ints đặc biệt gọi là Con trỏ có thể được thêm vào Stack, lưu địa chỉ bộ nhớ cho byte đầu tiên của Object và Object đó sống trong Heap. Heap là một mớ hỗn độn và hơi tốn kém để duy trì, bởi vì không giống như các biến Stack, chúng không chồng chất lên nhau và sau đó xuống khi chương trình thực thi. Chúng có thể đến và đi không theo trình tự cụ thể, và chúng có thể phát triển và co lại.

Đối phó với con trỏ là khó khăn. Chúng là nguyên nhân gây rò rỉ bộ nhớ, tràn bộ đệm và thất vọng. C # để giải cứu.

Ở cấp độ cao hơn, C #, bạn không cần phải suy nghĩ về con trỏ - khung .Net (viết bằng C ++) nghĩ về những điều này cho bạn và giới thiệu chúng với bạn dưới dạng Tham chiếu đến Đối tượng và để thực hiện, cho phép bạn lưu trữ các giá trị đơn giản hơn như bools, byte và ints là các loại giá trị. Bên dưới mui xe, các Đối tượng và nội dung bắt đầu một Lớp đi trên Heap được quản lý bộ nhớ đắt tiền, trong khi Các loại giá trị đi vào cùng một Stack mà bạn có ở cấp độ C - siêu nhanh.

Để giữ cho sự tương tác giữa hai khái niệm cơ bản khác nhau về bộ nhớ (và chiến lược lưu trữ) đơn giản theo quan điểm của người viết mã, Các loại giá trị có thể được đóng hộp bất cứ lúc nào. Quyền anh khiến giá trị được sao chép từ Stack, đặt vào Object và được đặt trên Heap - đắt hơn, nhưng tương tác trôi chảy với thế giới Tham chiếu. Như các câu trả lời khác chỉ ra, điều này sẽ xảy ra khi bạn nói ví dụ:

bool b = false; // Cheap, on Stack
object o = b; // Legal, easy to code, but complex - Boxing!
bool b2 = (bool)o; // Unboxing!

Một minh họa mạnh mẽ về lợi thế của Boxing là kiểm tra null:

if (b == null) // Will not compile - bools can't be null
if (o == null) // Will compile and always return false

Đối tượng o của chúng tôi về mặt kỹ thuật là một địa chỉ trong Stack chỉ đến một bản sao của bool b của chúng tôi, đã được sao chép vào Heap. Chúng tôi có thể kiểm tra o cho null vì bool đã được đóng hộp và đặt ở đó.

Nói chung, bạn nên tránh Boxing trừ khi bạn cần nó, ví dụ để truyền int / bool / bất cứ thứ gì làm đối tượng cho một đối số. Có một số cấu trúc cơ bản trong .Net vẫn yêu cầu chuyển Loại giá trị làm đối tượng (và do đó yêu cầu Quyền anh), nhưng đối với hầu hết các phần bạn không bao giờ cần phải đóng hộp.

Một danh sách không đầy đủ các cấu trúc C # lịch sử yêu cầu Quyền anh, mà bạn nên tránh:

  • Hệ thống Sự kiện hóa ra có Điều kiện Cuộc đua khi sử dụng nó một cách ngây thơ và nó không hỗ trợ async. Thêm vào vấn đề Boxing và có lẽ nên tránh. (Bạn có thể thay thế nó bằng hệ thống sự kiện không đồng bộ sử dụng Generics.)

  • Các mô hình Threading và Timer cũ buộc Box trên các tham số của chúng nhưng đã được thay thế bằng async / await, nó sạch hơn và hiệu quả hơn.

  • Bộ sưu tập .Net 1.1 hoàn toàn dựa vào Quyền anh, vì chúng xuất hiện trước Generics. Chúng vẫn đang được sử dụng trong System.Collections. Trong bất kỳ mã mới nào, bạn nên sử dụng Bộ sưu tập từ System.Collections.Generic, ngoài việc tránh Boxing còn cung cấp cho bạn loại an toàn mạnh hơn .

Bạn nên tránh khai báo hoặc chuyển Loại giá trị của mình dưới dạng đối tượng, trừ khi bạn phải giải quyết các vấn đề lịch sử nêu trên buộc Boxing và bạn muốn tránh hiệu năng của Boxing sau này khi bạn biết rằng dù sao nó cũng sẽ bị đóng hộp.

Theo gợi ý của Mikael dưới đây:

Làm cái này

using System.Collections.Generic;

var employeeCount = 5;
var list = new List<int>(10);

Không phải cái này

using System.Collections;

Int32 employeeCount = 5;
var list = new ArrayList(10);

Cập nhật

Câu trả lời này ban đầu đề xuất Int32, Bool, vv gây ra quyền anh, trong khi thực tế chúng là bí danh đơn giản cho các loại giá trị. Đó là, .Net có các loại như Bool, Int32, String và C # bí danh chúng thành bool, int, chuỗi, mà không có bất kỳ sự khác biệt về chức năng.


4
Bạn đã dạy tôi điều mà hàng trăm lập trình viên và các chuyên gia CNTT không thể giải thích trong nhiều năm, nhưng thay đổi nó để nói những gì bạn nên làm thay vì những gì nên tránh, bởi vì nó hơi khó để tuân theo .. các quy tắc cơ bản thường không đi theo 1 bạn sẽ không làm điều này, thay vào đó hãy làm điều này
Mikael Puusaari

2
Câu trả lời này đã được đánh dấu là TRẢ LỜI hàng trăm lần!
Pouyan

3
không có "Int" trong c #, có int và Int32. Tôi tin rằng bạn đã sai khi nói một loại là loại giá trị và loại kia là loại tham chiếu bao bọc loại giá trị. trừ khi tôi nhầm, điều đó đúng trong Java, nhưng không phải là C #. Trong C #, những cái hiển thị màu xanh lam trong IDE là bí danh cho định nghĩa cấu trúc của chúng. Vậy: int = Int32, bool = Boolean, string = String. Lý do để sử dụng bool trên Boolean là vì nó được đề xuất như vậy trong các hướng dẫn và quy ước thiết kế MSDN. Nếu không, tôi thích câu trả lời này. Nhưng tôi sẽ bỏ phiếu cho đến khi bạn chứng minh tôi sai hoặc sửa nó trong câu trả lời của bạn.
Heriberto Lugo

2
Nếu bạn khai báo một biến là int và một biến khác là Int32, hoặc bool và Boolean - nhấp chuột phải và xem định nghĩa, bạn sẽ kết thúc trong cùng một định nghĩa cho một cấu trúc.
Heriberto Lugo

2
@HeribertoLugo là chính xác, dòng "Bạn nên tránh khai báo các loại giá trị của bạn là Bool thay vì bool" bị nhầm. Như OP chỉ ra, bạn nên tránh khai báo bool của bạn (hoặc Boolean hoặc bất kỳ loại giá trị nào khác) làm Object. bool / Boolean, int / Int32, chỉ là bí danh giữa C # và .NET: docs.microsoft.com/en-us/dotnet/csharp/lingu-reference/iêu
STW

21

Quyền anh không thực sự là thứ bạn sử dụng - đó là thứ mà thời gian chạy sử dụng để bạn có thể xử lý các loại tham chiếu và giá trị theo cùng một cách khi cần thiết. Ví dụ: nếu bạn đã sử dụng ArrayList để giữ danh sách các số nguyên, các số nguyên được đóng hộp để khớp với các khe loại đối tượng trong ArrayList.

Sử dụng các bộ sưu tập chung bây giờ, điều này khá nhiều đi. Nếu bạn tạo một List<int>, không có quyền anh được thực hiện - List<int>có thể giữ các số nguyên trực tiếp.


Bạn vẫn cần quyền anh cho những thứ như định dạng chuỗi tổng hợp. Bạn có thể không thấy nó thường xuyên khi sử dụng thuốc generic, nhưng nó chắc chắn vẫn còn đó.
Jeremy S

1
đúng - nó cũng xuất hiện mọi lúc trong ADO.NET - các giá trị tham số sql đều là 'đối tượng cho dù kiểu dữ liệu thực là gì
Ray

11

Quyền anh và Unboxing được sử dụng đặc biệt để coi các đối tượng loại giá trị là loại tham chiếu; chuyển giá trị thực của chúng sang heap được quản lý và truy cập giá trị của chúng bằng cách tham chiếu.

Không có quyền anh và unboxing, bạn không bao giờ có thể vượt qua các loại giá trị bằng cách tham chiếu; và điều đó có nghĩa là bạn không thể truyền các loại giá trị như các thể hiện của Object.


vẫn trả lời tốt đẹp sau gần 10 năm thưa ông +1
snr

1
Chuyển qua tham chiếu các kiểu số tồn tại trong các ngôn ngữ không có quyền anh và các ngôn ngữ khác triển khai xử lý các loại giá trị như các thể hiện của Object mà không có quyền anh và chuyển giá trị sang heap (ví dụ: triển khai các ngôn ngữ động trong đó các con trỏ được căn chỉnh theo ranh giới 4 byte sử dụng bốn giới hạn thấp hơn các bit tham chiếu để chỉ ra rằng giá trị là một số nguyên hoặc ký hiệu chứ không phải là một đối tượng đầy đủ; các loại giá trị đó là bất biến và có cùng kích thước với một con trỏ).
Pete Kirkham

8

Nơi cuối cùng tôi phải bỏ hộp một cái gì đó là khi viết một số mã lấy một số dữ liệu từ cơ sở dữ liệu (Tôi không sử dụng LINQ sang SQL , chỉ đơn giản là ADO.NET cũ ):

int myIntValue = (int)reader["MyIntValue"];

Về cơ bản, nếu bạn đang làm việc với các API cũ hơn trước tướng, bạn sẽ gặp quyền anh. Ngoài ra, nó không phải là phổ biến.


4

Quyền anh là bắt buộc, khi chúng ta có một hàm cần đối tượng làm tham số, nhưng chúng ta có các loại giá trị khác nhau cần được truyền, trong trường hợp đó trước tiên chúng ta cần chuyển đổi các loại giá trị thành các loại dữ liệu đối tượng trước khi chuyển nó sang hàm.

Tôi không nghĩ đó là sự thật, thay vào đó hãy thử điều này:

class Program
    {
        static void Main(string[] args)
        {
            int x = 4;
            test(x);
        }

        static void test(object o)
        {
            Console.WriteLine(o.ToString());
        }
    }

Điều đó rất tốt, tôi đã không sử dụng quyền anh / unboxing. (Trừ khi trình biên dịch làm điều đó đằng sau hậu trường?)


Đó là bởi vì mọi thứ thừa hưởng từ System.Object và bạn đang cung cấp cho phương thức một đối tượng có thêm thông tin, vì vậy về cơ bản, bạn đang gọi phương thức thử nghiệm với những gì nó đang mong đợi và bất cứ điều gì nó có thể mong đợi vì nó không mong đợi gì cả. Rất nhiều thứ trong .NET được thực hiện ở hậu trường và lý do tại sao nó là ngôn ngữ rất đơn giản để sử dụng
Mikael Puusaari

1

Trong .net, mọi phiên bản của Object hoặc bất kỳ loại nào có nguồn gốc từ đó, bao gồm một cấu trúc dữ liệu chứa thông tin về loại của nó. Các loại giá trị "thực" trong .net không chứa bất kỳ thông tin nào như vậy. Để cho phép dữ liệu trong các loại giá trị được thao tác bởi các thường trình sẽ nhận các loại có nguồn gốc từ đối tượng, hệ thống sẽ tự động xác định cho mỗi loại giá trị một loại lớp tương ứng có cùng các thành viên và trường. Quyền anh tạo ra một thể hiện mới của loại lớp này, sao chép các trường từ một thể hiện loại giá trị. Bỏ hộp sao chép các trường từ một thể hiện của loại lớp sang một thể hiện của loại giá trị. Tất cả các loại lớp được tạo từ các loại giá trị đều được lấy từ lớp ValueType có tên trớ trêu (mà, mặc dù tên của nó, thực sự là một loại tham chiếu).


0

Khi một phương thức chỉ lấy một kiểu tham chiếu làm tham số (giả sử một phương thức chung bị ràng buộc là một lớp thông qua newràng buộc), bạn sẽ không thể truyền một kiểu tham chiếu cho nó và phải đóng hộp nó.

Điều này cũng đúng với bất kỳ phương thức nào lấy objecttham số - đây sẽ phải là kiểu tham chiếu.


0

Nói chung, bạn thường muốn tránh đấm bốc các loại giá trị của mình.

Tuy nhiên, hiếm khi xảy ra trường hợp này hữu ích. Ví dụ: nếu bạn cần nhắm mục tiêu khung 1.1, bạn sẽ không có quyền truy cập vào các bộ sưu tập chung. Bất kỳ việc sử dụng các bộ sưu tập nào trong .NET 1.1 sẽ yêu cầu xử lý loại giá trị của bạn dưới dạng System.Object, điều này gây ra quyền anh / unboxing.

Vẫn có trường hợp này hữu ích trong .NET 2.0+. Bất cứ lúc nào bạn muốn tận dụng thực tế là tất cả các loại, bao gồm các loại giá trị, có thể được coi là một đối tượng trực tiếp, bạn có thể cần phải sử dụng quyền anh / unboxing. Điều này đôi khi có thể hữu ích, vì nó cho phép bạn lưu bất kỳ loại nào trong bộ sưu tập (bằng cách sử dụng đối tượng thay vì T trong bộ sưu tập chung), nhưng nói chung, tốt hơn là tránh điều này, vì bạn đang mất an toàn loại. Tuy nhiên, một trường hợp thường xuyên xảy ra với quyền anh là khi bạn đang sử dụng Reflection - nhiều cuộc gọi trong phản xạ sẽ yêu cầu quyền anh / bỏ hộp khi làm việc với các loại giá trị, vì loại này không được biết trước.


0

Quyền anh là việc chuyển đổi một giá trị thành một loại tham chiếu với dữ liệu ở một số phần bù trong một đối tượng trên heap.

Đối với những gì đấm bốc thực sự làm. Dưới đây là một số ví dụ

Mono 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_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;

Unboxing trong Mono là một quá trình truyền con trỏ ở phần bù của 2 con trỏ trong đối tượng (ví dụ 16 byte). A gpointerlà một void*. Điều này có ý nghĩa khi nhìn vào định nghĩa MonoObjectvì nó rõ ràng chỉ là một tiêu đề cho dữ liệu.

C ++

Để đóng một giá trị trong C ++, bạn có thể làm một cái gì đó như:

#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;
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.