ทำไมโอเปอเรเตอร์ NOT แบบลอจิคัลในภาษา C-style“!” และไม่ใช่“ ~~”?


39

สำหรับตัวดำเนินการไบนารีเรามีตัวดำเนินการทั้งสองบิตและตรรกะ:

& bitwise AND
| bitwise OR

&& logical AND
|| logical OR

ไม่ใช่ (โอเปอเรเตอร์ unary) ทำงานแตกต่างกัน มี ~ สำหรับ bitwise และ! สำหรับตรรกะ

ฉันจำไม่ได้ว่าเป็นการดำเนินการที่ไม่เป็นเอกเทศเมื่อเทียบกับ AND และหรือ แต่ฉันไม่สามารถคิดได้ว่าทำไมนักออกแบบเลือกที่จะเบี่ยงเบนจากหลักการที่ว่าซิงเกิลเป็น bitwise และ Double นั้นมีเหตุผลที่นี่และเปลี่ยนไปใช้ตัวละครอื่นแทน ฉันเดาว่าคุณสามารถอ่านมันผิดเช่นการดำเนินการสองบิตที่จะคืนค่าตัวถูกดำเนินการ แต่นั่นไม่ได้เป็นปัญหาที่แท้จริงสำหรับฉัน

มีเหตุผลที่ฉันขาดหายไปหรือไม่?


7
เพราะ ... ถ้า !! ไม่ได้เป็นตรรกะฉันจะเปลี่ยน 42 เป็น 1 ได้อย่างไร :)
candied_orange

9
จะ~~แล้วไม่ได้รับความสอดคล้องกันมากขึ้นสำหรับตรรกะไม่ได้ถ้าคุณทำตามรูปแบบที่ผู้ประกอบการตรรกะเป็นสองเท่าของผู้ประกอบการระดับบิตหรือไม่
Bart van Ingen Schenau

9
ขั้นแรกถ้าเป็นเพื่อความมั่นคงมันจะเป็น ~ และ ~~ การเพิ่มและและหรือเกี่ยวข้องกับการลัดวงจร และตรรกะไม่ได้มีไฟฟ้าลัดวงจร
Christophe

3
ฉันสงสัยว่าเหตุผลการออกแบบที่สำคัญคือความชัดเจนและความแตกต่างในกรณีการใช้งานทั่วไป ตัวดำเนินการไบนารี (นั่นคือสองตัวถูกดำเนินการ) เป็นมัด (และมีแนวโน้มที่จะถูกคั่นด้วยช่องว่าง) ในขณะที่ตัวดำเนินการยูนารีเป็นคำนำหน้า (และมีแนวโน้มที่จะไม่เว้นระยะ)
Steve

7
ในฐานะที่เป็นความคิดเห็นบางส่วนได้พูดพาดพิงไปแล้ว (และสำหรับผู้ที่ไม่ต้องการที่จะทำตามลิงค์นี้ , !!fooคือไม่ผิดปกติ (ไม่ได้ไม่ธรรมดา) สำนวนมัน normalizes อาร์กิวเมนต์ศูนย์หรือภัณฑ์เพื่อ?. 0หรือ1.
คี ธ ธ อมป์สัน

คำตอบ:


108

น่าแปลกที่ประวัติศาสตร์ของภาษาซีสไตล์ไม่ได้ขึ้นต้นด้วย C

เดนนิสริตชี่อธิบายความท้าทายของการเกิดของซีในบทความนี้ได้เป็นอย่างดี

เมื่ออ่านมันจะเห็นได้ชัดว่า C สืบทอดส่วนหนึ่งของการออกแบบภาษาจากBCPLรุ่นก่อนโดยเฉพาะอย่างยิ่งผู้ปฏิบัติงาน ส่วน“ทารกแรกเกิด C” ของบทความดังกล่าวอธิบายถึงวิธีการ BCPL ของ&และ|ถูกอุดมไปด้วยสองผู้ประกอบการใหม่และ&& ||เหตุผลคือ:

  • ต้องการลำดับความสำคัญที่แตกต่างกันเนื่องจากการใช้งานร่วมกับ ==
  • ตรรกะการประเมินผลที่แตกต่างกันจากซ้ายไปขวาประเมินผลด้วยการลัดวงจร (เช่นเมื่อaเป็นfalseในa&&b, bไม่ได้รับการประเมิน)

เป็นที่น่าสนใจว่าการเสแสร้งนี้ไม่ได้สร้างความกำกวมให้กับผู้อ่าน: a && bจะไม่ถูกตีความอย่างผิดa(&(&b))ๆ จากมุมมองการแยกวิเคราะห์ไม่มีความกำกวมอย่างใดอย่างหนึ่ง: &bสามารถทำให้รู้สึกว่าbเป็น lvalue แต่มันจะเป็นตัวชี้ในขณะที่ bitwise &จะต้องมีตัวถูกดำเนินการจำนวนเต็มดังนั้นตรรกะและจะเป็นทางเลือกที่สมเหตุสมผลเท่านั้น

BCPL ใช้~สำหรับการปฏิเสธแบบ bitwise แล้ว ดังนั้นจากมุมมองของความมั่นคงมันอาจเป็นสองเท่าที่จะให้ a ~~เพื่อให้มันมีความหมายเชิงตรรกะ แต่น่าเสียดายที่นี้จะได้รับคลุมเครือมากตั้งแต่~เป็นผู้ประกอบการเอกภาค: ยังอาจหมายถึง~~b ~(~b))นี่คือเหตุผลที่จะต้องเลือกสัญลักษณ์อื่นสำหรับการปฏิเสธที่หายไป


10
โปรแกรมแยกวิเคราะห์ไม่สามารถแยกแยะความแตกต่างระหว่างสองสถานการณ์ได้ดังนั้นผู้ออกแบบภาษาต้องทำเช่นนั้น
BobDalgleish

16
@Steve: แน่นอนว่ามีปัญหาที่คล้ายกันอยู่แล้วในภาษา C และ C เมื่อ parser เห็น(t)+1ว่ามีการเพิ่ม(t)และ1หรือมันเป็นของ+1การพิมพ์tหรือไม่ การออกแบบ C ++ ต้องแก้ปัญหาของวิธีการเทมเพลต lex ที่มี>>อย่างถูกต้อง และอื่น ๆ
Eric Lippert

6
@ user2357112 ฉันคิดว่าประเด็นก็คือมันไม่เป็นไรที่จะมี tokenizer สุ่มสี่สุ่มห้า&&เป็น&&โทเค็นเดียวและไม่เป็นสอง&โทเค็นเนื่องจากการa & (&b)ตีความไม่ใช่สิ่งที่สมเหตุสมผลในการเขียนดังนั้นมนุษย์จะไม่เคยรู้สึกอย่างนั้นและประหลาดใจด้วย a && bคอมไพเลอร์รักษามันเป็น ในขณะที่ทั้งสอง!(!a)และ!!aเป็นสิ่งที่เป็นไปได้สำหรับมนุษย์ที่จะหมายถึงดังนั้นมันเป็นความคิดที่ดีสำหรับคอมไพเลอร์ในการแก้ไขความคลุมเครือด้วยกฎระดับโทเค็นโดยพลการ
เบ็น

17
!!ไม่เพียง แต่เป็นไปได้ / สมเหตุสมผลในการเขียนเท่านั้น แต่ยังเป็นสำนวนที่แปลงให้เป็นแบบบูล
..

4
ฉันคิดว่า dan04 หมายถึงความกำกวมของ--avs -(-a)ซึ่งทั้งสองอย่างนี้มีความถูกต้องทางวากยสัมพันธ์ แต่มีความหมายต่างกัน
Ruslan

49

ฉันไม่สามารถคิดถึงเหตุผลที่นักออกแบบเลือกที่จะเบี่ยงเบนจากหลักการที่ว่าซิงเกิลเป็น bitwise และ double เป็นตรรกะที่นี่

นั่นไม่ใช่หลักการในตอนแรก เมื่อคุณตระหนักว่ามันเหมาะสมแล้ว

วิธีที่ดีกว่าที่จะคิดว่า&เทียบ&&ไม่ได้เป็นไบนารีและบูลีน วิธีที่ดีคือการคิดว่าพวกเขาเป็นความกระตือรือร้นและขี้เกียจ &ผู้ประกอบการดำเนินการด้านซ้ายและขวาแล้วคำนวณผล &&ผู้ประกอบการดำเนินการด้านซ้ายและจากนั้นดำเนินการทางด้านขวาเท่านั้นหากมีความจำเป็นในการคำนวณผล

ยิ่งไปกว่านั้นแทนที่จะคิดถึง "ไบนารี" และ "บูลีน" ลองคิดถึงสิ่งที่เกิดขึ้นจริง รุ่น "ฐาน" เป็นเพียงการทำการดำเนินการบูลีนในอาร์เรย์ของ Booleans ที่ได้รับการบรรจุลงในคำหนึ่ง

งั้นเรามารวมกัน มันสมเหตุสมผลหรือไม่ที่จะทำกิจการขี้เกียจกับกลุ่มบูลีน ? ไม่เพราะไม่มี "ด้านซ้าย" ให้ตรวจสอบก่อน มี 32 "ด้านซ้าย" เพื่อตรวจสอบก่อน ดังนั้นเราจึง จำกัด การทำงานแบบขี้เกียจไว้ที่บูลีนเดี่ยวและนั่นคือจุดที่คุณเข้าใจว่าหนึ่งในนั้นคือ "ไบนารี่" และอีกอันคือ "บูลีน" มาจาก แต่นั่นเป็นผลมาจากการออกแบบไม่ใช่การออกแบบเอง!

และเมื่อคุณคิดว่ามันเป็นอย่างนั้นมันจะกลายเป็นที่ชัดเจนว่าทำไมไม่มีและไม่มีการ!! ^^ตัวดำเนินการเหล่านั้นไม่มีคุณสมบัติที่คุณสามารถข้ามการวิเคราะห์หนึ่งในตัวถูกดำเนินการ ไม่มี "ขี้เกียจ" หรือnotxor

ภาษาอื่นทำให้ชัดเจนยิ่งขึ้น บางภาษาใช้andเพื่อหมายถึง "กระตือรือร้นและ" แต่and alsoหมายถึง "ขี้เกียจและ" เป็นต้น และภาษาอื่น ๆ นอกจากนี้ยังทำให้มันชัดเจนมากขึ้นว่า&และ&&ไม่ได้เป็น "ฐาน" และ "บูลีน"; ใน C # เช่นทั้งสองเวอร์ชันสามารถใช้ Booleans เป็นตัวถูกดำเนินการ


2
ขอขอบคุณ. นี่คือเครื่องเปิดตาที่แท้จริงสำหรับฉัน น่าเสียดายที่ฉันไม่สามารถตอบได้สองคำตอบ
Martin Maat

10
ผมไม่คิดว่านี่เป็นวิธีที่ดีที่จะคิดและ& &&ในขณะที่ความกระตือรือร้นเป็นหนึ่งในความแตกต่างระหว่าง&และ&&, &ทำงานสมบูรณ์แตกต่างจากรุ่นที่กระตือรือร้นของ&&โดยเฉพาะอย่างยิ่งในภาษาที่&&สนับสนุนประเภทอื่น ๆ กว่าชนิดบูลีนโดยเฉพาะ
user2357112

14
ยกตัวอย่างเช่นใน C และ C ++ มีผลแตกต่างอย่างสิ้นเชิงจาก1 & 2 1 && 2
user2357112

7
@ZizyArcher: ดังที่ฉันได้กล่าวไว้ในความคิดเห็นข้างต้นการตัดสินใจที่จะละเว้นboolประเภทใน C นั้นมีผลกระทบแบบน็อคเอาต์ เราต้องการทั้งคู่!และ~เพราะมีความหมายว่า "ปฏิบัติต่อ int เหมือนกับบูลีนเดี่ยว" และอีกวิธีหนึ่งก็คือ "ปฏิบัติต่อ int ในฐานะบูลีนที่จัดเรียงอย่างแน่นหนา" หากคุณมีบูลและประเภท int แยกจากกันคุณสามารถมีโอเปอเรเตอร์เดียวได้ซึ่งในความคิดของฉันน่าจะเป็นการออกแบบที่ดีกว่า แต่เราเกือบ 50 ปีแล้ว C # รักษาการออกแบบนี้เพื่อความคุ้นเคย
Eric Lippert

3
@ สตีฟ: ถ้าคำตอบดูไร้สาระแล้วฉันได้ทำข้อโต้แย้งที่แสดงออกมาไม่ดีที่ไหนสักแห่งและเราไม่ควรที่จะพึ่งพาการโต้แย้งจากผู้มีอำนาจ คุณสามารถพูดเพิ่มเติมเกี่ยวกับสิ่งที่ดูเหมือนไร้สาระเกี่ยวกับมันได้หรือไม่
Eric Lippert

21

TL; DR

C สืบทอด!และ~ตัวดำเนินการจากภาษาอื่น ทั้งสอง&&และ||ถูกเพิ่มเข้ามาในอีกหลายปีต่อมาโดยบุคคลอื่น

คำตอบยาว ๆ

ในอดีต C พัฒนาจากภาษาต้น B ซึ่งอยู่บนพื้นฐานของ BCPL ซึ่งขึ้นอยู่กับ CPL ซึ่งขึ้นอยู่กับ Algol

Algol , ผู้ยิ่งใหญ่ของ C ++, Java และ C #, นิยามจริงและเท็จในแบบที่ทำให้รู้สึกว่าสัญชาตญาณของโปรแกรมเมอร์:“ ค่าความจริงซึ่งถือว่าเป็นเลขฐานสอง (จริงตรงกับ 1 และเท็จถึง 0), เช่นเดียวกับค่าสำคัญที่แท้จริง " อย่างไรก็ตามข้อเสียอย่างหนึ่งของเรื่องนี้ก็คือตรรกะและค่าบิตมินิไม่สามารถเป็นการดำเนินการแบบเดียวกันได้: ในคอมพิวเตอร์สมัยใหม่ใด ๆ~0เท่ากับ -1 มากกว่า 1 และ~1เท่ากับ -2 มากกว่า 0 (แม้แต่ในเมนเฟรมอายุหกสิบปีที่~0แสดงถึง - 0 หรือINT_MIN, ~0 != 1บน CPU ทุกที่เคยทำและมาตรฐานภาษา C จำเป็นต้องมีมันเป็นเวลาหลายปีในขณะที่ส่วนใหญ่ของภาษาลูกสาวของตนไม่ได้รำคาญที่ให้การสนับสนุนการลงชื่อเข้าใช้และขนาดหรือ one's-สมบูรณ์ at all.)

อัลกอลทำสิ่งนี้โดยการมีโหมดที่แตกต่างกันและการตีความตัวดำเนินการต่างกันในโหมดบูลีนและอินทิกรั นั่นคือการดำเนินการระดับบิตคือหนึ่งในประเภทจำนวนเต็มและการดำเนินการทางตรรกะเป็นหนึ่งในประเภทบูลีน

BCPL มีประเภทบูลีนแยกต่างหาก แต่เป็นnotโอเปอเรเตอร์เดียวสำหรับทั้งบิตและตรรกะไม่ใช่ วิธีที่ผู้เบิกทางก่อนหน้านี้ของ C ทำผลงานนั้นคือ:

ค่า Rvalue ของความจริงเป็นรูปแบบบิตที่ประกอบด้วยทั้งหมด Rvalue ของเท็จเป็นศูนย์

สังเกตได้ว่า true = ~ false

(คุณจะสังเกตว่าคำว่าrvalueได้พัฒนาขึ้นเพื่อหมายถึงบางสิ่งที่แตกต่างอย่างสิ้นเชิงในภาษาตระกูล C ปัจจุบันเราจะเรียกว่า "การแทนวัตถุ" ใน C. )

คำจำกัดความนี้จะอนุญาตให้ใช้ตรรกะและค่าบิตมิชชันไม่ให้ใช้คำสั่งภาษาเครื่องเดียวกัน ถ้า C #define TRUE -1ได้ไปเส้นทางที่ส่วนหัวของไฟล์ทั่วโลกจะบอกว่า

แต่ภาษาการเขียนโปรแกรม Bนั้นถูกพิมพ์อย่างอ่อนและไม่มีประเภทบูลีนหรือแม้แต่ชนิดทศนิยม ทุกสิ่งทุกอย่างมีค่าเทียบเท่าintกับตัวตายตัวแทนของตัวเอง C. สิ่งนี้ทำให้เป็นความคิดที่ดีสำหรับภาษาที่จะกำหนดว่าเกิดอะไรขึ้นเมื่อโปรแกรมใช้ค่าอื่นนอกเหนือจากจริงหรือเท็จเป็นค่าตรรกะ ก่อนอื่นนิยามนิพจน์ที่เป็นความจริงว่า“ ไม่เท่ากับศูนย์” สิ่งนี้มีประสิทธิภาพสำหรับมินิคอมพิวเตอร์ที่มันวิ่งซึ่งมีธงซีพียูเป็นศูนย์

ในขณะนั้นมีทางเลือก: ซีพียูตัวเดียวกันก็มีค่าสถานะเป็นลบและค่าความจริงของ BCPL คือ -1 ดังนั้น B อาจได้กำหนดจำนวนลบทั้งหมดเป็นความจริงและตัวเลขที่ไม่เป็นลบทั้งหมดเป็นเท็จ (มีสิ่งหนึ่งที่เหลืออยู่ของวิธีการนี้: การเรียกใช้ระบบจำนวนมากใน UNIX ที่พัฒนาโดยคนเดียวกันในเวลาเดียวกันกำหนดรหัสข้อผิดพลาดทั้งหมดเป็นจำนวนเต็มลบการโทรของระบบจำนวนมากส่งคืนหนึ่งในค่าลบที่แตกต่างกันหลายรายการ จะขอบคุณ: มันอาจจะเลวร้ายยิ่ง!

แต่กำหนดTRUEเป็น1และFALSEเป็น0ใน B หมายความว่าตัวตนtrue = ~ falseไม่ได้จัดขึ้นและมันได้ลดลงพิมพ์ที่แข็งแกร่งที่ได้รับอนุญาต Algol จะกระจ่างระหว่างค่าที่เหมาะสมและการแสดงออกเชิงตรรกะ สิ่งนั้นต้องการโอเปอเรเตอร์ที่ไม่ใช่ตรรกะใหม่และผู้ออกแบบเลือก!อาจเป็นเพราะไม่เท่ากับ - อยู่แล้ว!=ซึ่งมีลักษณะเหมือนแถบแนวตั้งผ่านเครื่องหมายเท่ากับ พวกเขาไม่ได้ทำตามแบบแผนเดียวกัน&&หรือ||เพราะยังไม่มีใครเลย

เนื้อหาที่ควรมี: ตัว&ดำเนินการใน B เสียตามที่ออกแบบไว้ ใน B และใน C 1 & 2 == FALSEถึงแม้ว่า1และ2เป็นทั้งค่าจริงและไม่มีวิธีที่ใช้งานง่ายเพื่อแสดงการดำเนินการเชิงตรรกะใน B นั่นคือข้อผิดพลาดหนึ่งที่ C พยายามที่จะแก้ไขบางส่วนโดยการเพิ่ม&&และ||แต่ข้อกังวลหลักในเวลานั้นคือ ในที่สุดทำให้การลัดวงจรทำงานและทำให้โปรแกรมทำงานได้เร็วขึ้น ข้อพิสูจน์เรื่องนี้คือไม่มี^^: 1 ^ 2เป็นคุณค่าที่แท้จริงแม้ว่าตัวถูกดำเนินการทั้งสองของมันเป็นความจริง แต่ก็ไม่สามารถได้รับประโยชน์จากการลัดวงจร


4
+1 ฉันคิดว่านี่เป็นไกด์นำเที่ยวที่ดีมากเกี่ยวกับวิวัฒนาการของผู้ให้บริการเหล่านี้
Steve

BTW, สัญญาณ / ขนาดและเครื่องประกอบของตัวเองก็ต้องการบิตทริกเกอร์ที่แยกต่างหากกับการปฏิเสธตรรกะแม้ว่าอินพุตจะถูกบูลีนแล้ว ~0(ตั้งค่าบิตทั้งหมด) เป็นศูนย์ลบหนึ่งของส่วนประกอบ (หรือการเป็นตัวแทนกับดัก) เครื่องหมาย / ขนาด~0คือจำนวนลบที่มีขนาดสูงสุด
Peter Cordes

@PeterCordes คุณพูดถูก ฉันแค่มุ่งเน้นไปที่เครื่องจักรเสริมสองอย่างเพราะมันมีความสำคัญมากกว่า อาจจะคุ้มค่าเชิงอรรถ
Davislor

ฉันคิดว่าความคิดเห็นของฉันเพียงพอ แต่ใช่บางทีอาจเป็นการสอดแทรก (ไม่ทำงานสำหรับส่วนเติมเต็มหรือเครื่องหมาย / ขนาด) อย่างใดอย่างหนึ่งจะเป็นการแก้ไขที่ดี
Peter Cordes
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.