Tại sao các biến cục bộ không được khởi tạo trong Java?


103

Có lý do gì khiến các nhà thiết kế Java cảm thấy rằng các biến cục bộ không nên được cung cấp một giá trị mặc định không? Nghiêm túc mà nói, nếu các biến cá thể có thể được cung cấp một giá trị mặc định, thì tại sao chúng ta không thể làm điều tương tự cho các biến cục bộ?

Và nó cũng dẫn đến các vấn đề như được giải thích trong nhận xét này cho một bài đăng trên blog :

Quy tắc này khó chịu nhất khi cố gắng đóng một tài nguyên trong một khối cuối cùng. Nếu tôi khởi tạo tài nguyên bên trong một lần thử, nhưng cuối cùng cố gắng đóng nó trong vòng, tôi gặp lỗi này. Nếu tôi di chuyển bản thuyết minh ra bên ngoài lần thử, tôi gặp một lỗi khác cho biết rằng nó phải có trong lần thử.

Rất bực bội.



1
Xin lỗi về điều đó ... câu hỏi này không bật lên khi tôi nhập câu hỏi .. tuy nhiên, tôi đoán có sự khác biệt giữa hai câu hỏi ... Tôi muốn biết tại sao các nhà thiết kế Java lại thiết kế nó theo cách này, trong khi câu hỏi mà bạn chỉ đến không hỏi điều đó ...
Shivasubramanian A

Xem thêm câu hỏi C # có liên quan này: stackoverflow.com/questions/1542824/…
Raedwald

Đơn giản - vì trình biên dịch dễ dàng theo dõi các biến cục bộ chưa được khởi tạo. Nếu nó có thể giống với các biến khác, nó sẽ. Trình biên dịch chỉ cố gắng giúp bạn.
gỉ sắt

Câu trả lời:


62

Các biến cục bộ được khai báo hầu hết để thực hiện một số phép tính. Vì vậy, lập trình viên quyết định đặt giá trị của biến và nó không được lấy giá trị mặc định. Nếu do nhầm lẫn, người lập trình đã không khởi tạo một biến cục bộ và nó nhận giá trị mặc định, thì đầu ra có thể là một giá trị không mong muốn nào đó. Vì vậy, trong trường hợp các biến cục bộ, trình biên dịch sẽ yêu cầu người lập trình khởi tạo với một số giá trị trước khi họ truy cập biến để tránh việc sử dụng các giá trị không xác định.


23

"Vấn đề" mà bạn liên kết đến dường như đang mô tả tình huống này:

SomeObject so;
try {
  // Do some work here ...
  so = new SomeObject();
  so.DoUsefulThings();
} finally {
  so.CleanUp(); // Compiler error here
}

Khiếu nại của người bình luận là trình biên dịch chệch dòng trong finallyphần, cho rằng socó thể chưa được khởi tạo. Nhận xét sau đó đề cập đến một cách viết mã khác, có thể là như thế này:

// Do some work here ...
SomeObject so = new SomeObject();
try {
  so.DoUsefulThings();
} finally {
  so.CleanUp();
}

Người bình luận không hài lòng với giải pháp đó vì trình biên dịch sau đó nói rằng mã "phải được thử." Tôi đoán điều đó có nghĩa là một số mã có thể tạo ra một ngoại lệ không được xử lý nữa. Tôi không chắc. Cả hai phiên bản mã của tôi đều không xử lý bất kỳ ngoại lệ nào, vì vậy mọi thứ liên quan đến ngoại lệ trong phiên bản đầu tiên sẽ hoạt động tương tự trong phiên bản thứ hai.

Dù sao, phiên bản thứ hai của mã này là cách chính xác để viết nó. Trong phiên bản đầu tiên, thông báo lỗi của trình biên dịch là đúng. Các sobiến có thể được uninitialized. Đặc biệt, nếu phương thức SomeObjectkhởi tạo bị lỗi, sosẽ không được khởi tạo, và do đó sẽ là một lỗi khi cố gắng gọi so.CleanUp. Luôn nhập tryphần sau khi bạn đã có được tài nguyên mà finallyphần hoàn thiện.

Khối try- finallysau khi sokhởi tạo chỉ có để bảo vệ SomeObjectcá thể, để đảm bảo rằng nó được dọn dẹp bất kể điều gì khác xảy ra. Nếu có những thứ khác cần chạy, nhưng chúng không liên quan đến việc liệu SomeObjectcá thể đó có phải là thuộc tính được cấp phát hay không, thì chúng nên chuyển sang một try - finallykhối khác, có thể là khối bao bọc khối mà tôi đã trình bày.

Việc yêu cầu các biến được gán thủ công trước khi sử dụng không dẫn đến các vấn đề thực sự. Nó chỉ dẫn đến những phức tạp nhỏ, nhưng mã của bạn sẽ tốt hơn cho nó. Bạn sẽ có các biến có phạm vi hạn chế hơn và try- finallycác khối không cố gắng bảo vệ quá nhiều.

Nếu các biến cục bộ có giá trị mặc định, thì sotrong ví dụ đầu tiên sẽ là null. Điều đó thực sự sẽ không giải quyết được gì. Thay vì gặp lỗi thời gian biên dịch trong finallykhối, bạn có thể NullPointerExceptionẩn nấp ở đó có thể ẩn bất kỳ ngoại lệ nào khác có thể xảy ra trong phần "Thực hiện một số công việc tại đây" của mã. (Hay các ngoại lệ trong finallycác phần tự động liên kết với ngoại lệ trước đó? Tôi không nhớ. Mặc dù vậy, bạn sẽ có thêm một ngoại lệ theo cách của ngoại lệ thực.)


2
tại sao không chỉ có if (so! = null) ... trong khối cuối cùng?
izb

điều đó vẫn sẽ gây ra cảnh báo / lỗi trình biên dịch - tôi không nghĩ rằng trình biên dịch hiểu điều đó nếu kiểm tra (nhưng tôi chỉ làm điều này tắt bộ nhớ, không được kiểm tra).
Chii

6
Tôi chỉ đặt SomeObject so = null trước khi thử và đặt null kiểm tra mệnh đề cuối cùng. Bằng cách này sẽ không có cảnh báo trình biên dịch.
Juha Syrjälä

Tại sao phải phức tạp hóa mọi thứ? Viết khối try-last theo cách này và bạn BIẾT biến có giá trị hợp lệ. Không cần kiểm tra null.
Rob Kennedy

1
Rob, ví dụ "SomeObject ()" mới của bạn rất đơn giản và không có ngoại lệ nào nên được tạo ở đó, nhưng nếu lệnh gọi có thể tạo ra ngoại lệ thì tốt hơn nên để nó xảy ra bên trong khối thử để nó có thể được xử lý.
Sarel Botha

12

Hơn nữa, trong ví dụ bên dưới, một ngoại lệ có thể đã được ném vào bên trong cấu trúc SomeObject, trong trường hợp đó, biến 'so' sẽ là null và lệnh gọi tới CleanUp sẽ ném ra một NullPointerException

SomeObject so;
try {
  // Do some work here ...
  so = new SomeObject();
  so.DoUsefulThings();
} finally {
  so.CleanUp(); // Compiler error here
}

Điều tôi có xu hướng làm là:

SomeObject so = null;
try {
  // Do some work here ...
  so = new SomeObject();
  so.DoUsefulThings();
} finally {
  if (so != null) {
     so.CleanUp(); // safe
  }
}

12
Những gì bạn thích làm gì?
Electric Monk

2
Yup, xấu xí. Yup, đó là những gì tôi làm.
SMBiggs

@ElectricMonk Những hình thức bạn nghĩ là tốt hơn, là bạn thể hiện hoặc thể hiện ở đây getContents (..) Phương pháp: javapractices.com/topic/TopicAction.do?Id=126
Atom

11

Lưu ý rằng các biến thể hiện / thành viên cuối cùng không được khởi tạo theo mặc định. Bởi vì những điều đó là cuối cùng và không thể thay đổi trong chương trình sau đó. Đó là lý do mà Java không đưa ra bất kỳ giá trị mặc định nào cho chúng và buộc lập trình viên phải khởi tạo nó.

Mặt khác, các biến thành viên không phải cuối cùng có thể được thay đổi sau đó. Do đó, chính xác là trình biên dịch không cho phép chúng vẫn chưa được khởi tạo vì chúng có thể được thay đổi sau đó. Về biến cục bộ, phạm vi của biến cục bộ hẹp hơn nhiều. Trình biên dịch biết khi nào nó được sử dụng. Do đó, việc buộc lập trình viên khởi tạo biến có ý nghĩa.


9

Câu trả lời thực tế cho câu hỏi của bạn là vì các biến phương thức được khởi tạo bằng cách chỉ cần thêm một số vào con trỏ ngăn xếp. Để không chúng sẽ là một bước bổ sung. Đối với các biến lớp, chúng được đưa vào bộ nhớ khởi tạo trên heap.

Tại sao không thực hiện thêm bước? Lùi lại một bước - Không ai đề cập rằng "cảnh báo" trong trường hợp này là Điều Rất Tốt.

Bạn không bao giờ nên khởi tạo biến của mình thành 0 hoặc null trong lần chuyển đầu tiên (khi bạn viết mã lần đầu). Bạn có thể gán nó cho giá trị thực hoặc không gán nó vì nếu bạn không gán thì java có thể cho bạn biết khi nào bạn thực sự gặp khó khăn. Lấy câu trả lời của Electric Monk làm ví dụ tuyệt vời. Trong trường hợp đầu tiên, nó thực sự hữu ích một cách đáng kinh ngạc là nó cho bạn biết rằng nếu try () không thành công vì hàm tạo của SomeObject đã ném một ngoại lệ, thì cuối cùng bạn sẽ có một NPE. Nếu hàm tạo không thể đưa ra một ngoại lệ, thì nó không nên có trong lần thử.

Cảnh báo này là một trình kiểm tra lập trình viên tồi đa đường dẫn tuyệt vời đã giúp tôi khỏi làm những việc ngu ngốc vì nó kiểm tra mọi đường dẫn và đảm bảo rằng nếu bạn đã sử dụng biến trong một số đường dẫn thì bạn phải khởi tạo biến đó trong mọi đường dẫn đến nó . Bây giờ tôi không bao giờ khởi tạo các biến một cách rõ ràng cho đến khi tôi xác định rằng đó là điều chính xác cần làm.

Trên hết, không phải là tốt hơn nếu nói rõ ràng "int size = 0" thay vì "int size" và khiến lập trình viên tiếp theo tìm ra rằng bạn dự định nó bằng 0?

Mặt khác, tôi không thể đưa ra một lý do hợp lệ nào để yêu cầu trình biên dịch khởi tạo tất cả các biến chưa được khởi tạo thành 0.


1
Có, và có những người khác trong đó nó ít nhiều phải được khởi tạo thành null do cách dòng chảy của mã - tôi không nên nói "không bao giờ" cập nhật câu trả lời để phản ánh điều này.
Bill K

4

Tôi nghĩ mục đích chính là duy trì sự tương đồng với C / C ++. Tuy nhiên, trình biên dịch sẽ phát hiện và cảnh báo bạn về việc sử dụng các biến chưa được khởi tạo sẽ giúp giảm vấn đề xuống mức tối thiểu. Từ góc độ hiệu suất, sẽ nhanh hơn một chút khi cho phép bạn khai báo các biến chưa được khởi tạo vì trình biên dịch sẽ không phải viết câu lệnh gán, ngay cả khi bạn ghi đè giá trị của biến trong câu lệnh tiếp theo.


1
Có thể cho rằng, trình biên dịch có thể xác định xem bạn có luôn gán cho biến trước khi làm bất cứ điều gì với nó hay không và loại bỏ việc gán giá trị mặc định tự động trong trường hợp như vậy. Nếu trình biên dịch không thể xác định liệu một nhiệm vụ có xảy ra trước khi truy cập hay không, nó sẽ tạo ra nhiệm vụ mặc định.
Greg Hewgill

2
Có, nhưng người ta có thể tranh luận rằng nó cho phép lập trình viên biết nếu anh ta hoặc cô ta để biến không khởi tạo do nhầm lẫn.
Mehrdad Afshari

1
Trình biên dịch cũng có thể làm điều đó trong cả hai trường hợp. :) Cá nhân, tôi muốn rằng trình biên dịch xử lý một biến chưa được khởi tạo là một lỗi. Nó có nghĩa là tôi có thể đã mắc lỗi ở đâu đó.
Greg Hewgill

Tôi không phải dân Java, nhưng tôi thích cách xử lý của C #. Sự khác biệt là trong trường hợp đó, trình biên dịch đã phải đưa ra cảnh báo, mà có thể làm cho bạn có được một vài trăm cảnh báo cho chương trình đúng của bạn;)
Mehrdad Afshari

Nó có cảnh báo cho các biến thành viên không?
Adeel Ansari

4

(Có vẻ lạ khi đăng một câu trả lời mới rất lâu sau câu hỏi, nhưng một bản sao đã xuất hiện.)

Đối với tôi, lý do xuất phát từ điều này: Mục đích của các biến cục bộ khác với mục đích của các biến cá thể. Các biến cục bộ sẽ được sử dụng như một phần của phép tính; các biến thể hiện ở đó để chứa trạng thái. Nếu bạn sử dụng một biến cục bộ mà không gán giá trị cho nó, đó gần như chắc chắn là một lỗi logic.

Điều đó nói rằng, tôi hoàn toàn có thể yêu cầu các biến cá thể luôn được khởi tạo rõ ràng; lỗi sẽ xảy ra trên bất kỳ phương thức khởi tạo nào mà kết quả cho phép một biến cá thể chưa được khởi tạo (ví dụ: không được khởi tạo khi khai báo và không phải trong phương thức khởi tạo). Nhưng đó không phải là quyết định Gosling, et. al., diễn ra vào đầu những năm 90, vì vậy chúng tôi ở đây. (Và tôi không nói rằng họ đã gọi nhầm.)

Tuy nhiên, tôi không thể thực hiện được các biến cục bộ mặc định. Có, chúng ta không nên dựa vào các trình biên dịch để kiểm tra lại logic của chúng ta, và một trình thì không, nhưng nó vẫn hữu ích khi trình biên dịch bắt được một trình biên dịch. :-)


"Điều đó nói rằng, tôi hoàn toàn có thể làm được việc yêu cầu các biến cá thể luôn được khởi tạo rõ ràng ..." , FWIW, là hướng mà họ đã thực hiện trong TypeScript.
TJ Crowder

3

Sẽ hiệu quả hơn khi không khởi tạo biến, và trong trường hợp là biến cục bộ, làm như vậy là an toàn, vì trình biên dịch có thể theo dõi quá trình khởi tạo.

Trong trường hợp bạn cần một biến để được khởi tạo, bạn luôn có thể tự làm điều đó, vì vậy nó không phải là một vấn đề.


3

Ý tưởng đằng sau các biến cục bộ là chúng chỉ tồn tại trong phạm vi giới hạn mà chúng cần thiết. Như vậy, sẽ có rất ít lý do cho sự không chắc chắn về giá trị, hoặc ít nhất, giá trị đó đến từ đâu. Tôi có thể hình dung ra nhiều lỗi phát sinh do có giá trị mặc định cho các biến cục bộ.

Ví dụ: hãy xem xét đoạn mã đơn giản sau ... ( NB hãy giả sử cho mục đích trình diễn rằng các biến cục bộ được gán một giá trị mặc định, như được chỉ định, nếu không được khởi tạo rõ ràng )

System.out.println("Enter grade");
int grade = new Scanner(System.in).nextInt(); //I won't bother with exception handling here, to cut down on lines.
char letterGrade; //let us assume the default value for a char is '\0'
if (grade >= 90)
    letterGrade = 'A';
else if (grade >= 80)
    letterGrade = 'B';
else if (grade >= 70)
    letterGrade = 'C';
else if (grade >= 60)
    letterGrade = 'D';
else
    letterGrade = 'F';
System.out.println("Your grade is " + letterGrade);

Khi tất cả được nói và hoàn thành, giả sử trình biên dịch đã gán giá trị mặc định là '\ 0' cho letterGrade , mã này như được viết sẽ hoạt động bình thường. Tuy nhiên, nếu chúng ta quên câu lệnh else thì sao?

Việc chạy thử mã của chúng tôi có thể dẫn đến kết quả sau

Enter grade
43
Your grade is

Kết quả này, trong khi được mong đợi, chắc chắn không phải là ý định của các coder. Thật vậy, có lẽ trong phần lớn các trường hợp (hoặc ít nhất là một số đáng kể), giá trị mặc định sẽ không phải là giá trị mong muốn , vì vậy trong đại đa số trường hợp, giá trị mặc định sẽ dẫn đến lỗi. Nó làm cho ý nghĩa hơn để buộc các coder để gán một giá trị ban đầu cho một biến địa phương trước khi sử dụng nó, vì đau buồn gỡ lỗi do quên = 1trong for(int i = 1; i < 10; i++)xa outweighs sự tiện lợi trong không cần phải bao gồm = 0trong for(int i; i < 10; i++).

Đúng là các khối try-catch-last có thể hơi lộn xộn (nhưng nó không thực sự là một khối catch-22 như câu trích dẫn dường như gợi ý), khi một đối tượng ném một ngoại lệ đã được kiểm tra vào hàm tạo của nó, nhưng đối với một lý do khác, một cái gì đó phải được thực hiện với đối tượng này ở cuối khối cuối cùng. Một ví dụ hoàn hảo về điều này là khi xử lý tài nguyên, tài nguyên này phải được đóng lại.

Một cách để xử lý điều này trong quá khứ có thể là như vậy ...

Scanner s = null; //declared and initialized to null outside the block. This gives us the needed scope, and an initial value.
try {
    s = new Scanner(new FileInputStream(new File("filename.txt")));
    int someInt = s.nextInt();
} catch (InputMismatchException e) {
    System.out.println("Some error message");
} catch (IOException e) {
    System.out.println("different error message"); 
} finally {
    if (s != null) //in case exception during initialization prevents assignment of new non-null value to s.
        s.close();
}

Tuy nhiên, đối với Java 7, khối cuối cùng này không còn cần thiết khi sử dụng try-with-resources, như vậy.

try (Scanner s = new Scanner(new FileInputStream(new File("filename.txt")))) {
...
...
} catch(IOException e) {
    System.out.println("different error message");
}

Điều đó nói rằng, (như tên cho thấy) điều này chỉ hoạt động với các tài nguyên.

Và trong khi ví dụ trước là một chút may mắn, điều này có lẽ nói lên cách try-catch-last hoặc các lớp này được triển khai hơn là nó nói về các biến cục bộ và cách chúng được triển khai.

Đúng là các trường được khởi tạo thành giá trị mặc định, nhưng điều này hơi khác một chút. Ví dụ: khi bạn nói, int[] arr = new int[10];ngay sau khi bạn khởi tạo mảng này, đối tượng sẽ tồn tại trong bộ nhớ tại một vị trí nhất định. Hãy giả sử một lúc rằng không có giá trị mặc định, nhưng thay vào đó giá trị ban đầu là bất kỳ chuỗi số 1 và số 0 nào xảy ra ở vị trí bộ nhớ đó vào lúc này. Điều này có thể dẫn đến hành vi không xác định trong một số trường hợp.

Giả sử chúng ta có ...

int[] arr = new int[10];
if(arr[0] == 0)
    System.out.println("Same.");
else
    System.out.println("Not same.");

Nó hoàn toàn có Same.thể được hiển thị trong một lần chạy và Not same.có thể được hiển thị trong một lần khác. Vấn đề có thể trở nên nghiêm trọng hơn khi bạn bắt đầu nói về các biến tham chiếu.

String[] s = new String[5];

Theo định nghĩa, mỗi phần tử của s phải trỏ đến một Chuỗi (hoặc là null). Tuy nhiên, nếu giá trị ban đầu là bất kỳ chuỗi số 0 và 1 nào xảy ra tại vị trí bộ nhớ này, thì không những không có gì đảm bảo rằng bạn sẽ nhận được cùng một kết quả mỗi lần, mà còn không có gì đảm bảo rằng đối tượng s [0] điểm thành (giả sử nó trỏ đến bất cứ thứ gì có ý nghĩa) thậm chí Chuỗi (có lẽ đó là Thỏ,: p )! Sự thiếu quan tâm đến kiểu này sẽ bay khi đối mặt với khá nhiều thứ tạo nên Java Java. Vì vậy, mặc dù có giá trị mặc định cho các biến cục bộ có thể được xem là tùy chọn tốt nhất, nhưng việc có các giá trị mặc định cho các biến cá thể là điều cần thiết hơn .


1

nếu tôi không sai, một lý do khác có thể là

Đưa ra giá trị mặc định của các biến thành viên là một phần của quá trình tải lớp

Tải lớp là thời gian chạy trong java có nghĩa là khi bạn tạo một đối tượng thì lớp được tải với việc tải lớp chỉ các biến thành viên được khởi tạo với giá trị mặc định JVM không mất thời gian để cung cấp giá trị mặc định cho các biến cục bộ của bạn vì một số phương thức sẽ không bao giờ được gọi bởi vì cuộc gọi phương thức có thể có điều kiện, vậy tại sao phải mất thời gian để cung cấp cho chúng giá trị mặc định và giảm hiệu suất nếu những giá trị mặc định đó sẽ không bao giờ được sử dụng.


0

Eclipse thậm chí còn cung cấp cho bạn cảnh báo về các biến chưa được khởi tạo, vì vậy dù sao thì nó cũng trở nên khá rõ ràng. Cá nhân tôi nghĩ rằng đó là một điều tốt khi đây là hành vi mặc định, nếu không ứng dụng của bạn có thể sử dụng các giá trị không mong muốn và thay vì trình biên dịch báo lỗi, nó sẽ không làm gì cả (nhưng có lẽ đưa ra cảnh báo) và sau đó bạn sẽ gãi suy nghĩ của bạn về lý do tại sao một số thứ không hoàn toàn hoạt động theo cách chúng nên làm.


0

Các biến cục bộ được lưu trữ trên một ngăn xếp, nhưng các biến phiên bản được lưu trữ trên heap, vì vậy có một số khả năng giá trị trước đó trên ngăn xếp sẽ được đọc thay vì giá trị mặc định như xảy ra trong heap. Vì lý do đó, jvm không cho phép sử dụng một biến cục bộ mà không khởi tạo nó.


2
Sai ra Flat ... tất cả Java phi nguyên thủy được lưu trữ trong các đống bất kể khi nào và làm thế nào chúng được xây dựng
gshauger

Trước Java 7, các biến cá thể được lưu trữ trên heap và các biến cục bộ được tìm thấy trên ngăn xếp. Tuy nhiên, bất kỳ đối tượng nào mà tham chiếu biến cục bộ sẽ được tìm thấy trong heap. Kể từ Java 7, "Java Hotspot Server Compiler" có thể thực hiện "phân tích thoát" và quyết định phân bổ một số đối tượng trên ngăn xếp thay vì đống.
mamills

0

Biến cá thể sẽ có giá trị mặc định nhưng các biến cục bộ không thể có giá trị mặc định. Vì các biến cục bộ về cơ bản nằm trong các phương thức / hành vi, mục đích chính của nó là thực hiện một số hoạt động hoặc tính toán. Do đó, không phải là một ý kiến ​​hay khi đặt giá trị mặc định cho các biến cục bộ. Nếu không, bạn sẽ rất vất vả và mất thời gian để kiểm tra lý do của những câu trả lời không mong muốn.


-1

Câu trả lời là các biến thể hiện có thể được khởi tạo trong phương thức khởi tạo lớp hoặc bất kỳ phương thức lớp nào, Nhưng trong trường hợp là các biến cục bộ, một khi bạn đã xác định bất cứ điều gì trong phương thức sẽ tồn tại mãi mãi trong lớp.


-2

Tôi có thể nghĩ đến 2 lý do sau

  1. Như hầu hết các câu trả lời đã nói bằng cách đặt ràng buộc khởi tạo biến cục bộ, nó được đảm bảo rằng biến cục bộ được gán một giá trị như ý muốn của lập trình viên và đảm bảo tính toán kết quả mong đợi.
  2. Các biến cá thể có thể được ẩn bằng cách khai báo các biến cục bộ (cùng tên) - để đảm bảo hành vi mong đợi, các biến cục bộ buộc phải được bổ sung một giá trị. (Sẽ hoàn toàn tránh điều này mặc dù)

Các trường không thể được ghi đè. Tối đa, chúng có thể bị ẩn và tôi không biết việc ẩn sẽ cản trở việc kiểm tra khởi tạo như thế nào?
Meriton

Ngay hidden.If một quyết định để tạo ra biến cục bộ với cùng một tên như là ví dụ, vì hạn chế này, biến cục bộ sẽ được khởi tạo với giá trị được lựa chọn puposely (trừ giá trị của biến chẳng hạn)
Mitra
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.