ค่าสัมบูรณ์ที่ติดลบ และผลหารของสองค่าลบที่ยังติดลบ
ยอดคืนเงินของออเดอร์ใบหนึ่งมาถึง service ของคุณเป็น -2147483648 ซึ่งเป็นค่าที่ลบที่สุด
ที่ int เก็บได้ ค่านี้มาถึงได้จริง เพราะการลบที่ล้นในบทที่ 1 ก็ผลิตค่าในช่วงนี้ออกมาได้
ท่อของคุณทำสิ่งที่ดูปลอดภัยที่สุดเท่าที่จะทำได้ คือเรียก Math.abs เพื่อบังคับให้ยอดคืนเงิน
เป็นขนาดที่ไม่ติดลบ ก่อนเขียนลงบัญชี
Math.abs(Integer.MIN_VALUE) คืน -2147483648 และไม่โยนอะไรออกมาเลย บัญชีจึงได้
ขนาดที่ติดลบไปหนึ่งแถว โดยไม่มีบรรทัด log สักบรรทัดที่บอกว่ามีอะไรผิด
orderctl คือเครื่องมือประมวลผลออเดอร์ที่คอร์สภาษาในเสานี้สร้างซ้ำทีละภาษา repo ตัวอย่าง
กำลังจัดทำ ระหว่างนี้ทุกตัวเลขในบทนี้รันเองได้ด้วย npm run verify:integer-semantics
ซึ่งเรียก python3 scripts/integer-semantics/probe.py · ไม่ต้องมี Docker และไม่ต้องต่อเน็ต
ค่าที่ไม่มีคู่บวกของตัวเอง
หัวข้อที่มีชื่อว่า “ค่าที่ไม่มีคู่บวกของตัวเอง”ฝั่งลบของ 32 bit มีค่ามากกว่าฝั่งบวกอยู่หนึ่งค่า และค่าที่เกินมานั้นคือค่าที่ลบที่สุด
two’s complementtwo's complementการเข้ารหัสจำนวนเต็มมีเครื่องหมายที่ฝั่งลบมีค่ามากกว่าฝั่งบวกอยู่หนึ่งค่า · ค่าที่ลบที่สุดจึงไม่มีคู่บวกของตัวเอง และนั่นคือเหตุผลที่ค่าสัมบูรณ์ของมันยังติดลบ คือการเข้ารหัสจำนวนเต็มมีเครื่องหมายที่ int ของ Java
int32 ของ Go และ i32 ของ Rust ใช้ร่วมกัน ความไม่สมมาตรของมันอธิบายได้ด้วยการนับช่องล้วนๆ
int 32 bit แบ่งรูปแบบ bit ทั้งหมดออกเป็นสองฝั่ง และศูนย์กินไปหนึ่งช่องจากฝั่งที่ไม่ติดลบ
- ฝั่งที่ไม่ติดลบเหลือช่องให้ค่าบวกเพียง
2147483647ค่า - ฝั่งลบได้ไปเต็มๆ
2147483648ค่า
ผลคือค่าที่ลบที่สุดเป็น -2147483648 และค่าบวกที่ใหญ่ที่สุดเป็น 2147483647 สองค่านี้
ต่างกันอยู่หนึ่ง ค่าที่ลบที่สุดจึงเป็นค่าเดียวในชนิดนี้ที่ไม่มีคู่บวกของตัวเอง
เมื่อคู่บวกไม่มีอยู่ คำถามว่า “ค่าสัมบูรณ์ของมันคืออะไร” ก็ไม่มีคำตอบที่อยู่ในชนิดเดิม สิ่งที่แต่ละภาษาทำต่อจากตรงนั้นคือเนื้อของบทนี้ และทางเลือกมีอยู่สี่แบบเท่านั้น
สิบสองทรง วัดทีละทรง
หัวข้อที่มีชื่อว่า “สิบสองทรง วัดทีละทรง”สิบสองแถวนี้มีสองแถวที่หยุดโปรแกรมเอง สองแถวที่คืนสัญญาณให้คนที่ขอ ที่เหลือคืนค่าแล้วเดินต่อ
ทุกแถวถามคำถามเดียวกัน คือให้ค่าที่ลบที่สุดของจำนวนเต็ม 32 bit แล้วขอค่าสัมบูรณ์ ผลหาร
ด้วย -1 หรือเศษของการหารนั้น
| การเรียก | ผลที่วัดได้ |
|---|---|
Java Math.abs(Integer.MIN_VALUE) | -2147483648 ไม่มี exception |
Java Integer.MIN_VALUE / -1 | -2147483648 ไม่มี exception |
Java Integer.MIN_VALUE % -1 | 0 |
Java Math.absExact(Integer.MIN_VALUE) | โยน ArithmeticException |
Go int32 ค่าต่ำสุด / -1 | -2147483648 |
Go int32 ค่าต่ำสุด % -1 | 0 |
Rust i32::MIN.abs() · rustc absmin.rs | รหัสออก 101 · (panic) |
Rust i32::MIN.abs() · rustc -O absmin.rs | รหัสออก 0 · -2147483648 |
Rust i32::MIN.checked_abs() | None |
Rust i32::MIN.checked_div(-1) | None |
Rust i32::MIN.wrapping_abs() | -2147483648 |
Python abs(-2**31) | 2147483648 |
สามแถวแรกกับสองแถวของ Go เป็นห้าช่องที่เงียบสนิท ไม่มีช่องไหนในห้าช่องนี้โยน exception ไม่มีช่องไหนคืนสัญญาณ และไม่มีช่องไหนเขียนอะไรลง stderr
main.go ของ probe ประกาศ var mn int32 = -2147483648 กับ var neg int32 = -1 แล้วจึง
หาร ส่วน shapes.rs ห่อค่าที่มันวัดด้วย std::hint::black_box ก่อนใช้
เหตุผลเดียวกับกับดักของ literal ในบทที่ 1 คือสองภาษานี้ตัดสินการหารที่เห็นค่าทั้งคู่ล่วงหน้า ตั้งแต่ compile การวัดจึงต้องพาค่าข้ามพรมแดนของ runtime มาก่อน
กับดักนี้ผูกกับนิพจน์ ไม่ใช่กับภาษา absmin.rs ในหัวข้อถัดไปเขียน i32::MIN ลงไปตรงๆ
แล้วยังได้รหัสออก 101 อยู่ดี
เศษที่เป็นศูนย์ ไม่ได้แปลว่าไม่มีอะไรเกิดขึ้น
หัวข้อที่มีชื่อว่า “เศษที่เป็นศูนย์ ไม่ได้แปลว่าไม่มีอะไรเกิดขึ้น”Integer.MIN_VALUE % -1 ได้ 0 ซึ่งเป็นคำตอบที่ไม่ทำให้ใครสะดุด จนกระทั่งรู้ว่าการหาร
ที่อยู่คู่กับมันนั้นล้น
JLS หัวข้อ 15.17.3 ผูกสองค่านี้เข้าด้วยกันด้วยเอกลักษณ์ (a/b)*b+(a%b) ที่ต้องเท่ากับ a
แล้วระบุตรงๆ ว่าเอกลักษณ์นี้ยังเป็นจริงในกรณีพิเศษนี้ โดยที่เศษเป็น 0
เศษที่ถูกจึงอยู่ร่วมกับผลหารที่ผิดได้ในสมการเดียวกัน คนที่ตรวจว่า % -1 ให้ 0 แล้วสรุปว่า
การหารนั้นเรียบร้อย กำลังอ่านครึ่งเดียวของสิ่งที่สเปกเขียนไว้
สองที่ที่หยุดเอง และยามที่ต้องเรียกชื่อ
หัวข้อที่มีชื่อว่า “สองที่ที่หยุดเอง และยามที่ต้องเรียกชื่อ”สองที่แรกหยุดโปรแกรมโดยไม่ต้องขอ · ส่วนยามที่เหลือทำงานก็ต่อเมื่อคุณเรียกชื่อมัน
Java — absExact โยน ไม่ใช่ abs
หัวข้อที่มีชื่อว่า “Java — absExact โยน ไม่ใช่ abs”Math.abs กับ Math.absExact รับค่าเดียวกัน แต่ตอบคนละอย่าง ตัวแรกคืน -2147483648
ตัวที่สองโยนออกมาทั้งบรรทัด
java.lang.ArithmeticException: Overflow to represent absolute value of Integer.MIN_VALUEข้อความนี้ระบุกลไกไว้ในตัวเอง คือมันบอกว่าการแทนค่าสัมบูรณ์ต่างหากที่ล้น ไม่ใช่ตัวอาร์กิวเมนต์
ที่ผิด ซึ่งเป็นข้อมูลที่ abs ไม่ได้ให้เลยสักตัว
หน้า Math ของ Java 25 API เขียนความต่างข้อนี้ไว้ที่ entry ของ abs เอง โดยบอกว่าเมื่อ
อาร์กิวเมนต์เท่ากับ Integer.MIN_VALUE ผลลัพธ์คือค่าเดิมนั้น ซึ่งติดลบ และ absExact
โยน ArithmeticException ให้ค่าเดียวกันนี้
Rust — สวิตช์ตัวเดิมของบทที่ 1 กับนิพจน์คนละตัว
หัวข้อที่มีชื่อว่า “Rust — สวิตช์ตัวเดิมของบทที่ 1 กับนิพจน์คนละตัว”absmin.rs มีบรรทัดเดียว และมันคือทรงเดียวกับ a.rs ของบทที่ 1 คือ source ชุดเดียว
compiler ตัวเดียว ต่างกันที่ธงตัวเดียว
fn main() { println!("{}", i32::MIN.abs()); }rustc absmin.rsได้รหัสออก 101 และไม่มีอะไรออกทาง stdoutrustc -O absmin.rsได้รหัสออก 0 และ stdout เป็น-2147483648
บรรทัดข้อความของ panic บอกกลไกไว้ตรงตัว คือขั้นที่ล้นไม่ใช่การหาค่าสัมบูรณ์ แต่เป็นการใส่ เครื่องหมายลบให้ค่าที่ลบที่สุด
attempt to negate with overflowบรรทัดที่อยู่เหนือมันมีหมายเลขของ thread ซึ่งเปลี่ยนทุกครั้งที่รัน probe จึงผูกไว้ที่บรรทัด
ข้อความบรรทัดนี้บรรทัดเดียว เหมือนที่บทที่ 1 ทำกับ attempt to subtract with overflow
เอกสารของ i32::abs เขียนพฤติกรรมนี้ไว้ในหัวข้อ Overflow behavior ของมันเอง โดยบอกว่า
ค่าสัมบูรณ์ของ i32::MIN แทนด้วย i32 ไม่ได้ การคำนวณมันจึงล้น
ยามที่คืนค่า แทนที่จะหยุดโปรแกรม
หัวข้อที่มีชื่อว่า “ยามที่คืนค่า แทนที่จะหยุดโปรแกรม”checked_abs กับ checked_div ตอบคำถามเดียวกันโดยไม่ต้องพึ่งธงเลย ทั้งคู่คืน None
ซึ่งเป็นค่าที่ผู้เรียกต้องแกะออกมาก่อนใช้
i32::MIN.checked_abs()ได้Nonei32::MIN.checked_div(-1)ได้Nonei32::MIN.wrapping_abs()ได้-2147483648ซึ่งเป็นการประกาศว่าจะวนกลับ
สามทรงนี้ต่างกันที่ผู้เขียนเป็นคนเลือก ไม่ใช่ธงตอน compile เป็นคนเลือกให้ wrapping_abs
ให้ค่าเท่ากับ abs ที่ปิดการตรวจไว้พอดี แต่มันเขียนความตั้งใจนั้นลงไปในชื่อ
ยามทั้งสามแบบมีราคาต่างกัน และไม่มีแบบไหนฟรี
absExactกับchecked_absบังคับให้ผู้เรียกจัดการกรณีที่ทำไม่ได้ ซึ่งแปลว่าทุกจุดเรียก ยาวขึ้น และ path ของ error เพิ่มขึ้นมาหนึ่งเส้นต่อหนึ่งการคำนวณ-C overflow-checks=onครอบทุกการคำนวณในทั้ง binary จึงต้องจ่ายค่าตรวจแม้ในจุดที่ พิสูจน์แล้วว่าล้นไม่ได้absเปล่าๆ ไม่มีราคาตอนรัน ราคาของมันคือแถวที่ติดลบในบัญชี ซึ่งจ่ายทีหลังและจ่ายแพงกว่า
Python ไม่มีความกว้างให้หมด
หัวข้อที่มีชื่อว่า “Python ไม่มีความกว้างให้หมด”ภาษาเดียวในตารางที่ตอบเป็นค่าบวก เพราะมันไม่มีขอบให้ชน
abs(-2**31) ใน Python ได้ 2147483648 ซึ่งเป็นคำตอบทางคณิตศาสตร์ที่ถูกต้อง และเป็น
ค่าที่อีกสามภาษาในตารางแทนใน 32 bit ไม่ได้เลย
เหตุผลไม่ใช่ว่า Python ระวังกว่า แต่เป็นว่าจำนวนเต็มของมันไม่มีความกว้างตายตัวตั้งแต่แรก คำถามเรื่องค่าที่ไม่มีคู่บวกจึงไม่เกิดขึ้น
flowchart TD
A["ขอค่าสัมบูรณ์ของค่าที่ลบที่สุด หรือหารมันด้วย -1"] --> B{"ชนิดปลายทางมีความกว้างไหม"}
B -->|"ไม่มี · Python"| C["2147483648 · ค่าบวกตามคณิตศาสตร์"]
B -->|"มี 32 bit"| D{"เลือกการกระทำแบบไหน"}
D -->|"Math.abs · การหารเปล่าๆ · wrapping_abs · abs ที่ปิดการตรวจ"| E["-2147483648 · เงียบสนิท"]
D -->|"Math.absExact"| F["โยน ArithmeticException"]
D -->|"abs ที่เปิดการตรวจ"| G["panic · รหัสออก 101"]
D -->|"checked_abs · checked_div"| H["None"]
คำบรรยายภาพ: ช่องที่ตัดสินไม่ใช่ชื่อภาษา แต่เป็นสองช่องสี่เหลี่ยมข้าวหลามตัด คือชนิดปลายทาง มีความกว้างหรือไม่ และผู้เขียนเลือกการกระทำตัวไหน · แถวของเศษอยู่นอกแผนภาพนี้ เพราะคำตอบ ของมันถูกต้องอยู่แล้ว
จุดที่สองในห้าจุด
หัวข้อที่มีชื่อว่า “จุดที่สองในห้าจุด”บทนี้ปิดจุดที่สอง และรูปของมันเหมือนจุดที่หนึ่งทุกอย่าง คือค่าเปลี่ยนแล้วไม่มีอะไรแดง
ธีสิสของคอร์สนี้คือ ค่าจำนวนเต็มค่าหนึ่งเปลี่ยนได้ห้าจุดระหว่างทางผ่านระบบ และทุกจุด ในห้าจุดนั้นเงียบ จุดที่หนึ่งคือตอนความกว้างหมด และบทที่ 1 กับบทที่ 2 วัดมันไปแล้ว
จุดที่สองคือจุดที่บทนี้เพิ่งวัด คือตอนที่คำตอบที่ถูกต้องอยู่นอกชนิดเดิมไปหนึ่งค่าพอดี สิ่งที่ ตัดสินว่าคุณจะรู้หรือไม่รู้ คือชื่อการกระทำที่คุณเรียก ไม่ใช่ชื่อภาษาที่คุณใช้
ความเสียหายของจุดนี้ในโดเมนออเดอร์อ่านได้สองแบบ และทั้งสองแบบผ่าน validation ที่ตรวจ เฉพาะชนิดได้สบาย
- ยอดคืนเงินที่ผ่าน
Math.absแล้วยังติดลบ ถูกเขียนลงบัญชีเป็นขนาดที่ติดลบ - order id ที่หารด้วย
-1เพื่อพลิกเครื่องหมาย ได้ค่าเดิมกลับมา แล้วชี้ไปที่แถวเดิม
บทที่ 4 อยู่ที่จุดที่สามคือการหาร และเปลี่ยนคำถามจากค่าที่ลบที่สุดเป็นค่าลบธรรมดา ซึ่งพอเปลี่ยน แล้วสิ่งที่แยกภาษาออกจากกันไม่ใช่การล้นอีกต่อไป แต่เป็นทิศที่แต่ละภาษาปัดผลหาร และเครื่องหมาย ที่มันให้กับเศษ
-
Math (Java SE 25 & JDK 25) (เข้าถึง 2026-09-13) — entry ของ
abs(int)ระบุว่าเมื่ออาร์กิวเมนต์เท่ากับInteger.MIN_VALUEผลลัพธ์คือค่าเดิมซึ่งติดลบ · entry ของabsExact(int)ระบุว่าช่วง ของ two’s complement ไม่สมมาตรโดยมีค่าลบเกินมาหนึ่งค่า จึงโยนArithmeticException -
JLS SE 25 — §15.17.2 Division Operator / และ §15.17.3 Remainder Operator % (เข้าถึง 2026-09-13) — §15.17.2 เขียนกรณีพิเศษไว้ว่าเมื่อตัวตั้งเป็นจำนวนลบที่มีขนาดใหญ่ ที่สุดของชนิดนั้น และตัวหารเป็น
-1การล้นเกิดขึ้นและผลลัพธ์เท่ากับตัวตั้ง โดยไม่มี exception ถูกโยนออกมา · §15.17.3 ระบุว่าเอกลักษณ์ของเศษยังเป็นจริงในกรณีนั้น และเศษเป็น0 -
i32 — Rust (เข้าถึง 2026-09-13) — หัวข้อ Overflow behavior ของ
absระบุว่าค่าสัมบูรณ์ของi32::MINแทนด้วยi32ไม่ได้ ·checked_absระบุว่าคืนNoneเมื่อself == MIN·checked_divระบุว่าคืนNoneเมื่อตัวหารเป็น0หรือการหารนั้นล้น -
ตัวเลขทุกตัวในบทนี้มาจาก
scripts/integer-semantics/probe.pyซึ่งวัดทั้งชุดสามรอบ ต่อการเรียกหนึ่งครั้ง · วัดบนrustc 1.97.1go1.27.0JDK25.0.4.1และPython 3.14.6บน darwin/arm64 เมื่อ 2026-09-12
เช็กความเข้าใจ — บทที่ 3
ข้อ 1 / 3เรียก `Math.abs(Integer.MIN_VALUE)` ใน Java 25 แล้วได้อะไร