tl; dr
Đối với các trường , int b = b + 1
là bất hợp pháp vì b
là tham chiếu chuyển tiếp bất hợp pháp tới b
. Bạn thực sự có thể khắc phục điều này bằng cách viết int b = this.b + 1
, biên dịch mà không có khiếu nại.
Đối với các biến cục bộ , int d = d + 1
là bất hợp pháp vì d
không được khởi tạo trước khi sử dụng. Đây không phải là trường hợp của các trường luôn được khởi tạo mặc định.
Bạn có thể thấy sự khác biệt bằng cách cố gắng biên dịch
int x = (x = 1) + x;
như một khai báo trường và như một khai báo biến cục bộ. Cái trước sẽ thất bại, nhưng cái sau sẽ thành công, vì sự khác biệt về ngữ nghĩa.
Giới thiệu
Trước hết, các quy tắc cho bộ khởi tạo biến trường và biến cục bộ rất khác nhau. Vì vậy, câu trả lời này sẽ giải quyết các quy tắc trong hai phần.
Chúng tôi sẽ sử dụng chương trình thử nghiệm này trong suốt:
public class test {
int a = a = 1;
int b = b + 1;
public static void Main(String[] args) {
int c = c = 1;
int d = d + 1;
}
}
Khai báo của b
không hợp lệ và không thành công với một illegal forward reference
lỗi.
Khai báo của d
không hợp lệ và không thành công với một variable d might not have been initialized
lỗi.
Thực tế là các lỗi này khác nhau nên gợi ý rằng lý do của các lỗi cũng khác nhau.
Lĩnh vực
Các trình khởi tạo trường trong Java được điều chỉnh bởi JLS §8.3.2 , Khởi tạo trường.
Các phạm vi của một trường được quy định tại JLS §6.3 , Phạm vi của một tuyên bố.
Các quy tắc liên quan là:
- Phạm vi khai báo của một thành viên được
m
khai báo hoặc kế thừa bởi kiểu lớp C (§8.1.6) là toàn bộ phần thân của C, bao gồm mọi khai báo kiểu lồng nhau.
- Biểu thức khởi tạo cho các biến cá thể có thể sử dụng tên đơn giản của bất kỳ biến tĩnh nào được khai báo trong hoặc kế thừa bởi lớp, ngay cả một biến có khai báo xuất hiện ở dạng văn bản sau đó.
- Việc sử dụng các biến cá thể có khai báo xuất hiện dưới dạng văn bản sau khi sử dụng đôi khi bị hạn chế, mặc dù các biến cá thể này nằm trong phạm vi. Xem §8.3.2.3 để biết các quy tắc chính xác điều chỉnh tham chiếu chuyển tiếp đến các biến cá thể.
§8.3.2.3 cho biết:
Tuyên bố của một thành viên cần phải xuất hiện dưới dạng văn bản trước khi nó được sử dụng chỉ khi thành viên đó là một trường thể hiện (tương ứng là tĩnh) của một lớp hoặc giao diện C và tất cả các điều kiện sau đây được giữ nguyên:
- Việc sử dụng xảy ra trong bộ khởi tạo biến thể hiện (tương ứng là tĩnh) của C hoặc trong bộ khởi tạo biến thể (tương ứng là tĩnh) của C.
- Việc sử dụng không nằm ở phía bên trái của một bài tập.
- Cách sử dụng là thông qua một cái tên đơn giản.
- C là lớp trong cùng hoặc giao diện bao quanh việc sử dụng.
Bạn thực sự có thể tham chiếu đến các trường trước khi chúng được khai báo, ngoại trừ một số trường hợp nhất định. Những hạn chế này nhằm ngăn chặn mã như
int j = i;
int i = j;
khỏi biên dịch. Đặc tả Java cho biết "các hạn chế ở trên được thiết kế để bắt, tại thời điểm biên dịch, vòng tròn hoặc các khởi tạo không đúng định dạng khác."
Những quy tắc này thực sự sôi động để làm gì?
Nói tóm lại, các quy tắc về cơ bản nói rằng bạn phải khai báo trước một trường của một tham chiếu đến trường đó nếu (a) tham chiếu nằm trong bộ khởi tạo, (b) tham chiếu không được gán cho, (c) tham chiếu là tên đơn giản (không có định nghĩa như this.
) và (d) nó không được truy cập từ bên trong một lớp bên trong. Vì vậy, một tham chiếu chuyển tiếp thỏa mãn tất cả bốn điều kiện là không hợp pháp, nhưng một tham chiếu chuyển tiếp không thành công với ít nhất một điều kiện là OK.
int a = a = 1;
biên dịch vì nó vi phạm (b): tham chiếu a
đang được chỉ định cho, vì vậy việc tham chiếu a
trước a
khai báo hoàn chỉnh là hợp pháp .
int b = this.b + 1
cũng biên dịch vì nó vi phạm (c): tham chiếu this.b
không phải là một tên đơn giản (nó đủ điều kiện với this.
). Cấu trúc kỳ lạ này vẫn hoàn toàn được xác định rõ ràng, bởi vì this.b
có giá trị bằng không.
Vì vậy, về cơ bản, các hạn chế về tham chiếu trường trong bộ khởi tạo ngăn không cho int a = a + 1
biên dịch thành công.
Quan sát rằng khai báo trường int b = (b = 1) + b
sẽ không biên dịch được, vì cuối cùng b
vẫn là một tham chiếu chuyển tiếp bất hợp pháp.
Biến cục bộ
Khai báo biến cục bộ được điều chỉnh bởi JLS §14.4 , Tuyên bố khai báo biến cục bộ.
Các phạm vi của một biến địa phương được quy định tại JLS §6.3 , Phạm vi của một Tuyên bố:
- Phạm vi khai báo biến cục bộ trong một khối (§14.4) là phần còn lại của khối trong đó khai báo xuất hiện, bắt đầu với bộ khởi tạo của chính nó và bao gồm bất kỳ bộ khai báo nào khác ở bên phải trong câu lệnh khai báo biến cục bộ.
Lưu ý rằng bộ khởi tạo nằm trong phạm vi của biến được khai báo. Vậy tại sao không int d = d + 1;
biên dịch?
Nguyên nhân là do quy tắc gán xác định của Java ( JLS §16 ). Phép gán xác định về cơ bản nói rằng mọi quyền truy cập vào một biến cục bộ phải có một phép gán trước cho biến đó và trình biên dịch Java kiểm tra các vòng lặp và nhánh để đảm bảo rằng phép gán luôn xảy ra trước khi sử dụng (đây là lý do tại sao phép gán xác định có toàn bộ phần đặc tả với nó). Quy tắc cơ bản là:
- Đối với mọi truy cập của một biến cục bộ hoặc trường cuối cùng trống
x
, x
chắc chắn phải được chỉ định trước khi truy cập, nếu không xảy ra lỗi thời gian biên dịch.
Trong int d = d + 1;
, quyền truy cập vào d
được giải quyết cho biến cục bộ, nhưng vì d
chưa được chỉ định trước khi d
được truy cập, trình biên dịch gây ra lỗi. Trong int c = c = 1
, c = 1
xảy ra trước, cái nào chỉ định c
, và sau đó c
được khởi tạo thành kết quả của lần gán đó (là 1).
Lưu ý rằng do các quy tắc gán xác định, khai báo biến cục bộ int d = (d = 1) + d;
sẽ biên dịch thành công ( không giống như khai báo trường int b = (b = 1) + b
), bởi vì d
chắc chắn được gán vào thời điểm cuối cùng d
đạt được.
static
vào biến phạm vi lớp, như trongstatic int x = x + 1;
, bạn có gặp lỗi tương tự không? Bởi vì trong C # nó tạo ra sự khác biệt nếu nó tĩnh hoặc không tĩnh.