Tôi không thể sử dụng tất cả các phương thức tĩnh?


64

Sự khác biệt giữa hai phương thức UpdateSubject dưới đây là gì? Tôi cảm thấy sử dụng các phương thức tĩnh sẽ tốt hơn nếu bạn chỉ muốn hoạt động trên các thực thể. Trong những tình huống tôi nên đi với phương pháp không tĩnh?

public class Subject
{
    public int Id {get; set;}
    public string Name { get; set; }

    public static bool UpdateSubject(Subject subject)
    {
        //Do something and return result
        return true;
    }
    public bool UpdateSubject()
    {
        //Do something on 'this' and return result
        return true;
    }
}

Tôi biết tôi sẽ nhận được nhiều cú đá từ cộng đồng cho câu hỏi thực sự khó chịu này nhưng tôi không thể ngừng tự hỏi mình.

Điều này trở nên không thực tế khi xử lý thừa kế?

Cập nhật:
Nó đang xảy ra tại nơi làm việc của chúng tôi bây giờ. Chúng tôi đang làm việc trên một ứng dụng web asp.net 6 tháng với 5 nhà phát triển. Kiến trúc sư của chúng tôi đã quyết định chúng tôi sử dụng tất cả các phương thức tĩnh cho tất cả các API. Lý do của ông là phương pháp tĩnh có trọng lượng nhẹ và nó mang lại lợi ích cho các ứng dụng web bằng cách giảm tải máy chủ.


9
Rich Hickey sẽ khuyến khích một cái gì đó không thành ngữ như thế (tất cả các phương thức tĩnh). Nếu bạn thích một phong cách chức năng, bạn cũng có thể sử dụng ngôn ngữ chức năng;) Không phải mọi thứ trong phần mềm đều được mô hình hóa tốt nhất như một đối tượng.
Công việc

13
@Job: Tôi không thực sự thấy nó hoạt động như thế nào - nó mang tính thủ tục.
Aaronaught

2
@Job: Lớp này không phải là bất biến.
Aaronaught

3
@DonalFellows: Tất nhiên là bạn có thể, C # và F # đều là mô hình hỗn hợp. Nhưng thực tế vẫn còn, phương thức tĩnh! = Lập trình chức năng. FP có nghĩa là các hàm truyền / chuỗi, các kiểu dữ liệu không thay đổi, tránh các tác dụng phụ, v.v ... Điều đó hoàn toàn trái ngược với những gì đoạn mã trong OP làm.
Aaronaught

4
"Lý luận của ông là phương pháp tĩnh có trọng lượng nhẹ và nó mang lại lợi ích cho các ứng dụng web bằng cách giảm tải máy chủ". Điều đó là sai. Giữ máy chủ tải xuống?
mamcx

Câu trả lời:


87

Tôi sẽ giải quyết các vấn đề rõ ràng nhất của các phương thức tĩnh với các tham số "này" rõ ràng:

  1. Bạn mất công văn ảo và sau đó đa hình. Bạn không bao giờ có thể ghi đè phương thức đó trong một lớp dẫn xuất. Tất nhiên bạn có thể khai báo một phương thức new( static) trong một lớp dẫn xuất, nhưng bất kỳ mã nào truy cập vào nó đều phải nhận thức được toàn bộ hệ thống phân cấp lớp và thực hiện kiểm tra và truyền rõ ràng, đó chính xác là những gì OO cần tránh .

  2. Sắp xếp một phần mở rộng của # 1, bạn không thể thay thế các thể hiện của lớp bằng một giao diện , bởi vì các giao diện (trong hầu hết các ngôn ngữ) không thể khai báo staticcác phương thức.

  3. Sự dài dòng không cần thiết. Cái nào dễ đọc hơn: Subject.Update(subject)hay chỉ subject.Update()?

  4. Kiểm tra lập luận. Một lần nữa phụ thuộc vào ngôn ngữ, nhưng nhiều người sẽ biên dịch một kiểm tra ngầm để đảm bảo rằng thisđối số không nullnhằm ngăn chặn lỗi tham chiếu null tạo ra các điều kiện thời gian chạy không an toàn (loại tràn bộ đệm). Không sử dụng các phương thức cá thể, bạn phải thêm kiểm tra này một cách rõ ràng vào đầu mỗi phương thức.

  5. Thật khó hiểu. Khi một lập trình viên bình thường, hợp lý nhìn thấy một staticphương thức, anh ta sẽ tự nhiên cho rằng nó không yêu cầu một thể hiện hợp lệ (trừ khi nó có nhiều trường hợp, như một phương thức so sánh hoặc đẳng thức, hoặc dự kiến ​​có thể hoạt động trên các nulltham chiếu) . Nhìn thấy các phương pháp tĩnh được sử dụng theo cách này sẽ khiến chúng tôi thực hiện gấp đôi hoặc có thể gấp ba, và sau lần thứ 4 hoặc thứ 5, chúng tôi sẽ bị căng thẳng và tức giận và chúa sẽ giúp bạn nếu chúng tôi biết địa chỉ nhà của bạn.

  6. Đó là một hình thức trùng lặp. Điều thực sự xảy ra khi bạn gọi một phương thức cá thể là trình biên dịch hoặc thời gian chạy tìm kiếm phương thức trong bảng phương thức của kiểu và gọi nó bằng cách sử dụng thislàm đối số. Về cơ bản, bạn đang thực hiện lại những gì trình biên dịch đã làm. Bạn đang vi phạm DRY , lặp đi lặp lại cùng một tham số trong các phương thức khác nhau khi không cần thiết.

Thật khó để thụ thai của bất kỳ tốt lý do để thay thế phương pháp dụ với các phương pháp tĩnh. Xin đừng.


5
+1 cho dòng cuối cùng của bạn một mình. Tôi ước nhiều người sẽ nghĩ "phương thức cá thể" trước khi họ nghĩ "phương thức tĩnh".
Stuart Leyland-Cole

4
+1. Tôi sẽ thêm rằng các phương thức tĩnh làm cho việc viết các bài kiểm tra trở nên khó thực hiện, vì trong hầu hết các trường hợp, bạn không thể chế giễu chúng: một khi bất kỳ phần nào trong mã của bạn phụ thuộc vào một phương thức tĩnh, bạn không thể thay thế nó bằng không op trong bài kiểm tra của bạn. Một ví dụ điển hình trong thế giới .NET sẽ là các lớp như File, FileInfoDirectoryInfo(trên thực tế, có những thư viện ẩn chúng đằng sau các giao diện mà sau đó có thể được đưa vào khi cần thiết và bị chế giễu trong các bài kiểm tra).
sm

Tôi nghĩ rằng điểm của bạn về đa hình / kế thừa là moot. người ta không nên sử dụng tính kế thừa để tái sử dụng mã, vì vậy điều đó không liên quan. phần thừa kế cũng đáng ngờ. trong hầu hết các ngôn ngữ hiện đại, chữ ký phương thức là đa hình. nếu bạn thực hiện một cách tiếp cận chức năng, bạn bắt đầu chuyển qua các phương thức và bạn kết hợp các phương thức phức tạp với các phương thức đơn giản có thể hoán đổi cho nhau dựa trên giao diện của chúng. không có nhược điểm nào đối với các phương thức tĩnh không được bù đắp bằng cách đơn giản là thiết kế theo ý chúng, giống như với OOP.
sara

@sm đơn giản là không đúng. nếu bạn có một sự phụ thuộc lớn vào BẤT CỨ NƠI NÀO, tĩnh hay không, đó không thể là trong một bài kiểm tra đơn vị, thì đó là khoảng thời gian không thể kiểm chứng. tĩnh không gây ra điều này. một phương thức tĩnh có thể được đưa vào giống như cách một lớp biểu thị kết nối db có thể. bạn đang giả định rằng "tĩnh" có nghĩa là "tĩnh cộng với các phụ thuộc ẩn cứng chạm vào trạng thái toàn cầu và tài nguyên bên ngoài". điều đó không công bằng.
sara

@kai hãy thử tiêm một phương thức tĩnh làm Y thay vì X, bất kể X và Y là gì. Bạn không cần phải phụ thuộc vào bất cứ điều gì bên ngoài để được vít. Có lẽ bạn chỉ muốn thử nghiệm một số khía cạnh không yêu cầu tính toán dài X để được thực hiện. Làm thế nào để tiêm phương thức tĩnh ưa thích của bạn mà không tạo ra một trình bao bọc sau đó? Truyền các chức năng xung quanh không được hỗ trợ bởi mọi ngôn ngữ. Các phương thức tĩnh có các trường hợp sử dụng của chúng và tôi sử dụng chúng khi có ý nghĩa để làm như vậy . Đây là, như nó luôn xảy ra, một trường hợp "chỉ vì bạn không nhất thiết có nghĩa là bạn nên".
sm

13

Bạn đã mô tả khá chính xác cách bạn thực hiện OOP trong C, trong đó chỉ có các phương thức tĩnh và "đây" là một con trỏ cấu trúc. Và có, bạn có thể thực hiện đa hình thời gian chạy trong C bằng cách chỉ định một con trỏ hàm trong khi xây dựng sẽ được lưu trữ dưới dạng một phần tử của cấu trúc. Các phương pháp sơ thẩm có sẵn trong các ngôn ngữ khác chỉ là đường cú pháp mà về cơ bản thực hiện chính xác điều tương tự dưới mui xe. Một số ngôn ngữ, như python, nằm ở giữa, trong đó tham số "tự" được liệt kê rõ ràng, nhưng cú pháp đặc biệt để gọi nó xử lý tính kế thừa và đa hình cho bạn.


FWIW, với C, bạn có thể thực hiện công văn ảo như thế này: obj->type->mthd(obj, ...);và về cơ bản tương tự như những gì xảy ra với công văn ảo trong các ngôn ngữ khác (nhưng đằng sau hậu trường).
Donal Fellows

@Karl Bạn có thể vui lòng cung cấp một số loại tài liệu tham khảo bằng văn bản để bắt đầu đào sâu hơn?
Jorge Lavín

Bạn có thể muốn xem GObject như một triển khai tham chiếu tốt.
Karl Bielefeldt

10

Vấn đề với phương thức tĩnh đến thời điểm bạn cần một lớp con.

Các phương thức tĩnh không thể được ghi đè trong các lớp con, do đó các lớp mới của bạn không thể cung cấp các triển khai mới của các phương thức, làm cho chúng ít hữu ích hơn.


2
Đây không hẳn là một vấn đề. Một số ngôn ngữ cung cấp các cơ chế khác (phương thức không ảo trong C ++ và C #) để hoàn thành điều tương tự một cách có chủ ý - C # thậm chí còn cung cấp các phương thức "niêm phong" để tiếp tục mở rộng ý tưởng này! Rõ ràng, bạn sẽ không làm điều này chỉ vì cái quái quỷ đó, nhưng đó là một kỹ thuật tốt để nhận biết ...
Shog9

@Steve, điều đó không thay thế, vì bạn vẫn có thể gọi cả hai phương thức trực tiếp.

@Steve, trong các phương thức tĩnh Java không thể bị ghi đè.

@ Thorbjørn - đầu tiên, câu hỏi không được gắn thẻ Java hoặc bất kỳ ngôn ngữ OOP cụ thể nào khác. Quan trọng hơn, tôi không nói bạn có thể ghi đè chức năng thành viên tĩnh. Tôi đã cố gắng sử dụng một sự tương tự để làm cho một điểm. Khi tôi rõ ràng thất bại, ý kiến ​​đã xóa.
Steve314

2
Theo một nghĩa nào đó, đây vẫn là "triển khai phương pháp mới". Chỉ là không theo nghĩa OOP tiêu chuẩn. Cảm giác này giống như tôi đang nói "từ ngữ của bạn cũng không hoàn hảo, hãy nuôi dưỡng-na-nê-nuôi dưỡng" ;-)
Steve314

7

Nếu bạn khai báo một phương thức static, bạn không phải khởi tạo một đối tượng từ lớp (sử dụng newtừ khóa) để thực thi phương thức. Tuy nhiên, bạn không thể tham khảo bất kỳ biến thành viên nào trừ khi chúng là tĩnh, trong trường hợp đó, các biến thành viên đó thuộc về lớp, không phải là một đối tượng khởi tạo cụ thể của lớp.

Nói cách khác, statictừ khóa làm cho một khai báo là một phần của định nghĩa lớp, do đó nó trở thành một điểm tham chiếu duy nhất trên tất cả các phiên bản của lớp (các đối tượng được khởi tạo từ lớp).

Sau đó, theo sau, nếu bạn muốn nhiều bản sao đang chạy của lớp và bạn không muốn trạng thái (biến thành viên) được chia sẻ giữa tất cả các bản sao đang chạy của bạn (tức là mỗi đối tượng giữ trạng thái duy nhất của riêng nó), thì bạn không thể khai báo các biến thành viên tĩnh.

Nói chung, bạn chỉ nên sử dụng staticcác lớp và phương thức khi bạn đang tạo các phương thức tiện ích chấp nhận một hoặc nhiều tham số và trả về một đối tượng nào đó, mà không có bất kỳ tác dụng phụ nào (tức là thay đổi các biến trạng thái trong lớp)


1
Tôi mỏng OP hiểu staticnghĩa là gì , anh đang hỏi tại sao không chỉ tuyên bố mọi thứ là tĩnh.
Ed S.

1
Anh ta đang truyền trong một ví dụ của Subject- nhưng như một tham số rõ ràng, thay vì thistham số ngầm .
Steve314

Chà, vâng ... nhưng tôi không thấy quan điểm của bạn, tôi cho là vậy.
Ed S.

@Ed - chỉ có điều bạn có thể làm được khá nhiều thứ thông qua một tham số rõ ràng mà bạn có thể thông qua một thistham số ngầm . Có sự khác biệt về kỳ vọng, nhưng ngoài sự kế thừa, không có sự khác biệt thực sự trong những gì bạn có thể làm. Ví dụ, những thành viên không tĩnh này có thể được truy cập - thông qua tham số rõ ràng.
Steve314

Tôi đã hoạt động theo giả định rằng bạn không thể có được các thành viên riêng của đối số trong C # ngay cả thông qua một phương thức tĩnh được khai báo trong cùng một lớp (như bạn có thể trong C ++). Không chắc tại sao tôi nghĩ vậy, nhưng tôi đã sai.
Ed S.

3

Lý do mọi người lập luận rằng 'các phương thức tĩnh hiển thị thiết kế xấu' không nhất thiết là do các phương thức, đó thực sự là dữ liệu tĩnh, ẩn hoặc rõ ràng. Theo ẩn ý tôi có nghĩa là BẤT K state trạng thái nào trong chương trình không có trong (các) giá trị trả về hoặc tham số. Sửa đổi trạng thái trong một hàm tĩnh là một trở ngại cho lập trình thủ tục. Tuy nhiên, nếu hàm không sửa đổi trạng thái hoặc ủy quyền sửa đổi trạng thái cho các hàm đối tượng thành phần, thì thực tế nó là một mô hình chức năng hơn là thủ tục.

Nếu bạn không thay đổi trạng thái, các phương thức và lớp tĩnh có thể có nhiều lợi ích. Nó làm cho nó dễ dàng hơn nhiều để đảm bảo một phương thức là thuần túy, và do đó không có tác dụng phụ và bất kỳ lỗi nào đều có thể tái tạo hoàn toàn. Nó cũng đảm bảo rằng các phương thức khác nhau trong nhiều ứng dụng sử dụng thư viện dùng chung có thể được đảm bảo rằng chúng sẽ nhận được cùng một kết quả với cùng một đầu vào, giúp cả việc theo dõi lỗi và kiểm tra đơn vị dễ dàng hơn nhiều.

Cuối cùng, không có sự khác biệt trong thực tế giữa các đối tượng quá khổ thường thấy, chẳng hạn như 'StateManager' và dữ liệu tĩnh hoặc toàn cầu. Lợi ích của các phương thức tĩnh được sử dụng một cách chính xác là chúng có thể chỉ ra ý định của tác giả là không sửa đổi trạng thái cho các trình xem lại sau này.


1
Tuyên bố của bạn "... là một trở ngại cho lập trình thủ tục ..." khiến tôi nghĩ rằng bạn cho rằng nó rất tệ, tôi đoán đó là một phần của OO theo nghĩa nào đó.
NoChance

making ... unit testing much easier.Không, nó sẽ không. Nó làm cho nó làm cho bất kỳ mã nào gọi các phương thức tĩnh không thể kiểm tra đơn vị , vì bạn không thể tách nó khỏi phương thức tĩnh. Một cuộc gọi đến một phương thức tĩnh trở thành một phụ thuộc được mã hóa cứng cho lớp đó.
Người sử dụng Stuper

2

Tùy thuộc vào ngôn ngữ và những gì bạn đang cố gắng thực hiện, câu trả lời có thể là không có nhiều sự khác biệt, nhưng staticphiên bản có một chút lộn xộn hơn.

Như cú pháp ví dụ của bạn gợi ý Java hoặc C #, tôi nghĩ Thorbjørn Ravn Andersen có quyền chỉ ra vấn đề ghi đè (có ràng buộc muộn). Với phương thức tĩnh, không có tham số "đặc biệt" nào cho ràng buộc muộn được dựa trên, do đó bạn không thể có ràng buộc muộn.

Trong thực tế, một phương thức tĩnh chỉ là một hàm trong một mô-đun, mô-đun đó có cùng tên với lớp - nó thực sự không phải là một phương thức OOP.


2

Về cơ bản bạn đang đánh bại toàn bộ điểm của các đối tượng khi bạn làm điều này. Có một lý do chính đáng để chúng ta chuyển khá nhiều từ mã thủ tục sang mã hướng đối tượng.

Tôi KHÔNG BAO GIỜ khai báo một phương thức tĩnh lấy kiểu lớp làm tham số. Điều đó chỉ làm cho mã phức tạp hơn để không đạt được.


3
Bạn có thể làm điều đó nếu bạn đang truyền objectmột staticphương thức và trả về một phương thức mới object. Các lập trình viên chức năng làm điều này mọi lúc.
Robert Harvey

câu hỏi: bạn có coi Mathlớp tĩnh "phức tạp không có lợi" hay bạn thích nó hơn nếu tất cả các loại số có các phương thức ảo xác định giá trị tuyệt đối, phủ định, thêm, bình phương, gốc tùy ý, v.v. Tôi không nghĩ vậy. các đối tượng là gọn gàng khi bạn cần lưu trữ trạng thái đột biến ẩn, nơi bạn cần thực thi một số bất biến. trộn lẫn tất cả các phương thức có thể thực hiện được hoạt động trên một loại thành chính loại đó, THAT là một cái gì đó dẫn đến mã phức tạp và không thể nhầm lẫn. Tôi đồng ý với Robert, bạn có thể muốn xem các lập trình viên chức năng làm việc như thế nào, nó có thể là một công cụ mở mắt thực sự!
sara

@kai Như tôi đã nói, khá nhiều. Lớp Math là một ngoại lệ hợp lý, mặc dù việc gắn nó vào các kiểu số sẽ không phải là một ý tưởng tồi.
Loren Pechtel

2

Có rất nhiều câu trả lời tuyệt vời ở đây, và chúng sâu sắc và hiểu biết hơn nhiều so với bất cứ điều gì tôi có thể đưa ra như một câu trả lời, nhưng tôi cảm thấy có một điều gì đó không được quảng cáo:

"Kiến trúc sư của chúng tôi đã quyết định chúng tôi sử dụng tất cả các phương thức tĩnh cho tất cả các API. Lý do của anh ấy là các phương thức tĩnh có trọng lượng nhẹ và nó mang lại lợi ích cho các ứng dụng web bằng cách giảm tải máy chủ ."

(nhấn mạnh đậm là của tôi)

2c của tôi về phần này của câu hỏi là thế này:

Về mặt lý thuyết, những gì được nói là đúng: gọi một phương thức tĩnh chỉ có chi phí thực sự gọi phương thức đó. Việc gọi một phương thức không tĩnh (thể hiện) có thêm chi phí đầu tiên của việc khởi tạo đối tượng và tại một số điểm, phá hủy cá thể (bằng tay hoặc thông qua một số hình thức thu gom rác tự động, tùy thuộc vào nền tảng được sử dụng).

Vở kịch một chút của người ủng hộ ma quỷ về điều này: chúng ta có thể đi xa hơn và nói những thứ như:

  • điều này có thể trở nên thực sự tồi tệ nếu một cá thể được tạo cho mỗi lần gọi phương thức (thể hiện) (trái ngược với việc chỉ để nó tĩnh và gọi nó như thế)

  • tùy thuộc vào mức độ phức tạp của hàm tạo, kiểu siêu mẫu, các thành viên thể hiện khác và các yếu tố không xác định khác, chi phí phụ của lệnh gọi phương thức không tĩnh có thể thay đổi và trở nên thực sự lớn

Trong thực tế, IMHO, các điểm trên (và cách tiếp cận chung của "hãy sử dụng số liệu thống kê vì chúng nhanh hơn") là những lý lẽ / khẳng định của người rơm:

  • nếu mã là tốt và cụ thể hơn cho đối số này, nếu các thể hiện chỉ được tạo khi cần thiết (và bị hủy theo kiểu tối ưu), thì bạn không nhận được bất kỳ chi phí phụ nào khi sử dụng các phương thức bảo hiểm khi thích hợp (bởi vì nếu bạn chỉ tạo các đối tượng cần thiết, những đối tượng đó sẽ được tạo ngay cả khi phương thức đó được khai báo là tĩnh một số nơi khác = cùng chi phí phát sinh)

  • bằng cách lạm dụng khai báo phương thức tĩnh theo cách này, có thể ẩn một số vấn đề với mã của bạn liên quan đến cách tạo ra các thể hiện (vì phương thức này là mã mã hóa không chính xác có thể vượt qua cho đến sau này và điều đó không bao giờ tốt).


2

Đây là một câu hỏi rất thú vị có xu hướng có câu trả lời rất khác nhau tùy thuộc vào cộng đồng bạn đang hỏi nó. Các câu trả lời khác có vẻ là C # hoặc java bias: một ngôn ngữ lập trình (chủ yếu) là đối tượng thuần túy được định hướng và có một kiểu xem thành ngữ về hướng đối tượng là gì.

Trong các cộng đồng khác, chẳng hạn như C ++, hướng đối tượng được diễn giải với sự tự do hơn. Một số chuyên gia biết rõ đã nghiên cứu về chủ đề này và trên thực tế, kết luận rằng các chức năng miễn phí cải thiện việc đóng gói (nhấn mạnh là của tôi):

Bây giờ chúng ta đã thấy rằng một cách hợp lý để đánh giá mức độ đóng gói trong một lớp là đếm số lượng hàm có thể bị phá vỡ nếu việc triển khai của lớp thay đổi. Trong trường hợp đó, rõ ràng là một lớp có n hàm thành viên được gói gọn hơn một lớp có n + 1 hàm thành viên. Và quan sát đó là những gì biện minh cho lập luận của tôi về việc thích các chức năng không phải là thành viên không phải là thành viên

Scott Meyers

Đối với những người không quen thuộc với thuật ngữ C ++:

  • hàm thành viên = phương thức
  • Hàm không phải thành viên = phương thức tĩnh
  • không kết bạn = chỉ sử dụng API công khai
  • Hàm không phải thành viên không phải là thành viên = phương thức tĩnh chỉ sử dụng API công khai

Bây giờ, để trả lời câu hỏi của bạn:

Tôi không thể sử dụng tất cả các phương thức tĩnh?

Không, bạn không nên luôn luôn sử dụng các phương thức tĩnh.

Nhưng, bạn nên sử dụng chúng bất cứ khi nào bạn chỉ cần sử dụng API công khai của một lớp.


-3

trong Java ...

Các phương thức tĩnh không được ghi đè mà thay vào đó được ẩn đi và do đó nó sẽ là một sự khác biệt lớn (vấn đề?) Trong trường hợp mở rộng lớp.

Mặt khác, tôi nhận thấy rằng các lập trình viên Java có xu hướng nghĩ rằng tất cả ĐÃ trở thành đối tượng, mà không thực sự hiểu lý do tại sao và tôi không đồng ý.

Tĩnh nên được sử dụng cho mã cụ thể cho lớp. Tĩnh không hoạt động tốt với kế thừa.

Một số ví dụ tốt về việc sử dụng các phương thức tĩnh là các hàm độc lập không có tác dụng phụ.

cũng thấy từ khóa được đồng bộ hóa ...


2
Làm thế nào để phương thức tĩnh này ẩn và sự khác biệt và vấn đề này sẽ xuất hiện khi mở rộng lớp? Làm thế nào để đồng bộ hóa phù hợp với điều này? Trường hợp nào các vấn đề về tĩnh và thừa kế được gặp phải? Bạn có thể cung cấp một số mặt tốt và xấu của các phương thức tĩnh trong các thư viện Java cốt lõi và giải thích tại sao mỗi phương thức lại không?
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.