Tại sao toán tử KHÔNG logic trong các ngôn ngữ theo kiểu C,! Và không phải ~ ~ ~?


39

Đối với các toán tử nhị phân, chúng ta có cả toán tử bitwise và logic:

& bitwise AND
| bitwise OR

&& logical AND
|| logical OR

KHÔNG (một toán tử đơn nguyên) hành xử khác nhau mặc dù. Có ~ cho bitwise và! cho logic.

Tôi nhận ra KHÔNG phải là một hoạt động đơn phương trái ngược với AND và OR nhưng tôi không thể nghĩ ra lý do tại sao các nhà thiết kế chọn đi lệch khỏi nguyên tắc rằng đơn lẻ là bitwise và double là logic ở đây và thay vào đó là một ký tự khác. Tôi đoán bạn có thể đọc sai, giống như một phép toán đôi bit luôn trả về giá trị toán hạng. Nhưng đó không phải là một vấn đề thực sự với tôi.

Có một lý do tôi đang thiếu?


7
Bởi vì nếu !! có nghĩa là logic không, làm thế nào tôi có thể biến 42 thành 1? :)
candied_orange

9
~~Sau đó, sẽ không nhất quán hơn cho logic KHÔNG, nếu bạn theo mô hình toán tử logic là nhân đôi của toán tử bitwise?
Bart van Ingen Schenau

9
Đầu tiên, nếu nó là để thống nhất thì nó sẽ là ~ và ~ ~ Việc nhân đôi và và có liên quan đến ngắn mạch; và logic không phải là ngắn mạch.
Barshe

3
Tôi nghi ngờ lý do thiết kế cơ bản là sự rõ ràng và khác biệt trực quan, trong các trường hợp sử dụng điển hình. Các toán tử nhị phân (nghĩa là hai toán hạng) là infix (và có xu hướng được phân tách bằng dấu cách), trong khi các toán tử đơn nguyên là tiền tố (và có xu hướng không được đặt cách nhau).
Steve

7
Như một số ý kiến ​​đã ám chỉ (và đối với những người không muốn theo liên kết này , !!foolà một thành ngữ không phổ biến (không phổ biến?). Nó bình thường hóa một cuộc tranh luận không hoặc không khác biệt đối với 0hoặc 1.
Keith Thompson

Câu trả lời:


108

Kỳ lạ thay, lịch sử của ngôn ngữ lập trình kiểu C không bắt đầu bằng C.

Dennis Ritchie giải thích rõ những thách thức về sự ra đời của C trong bài viết này .

Khi đọc nó, rõ ràng là C đã thừa hưởng một phần thiết kế ngôn ngữ từ BCPL tiền nhiệm và đặc biệt là các nhà khai thác. Phần Giới thiệu sơ sinh Cv của bài viết nói trên giải thích cách BCPL &|được làm giàu với hai nhà khai thác mới &&||. Những lý do là:

  • ưu tiên khác nhau được yêu cầu do sử dụng kết hợp với ==
  • Logic đánh giá khác nhau: trái sang phải đánh giá với ngắn mạch (tức là khi ađang falsea&&b, bkhông được đánh giá).

Thật thú vị, việc nhân đôi này không tạo ra bất kỳ sự mơ hồ nào cho người đọc: a && bsẽ không bị hiểu sai là a(&(&b)). Từ quan điểm phân tích cú pháp, cũng không có sự mơ hồ: &bcó thể có ý nghĩa nếu blà một giá trị, nhưng nó sẽ là một con trỏ trong khi bitwise &sẽ yêu cầu một toán hạng nguyên, do đó logic AND sẽ là lựa chọn hợp lý duy nhất.

BCPL đã được sử dụng ~cho phủ định bitwise. Vì vậy, từ quan điểm về tính nhất quán, nó có thể đã được nhân đôi để đưa ra ~~ý nghĩa hợp lý của nó. Thật không may, điều này sẽ cực kỳ mơ hồ vì ~là một toán tử đơn nguyên: ~~bcũng có thể có nghĩa ~(~b)). Đây là lý do tại sao một biểu tượng khác phải được chọn cho phủ định bị thiếu.


10
Trình phân tích cú pháp không thể phân tán hai tình huống, do đó các nhà thiết kế ngôn ngữ phải làm như vậy.
BobDalgleish

16
@Steve: Thật vậy, có nhiều vấn đề tương tự đã có trong các ngôn ngữ giống như C và C. Khi trình phân tích cú pháp thấy (t)+1rằng đó là một bổ sung (t)1hoặc nó là một +1kiểu gõ t? Thiết kế C ++ đã phải giải quyết vấn đề làm thế nào để các mẫu lex chứa >>chính xác. Và như vậy.
Eric Lippert

6
@ user2357112 Tôi nghĩ rằng vấn đề là không sao khi tokenizer lấy một cách mù quáng &&như một &&mã thông báo và không phải là hai &mã thông báo, bởi vì a & (&b)cách giải thích không phải là một điều hợp lý để viết, vì vậy một con người sẽ không bao giờ có ý đó và bị bất ngờ bởi trình biên dịch coi nó là a && b. Trong khi cả hai !(!a)!!ađều là những điều có thể có đối với con người, thì đó là một ý tưởng tồi cho trình biên dịch để giải quyết sự mơ hồ với quy tắc cấp độ mã thông báo tùy ý.
Ben

17
!!không chỉ có thể / hợp lý để viết, mà là thành ngữ "chuyển đổi thành boolean" chính tắc.
R ..

4
Tôi nghĩ dan04 đang đề cập đến sự mơ hồ của --avs -(-a), cả hai đều hợp lệ về mặt cú pháp nhưng có ngữ nghĩa khác nhau.
Ruslan

49

Tôi không thể nghĩ ra lý do tại sao các nhà thiết kế chọn đi chệch khỏi nguyên tắc rằng đơn là bitwise và double là logic ở đây,

Đó không phải là nguyên tắc ở nơi đầu tiên; một khi bạn nhận ra điều đó, nó có ý nghĩa hơn.

Cách tốt hơn để nghĩ về &vs &&không phải là nhị phânBoolean . Cách tốt hơn là nghĩ về họ như háo hứclười biếng . Các &nhà điều hành thực hiện ở phía bên trái và bên phải và sau đó tính toán kết quả. Các &&nhà điều hành thực hiện ở phía bên trái, và sau đó thực hiện phía bên phải chỉ khi cần thiết để tính toán kết quả.

Hơn nữa, thay vì nghĩ về "nhị phân" và "Boolean", hãy nghĩ về những gì đang thực sự xảy ra. Phiên bản "nhị phân" chỉ thực hiện thao tác Boolean trên một mảng Booleans đã được đóng gói thành một từ .

Vì vậy, hãy đặt nó cùng nhau. Liệu nó có ý nghĩa gì khi thực hiện một thao tác lười biếng trên một mảng Booleans ? Không, bởi vì không có "bên trái" để kiểm tra trước. Có 32 "bên trái" để kiểm tra đầu tiên. Vì vậy, chúng tôi giới hạn các hoạt động lười biếng trong một Boolean duy nhất và đó là nơi mà trực giác của bạn rằng một trong số chúng là "nhị phân" và một là "Boolean" đến từ đó, nhưng đó là hệ quả của thiết kế, chứ không phải bản thân thiết kế!

Và khi bạn nghĩ về nó theo cách đó, nó trở nên rõ ràng tại sao không có !!và không có ^^. Cả hai toán tử này đều có thuộc tính mà bạn có thể bỏ qua việc phân tích một trong các toán hạng; không có "lười biếng" nothay xor.

Các ngôn ngữ khác làm cho điều này rõ ràng hơn; một số ngôn ngữ dùng andđể chỉ "háo hức và" nhưng and alsocó nghĩa là "lười biếng và" chẳng hạn. Và các ngôn ngữ khác cũng làm cho nó rõ ràng hơn &&&không phải là "nhị phân" và "Boolean"; trong C # chẳng hạn, cả hai phiên bản có thể lấy Booleans làm toán hạng.


2
Cảm ơn bạn. Đây là cái mở mắt thực sự cho tôi. Quá tệ, tôi không thể chấp nhận hai câu trả lời.
Martin Maat

10
Tôi không nghĩ rằng đây là một cách tốt để nghĩ &&&. Mặc dù sự háo hức là một trong những khác biệt giữa &&&, &hành xử hoàn toàn khác với một phiên bản háo hức &&, đặc biệt là trong các ngôn ngữ &&hỗ trợ các loại khác với loại boolean chuyên dụng.
user2357112 hỗ trợ Monica

14
Ví dụ, trong C và C ++, 1 & 2có kết quả hoàn toàn khác với 1 && 2.
user2357112 hỗ trợ Monica

7
@ZizyArcher: Như tôi đã lưu ý trong nhận xét ở trên, quyết định bỏ qua một boolloại trong C có tác dụng kích thích. Chúng ta cần cả hai !~bởi vì một nghĩa là "coi một int là một Boolean duy nhất" và một nghĩa là "coi một int là một mảng Booleans đóng gói". Nếu bạn có các kiểu bool và int riêng biệt thì bạn có thể chỉ có một toán tử, theo ý kiến ​​của tôi sẽ là thiết kế tốt hơn, nhưng chúng ta đã trễ 50 năm với cái đó. C # bảo tồn thiết kế này cho quen thuộc.
Eric Lippert

3
@Steve: Nếu câu trả lời có vẻ ngớ ngẩn thì tôi đã đưa ra một cuộc tranh luận được thể hiện kém ở đâu đó, và chúng ta không nên dựa vào một cuộc tranh luận từ chính quyền. Bạn có thể nói thêm về những gì có vẻ ngớ ngẩn về nó?
Eric Lippert

21

TL; DR

C được thừa hưởng !và các ~toán tử từ ngôn ngữ khác. Cả hai &&||được thêm vào nhiều năm sau bởi một người khác.

Câu trả lời dài

Trong lịch sử, C đã phát triển từ các ngôn ngữ B ban đầu, dựa trên BCPL, dựa trên CPL, dựa trên Algol.

Algol , ông cố của C ++, Java và C #, đã định nghĩa đúng và sai theo cách cảm nhận trực quan cho các lập trình viên: các giá trị chân lý, được coi là số nhị phân (đúng tương ứng với 1 và sai thành 0), là giống như giá trị tích phân nội tại. Tuy nhiên, một nhược điểm của điều này là logic và bitwise không thể hoạt động giống nhau: Trên bất kỳ máy tính hiện đại nào, ~0bằng -1 thay vì 1 và ~1bằng -2 thay vì 0. (Ngay cả trên một máy tính lớn sáu mươi tuổi có ~0đại diện - 0 hoặc INT_MIN, ~0 != 1trên mọi CPU từng được tạo ra, và tiêu chuẩn ngôn ngữ C đã yêu cầu nó trong nhiều năm, trong khi hầu hết các ngôn ngữ con gái của nó thậm chí không bận tâm đến việc hỗ trợ ký hiệu và cường độ của một người.)

Algol đã giải quyết vấn đề này bằng cách có các chế độ khác nhau và diễn giải các toán tử khác nhau trong chế độ boolean và tích phân. Đó là, một phép toán bitwise là một trên các kiểu số nguyên và một phép toán logic là một phép toán trên các kiểu boolean.

BCPL có một kiểu boolean riêng, nhưng một nottoán tử đơn , cho cả bitwise và logic thì không. Cách tiền thân đầu tiên này của C thực hiện công việc đó là:

Rvalue của true là một mẫu bit hoàn toàn bao gồm các mẫu; Giá trị sai là 0.

Lưu ý rằng true = ~ false

(Bạn sẽ quan sát rằng thuật ngữ rvalue đã phát triển có nghĩa là một cái gì đó hoàn toàn khác trong ngôn ngữ của gia đình C.

Định nghĩa này sẽ cho phép logic và bitwise không sử dụng cùng một hướng dẫn ngôn ngữ máy. Nếu C đã đi theo con đường đó, các tập tin tiêu đề trên toàn thế giới sẽ nói #define TRUE -1.

Nhưng ngôn ngữ lập trình B là một cách yếu ớt, đánh máy, và không có kiểu boolean hoặc thậm chí dấu chấm động. Mọi thứ đều tương đương intvới người kế nhiệm của nó, C. Điều này khiến ngôn ngữ nên xác định điều gì đã xảy ra khi một chương trình sử dụng một giá trị khác với đúng hoặc sai làm giá trị logic. Đầu tiên, nó định nghĩa một biểu thức trung thực là không bằng 0. Nó hoạt động hiệu quả trên các máy tính mini mà nó chạy, có cờ không CPU.

Vào thời điểm đó, có một giải pháp thay thế: các CPU tương tự cũng có cờ âm và giá trị thật của BCPL là -1, vì vậy B có thể đã định nghĩa tất cả các số âm là trung thực và tất cả các số không âm là giả. (Có một phần còn lại của phương pháp này: nhiều cuộc gọi hệ thống trong UNIX, được phát triển bởi cùng một người cùng một lúc, định nghĩa tất cả các mã lỗi là số nguyên âm. Nhiều cuộc gọi hệ thống của nó trả về một trong nhiều giá trị âm khác nhau khi thất bại.) hãy biết ơn: nó có thể tồi tệ hơn

Nhưng định nghĩa TRUEnhư 1FALSEnhư 0trong B có nghĩa là bản sắc true = ~ falsekhông còn được tổ chức, và nó đã đánh rơi gõ mạnh cho phép Algol để disambiguate giữa Bitwise và biểu thức logic. Điều đó đòi hỏi một toán tử không logic mới và các nhà thiết kế đã chọn !, có thể vì chưa bằng - đã có !=, trông giống như một thanh dọc thông qua một dấu bằng. Họ đã không tuân theo quy ước tương tự &&hoặc ||bởi vì chưa có ai tồn tại.

Có thể cho rằng, họ nên có: &toán tử trong B bị hỏng như thiết kế. Trong B và C, 1 & 2 == FALSEmặc dù 12cả hai đều là giá trị trung thực và không có cách trực quan nào để diễn tả hoạt động logic trong B. Đó là một sai lầm mà C đã cố gắng khắc phục một phần bằng cách thêm &&||, nhưng mối quan tâm chính lúc đó là cuối cùng có được ngắn mạch để làm việc và làm cho các chương trình chạy nhanh hơn. Bằng chứng của điều này là không có ^^: 1 ^ 2là một giá trị trung thực mặc dù cả hai toán hạng của nó là trung thực, nhưng nó không thể hưởng lợi từ ngắn mạch.


4
+1. Tôi nghĩ rằng đây là một tour du lịch có hướng dẫn khá tốt xung quanh sự phát triển của các nhà khai thác này.
Steve

BTW, ký hiệu / cường độ và các máy bổ sung của một người cũng cần phân tách bit bit so với phủ định logic, ngay cả khi đầu vào đã được booleanized. ~0(tất cả các bit được đặt) là một số không âm bổ sung (hoặc biểu diễn bẫy). Dấu hiệu / cường độ ~0là một số âm với cường độ tối đa.
Peter Cordes

@PeterCordes Bạn hoàn toàn đúng. Tôi chỉ tập trung vào hai máy bổ sung vì chúng quan trọng hơn nhiều. Có lẽ nó đáng giá một chú thích.
Davislor

Tôi nghĩ rằng nhận xét của tôi là đủ, nhưng vâng, có thể là một phụ huynh (không hoạt động cho phần bù 1 hoặc dấu / độ lớn) sẽ là một chỉnh sửa tốt.
Peter Cordes
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.