Tại sao một vòng lặp while (true) trong một constructor thực sự xấu?


47

Mặc dù một câu hỏi chung phạm vi của tôi là C # vì tôi biết rằng các ngôn ngữ như C ++ có ngữ nghĩa khác nhau về thực thi hàm tạo, quản lý bộ nhớ, hành vi không xác định, v.v.

Ai đó hỏi tôi một câu hỏi thú vị mà tôi không dễ trả lời.

Tại sao (hoặc hoàn toàn không?) Được coi là thiết kế tồi để cho phép một nhà xây dựng của một lớp bắt đầu một vòng lặp không bao giờ kết thúc (tức là vòng lặp trò chơi)?

Có một số khái niệm bị phá vỡ bởi điều này:

  • Giống như nguyên tắc ít ngạc nhiên nhất, người dùng không mong đợi nhà xây dựng hành xử như thế này.
  • Các bài kiểm tra đơn vị khó hơn vì bạn không thể tạo lớp này hoặc tiêm nó vì nó không bao giờ thoát khỏi vòng lặp.
  • Kết thúc vòng lặp (kết thúc trò chơi) sau đó về mặt khái niệm là thời gian mà nhà xây dựng kết thúc, đó cũng là số lẻ.
  • Về mặt kỹ thuật, một lớp như vậy không có thành viên nào ngoại trừ hàm tạo, điều này làm cho nó khó hiểu hơn (đặc biệt là đối với các ngôn ngữ không có triển khai)

Và sau đó là các vấn đề kỹ thuật:

  • Các constructor thực sự không bao giờ kết thúc, vậy điều gì xảy ra với GC ở đây? Có phải đối tượng này đã ở Gen 0?
  • Xuất phát từ một lớp như vậy là không thể hoặc ít nhất là rất phức tạp do thực tế là hàm tạo cơ sở không bao giờ trả về

Có một cái gì đó rõ ràng là xấu hoặc sai lệch với cách tiếp cận như vậy?


61
Tại sao nó lại tốt? Nếu bạn chỉ cần di chuyển vòng lặp chính của mình sang một phương thức (tái cấu trúc rất đơn giản) thì người dùng có thể viết mã không gây ngạc nhiên như thế này:var g = new Game {...}; g.MainLoop();
Brandin

61
Câu hỏi của bạn đã nêu 6 lý do không sử dụng nó và tôi muốn thấy một lý do duy nhất có lợi cho nó. Bạn cũng có thể hỏi tại sao nên đặt một while(true)vòng lặp trong trình thiết lập thuộc tính : new Game().RunNow = true?
Groo

38
Tương tự cực đoan: tại sao chúng ta nói rằng những gì Hitler đã làm là sai? Ngoại trừ phân biệt chủng tộc, bắt đầu Thế chiến II, giết chết hàng triệu người, bạo lực [vân vân, v.v ... vì hơn 50 lý do khác] anh ta không làm gì sai. Chắc chắn nếu bạn loại bỏ một danh sách lý do tùy ý về lý do tại sao một cái gì đó sai, bạn cũng có thể kết luận rằng điều đó là tốt, cho bất cứ điều gì theo nghĩa đen.
Bakuriu

4
Đối với bộ sưu tập rác (GC) (vấn đề kỹ thuật của bạn), sẽ không có gì đặc biệt về nó. Ví dụ tồn tại trước khi mã constructor thực sự chạy. Trong trường hợp này, thể hiện vẫn có thể truy cập được, do đó nó không thể được yêu cầu bởi GC. Nếu GC thiết lập, trường hợp này sẽ tồn tại và tại mỗi lần xuất hiện cài đặt GC, đối tượng sẽ được chuyển sang thế hệ tiếp theo theo quy tắc thông thường. Việc GC có diễn ra hay không, tùy thuộc vào việc (đủ) các đối tượng mới có được tạo hay không.
Jeppe Stig Nielsen

8
Tôi sẽ tiến thêm một bước, và nói rằng (đúng) là xấu, bất kể nó được sử dụng ở đâu. Nó ngụ ý rằng không có cách rõ ràng và rõ ràng để ngăn chặn vòng lặp. Trong ví dụ của bạn, không nên dừng vòng lặp khi trò chơi đã thoát hoặc mất tập trung?
Jon Anderson

Câu trả lời:


250

Mục đích của một nhà xây dựng là gì? Nó trả về một đối tượng mới được xây dựng. Một vòng lặp vô hạn làm gì? Nó không bao giờ trở lại. Làm thế nào các nhà xây dựng có thể trả về một đối tượng mới được xây dựng nếu nó hoàn toàn không trả về? Nó không thể.

Ergo, một vòng lặp vô hạn phá vỡ hợp đồng cơ bản của một nhà xây dựng: để xây dựng một cái gì đó.


11
Có lẽ họ có nghĩa là đặt trong khi (đúng) với một khoảng nghỉ ở giữa? Rủi ro, nhưng hợp pháp.
John Dvorak

2
Tôi đồng ý, điều này là chính xác - ít nhất. từ quan điểm học thuật. Giữ cho mọi chức năng trả về một cái gì đó.
Doc Brown

61
Hãy tưởng tượng sod nghèo tạo ra một lớp con của lớp đã nói ...
Newtopian

5
@JanDvorak: Vâng, đó là một câu hỏi khác. OP chỉ định rõ ràng "không bao giờ kết thúc" và đề cập đến một vòng lặp sự kiện.
Jörg W Mittag

4
@ JörgWMittag tốt, sau đó, yeah, đừng đặt nó trong một hàm tạo.
John Dvorak

45

Có một cái gì đó rõ ràng là xấu hoặc sai lệch với cách tiếp cận như vậy?

Phải, tất nhiên. Nó là không cần thiết, bất ngờ, vô dụng, không liên kết. Nó vi phạm các khái niệm hiện đại về thiết kế lớp (sự gắn kết, khớp nối). Nó phá vỡ hợp đồng phương thức (một hàm tạo có một công việc xác định và không chỉ là một phương thức ngẫu nhiên). Nó chắc chắn là không thể bảo trì tốt, các lập trình viên trong tương lai sẽ dành nhiều thời gian để cố gắng hiểu những gì đang xảy ra, và cố gắng đoán lý do tại sao nó được thực hiện theo cách đó.

Không có gì trong số này là "lỗi" theo nghĩa là mã của bạn không hoạt động. Nhưng nó có thể sẽ phải chịu chi phí thứ cấp rất lớn (liên quan đến chi phí viết mã ban đầu) về lâu dài bằng cách làm cho mã khó bảo trì hơn (nghĩa là khó thêm các bài kiểm tra, khó sử dụng lại, khó gỡ lỗi, khó mở rộng, v.v. .).

Nhiều / cải tiến hiện đại nhất trong các phương pháp phát triển phần mềm được thực hiện cụ thể để làm cho quá trình thực tế viết / kiểm tra / gỡ lỗi / bảo trì phần mềm dễ dàng hơn. Tất cả điều này bị phá vỡ bởi những thứ như thế này, nơi mã được đặt ngẫu nhiên vì nó "hoạt động".

Thật không may, bạn thường xuyên sẽ gặp các lập trình viên hoàn toàn không biết gì về điều này. Nó hoạt động, đó là nó.

Để kết thúc với một sự tương tự (một ngôn ngữ lập trình khác, ở đây vấn đề hiện tại là tính toán 2+2):

$sum_a = `bash -c "echo $((2+2)) 2>/dev/null"`;   # calculate
chomp $sum_a;                         # remove trailing \n
$sum_a = $sum_a + 0;                  # force it to be a number in case some non-digit characters managed to sneak in

$sum_b = 2+2;

Điều gì là sai với cách tiếp cận đầu tiên? Nó trả về 4 sau một khoảng thời gian hợp lý ngắn; Đúng Vậy. Các phản đối (cùng với tất cả các lý do thông thường mà nhà phát triển có thể đưa ra để bác bỏ chúng) là:

  • Khó đọc hơn (nhưng này, một lập trình viên giỏi có thể đọc nó dễ dàng và chúng tôi đã luôn làm nó như thế này; nếu bạn muốn tôi có thể cấu trúc lại nó thành một phương thức!)
  • Chậm hơn (nhưng đây không phải là nơi quan trọng về thời gian nên chúng ta có thể bỏ qua điều đó, và bên cạnh đó, tốt nhất chỉ nên tối ưu hóa khi cần thiết!)
  • Sử dụng nhiều nguồn hơn (một quy trình mới, nhiều RAM hơn, v.v. - nhưng dito, máy chủ của chúng tôi không đủ nhanh)
  • Giới thiệu các phụ thuộc vào bash(nhưng nó sẽ không bao giờ chạy trên Windows, Mac hoặc Android)
  • Và tất cả các lý do khác được đề cập ở trên.

5
"GC có thể ở trạng thái khóa đặc biệt trong khi chạy hàm tạo" - tại sao? Bộ cấp phát phân bổ trước, sau đó khởi chạy hàm tạo như một phương thức thông thường. Không có gì thực sự đặc biệt về nó, phải không? Và bộ cấp phát phải cho phép phân bổ mới (và những thứ này có thể kích hoạt các tình huống OOM) trong mọi cách của nhà xây dựng.
John Dvorak

1
@JanDvorak, Chỉ muốn đưa ra quan điểm rằng có thể có một số hành vi đặc biệt "ở đâu đó" khi ở trong một nhà xây dựng, nhưng bạn đã đúng, ví dụ tôi chọn là không thực tế. Đã gỡ bỏ.
AnoE

Tôi thực sự thích lời giải thích của bạn và rất thích đánh dấu cả hai câu trả lời là câu trả lời (có ít nhất một upvote). Sự tương tự là tốt đẹp và suy nghĩ và giải thích của bạn về chi phí thứ cấp cũng vậy nhưng tôi coi chúng là các yêu cầu phụ trợ cho mã (giống như các đối số của tôi). Hiện trạng của nghệ thuật là để kiểm tra mã, do đó khả năng kiểm tra vv là một yêu cầu không thực tế. Nhưng tuyên bố của @ JörgWMittag fundamental contract of a constructor: to construct somethinglà đúng chỗ.
Samuel

Sự tương tự là không tốt. Nếu tôi thấy điều này, tôi sẽ thấy một nhận xét ở đâu đó về lý do tại sao bạn sử dụng Bash để làm toán, và tùy thuộc vào lý do của bạn, nó có thể hoàn toàn tốt. Điều tương tự cũng không có tác dụng với OP, vì về cơ bản, bạn sẽ phải đưa ra lời xin lỗi ở mọi nơi bạn đã sử dụng hàm tạo "// Lưu ý: việc xây dựng đối tượng này sẽ khởi động một vòng lặp sự kiện không bao giờ quay trở lại".
Brandin

4
"Thật không may, bạn thường xuyên sẽ gặp các lập trình viên hoàn toàn không biết gì về tất cả những điều này. Nó hoạt động, đó là nó." Thật buồn nhưng rất đúng. Tôi nghĩ rằng tôi có thể nói cho tất cả chúng ta khi tôi nói rằng gánh nặng đáng kể duy nhất trong cuộc sống chuyên nghiệp của chúng tôi là, những người đó.
Cuộc đua nhẹ nhàng với Monica

8

Bạn đưa ra đủ lý do trong câu hỏi của mình để loại trừ cách tiếp cận này nhưng câu hỏi thực tế ở đây là "Có điều gì rõ ràng xấu hơn hay sai lệch với cách tiếp cận như vậy không?"

Mặc dù đầu tiên của tôi ở đây là điều này là vô nghĩa. Nếu hàm tạo của bạn không bao giờ kết thúc, thì không có phần nào khác của chương trình có thể nhận được tham chiếu đến đối tượng được xây dựng, vậy logic đằng sau việc đặt nó vào hàm tạo thay vì phương thức thông thường. Sau đó, tôi nhận ra rằng sự khác biệt duy nhất là trong vòng lặp của bạn, bạn có thể cho phép các tham chiếu đến thislối thoát được xây dựng một phần từ vòng lặp. Mặc dù điều này không bị giới hạn nghiêm ngặt trong tình huống này, nhưng nó đảm bảo rằng nếu bạn cho phép thisthoát ra, những tham chiếu đó sẽ luôn hướng đến một đối tượng không được xây dựng đầy đủ.

Tôi không biết liệu ngữ nghĩa xung quanh loại tình huống này có được xác định rõ trong C # hay không nhưng tôi sẽ cho rằng nó không thành vấn đề vì đó không phải là điều mà hầu hết các nhà phát triển muốn thử tìm hiểu.


Tôi giả sử rằng hàm tạo được chạy sau khi đối tượng được tạo, do đó, trong một ngôn ngữ lành mạnh bị rò rỉ, đây sẽ là hành vi được xác định hoàn hảo.
mroman

@mroman: có thể chấp nhận rò rỉ thistrong hàm tạo - nhưng chỉ khi bạn ở trong hàm tạo của lớp dẫn xuất nhất. Nếu bạn có hai lớp ABvới B : A, nếu hàm tạo của Arò rỉ this, các thành viên của Bvẫn sẽ không được khởi tạo (và các lệnh gọi hoặc phôi phương thức ảo Bcó thể phá hoại dựa trên điều đó).
hoffmale

1
Tôi đoán trong C ++ có thể là điên, vâng. Trong các ngôn ngữ khác, các thành viên nhận được một giá trị mặc định trước khi họ được gán các giá trị thực của họ, vì vậy thật ngu ngốc khi thực sự viết mã như vậy, hành vi vẫn được xác định rõ. Nếu bạn gọi các phương thức sử dụng các thành viên này, bạn có thể gặp phải NullPulumExceptions hoặc tính toán sai (vì các số mặc định là 0) hoặc những thứ tương tự nhưng về mặt kỹ thuật thì điều gì sẽ xảy ra.
mroman

@mroman Bạn có thể tìm thấy một tài liệu tham khảo chính xác cho CLR sẽ cho thấy đó là trường hợp không?
JimmyJames

Vâng, bạn có thể gán các giá trị thành viên trước khi hàm tạo được chạy để đối tượng tồn tại ở trạng thái hợp lệ với các thành viên được gán giá trị mặc định hoặc giá trị được gán cho chúng trước khi hàm tạo thậm chí chạy. Bạn không cần hàm tạo để khởi tạo thành viên. Hãy để tôi xem nếu tôi có thể tìm thấy nó ở đâu đó trong các tài liệu.
mroman

1

+1 Ít nhất là sự ngạc nhiên, nhưng đây là một khái niệm khó để nói rõ với các nhà phát triển mới. Ở mức độ thực dụng, khó có thể gỡ lỗi các ngoại lệ được nêu trong các hàm tạo, nếu đối tượng không khởi tạo, nó sẽ không tồn tại để bạn kiểm tra trạng thái hoặc đăng nhập từ bên ngoài của hàm tạo đó.

Nếu bạn cảm thấy cần phải thực hiện kiểu mẫu mã này, vui lòng sử dụng các phương thức tĩnh trên lớp thay thế.

Một constructor tồn tại để cung cấp logic khởi tạo khi một đối tượng được khởi tạo từ một định nghĩa lớp. Bạn xây dựng thể hiện đối tượng này vì nó là một thùng chứa gói một tập các thuộc tính và chức năng mà bạn muốn gọi từ phần còn lại của logic ứng dụng của bạn.

Nếu bạn không có ý định sử dụng đối tượng mà bạn đang "xây dựng" thì điểm bắt đầu của đối tượng ở vị trí đầu tiên là gì? Có một vòng lặp (đúng) trong hàm tạo một cách hiệu quả có nghĩa là bạn không bao giờ có ý định cho nó hoàn thành ...

C # là một ngôn ngữ hướng đối tượng rất phong phú với nhiều cấu trúc và mô hình khác nhau để bạn khám phá, biết các công cụ của mình và khi nào nên sử dụng chúng, dòng dưới cùng:

Trong C # không thực hiện logic mở rộng hoặc không bao giờ kết thúc trong các hàm tạo vì ... có những lựa chọn thay thế tốt hơn


1
anh ấy dường như không cung cấp bất cứ điều gì đáng kể qua các điểm được thực hiện và giải thích trong 9 câu trả lời trước
gnat

1
Này, tôi phải cho nó đi :) Tôi nghĩ điều quan trọng là phải nhấn mạnh rằng trong khi không có quy tắc nào chống lại việc này, bạn không nên thực tế rằng có những cách đơn giản hơn. Hầu hết các phản hồi khác tập trung vào tính kỹ thuật tại sao nó xấu, nhưng thật khó để bán cho một nhà phát triển mới nói rằng "nhưng nó được biên dịch, vì vậy tôi có thể để nó như vậy không"
Chris Schaller

0

Không có gì xấu về nó. Tuy nhiên, điều quan trọng là câu hỏi tại sao bạn chọn sử dụng cấu trúc này. Bạn đã nhận được gì khi làm điều này?

Trước hết, tôi sẽ không nói về việc sử dụng while(true)trong một nhà xây dựng. Có hoàn toàn không có gì sai với việc sử dụng cú pháp mà trong một constructor. Tuy nhiên, khái niệm "bắt đầu một vòng lặp trò chơi trong hàm tạo" là một vấn đề có thể gây ra một số vấn đề.

Điều này rất phổ biến trong các tình huống này khi có hàm tạo đơn giản là xây dựng đối tượng và có một hàm gọi vòng lặp vô hạn. Điều này cho phép người dùng mã của bạn linh hoạt hơn. Như một ví dụ về các loại vấn đề có thể xuất hiện là bạn hạn chế sử dụng đối tượng của mình. Nếu ai đó muốn có một đối tượng thành viên là "đối tượng trò chơi" trong lớp của họ, họ phải sẵn sàng chạy toàn bộ vòng lặp trò chơi trong hàm tạo của họ vì họ phải xây dựng đối tượng. Điều này có thể dẫn đến mã hóa bị tra tấn để giải quyết vấn đề này. Trong trường hợp chung, bạn không thể phân bổ trước đối tượng trò chơi của mình và sau đó chạy nó sau. Đối với một số chương trình không phải là vấn đề, nhưng có những lớp chương trình mà bạn muốn có thể phân bổ mọi thứ lên phía trước và sau đó gọi vòng lặp trò chơi.

Một trong những quy tắc của lập trình là sử dụng công cụ đơn giản nhất cho công việc. Đó không phải là một quy tắc khó và nhanh, nhưng nó là một quy tắc rất hữu ích. Nếu một lệnh gọi hàm là đủ, tại sao phải xây dựng một đối tượng lớp? Nếu tôi thấy một công cụ phức tạp mạnh hơn được sử dụng, tôi cho rằng nhà phát triển có lý do cho nó và tôi sẽ bắt đầu khám phá những loại thủ thuật kỳ quặc nào bạn có thể làm.

Có những trường hợp điều này có thể là hợp lý. Có thể có trường hợp nó thực sự trực quan để bạn có một vòng lặp trò chơi trong hàm tạo. Trong những trường hợp đó, bạn chạy với nó! Ví dụ, vòng lặp trò chơi của bạn có thể được nhúng trong một chương trình lớn hơn nhiều để xây dựng các lớp để thể hiện một số dữ liệu. Nếu bạn phải chạy một vòng lặp trò chơi để tạo dữ liệu đó, có thể rất hợp lý để chạy vòng lặp trò chơi trong một hàm tạo. Chỉ coi đây là một trường hợp đặc biệt: sẽ hợp lệ khi làm điều này bởi vì chương trình bao quát giúp nhà phát triển tiếp theo hiểu được bạn đã làm gì và tại sao.


Nếu việc xây dựng đối tượng của bạn yêu cầu một vòng lặp trò chơi, ít nhất hãy đặt nó vào một phương thức xuất xưởng. GameTestResults testResults = runGameTests(tests, configuration);hơn GameTestResults testResults = new GameTestResults(tests, configuration);.
dùng253751

@immibis Vâng, điều đó đúng, ngoại trừ những trường hợp không hợp lý. Nếu bạn đang làm việc với một hệ thống dựa trên sự phản chiếu để khởi tạo đối tượng cho bạn, bạn có thể không có tùy chọn để tạo phương thức xuất xưởng.
Cort Ammon

0

Ấn tượng của tôi là một số khái niệm đã bị lẫn lộn và dẫn đến một câu hỏi không được diễn đạt tốt.

Hàm tạo của một lớp có thể bắt đầu một luồng mới sẽ "không bao giờ" kết thúc (mặc dù tôi thích sử dụng while(_KeepRunning)hơn while(true)vì tôi có thể đặt thành viên boolean thành false ở đâu đó).

Bạn có thể trích xuất phương thức tạo và bắt đầu luồng đó thành một chức năng của riêng mình, để tách biệt việc xây dựng đối tượng và bắt đầu thực tế công việc của nó - Tôi thích cách này vì tôi kiểm soát tốt hơn quyền truy cập vào tài nguyên.

Bạn cũng có thể xem "Mẫu đối tượng hoạt động". Cuối cùng, tôi đoán câu hỏi được nhắm đến khi bắt đầu chuỗi "Đối tượng hoạt động" và sở thích của tôi là hủy ghép nối xây dựng và bắt đầu để kiểm soát tốt hơn.


0

Đối tượng của bạn nhắc tôi bắt đầu Nhiệm vụ . Đối tượng tác vụ có một hàm tạo, nhưng nó cũng có một loạt các phương thức tĩnh của nhà máy như : Task.Run, Task.StartTask.Factory.StartNew.

Những thứ này rất giống với những gì bạn đang cố gắng thực hiện và có khả năng đây là 'quy ước'. Vấn đề chính mà mọi người dường như có là việc sử dụng một nhà xây dựng này làm họ ngạc nhiên. @ JörgWMittag nói rằng nó phá vỡ một hợp đồng cơ bản , mà tôi cho rằng có nghĩa là Jörg rất ngạc nhiên. Tôi đồng ý và không có gì để thêm vào đó.

Tuy nhiên, tôi muốn đề xuất rằng OP thử các phương thức tĩnh của nhà máy. Sự ngạc nhiên tan biến và không có hợp đồng cơ bản nào bị đe dọa ở đây. Mọi người đã quen với các phương pháp tĩnh nhà máy làm những việc đặc biệt và chúng có thể được đặt tên theo đó.

Bạn có thể cung cấp các hàm tạo cho phép kiểm soát chi tiết tốt (như @Brandin gợi ý, đại loại như var g = new Game {...}; g.MainLoop();, phù hợp với người dùng không muốn bắt đầu trò chơi ngay lập tức và có thể muốn vượt qua nó trước. Và bạn có thể viết một cái gì đó var runningGame = Game.StartNew();để bắt đầu nó ngay lập tức dễ dàng.


Nó có nghĩa là Jörg về cơ bản là ngạc nhiên, có nghĩa là một bất ngờ như thế là một nỗi đau trong nguyên tắc cơ bản.
Steve Jessop

0

Tại sao một vòng lặp while (true) trong một constructor thực sự xấu?

Điều đó không tệ chút nào.

Tại sao (hoặc hoàn toàn không?) Được coi là thiết kế tồi để cho phép một nhà xây dựng của một lớp bắt đầu một vòng lặp không bao giờ kết thúc (tức là vòng lặp trò chơi)?

Tại sao bạn muốn làm điều này? Nghiêm túc. Lớp đó có một hàm duy nhất: hàm tạo. Nếu tất cả những gì bạn muốn là một hàm duy nhất, đó là lý do tại sao chúng ta có các hàm chứ không phải các hàm tạo.

Bạn đã đề cập đến một số lý do rõ ràng chống lại chính nó, nhưng bạn đã bỏ qua một trong những lý do lớn nhất:

Có những lựa chọn tốt hơn và đơn giản hơn để đạt được điều tương tự.

Nếu có 2 lựa chọn và một lựa chọn tốt hơn, thì điều đó không quan trọng bằng việc nó tốt hơn bao nhiêu, bạn đã chọn lựa chọn tốt hơn. Ngẫu nhiên, đó là lý do tại sao chúng tôi không hầu như không bao giờ sử dụng GoTo.


-1

Mục đích của một nhà xây dựng là, tốt, xây dựng đối tượng của bạn. Trước khi hàm tạo hoàn thành, về nguyên tắc không an toàn khi sử dụng bất kỳ phương thức nào của đối tượng đó. Tất nhiên, chúng ta có thể gọi các phương thức của đối tượng trong hàm tạo, nhưng sau đó mỗi lần chúng ta làm như vậy, chúng ta phải đảm bảo rằng làm như vậy là hợp lệ cho đối tượng ở trạng thái nửa bẻ hiện tại.

Bằng cách gián tiếp đưa tất cả logic vào hàm tạo, bạn thêm gánh nặng loại này lên chính mình. Điều này cho thấy rằng bạn không thiết kế sự khác biệt giữa khởi tạo và sử dụng đủ tốt.


1
điều này dường như không cung cấp bất cứ điều gì đáng kể qua các điểm được thực hiện và giải thích trong 8 câu trả lời trước
gnat
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.