Hãy Hạn Chế Dùng Boxing Và Unboxing Trong C#
Khi làm việc với C#, các bạn có lẽ đã quen thuộc với cấu trúc dữ liệu ArrayList, HashTable, điểm chung của các cấu trúc dữ liệu trên là đều lưu trữ dữ liệu dưới dạng object. Điều này thật dễ dàng khi bạn có thể lưu trữ bất kì kiểu dữ liệu nào(vì mọi kiểu dữ liệu đều kế thừa lớp System.Object). Tuy nhiên, vấn đề sẽ xảy ra khi bạn dùng các collection này để lưu trữ kiểu dữ liệu nguyên thủy(int, double,...) vì khi đó sẽ xảy ra quá trình boxing/unboxing. Vậy boxing/unboxing là gì và chúng ảnh hưởng như thế nào?
Boxing và Unboxing là gì?
Trước khi tìm hiểu boxing và unboxing thì cùng ôn lại một chúng về cách tổ chức dữ liệu trong C# nhé. Trong C# chúng ta có hai nhóm kiểu dữ liệu chính, đó là nhóm reference và nhóm value. Nhóm reference bao gồm các kiểu dữ liệu như class, interface và delegate, trong khi nhóm value bao gồm các kiểu định nghĩa là struct và enum. Cách tổ chức hai nhóm kiểu dữ liệu này vì thế cũng khác nhau. Giá trị của nhóm value được lưu trữ trong bộ nhớ stack còn nhóm reference được lưu trong bộ nhớ heap. Bạn cũng biết tất cả các kiểu dữ liệu dù là value hay reference thì hầu như đều kế thừa từ System.Object. Do đó, khi một collection lưu trữ dữ liệu dưới dạng object thì trên cơ bản nó có thể lưu trữ hầu hết mọi kiểu dữ liệu.
Boxing
Quá trình boxing xảy ra khi bạn truyền tham số kiểu value vào cho một tham số kiểu object. Khi đó giá trị của biến value sẽ được copy vào một object nằm trong heap. Boxing được hiểu nôm na như là đóng hộp và hộp ở đây là bộ nhớ heap. Biến value sau khi được copy vào heap thì sẽ được đóng hộp tại đây và đợi quá trình unboxing diễn ra.
Unboxing
Là quá trình ngược lại với boxing, quá trình unboxing sẽ lấy giá trị nằm trong box ra để sử dụng. Giá trị nằm trong box sẽ được copy lên bộ nhớ stack và sử dụng như một biến kiểu value bình thường.
Boxing/Unboxing thực chất là một quá trình trung gian để chuyển giá trị của biến value tới nơi cần sử dụng. Hình dung lại ví dụ trên một chút nhé. Bạn cần tạo ra một collection ArrayList lưu trữ các giá trị với kiểu int, lúc này các giá trị cần lưu trữ sẽ được copy giá trị vào một object nằm trên heap(boxing) và được lưu vào collection. Khi bạn cần truy xuất dữ liệu từ ArrayList, giá trị nằm trong heap sẽ được copy lên vùng nhớ stack và sử dụng(unboxing). Nhìn có vẻ đơn giản đúng không. Nhưng thật ra nếu bạn lưu dữ liệu không phải là int mà là kiểu struct thì quá trình đó sẽ phức tạp hơn và thực chất chúng ta cũng không nhìn thấy nó diễn ra như thế nào.
Làm sao để hạn chế Boxing/Unboxing
Để giải quyết vấn đề đó, từ phiên bản 2.0, C# đã tạo ra một thứ gọi là Generic. Để sử dụng Generic thì bạn cần sử dụng namespace System.Collections.Generic. Giả sử bạn cần lưu dữ liệu trong một danh sách động, bạn có thể sử dụng List<T> thay cho ArrayList như sau.
using System.Collections.Generic;
using System.Collections;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
// Khởi tạo một List lưu trữ các phần tử kiểu int
//List nằm trong namespace System.Collections.Generic
List<int> listInt = new List<int>();
// Khởi tạo một ArrayList để lưu trữ các phần tử kiểu int
// ArrayList nằm trong namespace System.Collections
ArrayList arrayList = new ArrayList();
}
}
}
Bạn có thể thấy khi mình sử dụng List thì phải khai báo thêm kiểu dữ liệu của các phần tử trong List(ở đây là int). Còn khi dùng ArrayList thì các bạn không cần cung cấp kiểu dữ liệu của phần tử trong đó vì tất cả sẽ được parse thành object trước khi lưu. Sau khi khởi tạo tôi sẽ add 100.000 phần tử kiểu int vào và đo performence xem có gì khác biệt nhé.
using System;
using System.Collections.Generic;
using System.Collections;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
List<int> listInt = new List<int>();
ArrayList arrListInt = new ArrayList();
// Bắt đầu đo thời gian khi add 100000 element vào List
var startTime1 = DateTime.Now;
for(int i = 1; i <= 100000; i++)
{
listInt.Add(i);
}
var time1 = DateTime.Now.Subtract(startTime1).Milliseconds;
Console.WriteLine("Time to add element into list is: " + time1);
// Bắt đầu đo thời gian khi add 100000 element vào ArrayList
var startTime2 = DateTime.Now;
for (int i = 1; i <= 100000; i++)
{
arrListInt.Add(i);
}
var time2 = DateTime.Now.Subtract(startTime2).Milliseconds;
Console.WriteLine("Time to add element array into list is: " + time2);
Console.ReadKey();
}
}
}
Chạy code trên bạn sẽ thấy thời gian khi sử dụng ArrayList để add 100.000 phần tử vào sẽ lâu hơn. Chưa nói đến những hệ thống cần lưu trữ dữ liệu lớn, ở đây chỉ mới dùng 100.000 phần tử kiểu int thì chúng ta đã thấy sự khác biệt rồi.
Kết luận
Boxing và Unboxing là một quá trình phức tạp và tốn thời gian. Vì vậy hãy tự mình chuyển đổi kiểu dữ liệu hoặc sử dụng Generic như mình vừa sử dụng ở trên