Đề tài Ứng dụng kỹ thuật tái cấu trúc mã nguồn để triển khai dò tìm và cải tiến các đoạn mã xấu trong chương trình c#

Trong qui trình phát triển phần mềm hiện nay, một thực tế đang tồn tại ở các công ty sản xuất phần mềm là các lập trình viên thường xem nhẹ việc tin h chỉnh mã nguồn và kiểm thử. Ngoài lý do đơn giản vì đó là một công việc nhàm chán, khó được chấp nhận đối với việc quản lý vì sự tốn kém và mất thời gian, còn một nguyên nhân khác là chúng ta không có những phương pháp và tiện ích tốt hỗ trợ cho những vi ệc này. Điều này dẫn đến việc phần lớn các phần mềm không được kiểm thử đầy đủ và phát hành với các nguy cơ lỗi tiềm ẩn.

pdf99 trang | Chia sẻ: vietpd | Lượt xem: 1572 | Lượt tải: 0download
Bạn đang xem trước 20 trang tài liệu Đề tài Ứng dụng kỹ thuật tái cấu trúc mã nguồn để triển khai dò tìm và cải tiến các đoạn mã xấu trong chương trình c#, để xem tài liệu hoàn chỉnh bạn click vào nút DOWNLOAD ở trên
BỘ GIÁO DỤC VÀ ĐÀO TẠO ĐẠI HỌC ĐÀ NẴNG          BÁO CÁO LUẬN VĂN THẠC SĨ KỸ THUẬT NGÀNH KHOA HỌC MÁY TÍNH TÊN ĐỀ TÀI: ỨNG DỤNG KỸ THUẬT TÁI CẤU TRÚC MÃ NGUỒN ĐỂ TRIỂN KHAI DÒ TÌM VÀ CẢI TIẾN CÁC ĐOẠN MÃ XẤU TRONG CHƢƠNG TRÌNH C# Họ tên HV : NHIÊU LẬP HÒA Họ tên CBHD : TS.NGUYỄN THANH BÌNH ĐÀ NẴNG, 11/2008 Luận văn tốt nghiệp cao học – Khóa 2005 - 2008 Học viên thực hiện: Nhiêu Lập Hòa 2 LỜI CAM ĐOAN Tôi xin cam đoan nội dung luận văn "Ứng dụng kỹ thuật tái cấu trúc mã nguồn để triển khai dò tìm và cải tiến các đọan mã xấu trong chƣơng trình C# ", dƣới sự hƣớng dẫn của TS. Nguyễn Thanh Bình, là công trình do tôi trực tiếp nghiên cứu. Tôi xin cam đoan các số liệu, kết quả nghiên cứu trong luận văn là trung thực và chƣa từng đƣợc công bố trong bất cứ công trình nào trƣớc đây. Tác giả Nhiêu Lập Hòa Luận văn tốt nghiệp cao học – Khóa 2005 - 2008 Học viên thực hiện: Nhiêu Lập Hòa 3 MỤC LỤC LỜI CAM ĐOAN ................................................................................................................ 2 MỤC LỤC ........................................................................................................................... 3 DANH MỤC HÌNH ẢNH ................................................................................................... 5 MỞ ĐẦU ............................................................................................................................. 6 CHƢƠNG I: KỸ THUẬT TÁI CẤU TRÚC MÃ NGUỒN (REFACTORING) .............. 7 I.1 ĐỊNH NGHĨA KỸ THUẬT TÁI CẤU TRÚC MÃ NGUỒN ................................ 7 I.1.1 Ví dụ minh họa................................................................................................. 7 I.1.2 Định nghĩa kỹ thuật tái cấu trúc mã nguồn ................................................... 19 I.2 HIỆU QUẢ CỦA TÁI CẤU TRÚC MÃ NGUỒN ................................................ 20 I.2.1 Refactoring cải thiện thiết kế phần mềm ....................................................... 20 I.2.2 Refactoring làm mã nguồn phần mềm dễ hiểu .............................................. 20 I.2.3 Refactoring giúp phát hiện và hạn chế lỗi ..................................................... 21 I.2.4 Refactoring giúp đấy nhanh quá trình phát triển phần mềm ......................... 21 I.3 KHI NÀO THỰC HIỆN TÁI CẤU TRÚC MÃ NGUỒN ..................................... 22 I.3.1 Refactor khi thêm chức năng ......................................................................... 22 I.3.2 Refactor khi cần sửa lỗi ................................................................................ 22 I.3.3 Refactor khi thực hiện duyệt chƣơng trình ................................................... 23 I.4 CÁC KỸ THUẬT TÁI CẤU TRÚC MÃ NGUỒN .............................................. 23 I.4.1 Danh mục các kỹ thuật tái cấu trúc mã nguồn ............................................... 23 I.5 NHẬN XÉT VÀ KẾT LUẬN ................................................................................ 26 CHƢƠNG II: LỖI CẤU TRÚC (BAD SMELLS) TRONG MÃ NGUỒN ................... 27 II.1 KHÁI NIỆM VỀ LỖI CẤU TRÚC (BAD SMELLS) ........................................ 27 II.2 LỖI CẤU TRÚC VÀ GIẢI PHÁP CẢI TIẾN ..................................................... 27 II.2.1 Duplicated Code - Trùng lặp mã ................................................................. 27 II.2.2 Long Method – Phƣơng thức phức tạp ......................................................... 28 II.2.3 Large Class – Qui mô lớp lớn ...................................................................... 30 II.2.4 Long Parameter List - Danh sách tham số quá dài ....................................... 31 II.2.5 Divergent Change – Cấu trúc lớp ít có tính khả biến .................................. 32 II.2.6 Shotgun Surgery – Lớp đƣợc thiết kế không hợp lý và bị phân rã ............ 32 II.2.7 Feature Envy – Phân bố phƣơng thức giữa các lớp không hợp lý .............. 33 II.2.8 Data Clumps – Gôm cụm dữ liệu ................................................................ 34 II.2.9 Primitive Obsession – Khả năng thể hiện dữ liệu của lớp bị hạn chế ......... 34 II.2.10 Switch Statements – Khối lệnh điều kiện rẽ hƣớng không hợp lý ........... 36 II.2.11 Lazy Class – Lớp đƣợc định nghĩa không cần thiết .................................. 38 II.2.12 Speculative Generality – Cấu trúc bị thiết kế dƣ thừa ............................... 38 II.2.13 Temporary Field – Lạm dụng thuộc tính tạm thời .................................... 39 II.2.14 Message Chains –Chuỗi phƣơng thức liên hoàn khó kiểm soát............... 39 II.2.15 Middle Man – Quan hệ ủy quyền không hợp lý/logic ............................... 39 II.2.16 Inapproprite Intimacy - Cấu trúc thành phần riêng không hợp lý ............ 41 II.2.17 Alternative Classes with Different Interfaces - Đặc tả lớp không rõ ràng 41 II.2.18 Incomplete Library Class – Sử dụng thƣ viện lớp chƣa đƣợc hòan chỉnh 41 II.2.19 Data Class – Lớp dữ liệu độc lập ............................................................. 42 II.2.20 Refused Bequest – Quan hệ kế thừa không hợp lý/logic ......................... 43 Luận văn tốt nghiệp cao học – Khóa 2005 - 2008 Học viên thực hiện: Nhiêu Lập Hòa 4 II.2.21 Comments – Chú thích không cần thiết .................................................... 43 II.3 NHẬN XÉT VÀ KẾT LUẬN .............................................................................. 44 CHƢƠNG III: NỀN TẢNG .NET VÀ NGÔN NGỮ LẬP TRÌNH C# ............................ 45 III.1 TỔNG QUAN VỀ NỀN TẢNG .NET .............................................................. 45 III.1.1 Định nghĩa .NET ........................................................................................ 45 III.1.2 Mục tiêu của .NET ..................................................................................... 45 III.1.3 Dịch vụ của .NET ....................................................................................... 45 III.1.4 Kiến trúc của .NET .................................................................................... 46 III.2 NGÔN NGỮ LẬP TRÌNH C# .......................................................................... 47 III.2.1 Tổng quan về ngôn ngữ lập trình C# ......................................................... 47 III.2.2 Đặc trƣng của các ngôn ngữ lập trình C# ................................................... 47 III.3 MÔI TRƢỜNG PHÁT TRIỂN ỨNG DỤNG VISUAL STUDIO .NET .......... 48 CHƢƠNG IV: ỨNG DỤNG KỸ THUẬT TÁI CẤU TRÚC MÃ NGUỒN ĐỂ DÒ TÌM VÀ CẢI TIẾN CÁC ĐOẠN MÃ XẤU TRONG CHƢƠNG TRÌNH C# ......... 49 IV.1 GIẢI PHÁP VÀ CÔNG CỤ HỖ TRỢ REFACTOR .......................................... 49 IV.1.1 Đặc tả giải pháp triển khai ......................................................................... 49 IV.1.2 Một số công cụ và tiện ích hỗ trợ việc dò tìm và cải tiến mã xấu ............. 50 IV.1.3 Thử nghiệm minh họa các công cụ hỗ trợ refactor trong VS.Net .............. 57 IV.1.4 Nhận xét và đánh giá .................................................................................. 80 IV.2 ỨNG DỤNG KỸ THUẬT TÁI CẤU TRÚC MÃ NGUỒN ĐỂ DÒ TÌM VÀ CẢI TIẾN CÁC ĐOẠN MÃ XẤU TRONG CHƢƠNG TRÌNH C#.......................... 81 IV.2.1 Thực hiện kỹ thuật tái cấu trúc mã nguồn trên chƣơng trình thực tế ......... 82 IV.2.2 Phân tích và đánh giá kết quả thực hiện .................................................... 94 IV.3 NHẬN XÉT VÀ KẾT LUẬN ............................................................................ 95 CHƢƠNG V: KẾT LUẬN ............................................................... 96 V.1 ĐÁNH GIÁ KẾT QUẢ CỦA ĐỀ TÀI ............................................................... 96 V.2 PHẠM VI ỨNG DỤNG .................................................................................... 96 V.3 HƢỚNG PHÁT TRIỂN ...................................................................................... 97 V.3.1 Triển khai áp dụng trên các ngôn ngữ khác ................................................ 97 V.3.2 Thử nghiệm xây dựng một refactoring tool tích hợp vào VS.NET ........... 97 TÀI LIỆU THAM KHẢO ................................................................................................. 98 Luận văn tốt nghiệp cao học – Khóa 2005 - 2008 Học viên thực hiện: Nhiêu Lập Hòa 5 DANH MỤC HÌNH ẢNH Tên hình ảnh Trang H.3.1: Kiến trúc nền tảng .NET 46 H.3.2: Môi trường phát triển ứng dụng VS.NET 48 H.4.1: Đặc tả kịch bản giải pháp triển khai 49 H.4.2: Trình chức năng refactor tích hợp trong VS.NET 50 H.4.3: Trình chức năng refactor của Visual Assit X for VS.NET 51 H.4.4: Trình chức năng refactor của C# Refactory for VS.NET 52 H.4.5: Trình chức năng refactor của .NET Refactor for .NET 53 H.4.6: Trình chức năng refactor của CodeIT.Once for .NET 54 H.4.7: Trình chức năng refactor của JetBrances ReShape 55 H.4.8: Trình chức năng refactor của DevExpress Refactor!™ Pro 56 H.4.9: Minh họa kỹ thuật Change Signature trong JetBrains ReSharper 58 H.4.10: Kết quả minh họa kỹ thuật Change Signature 58 H.4.11: Minh họa kỹ thuật Convert Method to Property của CodeIT.Once 60 H.4.12: Minh họa kỹ thuật Convert Method to Property của ReSharper 61 H.4.13: Kết quả kỹ thuật Convert Method to Property 61 H.4.14: Minh họa kỹ thuật Decompose/Simplify Conditional 63 H.4.15: Kết quả kỹ thuật Decompose/Simplify Conditional 63 H.4.16: Minh họa kỹ thuật Encapsulate Field của Refactor trong VS.NET 65 H.4.17: Minh họa kỹ thuật Encapsulate Field của Visual Assit X for .NET 66 H.4.18: Kết quả kỹ thuật Encapsulate Field 66 H.4.19: Minh họa kỹ thuật Extract Interface của Refactor trong VS.NET 68 H.4.20: Minh họa kỹ thuật Extract Interface của CodeIT.Once 69 H.4.21: Kết quả kỹ thuật Extract Interface 69 H.4.22: Minh họa kỹ thuật Extract Method của Refactor trong VS.NET 71 H.4.23: Kết quả kỹ thuật Extract Method 71 H.4.24: Minh họa kỹ thuật Inline Variable của CodeIT.Once for .NET 73 H.4.25: Kết quả kỹ thuật Inline Variable 73 H.4.26: Minh họa kỹ thuật Promote Local Variable to Parameter của VS.NET 75 H.4.27: Minh họa kỹ thuật Promote Local Variable to Parameter của CodeIT.Once 75 H.4.28: Minh họa kỹ thuật Promote Local Variable to Parameter của ReSharper 76 H.4.29: Kết quả kỹ thuật Promote Local Variable to Parameter 76 H.4.30: Minh họa kỹ thuật Rename Variables của Refactor trong VS.NET 78 H.4.31: Minh họa kỹ thuật Rename Variables của Visual Assit X 79 H.4.32: Kết quả kỹ thuật Rename Variables 79 H.4.33: Sơ đồ lớp của chương trình khi chưa refactoring 82 H.4.34: Màn hình kết quả chạy chương trình khi chưa refactoring 84 H.4.35: Sơ đồ lớp của chương trình sau khi refactoring 91 H.4.36: Màn hình kết quả chạy chương trình sau khi refactoring 93 Luận văn tốt nghiệp cao học – Khóa 2005 - 2008 Học viên thực hiện: Nhiêu Lập Hòa 6 MỞ ĐẦU Trong qui trình phát triển phần mềm hiện nay, một thực tế đang tồn tại ở các công ty sản xuất phần mềm là các lập trình viên thƣờng xem nhẹ việc tinh chỉnh mã nguồn và kiểm thử. Ngoài lý do đơn giản vì đó là một công việc nhàm chán, khó đƣợc chấp nhận đối với việc quản lý vì sự tốn kém và mất thời gian, còn một nguyên nhân khác là chúng ta không có những phƣơng pháp và tiện ích tốt hỗ trợ cho những việc này. Điều này dẫn đến việc phần lớn các phần mềm không đƣợc kiểm thử đầy đủ và phát hành với các nguy cơ lỗi tiềm ẩn. Phƣơng thức phát triển phần mềm linh hoạt[15] bắt đầu xuất hiện vào đầu những năm 90 với mục tiêu là phần mềm phải có khả năng biến đổi, phát triển và tiến hóa theo thời gian mà không cần phải làm lại từ đầu. Phƣơng thức này đƣợc thực hiện dựa trên hai kỹ thuật chính là tái cấu trúc mã nguồn (refactoring) và kiểm thử (developer testing). Vì thế việc nghiên cứu và ứng dụng kỹ thuật tái cấu trúc mã nguồn nhằm tối ƣu hóa mã nguồn và nâng cao hiệu quả kiểm thử là một nhu cầu cần thiết trong quá trình thực hiện và phát triển phần mềm. Đề tài “Ứng dụng kỹ thuật tái cấu trúc mã nguồn để triển khai dò tìm và cải tiến các đoạn mã xấu trong chƣơng trình C#” đƣợc thực hiện với mục đích nghiên cứu cơ sở lý thuyết kỹ thuật tái cấu trúc mã nguồn và áp dụng để triển khai việc dò tìm và cải tiến mã xấu (lỗi cấu trúc) trong các chƣơng trình hiện đại và phổ biến hiện nay (C#). Toàn bộ nội dung của luận văn bao gồm các chƣơng: Chƣơng 1: Kỹ thuật tái cấu trúc mã nguồn (refectoring) Chƣơng 2: Mã xấu (bad smells) và giải pháp cải tiến dựa trên refactoring Chƣơng 3: Nền tảng .NET và ngôn ngữ lập trình C# Chƣơng 4: Ứng dụng kỹ thuật tái cấu trúc mã nguồn để dò tìm và cải thiện mã xấu trong các chƣơng trình C# Chƣơng 5: Kết luận Luận văn tốt nghiệp cao học – Khóa 2005 - 2008 Học viên thực hiện: Nhiêu Lập Hòa 7 CHƢƠNG I: KỸ THUẬT TÁI CẤU TRÚC MÃ NGUỒN (REFACTORING) I.1 ĐỊNH NGHĨA KỸ THUẬT TÁI CẤU TRÚC MÃ NGUỒN I.1.1 Ví dụ minh họa Phƣơng thức tiếp cận và tìm hiểu hiệu quả nhất với một khái niệm hay một kỹ thuật mới trong tin học là thông qua các ví dụ minh họa [11]. Với ví dụ dƣới đây, chúng ta sẽ hiểu refactoring là gì cũng nhƣ cách thực hiện và hiệu quả của nó trong qui trình công nghệ phát triển phần mềm. Bài toán ví dụ: Chương trình trả lại kết quả danh sách các số nguyên tố. (Bài toán này sử dụng thuật toán Eratosthenes) Nội dung thuật toán Eratosthenes: - Viết một danh sách các số từ 2 tới maxNumbers mà ta cần tìm. Gọi là list A. - Viết số 2, số nguyên tố đầu tiên, vào một list kết quả. Gọi là list B. - Xóa bỏ 2 và bội của 2 khỏi list A. - Số đầu tiên còn lại trong list A là số nguyên tố. Viết số này sang list B. - Xóa bỏ số đó và tất cả bội của nó khỏi list A. - Lặp lại các bƣớc 4 and 5 cho tới khi không còn số nào trong list A. Chƣơng trình khởi đầu: public class PrimeNumbersGetter { private int maxNumber; public PrimeNumbersGetter(int maxNumber){ this.maxNumber = maxNumber; } public int[] GetPrimeNumbers() { // Use Eratosthenes's sieve bool[] numbers = new bool[maxNumber + 1]; for (int i = 0; i < numbers.Length; ++i){ umbers[i] = true; } int j = 2; while (j <= (int)Math.Sqrt(maxNumber) + 1) { for (int k = j + j; k <= maxNumber; k += j){ numbers[k] = false; } j++; while (!numbers[j]) { j++; if (j > maxNumber) break; } } } Luận văn tốt nghiệp cao học – Khóa 2005 - 2008 Học viên thực hiện: Nhiêu Lập Hòa 8 List l = new List(); for (int k = 2; k <= maxNumber; ++k) { if (numbers[k]) l.Add(k); } return l.ToArray(); } Trƣớc khi refactoring, chúng ta cần viết kiểm thử cho phần mã nguồn đó. Phần kiểm thử này là yếu tố cần thiết bởi vì quá trình refactoring có thể phát sinh lỗi. Mỗi khi chúng ta thực hiện một lần refactoring, chúng ta nên thực hiện kiểm thử lại chƣơng trình một lần, để đảm bảo chƣơng trình không bị lỗi. Kiểm thử tốt làm giảm thời gian cần thiết để tìm lỗi. Chúng ta nên thực hiện xen kẽ việc kiểm thử và refactoring để mỗi khi có lỗi phát sinh, thì cũng không quá khó để tìm ra lỗi đó. Trong ví dụ này, chúng có thể thêm đoạn mã kiểm thử nhƣ sau: public class Program { public static void Main(string[] args) { if (!IsEqualNumbers(new PrimeNumbersGetter(4).GetPrimeNumbers(), new int[] { 2, 3 })) return; if (!IsEqualNumbers(new PrimeNumbersGetter(5).GetPrimeNumbers(), new int[] { 2, 3, 5 })) return; if (!IsEqualNumbers(new PrimeNumbersGetter(6).GetPrimeNumbers(), new int[] { 2, 3, 5 })) return; if (!IsEqualNumbers(new PrimeNumbersGetter(100).GetPrimeNumbers(), new int[] { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43,47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97 })) return; Console.WriteLine("Success!"); } private static bool IsEqualNumbers(int[] numbers1, int[] numbers2){ if (numbers1.Length != numbers2.Length) return false; for (int i = 0; i < numbers1.Length; ++i) { if (numbers1[i] != numbers2[i]) return false; } } return true; } Ta nhận thấy rằng phƣơng thức này quá dài, và nó xử lý rất nhiều công việc khác nhau. Trong trƣờng hợp này, nên sử dụng kĩ thuật “Extract Method” trong các kĩ thuật refactoring nhằm tạo ra các phƣơng thức nhỏ hơn, dễ đọc và dễ bảo trì khi có yêu cầu thay đổi chƣơng trình. Với đoạn mã nguồn khởi tạo list các số ban đầu ( list A ): Luận văn tốt nghiệp cao học – Khóa 2005 - 2008 Học viên thực hiện: Nhiêu Lập Hòa 9 bool[] numbers = new bool[maxNumber + 1]; for (int i = 0; i < numbers.Length; ++i){ numbers[i] = true; } Ta nên trích xuất nó thành một phƣơng thức khác, sử dụng “Extract Method”. public int[] GetPrimeNumbers() { bool[] numbers = InitialNumbers(); // Other codes. } private bool[] InitialNumbers(){ bool[] numbers = new bool[maxNumber + 1]; for (int i = 0; i < numbers.Length; ++i){ numbers[i] = true; } return numbers; } Sau khi thực hiện việc refactoring nhƣ trên, chúng ta nên nhớ rằng phải thực hiện chạy lại chƣơng trình kiểm thử để đảm báo rằng việc refactoring không làm thay đổi tính đúng đắn của chƣơng trình. Tƣơng tự với đoạn mã nguồn thực hiện xuất ra danh sách kết quả chứa các số nguyên tố (list B) List l = new List(); for (int k = 2; k <= maxNumber; ++k) { if (numbers[k]) l.Add(k); } return l.ToArray(); Ta cũng tách nó ra thành một phƣơng thức khác public int[] GetPrimeNumbers() { // Other codes. return GetPrimeNumbersArray(numbers); } private int[] GetPrimeNumbersArray(bool[] numbers) { List l = new List(); for (int k = 2; k <= maxNumber; ++k) { if (numbers[k]) l.Add(k); } return l.ToArray(); } Luận văn tốt nghiệp cao học – Khóa 2005 - 2008 Học viên thực hiện: Nhiêu Lập Hòa 10 Bây giờ chúng ta sẽ tinh chỉnh ở phần mã nguồn còn lại, đó là vòng lặp while int j = 2; while (j <= (int)Math.Sqrt(maxNumber) + 1){ for (int k = j + j; k <= maxNumber; k += j){ numbers[k] = false; } j++; while (!numbers[j]){ j++; if (j > maxNumber) break; } } Với đoạn mã nguồn trên, câu lệnh if là không cần thiết, ta có thể bỏ đi, đƣa điều kiện lên vòng while nhƣ sau: int j = 2; while (j <= (int)Math.Sqrt(maxNumber) + 1) { for (int k = j + j; k <= maxNumber; k += j) { numbers[k] = false; } j++; while (!numbers[j] && j < maxNumber) { j++; } } Ta thấy rằng, câu lệnh điều khiển trong vòng for không “đẹp” và khó đọc, ta nên sửa đổi tên biến k thành i. int j = 2; while (j <= (int)Math.Sqrt(maxNumber) + 1) { for (int i = 2; i * j <= maxNumber; i++){ numbers[i * j] = false; } j++; while (!numbers[j] && j < maxNumber) { j++; } } Với vòng while ở trên, mục đích chỉ là duyệt danh sách các phần tử trong list A, nên ta có thể chuyển sang sử dụng vòng lặp for nhƣ sau: for (int j = 2; j <= (int)Math.Sqrt(maxNumber) + 1; j++) { if (!numbers[j]) continue; for (int i = 2; i * j <= maxNumber; i++) { numbers[i * j] = false; } } Luận văn tốt nghiệp cao học – Khóa 2005 - 2008 Học viên thực hiện: Nhiêu Lập Hòa 11 Với đoạn mã nguồn for (int i = 2; i * j <= maxNumber; i++) { numbers[i * j] = false; } Thực hiện việc xóa bỏ các bội số của các số nguyên tố. Do đó, có thể tách chúng ra thành một phƣơng thức. Kết quả thu đƣợc là: public int[] GetPrimeNumbers() { bool[] numbers = InitialNumbers(); for (int j = 2; j <= (int)Math.Sqrt(maxNumber) + 1; j++) { if (!numbers[j]) continue; RemoveMultiple(numbers, j); } return GetPrimeNumbersArray(numbers); } private void RemoveMultiple(bool[] numbers, int j) { for (int i = 2; i * j <= maxNumber; i++) { numbers[i * j] = false; } } Tiếp tục với đoạn mã nguồn private void RemoveMultiple(bool[] numbers, int j) { for (int i = 2; i * j <= maxNumber; i++) { numbers[i * j] = false; } } Ta
Tài liệu liên quan