Trong lập trình, && là phép "và", || (hai gạch đứng) là phép "hoặc", và trong phần lớn ngôn ngữ phổ biến chúng được đánh giá tắt: với A && B, nếu A sai thì B không được tính nữa; với A || B, nếu A đúng thì B bị bỏ qua. Nhờ vậy có thể viết điều kiện "kiểm an toàn trước, dùng sau" — nhưng cũng vì vậy mà thứ tự hai vế, độ ưu tiên toán tử và cách phủ định điều kiện trở thành nguồn lỗi kinh điển.
Từ câu nói thường tới toán tử
Một cửa hàng ghi: "Miễn phí giao hàng nếu đơn từ 500 nghìn và trong nội thành." Và: "Được đổi trả nếu còn hoá đơn hoặc mua qua tài khoản thành viên." Viết thành mã:
mienPhi = (tongTien >= 500000) && noiThanh;
duocDoi = conHoaDon || laThanhVien;
Về logic, && chính là phép hội ∧ ("và": chỉ đúng khi cả hai đúng), || là phép tuyển bao hàm ∨ ("hoặc": đúng khi ít nhất một vế đúng), còn dấu chấm than ! là phủ định ¬ ("không"):
| A | B | A && B | A || B | !A |
|---|---|---|---|---|
| sai | sai | sai | sai | đúng |
| sai | đúng | sai | đúng | đúng |
| đúng | sai | sai | đúng | sai |
| đúng | đúng | đúng | đúng | sai |
Lưu ý: || là "hoặc" bao hàm — cả hai đúng thì vẫn đúng. Người còn hoá đơn mà cũng là thành viên thì vẫn được đổi.
Đánh giá tắt: vì sao được phép bỏ vế sau
Nhìn hai dòng đầu của bảng: hễ A sai thì A && B sai, bất kể B. Nhìn hai dòng cuối: hễ A đúng thì A || B đúng, bất kể B. Về logic, đó là hai hằng đúng: sai ∧ B luôn sai, đúng ∨ B luôn đúng. Máy tận dụng điều này: biết chắc kết quả rồi thì không tính tiếp.
Tiết kiệm thời gian chỉ là lợi ích phụ. Lợi ích chính là viết được điều kiện bảo vệ (guard):
if (soHocSinh != 0 && tongDiem / soHocSinh >= 8) { … }
Nếu lớp không có học sinh, vế trái sai, phép chia cho 0 ở vế phải không bao giờ chạy. Tương tự với danh sách có thể rỗng hay biến có thể chưa có giá trị:
if (ds != null && ds.length > 0) { … }
Đổi thứ tự là hỏng
Trong logic, A ∧ B và B ∧ A tương đương. Trong chương trình có đánh giá tắt thì không hẳn: viết ngược thành tongDiem / soHocSinh >= 8 && soHocSinh != 0 thì phép chia chạy trước, lớp rỗng là lỗi. Quy tắc nhớ: vế kiểm an toàn đứng bên trái.
Hệ quả thứ hai: nếu vế phải có tác dụng phụ (ghi nhật ký, tăng biến đếm, gửi thư), nó có thể không chạy. Đoạn daDangNhap || ghiNhatKy("khách vào") sẽ không ghi gì khi người dùng đã đăng nhập. Muốn chắc một việc được làm, hãy tách nó ra câu lệnh riêng, đừng nhét vào vế phải.
Mỗi ngôn ngữ một kiểu
| Ngôn ngữ | "Và" / "hoặc" đánh giá tắt | Ghi chú |
|---|---|---|
| C, C++, Java, C#, JavaScript, PHP | && và || | & và | là toán tử khác (theo bit, hoặc tính đủ hai vế) |
| Python | and, or, not | and/or trả về chính một trong hai vế, không nhất thiết True/False |
| Visual Basic .NET | AndAlso, OrElse | And, Or trong VB.NET tính đủ cả hai vế |
| SQL | AND, OR | Nhiều hệ quản trị CSDL không cam kết thứ tự tính; đừng dựa vào vế trái để bảo vệ vế phải |
Python và JavaScript: "hoặc" trả về giá trị
Trong Python, 0 or 5 cho ra 5, "" or "khách" cho ra "khách": or trả về vế đầu tiên "được coi là đúng", nếu không có thì trả vế cuối. JavaScript làm y hệt với ||. Người ta hay dùng điều này để đặt giá trị mặc định:
soLuong = soLuongNhap || 1;
Lỗi nằm ở chỗ số 0, chuỗi rỗng, giá trị false đều bị coi là "sai". Khách cố ý nhập số lượng 0 thì vẫn bị đổi thành 1. JavaScript có toán tử riêng ?? chỉ thay khi giá trị là null hoặc undefined: soLuongNhap ?? 1 giữ nguyên số 0.
Lỗi hay gặp, từng cái một
1. Viết "x bằng 1 hoặc 2" như nói
Câu if (x == 1 || 2) trong C, JavaScript (hay if x == 1 or 2: trong Python) không có nghĩa "x bằng 1 hoặc x bằng 2". Máy đọc là (x == 1) || 2, mà số 2 khác 0 luôn được coi là đúng, nên điều kiện luôn đúng. Viết đúng: x == 1 || x == 2; trong Python có thể viết x in (1, 2).
2. "Hoặc" thay cho "và" khi kiểm khoảng
Muốn "điểm từ 0 tới 10" mà viết diem >= 0 || diem <= 10 thì mọi số đều lọt: số âm thoả vế phải, số 100 thoả vế trái. Phải là diem >= 0 && diem <= 10. Còn kiểu viết như toán 0 <= diem <= 10: trong Python đúng nghĩa, nhưng trong C hay JavaScript máy tính (0 <= diem) trước ra đúng/sai, rồi đem so với 10 — luôn ra đúng (đúng hay sai đổi thành 1 hay 0, mà cả hai đều nhỏ hơn 10).
3. Độ ưu tiên: && đứng trên ||
Giống phép nhân đứng trên phép cộng, && được tính trước ||. Câu laVip || tongTien >= 500000 && coMaGiam được đọc là laVip || (tongTien >= 500000 && coMaGiam). Nếu ý người viết là "(VIP hoặc đơn lớn) và có mã" thì kết quả khác hẳn: khách VIP không có mã vẫn được giảm. Thử với laVip = đúng, tongTien = 100000, coMaGiam = sai: cách máy đọc cho đúng; cách người viết muốn cho (đúng || sai) && sai = sai. Cách chữa duy nhất đáng tin: luôn đặt ngoặc khi trộn && với ||.
4. Nhầm & với &&
Trong C, JavaScript, & là phép "và" theo từng bit, và nó có độ ưu tiên thấp hơn phép so sánh ==. Câu if (x & 1 == 0) (định kiểm x chẵn) bị đọc thành x & (1 == 0), tức x & 0, luôn bằng 0, điều kiện luôn sai. Viết đúng: (x & 1) == 0. Trong Java, & dùng cho hai giá trị đúng/sai là phép "và" không đánh giá tắt — cả hai vế đều chạy, nên điều kiện bảo vệ mất tác dụng.
5. PHP có hai bộ "và"
PHP có cả && và chữ and, nhưng and có độ ưu tiên thấp hơn cả phép gán. Câu $ok = true and false; gán true cho $ok rồi mới tính and với false. Dùng && trong biểu thức gán, hoặc đặt ngoặc.
6. Phủ định sai điều kiện
Vòng lặp "đi tiếp khi chưa hết danh sách và chưa gặp số 0". Điều kiện dừng là "hết danh sách hoặc gặp số 0": i >= n || a[i] == 0. Điều kiện chạy tiếp là phủ định của nó. Theo luật De Morgan, ¬(P ∨ Q) tương đương ¬P ∧ ¬Q, nên:
while (i < n && a[i] != 0) { i++; }
Lỗi thường gặp là chỉ đảo từng vế mà quên đổi || thành &&: i < n || a[i] != 0. Khi i = n, vế trái sai nên máy tính vế phải a[n] — đọc ra ngoài danh sách. Để ý luôn: vế i < n đứng bên trái chính là điều kiện bảo vệ cho a[i].
Ví dụ giải đầy đủ: rút gọn một điều kiện rối
Đoạn mã kiểm quyền vào phòng thi: !(!coThe || !dungGio) || laGiamThi.
- Áp De Morgan cho phần trong ngoặc: !(!coThe || !dungGio) = !!coThe && !!dungGio.
- Hai lần phủ định triệt tiêu: = coThe && dungGio.
- Cả điều kiện: (coThe && dungGio) || laGiamThi — "có thẻ và đến đúng giờ, hoặc là giám thị".
Thử lại với coThe = đúng, dungGio = sai, laGiamThi = sai. Bản gốc: !coThe = sai, !dungGio = đúng, sai || đúng = đúng, phủ định thành sai; sai || sai = sai. Bản rút gọn: (đúng && sai) || sai = sai. Khớp. Thử thêm coThe = sai, dungGio = sai, laGiamThi = đúng: bản gốc ra (…) || đúng = đúng; bản rút gọn sai || đúng = đúng. Khớp.
Bài tập
- Với a = 0, đoạn a != 0 && 10 / a > 2 có lỗi chia cho 0 không? — Đáp án: không; vế trái sai nên vế phải không được tính, cả biểu thức sai.
- Python: "" or "mặc định" và "An" or "mặc định" cho ra gì? — Đáp án: "mặc định" và "An".
- Đóng ngoặc cho đúng cách máy đọc: a || b && c || d. — Đáp án: a || (b && c) || d.
- Viết phủ định của tuoi >= 18 && coGiayTo không dùng dấu ! trước ngoặc. — Đáp án: tuoi < 18 || !coGiayTo.
- Vì sao x > 5 || x < 10 luôn đúng với mọi số x? — Đáp án: số nào không lớn hơn 5 thì nhỏ hơn hoặc bằng 5, tức nhỏ hơn 10, nên ít nhất một vế luôn đúng.
Dùng vào đâu
- Viết mã an toàn: xếp vế kiểm null, kiểm rỗng, kiểm chia 0 lên trước; đặt ngoặc mỗi khi trộn && với ||; không để tác dụng phụ ở vế phải.
- Đọc mã người khác: gặp điều kiện nhiều tầng phủ định, áp De Morgan và rút gọn như ví dụ trên trước khi sửa.
- Kiểm thử: với điều kiện có hai vế, thử đủ bốn tổ hợp đúng/sai — tức đi hết bảng chân trị.
- Học tiếp: luật De Morgan và bảng chân trị trong danh mục suy diễn hợp lệ là nền của toàn bộ bài này.
Câu hỏi thường gặp
Đánh giá tắt (short-circuit) là gì?
Là cách tính biểu thức logic dừng ngay khi đã biết chắc kết quả. Với A && B, nếu A sai thì B không được tính; với A || B, nếu A đúng thì B không được tính. Phần lớn ngôn ngữ phổ biến như C, Java, JavaScript, Python đều làm vậy.
&& và & khác nhau thế nào?
&& là phép "và" logic có đánh giá tắt. & trong C, JavaScript là phép "và" theo từng bit và có độ ưu tiên thấp hơn phép so sánh; trong Java, & dùng với giá trị đúng/sai thì tính đủ cả hai vế. Dùng nhầm có thể làm điều kiện luôn sai hoặc mất tác dụng bảo vệ.
Trong biểu thức có cả && và || thì cái nào tính trước?
Trong C, Java, JavaScript, Python, phép "và" có độ ưu tiên cao hơn phép "hoặc", giống phép nhân trước phép cộng. Vì vậy a || b && c được hiểu là a || (b && c). Nên đặt ngoặc để người đọc khỏi phải nhớ.
Vì sao if (x == 1 || 2) luôn đúng?
Vì máy đọc là (x == 1) || 2, và số 2 khác 0 luôn được coi là đúng. Muốn kiểm x bằng 1 hoặc 2 phải viết x == 1 || x == 2.
Phủ định một điều kiện có && thế nào cho đúng?
Dùng luật De Morgan: phủ định của "A và B" là "không A hoặc không B", phủ định của "A hoặc B" là "không A và không B". Lỗi hay gặp là đảo từng vế mà quên đổi && thành || hoặc ngược lại.