Hạn chế sử dụng boxing/unboxing – C# best practices

0

Khi làm việc với C#, bạn hẳn đã thường xuyên gặp các phương thức yêu cầu kiểu của dữ liệu đầu vào là object (hoặc System.Object). Ví dụ, các phương thức như string.Format (định dạng chuỗi ký tự), Console.Write/WriteLine (in kết quả ra console), v.v.. Bạn cũng gặp các collection class như ArrayList (mảng động) mà mỗi phần tử của nó thuộc kiểu object. Các phương thứcclass như vậy có rất nhiều trong C# .NET.

Khi sử dụng các phương thức và class như thế, bạn dễ dàng cung cấp giá trị thuộc bất kỳ kiểu nào. Thật đơn giản và tiện lợi.

Tuy nhiên, mọi thứ không hoàn toàn tốt đẹp như bạn nghĩ.

Khi truyền một giá trị thuộc kiểu value type trong những trường hợp tương tự, C# phải thực hiện boxing và unboxing để có thể sử dụng giá trị đó. Boxing/unboxing có thể gây ra những vấn đề bạn không ngờ tới.

Trong bài viết này chúng ta sẽ trao đổi một số kinh nghiệm liên quan đến boxing/unboxing và giải pháp cho nó.

Boxing và unboxing là gì?

Khi học lập trình C# hẳn bạn đã biết C# có hai nhóm kiểu dữ liệu: nhóm reference và nhóm value. Nhóm reference bao gồm các kiểu được định nghĩa là class, interface, delegate. Nhóm value bao gồm các kiểu định nghĩa là enum struct.

Hai nhóm kiểu dữ liệu này rất khác biệt nhau về cách nó được tổ chức trong bộ nhớ cũng như cách truyền làm tham số. Giá trị của các kiểu trong nhóm value được lưu trong stack, còn của nhóm reference được lưu trong heap. Khi truyền làm tham số, các giá trị của nhóm value được copy sang nơi sử dụng, các giá trị của nhóm reference chỉ truyền địa chỉ của nó tới nơi sử dụng.

Bạn cũng biết, System.Object (hay object trong C#) là kiểu dữ liệu đứng ở đỉnh của chuỗi kế thừa và nó cũng là một kiểu reference. Hầu như tất cả các kiểu dữ liệu trong C# (dù là reference hay value) đều là kiểu con kế thừa từ object.

Như vậy, nếu nhìn từ khía cạnh “hướng đối tượng” với cơ chế kế thừa và đa hình thì biến của kiểu con nào cũng được xem là biến của kiểu object. Và do đó, ở bất kỳ phương thức nào cần tham số kiểu object thì có thể truyền biến thuộc mọi kiểu giá trị.

Tuy nhiên, do đặc điểm khác nhau về tổ chức trong bộ nhớ, biến thuộc kiểu value không truyền được cho tham số kiểu object một cách đơn giản như vậy.

Cơ chế boxing và unboxing được sử dụng làm cầu nối giữa hai loại kiểu này.

Boxing

Khi bạn truyền một biến thuộc kiểu value vào cho tham số kiểu object sẽ diễn ra quá trình boxing.

Giá trị của biến sẽ được copy vào một object nằm trong heap để biến giá trị đó thành kiểu reference.

Để dễ hình dung, hãy tưởng tượng giờ trong heap tạo ra một cái hộp. Giá trị của biến value sẽ được copy (từ stack) đặt vào trong cái hộp này (trong heap). Cái hộp này giờ hoàn toàn tương tự như một biến reference bình thường.

Unboxing

Ở chiều ngược lại, tại nơi sử dụng, giá trị copy nằm trong box sẽ được lấy ra để sử dụng. Việc lấy ra này thực chất là tạo một bản sao của giá trị đó trong stack để nó được sử dụng như một biến value bình thường. Quá trình ngược này được gọi là unboxing.

Như vậy có thể thấy, boxing và unboxing có thể xem như một quá trình trung gian để trung chuyển giá trị của biến value type tới nơi cần sử dụng. Trong quá trình này, giá trị của biến value hai lần được sao chép.

Vấn đề nằm ở chỗ, việc thực hiện quá trình này không hề đơn giản, nhất là khi có số lượng lớn biến cần boxing/unboxing. Với những giá trị đơn giản (như int, bool, v.v.) thì còn đỡ. Đối với những struct phức tạp hơn (ví dụ DateTime), quá trình copy mất nhiều thời gian để xử lý.

Tuy nhiên, do quá trình này thực hiện hoàn toàn tự động, bạn thậm chí còn không để ý rằng nó đang diễn ra nữa.

Do vậy, nếu có thể, hãy hạn chế tối đa thực hiện boxing/unboxing.

Hạn chế boxing/unboxing

Trước hết bạn cần hiểu vì sao lại phải sử dụng kiểu object làm tham số, và đây là nguồn gốc của boxing/unboxing.

Do đặc thù của C# là định kiểu mạnh, một tham số đòi hỏi giá trị truyền vào phải phù hợp. Vì thế, nếu muốn một tham số có thể nhận giá trị thuộc nhiều kiểu khác nhau, người ta buộc phải để kiểu của tham số là object.

Ví dụ, phương thức Format của string cần nhận giá trị thuộc mọi kiểu dữ liệu để tạo xâu, do đó nó chấp nhận một mảng các object làm tham số. Lớp collection ArrayList, vì để có thể chứa được mọi loại giá trị, nó phải để kiểu của phần tử là object.

Ở phiên bản đầu tiên của C# chưa xuất hiện khái niệm generic. Kiểu object gần như là giải pháp duy nhất để một tham số có thể nhận giá trị thuộc nhiều kiểu khác nhau. Từ C# 2, bạn nên sử dụng generic nếu class/phương thức/interface muốn làm việc với nhiều kiểu dữ liệu khác nhau.

Như vậy, tùy tình huống, có một số giải pháp khác nhau để hạn chế sử dụng boxing/unboxing.

Gọi ToString() trước khi định dạng xâu

Đối với các phương thức liên quan đến định dạng chuỗi (như string.Format, Console.Write/WriteLine, v.v.), bạn có thể tự mình xây dựng và gọi phương thức ToString(). Việc này giúp bạn biến giá trị của biến thành chuỗi ký tự tương ứng trước. Vì string là kiểu reference, sẽ không xảy ra boxing khi tạo xâu mới.

Ví dụ:

int firstNumber = 1, secondNumber = 2, thirdNumber = 3;
// Xảy ra boxing với ba biến trên
Console.WriteLine($"A few numbers: {firstNumber}, {secondNumber}, {thirdNumber}");
// Sẽ không xảy ra boxing
Console.WriteLine($@"A few numbers: {firstNumber.ToString()}, {secondNumber.ToString()}, {thirdNumber.ToString()}");

Sử dụng generic collection

Giả sử bạn cần lưu dữ liệu trong một danh sách động.

Đừng sử dụng các class cũ như ArrayList hoặc bất kỳ loại danh sách nào mà phần tử thuộc loại object (System.Object). Hãy tìm hiểu các generic collection tương tự.

Từ C# 2.0 bạn có cả một loạt generic collection như List, SortedList, Queue, Stack, Dictionary. Hãy sử dụng các class này.

Hầu như mọi collection xây dựng về sau này đều là generic (như BindingList, ObservableCollection).

Sử dụng generic nếu cần hỗ trợ nhiều kiểu dữ liệu

Khi bạn tự xây dựng class, phương thức, interface, v.v., và bạn muốn nó hỗ trợ nhiều kiểu dữ liệu khác nhau. Thay vì sử dụng tham số kiểu object, hãy sử dụng lập trình generic.

Generic có thể áp dụng cho bất kỳ thành phần nào có liên quan đến “kiểu dữ liệu”. Generic giúp “tham số hóa” kiểu dữ liệu. Nói cách khác, với generic, kiểu dữ liệu giờ cũng có thể trở thành một loại tham số.

Do đó bạn cũng chỉ cần viết code một lần và sử dụng nó cho nhiều kiểu dữ liệu khác nhau. Hiệu quả của nó tương tự như khi sử dụng kiểu object (về hình thức) nhưng không xảy ra quá trình boxing/unboxing phức tạp như trên.

Kết luận

Trong bài viết này chúng ta đã trao đổi về những gì diễn ra khi truyền một biến thuộc kiểu value cho tham số kiểu object. C# sẽ tự động thực hiện boxing/unboxing để hỗ trợ truyền tham số này. Do boxing/unboxing là một quá trình phức tạp, bạn nên hạn chế tối đa việc sử dụng boxing/unboxing. Nếu có thể, hãy tự mình thực hiện chuyển đổi dữ liệu hoặc sử dụng generic để thay thế.

+ Nếu bạn thấy site hữu ích, trước khi rời đi hãy giúp đỡ site bằng một hành động nhỏ để site có thể phát triển và phục vụ bạn tốt hơn.
+ Nếu bạn thấy bài viết hữu ích, hãy giúp chia sẻ tới mọi người.
+ Nếu có thắc mắc hoặc cần trao đổi thêm, mời bạn viết trong phần thảo luận cuối trang.
Cảm ơn bạn!

Theo dõi
Thông báo của
guest

0 Thảo luận
Phản hồi nội tuyến
Xem tất cả bình luận