Điều gì làm cho nhà điều hành của Scala làm quá tải các ứng dụng tốt của Google


155

Quá tải toán tử trong C ++ được nhiều người coi là A Bad Thing (tm) và một lỗi không được lặp lại trong các ngôn ngữ mới hơn. Chắc chắn, đó là một tính năng đặc biệt bị bỏ khi thiết kế Java.

Bây giờ tôi đã bắt đầu đọc trên Scala, tôi thấy rằng nó có những thứ trông rất giống như quá tải toán tử (mặc dù về mặt kỹ thuật nó không có quá tải toán tử vì nó không có toán tử, chỉ có các hàm). Tuy nhiên, nó dường như không khác biệt về chất với quá tải toán tử trong C ++, khi tôi nhớ các toán tử được định nghĩa là các hàm đặc biệt.

Vì vậy, câu hỏi của tôi là điều gì làm cho ý tưởng định nghĩa "+" trong Scala trở thành một ý tưởng tốt hơn so với trong C ++?


27
Cả C ++ và Scala đều không được định nghĩa bởi sự đồng thuận phổ quát giữa tất cả các lập trình viên. Tôi không nghĩ có bất kỳ mâu thuẫn nào giữa thực tế là một số người hay nói xấu về C ++ và thực tế là một số người không nói xấu về Scala.
Steve Jessop

16
Không có gì xấu về quá tải toán tử trong C ++.
Cún con

5
Điều này không có gì mới nhưng cách tôi bảo vệ C ++ khi quá tải toán tử và các tính năng "nâng cao" khác bị đặt câu hỏi rất đơn giản: C ++ cung cấp cho chúng tôi tất cả sức mạnh để sử dụng / lạm dụng nó khi chúng tôi thấy phù hợp. Tôi luôn thích cách chúng ta được coi là có năng lực và tự chủ và không cần những quyết định như thế này đối với chúng ta.
Elliott

Scala được thiết kế như hàng thập kỷ sau c ++. Hóa ra người đứng đằng sau nó lại rất thông thạo về ngôn ngữ lập trình. Không có gì xấu cả, nếu bạn gắn bó với c ++ hoặc Scala trong 100 năm nữa thì có lẽ cả hai đều xấu! Bị thiên vị rõ ràng là bản chất của chúng ta nhưng chúng ta có thể chống lại nó, chỉ cần nhìn vào lịch sử của công nghệ, mọi thứ trở nên lỗi thời.
Nader Ghanbari

Câu trả lời:


242

C ++ thừa hưởng các toán tử màu xanh thực sự từ C. Điều đó có nghĩa là "+" trong 6 + 4 rất đặc biệt. Chẳng hạn, bạn không thể lấy một con trỏ tới hàm + đó.

Scala mặt khác không có nhà khai thác theo cách đó. Nó chỉ có tính linh hoạt cao trong việc xác định tên phương thức cộng với một chút ưu tiên được xây dựng cho các ký hiệu không phải từ. Vì vậy, về mặt kỹ thuật Scala không có quá tải toán tử.

Dù bạn muốn gọi nó là gì, quá tải toán tử không hẳn là xấu, ngay cả trong C ++. Vấn đề là khi các lập trình viên xấu lạm dụng nó. Nhưng thành thật mà nói, tôi cho rằng việc lấy đi khả năng của các lập trình viên để lạm dụng quá tải toán tử không làm giảm khả năng sửa chữa tất cả những điều mà lập trình viên có thể lạm dụng. Câu trả lời thực sự là cố vấn. http://james-iry.blogspot.com/2009/03/operator-overloading-ad-absurdum.html

Hơn thế nữa, có sự khác biệt giữa quá tải toán tử của C ++ và cách đặt tên phương thức linh hoạt của Scala, IMHO, làm cho Scala vừa ít sử dụng hơn vừa dễ sử dụng hơn.

Trong C ++, cách duy nhất để có được ký hiệu sửa lỗi là sử dụng các toán tử. Nếu không, bạn phải sử dụng object.message (đối số) hoặc con trỏ-> messsage (đối số) hoặc hàm (argument1, argument2). Vì vậy, nếu bạn muốn một kiểu DSLish nhất định cho mã của mình thì sẽ có áp lực phải sử dụng các toán tử.

Trong Scala, bạn có thể nhận được ký hiệu infix với bất kỳ tin nhắn nào gửi. "đối số thông điệp đối tượng" là hoàn toàn ổn, điều đó có nghĩa là bạn không cần sử dụng các ký hiệu không phải từ chỉ để có ký hiệu infix.

Quá tải toán tử C ++ được giới hạn ở cơ bản các toán tử C. Kết hợp với giới hạn chỉ các toán tử mới có thể được sử dụng, điều này gây áp lực cho mọi người khi cố gắng ánh xạ một loạt các khái niệm không liên quan đến một vài biểu tượng như "+" và ">>"

Scala cho phép một loạt các ký hiệu không phải từ hợp lệ làm tên phương thức. Chẳng hạn, tôi đã có một DSL Prolog-ish nhúng, nơi bạn có thể viết

female('jane)!         // jane is female
parent('jane,'john)!   // jane is john's parent
parent('jane, 'wendy)! // jane is wendy's parent

mother('Mother, 'Child) :- parent('Mother, 'Child) & female('Mother) //'// a mother of a child is the child's parent and is female

mother('X, 'john)?  // find john's mother
mother('jane, 'X)?  // find's all of jane's children

Các biểu tượng: -,!,?, Và & được định nghĩa là các phương thức thông thường. Chỉ trong C ++ và sẽ hợp lệ nên một nỗ lực ánh xạ DSL này vào C ++ sẽ yêu cầu một số biểu tượng đã gợi lên các khái niệm rất khác nhau.

Tất nhiên, điều này cũng mở ra Scala cho một loại lạm dụng khác. Trong Scala, bạn có thể đặt tên cho một phương thức $! & ^% Nếu bạn muốn.

Đối với các ngôn ngữ khác, như Scala, linh hoạt trong việc sử dụng hàm không phải từ và tên phương thức, xem Smalltalk, như Scala, mỗi "toán tử" chỉ là một phương thức khác và Haskell cho phép lập trình viên xác định quyền ưu tiên và tính cố định của tên được đặt linh hoạt chức năng.


Lần cuối tôi kiểm tra, 3.operator + (5) đã hoạt động. Tôi thực sự ngạc nhiên rằng & (3.operator +) thì không.
Joshua

ví dụ, bạn có thể xác nhận (nữ ("jane")) trong c ++. Điều đó sẽ không gây nhầm lẫn chút nào - gật đầu lại với bài đăng của james-iry về việc nó không phải là toán tử + là một điều xấu, nhưng các lập trình viên ngu ngốc thì có.
pm100

1
@Joshua int main() {return (3).operator+(5);}kết quả trongerror: request for member ‘operator+’ in ‘3’, which is of non-class type ‘int’
zildjohn01

Đó là một mớ hỗn độn kiêu ngạo: "quá tải toán tử vốn không phải là xấu, ngay cả trong C ++. Vấn đề là khi các lập trình viên xấu lạm dụng nó." Nếu một cái gì đó dễ dàng bị lạm dụng với lợi ích khá ít từ việc sử dụng nó, thì kết quả chung là người tiếp theo duy trì mã của bạn sẽ mất năng suất trong việc giải mã các phần kỳ lạ trong mã của bạn. Mặt khác: Rất nhiều thông tin và câu trả lời bằng văn bản.
Jukka Dahlbom

@JukkaDahlbom Sự tồn tại của con trỏ thông minh làm cho lợi ích của chính nó lớn. Và sau đó, bạn có lambdas, loại số do người dùng xác định, loại khoảng thời gian ...
Alexey Romanov

66

Quá tải toán tử trong C ++ được nhiều người coi là A Bad Thing (tm)

Chỉ do người ngu dốt. Nó là hoàn toàn bắt buộc trong một ngôn ngữ như C ++, và đáng chú ý là các ngôn ngữ khác bắt đầu có quan điểm "thuần túy", đã thêm nó một khi các nhà thiết kế của họ phát hiện ra nó cần thiết như thế nào.


30
Tôi thực sự đồng ý với Neil. Quá tải toán tử là điều cần thiết nếu bạn muốn trình bày các biến / hằng / đối tượng / thể hiện dưới dạng các thực thể đại số ... và để mọi người hiểu các tương tác của chúng theo kiểu toán học - đó là cách lập trình hoạt động IMHO.
Massa

16
+1, Quá tải toán tử trong C ++ là tốt. Ví dụ, nó làm cho toán học vectơ sạch hơn rất nhiều. Giống như nhiều tính năng của C ++, bạn nên sử dụng năng lượng một cách cẩn thận.
John Smith

7
@Kristo Vì C ++ sử dụng các giá trị phải được gán và sao chép. Điều cần thiết là phải kiểm soát điều đó, vì vậy bạn phải có thể chỉ định toán tử gán cho một loại nhất định, ở mức tối thiểu.

7
@Kristo: bởi vì một ý định của C ++ là cho phép các loại do người dùng xác định thực hiện mọi thứ mà các loại dựng sẵn làm (mặc dù chúng được xử lý khác nhau trong một số ngữ cảnh như chuyển đổi ngầm). Nếu bạn muốn triển khai một số nguyên 27 bit, thì bạn có thể, và sử dụng nó sẽ giống như sử dụng int. Nếu không quá tải toán tử, sẽ không thể sử dụng UDT với cú pháp giống như các kiểu dựng sẵn, và do đó, ngôn ngữ kết quả sẽ không "giống như C ++" theo nghĩa này.
Steve Jessop

8
"cách đó nằm ở sự điên rồ" - thậm chí tệ hơn, cách đó nằm ở std :: vector <bool>!
Steve Jessop

42

Quá tải toán tử chưa bao giờ được coi là một ý tưởng tồi trong C ++ - chỉ việc lạm dụng quá tải toán tử được cho là một ý tưởng tồi. Người ta không thực sự cần quá tải toán tử trong một ngôn ngữ vì dù sao chúng có thể được mô phỏng với nhiều lời gọi hàm dài hơn. Việc tránh quá tải toán tử trong Java làm cho việc triển khai và đặc tả Java đơn giản hơn một chút và nó buộc các lập trình viên không được lạm dụng các toán tử. Đã có một số tranh luận trong cộng đồng Java về việc giới thiệu quá tải toán tử.

Những ưu điểm và nhược điểm của quá tải toán tử trong Scala cũng giống như trong C ++ - bạn có thể viết mã tự nhiên hơn nếu bạn sử dụng quá tải toán tử một cách thích hợp - và mã khó hiểu hơn, khó hiểu hơn nếu bạn không.

FYI: Các toán tử không được định nghĩa là các hàm đặc biệt trong C ++, chúng hoạt động giống như bất kỳ hàm nào khác - mặc dù có một số khác biệt trong tra cứu tên, cho dù chúng có phải là hàm thành viên hay không và chúng có thể được gọi theo hai cách: 1 ) cú pháp toán tử và 2) cú pháp toán tử-hàm-toán tử.


"Người ta không thực sự cần quá tải toán tử trong một ngôn ngữ vì chúng có thể được mô phỏng với nhiều lời gọi hàm dài hơn." Một người thậm chí không thực sự cần các nhà khai thác theo logic đó. Tại sao không chỉ sử dụng add(2, multiply(5, 3))?
Joe Z.

Đó là một trường hợp phù hợp với các ký hiệu thông thường được sử dụng. Hãy xem xét các nhà toán học và vật lý học, họ có thể hiểu và sử dụng thư viện C ++ cung cấp quá tải cho các toán tử dễ dàng hơn nhiều. Họ thà tập trung vào phương trình hơn là ngôn ngữ lập trình.
Phil Wright

19

Bài viết này - " Di sản tích cực của C ++ và Java " - trả lời trực tiếp câu hỏi của bạn.

"C ++ có cả phân bổ ngăn xếp và phân bổ heap và bạn phải quá tải các toán tử của mình để xử lý tất cả các tình huống và không gây rò rỉ bộ nhớ. Thật khó, Java, tuy nhiên, có một cơ chế phân bổ lưu trữ duy nhất và trình thu gom rác, khiến cho toán tử quá tải tầm thường". ..

Java nhầm (theo tác giả) đã bỏ qua quá tải toán tử vì nó phức tạp trong C ++, nhưng quên mất tại sao (hoặc không nhận ra rằng nó không áp dụng cho Java).

Rất may, các ngôn ngữ cấp cao hơn như Scala cung cấp tùy chọn cho nhà phát triển, trong khi vẫn chạy trên cùng một JVM.


14
Eckel là nguồn duy nhất tôi từng thấy cho ý tưởng rằng quá tải toán tử đã bị loại bỏ khỏi Java vì các biến chứng trong C ++ và anh ta không nói nguồn của mình là gì. Tôi sẽ giảm giá nó. Tất cả các nguồn khác tôi đã nói rằng nó đã bị bỏ do lạm dụng tiềm năng. Xem gotw.ca/publications/c_family_interview.htmlmnewt.com/wohler/articles/james-gosling-ramblings-1.html . Chỉ cần trang tìm kiếm chúng cho "quá tải toán tử."
James Iry

9

Không có gì sai với quá tải toán tử. Trong thực tế, có điều gì đó không đúng khi không có quá tải toán tử cho các kiểu số. (Hãy xem một số mã Java sử dụng BigInteger và BigDecimal.)

C ++ có truyền thống lạm dụng tính năng này. Một ví dụ thường được trích dẫn là các toán tử bithift bị quá tải để thực hiện I / O.


Các nhà khai thác << và >> đang chỉ ra cách chuyển giao trực quan, họ có nghĩa là thực hiện I / O, đó không phải là lạm dụng, đó là từ thư viện tiêu chuẩn và điều thực tế. Chỉ cần nhìn vào "cin >> cái gì đó", những gì đi đâu? Từ cin, đến một cái gì đó, rõ ràng.
peenut

7
@peenut: Nhưng công dụng ban đầu của chúng là dịch chuyển bit. "Thư viện chuẩn" sử dụng toán tử theo cách hoàn toàn sai với định nghĩa ban đầu.
Joe Z.

1
Tôi chắc chắn rằng tôi đã đọc được ở đâu đó rằng Bjarne Stroustrup (người tạo ra C ++) đã thử nghiệm sử dụng =thay vì <<>>trong những ngày đầu của C ++, nhưng gặp phải vấn đề vì nó không có quyền ưu tiên của nhà điều hành (nghĩa là nó tìm kiếm đối số bên trái hoặc bên phải đầu tiên). Vì vậy, bàn tay của anh ấy có một chút ràng buộc về những gì anh ấy có thể sử dụng.
Phil Wright

8

Nói chung nó không phải là một điều xấu.
Các ngôn ngữ mới như C # cũng có quá tải toán tử.

Đó là lạm dụng quá tải nhà điều hành là một điều xấu.

Nhưng cũng có vấn đề với quá tải toán tử như được định nghĩa trong C ++. Bởi vì các toán tử quá tải chỉ là đường cú pháp cho các cuộc gọi phương thức, chúng hoạt động giống như phương thức. Mặt khác, các toán tử tích hợp thông thường không hoạt động như các phương thức. Những sự không nhất quán này có thể gây ra vấn đề.

Tắt đầu của các nhà khai thác đầu của tôi ||&&.
Các phiên bản được xây dựng trong số này là các toán tử cắt ngắn. Điều này không đúng với các phiên bản quá tải và đã gây ra một số vấn đề.

Thực tế là + - * / tất cả trả về cùng loại mà chúng hoạt động (sau khi quảng cáo nhà điều hành)
Các phiên bản bị quá tải có thể trả về bất cứ điều gì (Đây là nơi lạm dụng đặt vào, Nếu nhà điều hành của bạn bắt đầu trả lại một số loại trọng tài mà người dùng không mong đợi mọi thứ đi xuống đồi).


8

Quá tải toán tử không phải là thứ mà bạn thực sự "cần" rất thường xuyên, nhưng khi sử dụng Java, nếu bạn đạt đến một điểm mà bạn thực sự cần nó, nó sẽ khiến bạn muốn tách móng tay ra để bạn có cớ ngừng gõ .

Mã mà bạn vừa tìm thấy tràn ra từ lâu? Phải, bạn sẽ phải nhập lại toàn bộ lô để làm cho nó hoạt động với BigInteger. Không có gì bực bội hơn khi phải phát minh lại bánh xe chỉ để thay đổi loại biến.


6

Guy Steele lập luận rằng quá tải toán tử cũng phải có trong Java, trong bài phát biểu quan trọng "Phát triển ngôn ngữ" - có một video và phiên âm từ đó, và đó thực sự là một bài phát biểu tuyệt vời. Bạn sẽ tự hỏi những gì anh ấy đang nói về vài trang đầu tiên, nhưng nếu bạn tiếp tục đọc, bạn sẽ thấy điểm và đạt được sự giác ngộ. Và thực tế là anh ấy có thể làm một bài phát biểu như vậy cũng rất tuyệt vời.

Đồng thời, bài nói chuyện này đã truyền cảm hứng cho rất nhiều nghiên cứu cơ bản, có thể bao gồm cả Scala - đó là một trong những bài báo mà mọi người nên đọc để làm việc trong lĩnh vực này.

Quay lại vấn đề, các ví dụ của anh chủ yếu là về các lớp số (như BigInteger và một số thứ lạ hơn), nhưng điều đó không cần thiết.

Tuy nhiên, sự thật là việc lạm dụng quá tải toán tử có thể dẫn đến kết quả khủng khiếp và thậm chí việc sử dụng đúng cách có thể làm phức tạp vấn đề, nếu bạn cố đọc mã mà không nghiên cứu một chút các thư viện mà nó sử dụng. Nhưng đó có phải là một ý tưởng tốt? OTOH, các thư viện như vậy có nên cố gắng đưa vào bảng cheat toán tử cho toán tử của họ không?


4

Tôi tin rằng MỌI câu trả lời đã bỏ lỡ điều này. Trong C ++, bạn có thể quá tải các toán tử tất cả những gì bạn muốn, nhưng bạn không thể thực hiện quyền ưu tiên mà chúng được đánh giá. Scala không có vấn đề này, IIRC.

Đối với nó là một ý tưởng tồi, bên cạnh các vấn đề ưu tiên, mọi người đưa ra ý nghĩa thực sự cho các nhà khai thác, và nó hiếm khi hỗ trợ khả năng đọc. Thư viện Scala đặc biệt tệ cho việc này, những biểu tượng ngớ ngẩn mà bạn phải ghi nhớ mỗi lần, với những người duy trì thư viện dán đầu vào cát nói, 'bạn chỉ cần học nó một lần'. Tuyệt vời, bây giờ tôi cần học một số cú pháp khó hiểu của tác giả 'thông minh' * số lượng thư viện tôi quan tâm để sử dụng. Sẽ không tệ lắm nếu tồn tại một quy ước LUÔN LUÔN cung cấp một phiên bản biết chữ của các nhà khai thác.


1
Scala cũng có quyền ưu tiên vận hành cố định, phải không?
skaffman

Tôi tin là có, nhưng nó rất xa. Hơn nữa, Scala có thời gian khai thác ít hơn. +, -, * là các phương thức, không phải toán tử, IIRC. Đó là lý do tại sao 2 + 3 * 2, không phải là 8, là 10.
Saem

7
Scala có một hệ thống ưu tiên dựa trên ký tự đầu tiên của biểu tượng. scala> 2 + 3 * 2 res0: Int = 8
James Iry

3

Quá tải toán tử không phải là một phát minh của C ++ - nó đến từ Algol IIRC và thậm chí cả Gosling cũng không cho rằng đó là một ý tưởng tồi nói chung.


Chắc chắn, nhưng trong phiên bản C ++ của nó, nó đã đạt được một không khí chung của sự bất đồng.
skaffman

5
Bạn có ý nghĩa gì bởi "không khí chung của sự bất đồng"? Hầu hết những người tôi biết đều sử dụng các ngôn ngữ hỗ trợ quá tải toán tử (C ++, C #) và tôi chưa bao giờ nghe thấy bất kỳ khiếu nại nào.
Nemanja Trifunovic

Tôi đang nói về kinh nghiệm lâu năm của mình với tiền ANSI C ++ và tôi chắc chắn nhớ lại một sự không thích chung của họ. Có lẽ tình hình đã được cải thiện với ANSI C ++ hoặc mọi người chỉ học cách không lạm dụng nó.
skaffman

1
Nói như một người đã sử dụng C ++ kể từ những ngày trước (giữa thập niên 80) tôi có thể đảm bảo với bạn rằng việc giới thiệu tiêu chuẩn ISO không ảnh hưởng đến định kiến ​​của mọi người về quá tải nhà điều hành.

3

Điều duy nhất được biết sai trong C ++ là thiếu khả năng quá tải [] = như một toán tử riêng biệt. Điều này có thể khó thực hiện trong trình biên dịch C ++ vì có lẽ không phải là lý do rõ ràng nhưng rất đáng giá.


2

Như các câu trả lời khác đã chỉ ra; Quá tải toán tử tự nó không nhất thiết là xấu. Điều gì là xấu khi nó được sử dụng theo những cách làm cho mã kết quả không rõ ràng. Nói chung khi sử dụng chúng, bạn cần làm cho chúng làm điều đáng ngạc nhiên nhất (có toán tử + phép chia sẽ gây rắc rối cho việc sử dụng lớp hợp lý) hoặc như Scott Meyers nói:

Khách hàng đã biết các kiểu như int cư xử như thế nào, vì vậy bạn nên cố gắng để các kiểu của mình cư xử theo cùng một cách bất cứ khi nào hợp lý ... Khi nghi ngờ, hãy làm như ints làm . (Từ mục C ++ hiệu quả phiên bản thứ 3 18)

Bây giờ một số người đã đưa toán tử quá tải đến mức cực đoan với những thứ như boost :: Spirit . Ở cấp độ này, bạn không biết nó được thực hiện như thế nào nhưng nó tạo ra một cú pháp thú vị để đạt được những gì bạn muốn thực hiện. Tôi không chắc điều này tốt hay xấu. Nó có vẻ tốt, nhưng tôi đã không sử dụng nó.


Tôi không tranh luận hoặc chống lại việc quá tải nhà điều hành ở đây, tôi không tìm kiếm người để biện minh cho họ.
skaffman

Sprint không đến bất cứ nơi nào gần ví dụ tồi tệ nhất mà tôi gặp - bạn sẽ thấy thư viện cơ sở dữ liệu RogueWave sẽ làm gì!

Tôi đồng ý rằng Spirit lạm dụng các nhà khai thác, nhưng tôi thực sự không thể nghĩ ra cách nào tốt hơn để làm điều đó.
Zifre

1
Tôi không nghĩ rằng tinh thần khá lạm dụng các nhà khai thác, nhưng nó đẩy nó. Tôi đồng ý thực sự không có cách nào khác để làm điều đó. Về cơ bản, nó tạo ra DSL trong cú pháp của C ++. Rất xa với những gì C ++ được thiết kế để làm. Vâng, có những ví dụ tồi tệ hơn nhiều :) Nói chung tôi sử dụng chúng khi thích hợp. Chủ yếu chỉ là các toán tử phát trực tuyến để gỡ lỗi dễ dàng hơn \ ghi nhật ký. Và thậm chí ở đó chỉ là đường chuyển tiếp đến một phương thức được thực hiện trong lớp.
Matt Giá

1
Đó là một câu hỏi về hương vị; nhưng các thư viện kết hợp trình phân tích cú pháp, trong các ngôn ngữ chức năng, các toán tử quá tải theo cách rất giống với Spirit và không ai tranh luận về điều đó. Có rất nhiều lý do kỹ thuật để chúng tốt hơn - Google cho "ngôn ngữ cụ thể của miền được nhúng" để tìm nhiều tài liệu giải thích rằng theo quan điểm chung và Google cho "trình kết hợp trình phân tích cú pháp scala" cho các ví dụ thực tế trong trường hợp này. Đúng là trong các ngôn ngữ chức năng, cú pháp kết quả thường đẹp hơn - ví dụ, bạn không cần thay đổi ý nghĩa của >> đối với các trình phân tích cú pháp.
Blaisorblade

2

Tôi chưa bao giờ thấy một bài báo tuyên bố rằng quá tải toán tử của C ++ là xấu.

Toán tử xác định người dùng cho phép mức độ biểu cảm và khả năng sử dụng cao hơn đối với người dùng ngôn ngữ.


1

Tuy nhiên, nó dường như không khác biệt về chất với quá tải toán tử trong C ++, khi tôi nhớ các toán tử được định nghĩa là các hàm đặc biệt.

AFAIK, Không có gì đặc biệt trong các chức năng vận hành so với các chức năng thành viên "bình thường". Tất nhiên bạn chỉ có một nhóm toán tử nhất định mà bạn có thể quá tải, nhưng điều đó không làm cho chúng trở nên rất đặc biệt.

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.