ข้าม​ไป​ยัง​เนื้อหา

สตริง​ที่​ถูก​สั่ง​ให้​กลาย​เป็น​ตัวเลข

ลูกค้า​กด​ยืน​ยัน​ออ​เด​อร์​จาก​หน้า​เว็บ แล้ว orderId กับ total เดิน​ขึ้น​มา​ถึง service ของ​คุณ เป็น​ข้อความ​สอง​เส้น ไม่ใช่​ตัวเลข​สอง​ค่า

ช่อง​ใน​ฟอร์ม​เป็น​สตริง query parameter เป็น​สตริง และ header ก็​เป็น​สตริง ฝั่ง​ที่​ส่ง​ไม่มี​ชนิด จำนวนเต็ม​ให้​ใช้​ตั้งแต่​ต้น การ​แปลง​จึง​เป็น​คำ​สั่ง​ที่​คุณ​ต้อง​เขียน​เอง​ทุก​ครั้ง

คำถาม​ของ​บท​นี้​ไม่ใช่​ว่า​ภาษา​ไหน parse ได้​ถูก​กว่า​กัน แต่​เป็น​ว่า เมื่อ​สตริง​นั้น​ไม่​พอดี​กับ​ชนิด ปลาย​ทาง คุณ​ถือ​อะไร​อยู่​ใน​มือ​ที่​บรรทัด​ถัด​ไป คำ​ตอบ​ที่​วัด​ได้​มี​สี่​แบบ และ​สอง​แบบ​ใน​นั้น​คือ​ตัวเลข ที่​หน้าตา​ใช้​งาน​ได้

📦 orderctl — ของ​ชิ้น​เดียวกัน​กับ​คอร์ส​ภาษา​อื่น​ใน​เสา​นี้

orderctl คือ​เครื่อง​มือ​ประมวล​ผล​ออ​เด​อร์​ที่​คอร์ส​ภาษา​ใน​เสา​นี้​สร้าง​ซ้ำ​ที​ละ​ภาษา repo ตัวอย่าง กำลัง​จัด​ทำ ระหว่าง​นี้​ทุก​ตัวเลข​ใน​บท​นี้​รัน​เอง​ได้​ด้วย npm run verify:integer-semantics ซึ่ง​เรียก python3 scripts/integer-semantics/probe.py · ไม่​ต้อง​มี Docker และ​ไม่​ต้อง​ต่อ​เน็ต

สิบ​สอง​แถว​นี้​รับ​สตริง​จาก​ชุด​สี่​เส้น​เดียวกัน และ​ต่าง​กัน​ที่​ค่า​กับ error ที่​ออก​มา​จาก​การ​เรียก​นั้น

สตริง​ที่ probe ป้อน​เข้าไป​มี​สี่​เส้น คือ 3000000000 ที่​เกิน​ช่วง​ของ​ชนิด 32 bit ที่​มี​เครื่องหมาย · 9007199254740993 ที่​อยู่​นอก​ช่วง safe integer ของ double · 12345678901234567890 ที่​ยาว​ยี่สิบ​หลัก และ 12ab ที่​ไม่ใช่​ตัวเลข​ทั้ง​เส้น

การ​เรียกค่าที่​คืน​มาerror ที่​ออก​มา​พร้อม​กัน
Java Integer.parseInt("3000000000")(ไม่มี)NumberFormatException
Java Long.parseLong("9007199254740993")9007199254740993(ไม่มี)
Java Long.parseLong("12345678901234567890")(ไม่มี)NumberFormatException
Go strconv.Atoi("12345678901234567890")9223372036854775807value out of range
Go strconv.ParseInt("3000000000", 10, 32)2147483647value out of range
Node parseInt("9007199254740993")9007199254740992(ไม่มี)
Node Number("12345678901234567890")12345678901234567000(ไม่มี)
Node BigInt("12345678901234567890")12345678901234567890(ไม่มี)
Node parseInt("12ab")12(ไม่มี)
Node Number("12ab")NaN(ไม่มี)
Python int("12345678901234567890")12345678901234567890(ไม่มี)
Python int("12ab")(ไม่มี)ValueError

สิบ​สอง​แถว​นี้​แบ่ง​ออก​เป็น​สาม​กอง​ตาม​สิ่ง​ที่​เหลือ​อยู่​ใน​มือ​ผู้​เรียก

  • สาม​แถว​ไม่มี​ค่า​คืน​มา​เลย คือ​สอง​แถว​ของ Java และ​แถว​สุดท้าย​ของ Python
  • สอง​แถว​คืน​ทั้ง​ค่า​และ error พร้อม​กัน และ​ทั้ง​สอง​แถว​เป็น​ของ Go
  • เจ็ด​แถว​คืน​ค่า​มา​โดย​ไม่มี error สัก​ตัว คือ​ห้า​แถว​ของ Node หนึ่ง​แถว​ของ Java และ​หนึ่ง​แถว​ของ Python

กอง​ที่​สาม​คือ​กอง​ที่​ใหญ่​ที่สุด และ​สี่​แถว​ใน​นั้น​คืน​ค่าที่​ไม่​เท่ากับ​สตริง​ที่​รับ​เข้าไป สาม​แถว​ใน​สี่​แถว นั้น​คืน​จำนวนเต็ม​ที่​หน้าตา​ใช้​งาน​ได้ ส่วน​แถว​ที่​เหลือ​คืน NaN ซึ่ง​ประกาศ​ตัว​ว่า​ผิด

ภาษา​ทั้ง​สี่​รับ​งาน​เดียวกัน และ​ต่าง​กัน​ที่​สิ่ง​ที่​มัน​ทำ​เมื่อ​ทำงาน​นั้น​ไม่​สำเร็จ

พรมแดน​ของ​การ parse มีน​โย​บาย​อยู่​สี่​แบบ คือ​โยน · คืน​ค่า​พร้อม error · ไม่​ล้ม​เหลว · และ​ตรง​เป๊ะ หัวข้อ​ย่อย​สี่​หัวข้อ​ข้าง​ล่าง​วัด​ที​ละ​แบบ​จาก​ทรง​ที่ probe รัน​จริง

นโยบาย​ที่​หนึ่ง · Java โยน แล้ว​ไม่มี​ค่า​ให้​ถือ​ต่อ

หัวข้อ​ที่​มีชื่อ​ว่า “นโยบาย​ที่​หนึ่ง · Java โยน แล้ว​ไม่มี​ค่า​ให้​ถือ​ต่อ”

ทรง​ของ Java ทั้ง​สาม​ทรง​เรียก​ผ่าน​ตัว​ช่วย​ชื่อ thrown ซึ่ง​เก็บ​ชื่อ class กับ​ข้อความ​ของ​สิ่ง​ที่​โยน เมื่อ​มี​การ​โยน และ​เก็บ​ค่าที่​ได้​เมื่อ​ไม่มี

scripts/integer-semantics/runners/Shapes.java
thrown("parseInt3e9", () -> Integer.parseInt("3000000000"));
thrown("parseLongSafe", () -> Long.parseLong("9007199254740993"));
thrown("parseLongBig", () -> Long.parseLong("12345678901234567890"));

ทรง​แรก​กับ​ทรง​ที่​สาม​โยน และ probe บันทึก​สอง​บรรทัด​นี้​ไว้ โดย​ต่อ​ชื่อ class เข้า​กับ​ข้อความ​ของ exception ด้วย ": " ซึ่ง​เป็น​รูป​เดียว​กับ​บรรทัด​หัว​ของ stack trace

ค่าที่ probe บันทึก​ไว้​ใน​ช่อง parseInt3e9
java.lang.NumberFormatException: For input string: "3000000000"
ค่าที่ probe บันทึก​ไว้​ใน​ช่อง parseLongBig
java.lang.NumberFormatException: For input string: "12345678901234567890"

สอง​บรรทัด​นี้​ไม่มี​ค่าที่​ขยับ​ระหว่าง​รอบ​อยู่​เลย ไม่มี​หมายเลข thread ไม่มี​ที่​อยู่​ใน​หน่วย​ความ​จำ probe จึง​ผูก​มัน​ไว้​ได้​ทั้ง​บรรทัด และ​ช่อง​ค่า​ของ​ทั้ง​สอง​ทรง​เป็น​สตริง​ว่าง

ทรง​ที่​สอง​ไม่​โยน มัน​คืน 9007199254740993 ครบ​ทุก​หลัก ซึ่ง​บอก​ว่า​สิ่ง​ที่ Java ปฏิเสธ​ไม่ใช่ ความ​ยาว​ของ​สตริง แต่​เป็นการ​ที่​ค่า​นั้น​ไม่​พอดี​กับ​ความ​กว้าง​ของ​ชนิด​ที่​คุณ​เลือก​ไว้​รับ

probe เรียก Integer.parseInt รูป​อาร์กิวเมนต์​เดียว ส่วน​เงื่อนไข​ของ​การ​โยน​เขียน​ไว้​ที่​รูป​สอง อาร์กิวเมนต์ ข้อ​ต่อ​ระหว่าง​สอง​รูป​อยู่​ใน​เอกสาร​ของ​รูป​อาร์กิวเมนต์​เดียว​เอง ที่​เขียน​ว่า​ค่าที่​ได้ is returned, exactly as if the argument and the radix 10 were given as arguments to the parseInt(java.lang.String, int) method

เอกสาร​ของ Integer.parseInt(String, int) จึง​ไล่​เงื่อนไข​ที่​ทำให้​เกิด NumberFormatException ไว้​สี่​ข้อ​ให้​ทั้ง​สอง​รูป สาม​ข้อ​แรก​เป็น​เรื่อง​รูป​ร่าง​ของ​สตริง​กับ​ฐาน​ที่​ใช้ ส่วน​ข้อ​สุดท้าย​คือ The value represented by the string is not a value of type int ซึ่ง​เป็น​ข้อ​ที่​ตรง​กับ​ทรง​ที่ probe วัด

นโยบาย​ที่​ไม่​คืน​อะไร​เลย คือ​นโยบาย​ที่​ทดสอบ​ง่าย​ที่สุด

เส้น​ทาง​ที่​ล้ม​เหลว​ของ Java ไม่​ได้​ผลิต​ค่า​ออก​มา​ให้​ถือ​เลย​สัก​ค่า คุณ​จึง​ไม่มี​ทาง​ถือตัว​เลข​ผิดๆ ไว้​โดย​ไม่รู้​ตัว​จาก​ทรง​เหล่า​นี้ ผู้​เรียก​ที่​ไม่ catch จะ​ได้ exception ขึ้น​ไป​ถึง​ชั้น​บน

ราคา​ที่​ต้อง​จ่าย​คือ​คุณ​ต้อง​เขียน​เส้น​ทาง​นั้น​เอง​ทุก​ที่​ที่ parse การ​ลืม catch หนึ่ง​ที่​แปล​ว่า request ใบ​นั้น​จบ​ด้วย error ที่​ชั้น​บน​สุด แทนที่​จะ​จบ​ด้วย​ข้อความ​บอก​ผู้​ใช้​ว่า​ช่อง​ไหน​กรอก​ผิด

นโยบาย​ที่​สอง · Go คืน​ค่า​มา​ด้วย และ​ค่า​นั้น​คือ​ขอบ​ของ​ชนิด​เมื่อ​ค่า​เกิน​ช่วง

หัวข้อ​ที่​มีชื่อ​ว่า “นโยบาย​ที่​สอง · Go คืน​ค่า​มา​ด้วย และ​ค่า​นั้น​คือ​ขอบ​ของ​ชนิด​เมื่อ​ค่า​เกิน​ช่วง”

Go คืน​สอง​ค่า​จาก​การ​เรียก​ครั้ง​เดียว และ probe อ่าน​ทั้ง​สอง​ค่า​ทุก​ครั้ง ไม่​ได้​ทิ้ง​ค่าที่​สอง​ทิ้ง

scripts/integer-semantics/runners/main.go
ai, aerr := strconv.Atoi("12345678901234567890")
out["atoiBig"] = fmt.Sprintf("%d", ai)
out["atoiBigErr"] = errText(aerr)
pi, ierr := strconv.ParseInt("3000000000", 10, 32)
out["parseInt32"] = fmt.Sprintf("%d", pi)
out["parseInt32Err"] = errText(ierr)

ช่อง error ของ​ทั้ง​สอง​ทรง​ไม่​ว่าง และ probe เก็บ​ข้อความ​ของ​มัน​มา​ทั้ง​บรรทัด

ค่าที่ probe บันทึก​ไว้​ใน​ช่อง atoiBigErr และ parseInt32Err
strconv.Atoi: parsing "12345678901234567890": value out of range
strconv.ParseInt: parsing "3000000000": value out of range

ส่วน​ช่อง​ค่า​ของ​ทั้ง​สอง​ทรง​ก็​ไม่​ว่าง​เหมือน​กัน atoiBig เป็น 9223372036854775807 และ parseInt32 เป็น 2147483647 ซึ่ง​เป็น​ค่า​มาก​ที่สุด​ของ​ชนิด 64 bit กับ​ของ​ชนิด 32 bit ที่​มี​เครื่องหมาย ตาม​ลำดับ

เอกสาร​ของ strconv เขียน​กฎ​นี้​ไว้​ใน​คำ​อธิบาย​ของ ParseInt ตรงๆ

Go package documentation · func ParseInt
If s is empty or contains invalid digits, err.Err = ErrSyntax and the returned value is 0;
if the value corresponding to s cannot be represented by a signed integer of the given
size, err.Err = ErrRange and the returned value is the maximum magnitude integer of the
appropriate bitSize and sign.

ประโยค​นี้​มี​สอง​ครึ่ง และ​สอง​ทรง​ที่​บท​นี้​วัด​อยู่​ใน​ครึ่ง​หลัง​ทั้ง​คู่ คือ​ครึ่ง​ของ ErrRange ส่วน ครึ่ง​ของ ErrSyntax ที่​คืน 0 ไม่มี​ทรง​ไหน​ใน​บท​นี้​วัด​ไว้ ข้อ​อ้าง​ทุก​ข้อ​ข้าง​ล่าง​จึง​เป็น​ข้อ​อ้าง ของ​ครึ่ง​ที่​ค่า​เกิน​ช่วง ไม่ใช่​ของ​ทุก​การ​เรียก​ที่​ล้ม​เหลว

หน้า​เดียวกัน​ยัง​เขียน​ว่า Atoi is equivalent to ParseInt(s, 10, 0), converted to type int ดัง​นั้น bitSize ของ Atoi คือ 0 ซึ่ง​หมาย​ถึง​ชนิด int ความ​กว้าง​ของ int ขึ้น​กับ เครื่อง และ​เครื่อง​ที่ probe รัน​คือ darwin/arm64 ซึ่ง​ให้ 9223372036854775807

บรรทัด​ที่​ทุก​คน​เขียน กับ​ค่าที่​มัน​ถือ​ไว้

สำนวน​ที่​เห็น​บ่อย​ที่สุด​คือ​การ​ทิ้ง​ค่าที่​สอง​ทิ้ง​ด้วย _ เพราะ​มัน​สั้น​กว่า​และ compiler ไม่​บ่น

สำนวน​ที่​ต่าง​จาก​ทรง​ที่ probe วัด แค่​การ​ทิ้ง​ค่าที่​สอง​ทิ้ง
n, _ := strconv.Atoi(orderIdFromQuery)

ค่าตัว​แรก​ที่ probe อ่าน​ได้​จาก​การ​เรียก​แบบ​เดียวกัน​คือ 9223372036854775807 การ​ทิ้ง error ทิ้ง​จึง​ไม่​ได้​ทำให้ n เป็น 0 มัน​เพียง​ลบ​สิ่ง​เดียว​ที่​บอก​ว่า n ไม่ใช่​ค่าที่​ส่ง​มา​ออก​ไป

นี่​คือ​จุด​ที่​คม​ที่สุด​ของ​บท​นี้ ผู้​เรียก​ที่​คุ้น​กับ​ภาษา​ที่​โยน จะ​อ่าน​บรรทัด​นี้​ว่า “ถ้า​แปลง​ไม่​ได้​ก็ได้ ศูนย์” แล้ว​เขียน​เงื่อนไข if n == 0 ไว้​ดัก ซึ่ง​เป็น​เงื่อนไข​ที่​ไม่มี​วัน​เป็น​จริง​ใน​ทรง​นี้

นโยบาย​ที่​สาม · Node ไม่​ล้ม​เหลว​สัก​ทรง​ใน​ห้า​ทรง​ที่​วัด

หัวข้อ​ที่​มีชื่อ​ว่า “นโยบาย​ที่​สาม · Node ไม่​ล้ม​เหลว​สัก​ทรง​ใน​ห้า​ทรง​ที่​วัด”

ทรง​ของ Node มี​ห้า​ทรง และ​ไม่มี​ทรง​ไหน​โยน​อะไร​ออก​มา​เลย​สัก​ทรง ข้อ​อ้าง​นี้​เป็น​ของ​ห้า​ทรง​นี้ ไม่ใช่​ของ​ทุก​คำ​สั่ง​ใน​ภาษา

scripts/integer-semantics/runners/shapes.mjs
out.parseIntSafe = S(parseInt("9007199254740993"));
out.numberBig = S(Number("12345678901234567890"));
out.bigintBig = BigInt("12345678901234567890").toString();
out.parseIntTrailing = S(parseInt("12ab"));
out.numberTrailing = S(Number("12ab"));

สอง​ทรง​สุดท้าย​รับ​สตริง​เส้น​เดียวกัน คือ 12ab แล้ว​ตอบ​คนละ​อย่าง parseInt ให้ 12 ส่วน Number ให้ NaN นี่​คือ​อินพุต​เดียวกัน สอง​คำ​ตอบ ใน​ภาษา​เดียว

MDN อธิบาย​ฝั่ง parseInt ไว้​ว่า เมื่อ​มัน​เจอ​อักขระ​ที่​ไม่ใช่​ตัวเลข​ของ​ฐาน​ที่​ใช้​อยู่ มัน​จะ​ข้าม อักขระ​นั้น​และ​ทุก​อักขระ​ที่​ตาม​มา แล้ว​คืน​ค่า​จำนวนเต็ม​เท่า​ที่ parse มา​ได้​ถึง​ตรง​นั้น

ส่วน​ฝั่ง Number MDN อธิบาย​ขั้น​ตอน​ของ​การ​แปลง​เป็น​ตัวเลข​ไว้​ว่า สตริง​ถูก​แปลง​ด้วย​การ parse มัน​เสมือน​เป็น number literal และ Parsing failure results in NaN

ทรง​แรก​ไม่มี​อักขระ​แปลก​ปลอม​เลย 9007199254740993 เป็น​ตัวเลข​ฐาน​สิบ​ทุก​หลัก แต่​ผล​ที่​วัด​ได้ คือ 9007199254740992 ซึ่ง​หาย​ไป​หนึ่ง เพราะ​ค่าที่ parseInt คืน​เป็น number ของ JavaScript

MDN เขียน​ข้อ​จำกัด​นี้​ไว้​ใน​หน้า​เดียวกัน ว่า parseInt() คืน number จึง​เสีย​ความ​แม่นยำ​ได้​ถ้า จำนวนเต็ม​ที่​สตริง​นั้น​แทน​อยู่​นอก​ช่วง safe integer แล้ว​ชี้​ไป​ที่ BigInt() ซึ่ง parse จำนวนเต็ม ความ​ยาว​เท่าไร​ก็ได้​อย่าง​ตรง​ตัว

ทรง​ที่​สาม​คือ​ทางออก​ที่​หน้า​นั้น​ชี้​ไป BigInt("12345678901234567890") คืน 12345678901234567890 ครบ​ยี่สิบ​หลัก ขณะ​ที่ Number กับ​สตริง​เส้น​เดียวกัน​คืน 12345678901234567000 ซึ่ง​ลงท้าย​ด้วย​ศูนย์​สาม​ตัว​ที่​ไม่​ได้​ส่ง​มา

int ของ Python ไม่มี​ความ​กว้าง จึง​ไม่มี​เงื่อนไข “ค่า​เกิน​ช่วง” ให้​ล้ม​เหลว​ตั้งแต่​แรก สิ่ง​ที่ int() ปฏิเสธ​จึง​ไม่ใช่​ขนาด​ของ​ค่า แต่​เป็น​รูป​ร่าง​ของ​สตริง และ​ความ​ยาว​ของ​สตริง

scripts/integer-semantics/runners/shapes.py
out["intBig"] = str(int("12345678901234567890"))
try:
out["intTrailing"] = str(int("12ab"))
out["intTrailingErr"] = ""
except ValueError as exc:
out["intTrailing"] = ""
out["intTrailingErr"] = f"{type(exc).__name__}: {exc}"

ทรง​แรก​คืน 12345678901234567890 ครบ​ทุก​หลัก ส่วน​ทรง​ที่​สอง​เข้าไป​ใน​กิ่ง except และ probe บันทึก​ชื่อ exception ต่อ​ด้วย​ข้อความ​ของ​มัน​ไว้​บรรทัด​นี้

ค่าที่ probe บันทึก​ไว้​ใน​ช่อง intTrailingErr
ValueError: invalid literal for int() with base 10: '12ab'

เอกสาร​ของ int ไล่​สิ่ง​ที่​สตริง​มี​ได้​ไว้​ครบ คือ​เครื่องหมาย + หรือ - นำ​หน้า​โดย​ไม่​เว้น​วรรค · ศูนย์​นำ​หน้า · ช่อง​ว่าง​ล้อม​รอบ · และ​ขีด​ล่าง​เดี่ยวๆ คั่น​ระหว่าง​ตัวเลข สตริง 12ab ไม่​ตรง​กับ ข้อ​ไหน​เลย เพราะ ab ไม่ใช่​ตัวเลข​ของ​ฐาน​สิบ

รูป​ร่าง​ไม่ใช่​สิ่ง​เดียว​ที่ int() ปฏิเสธ​ได้ หน้า​เดียวกัน​มี​หมายเหตุ​ของ Python 3.11 ที่​บอก​ว่า สตริง​ที่​ป้อน​เข้า int และ​ข้อความ​ที่ int เขียน​ออก​มา ถูก​จำกัด​ความ​ยาว​ได้​เพื่อ​กัน​การ​โจมตี​แบบ ปฏิเสธ​การ​ให้​บริการ แล้ว​เขียน​ต่อว่า A ValueError is raised when the limit is exceeded while converting a string to an int

สตริง​ที่​เป็น​ตัวเลข​ฐาน​สิบ​ทุก​หลัก​จึง​ยัง​ล้ม​เหลว​ได้ ถ้า​มัน​ยาว​เกิน​ขีด​จำกัด​นั้น ขนาด​ของ​ขีด​จำกัด​ไม่​ได้ อยู่​ใน measured.json เพราะ probe ของ​คอร์ส​นี้​ไม่​ได้​วัด​มัน บท​นี้​จึง​ไม่​พิมพ์​ตัวเลข​ของ​มัน​ออก​มา สิ่ง​ที่​หน้าที่​อ้าง​ยืนยัน​ได้​คือ int() มี​ทาง​ล้ม​เหลว​มากกว่า​หนึ่ง​ทาง และ​ทั้ง​สอง​ทาง​ยก ValueError

flowchart TD
  S["สตริง 12345678901234567890 จากภายนอก"] --> J["Java · Long.parseLong"]
  S --> G["Go · strconv.Atoi"]
  S --> N["Node · Number"]
  S --> P["Python · int"]
  J --> J1["โยน NumberFormatException · ไม่มีค่าให้ถือ"]
  G --> G1["คืน 9223372036854775807 พร้อม error"]
  N --> N1["คืน 12345678901234567000 ไม่มี error"]
  P --> P1["คืน 12345678901234567890 ครบทุกหลัก"]
  G1 --> D{"ผู้เรียกอ่านค่าที่สองหรือไม่"}
  D -->|"อ่าน err"| DX["หยุดที่พรมแดน · รูปเดียวกับฝั่ง Java คือไม่มีตัวเลขให้ถือต่อ"]
  D -->|"ทิ้งด้วย _"| DY["ถือค่าที่ขอบต่อไป · รูปเดียวกับฝั่ง Node คือถือตัวเลขคนละค่ากับอินพุต"]

คำ​บรรยาย​ภาพ: สี่​กิ่ง​บน​รับ​สตริง​เส้น​เดียวกัน สอง​กิ่ง​ล่าง​เทียบ​ที่​รูป​ของ​ผลลัพธ์ ไม่ใช่​ที่​ตัวเลข ซึ่ง​ไม่​เท่า​กัน​สัก​คู่ · และ​กิ่ง​ของ Go เป็น​กิ่ง​เดียว​ที่​ยัง​แตก​ต่อ​ได้​อีก เพราะ​นโยบาย​ของ​มัน​มอบ​การ​ตัดสิน​ใจ​กลับ​มา​ที่​ผู้​เรียก ส่วน​อีก​สาม​กิ่ง​ตัดสิน​ใจ​แทน​ไป​แล้ว​ตั้งแต่​ใน​ภาษา

query parameter ใบ​เดียว​เดิน​เข้าไป​หา​สี่ service แล้ว​ออก​มา​เป็น​ผล​สี่​แบบ​ที่​ไม่​ตรง​กัน​เลย

orderId ที่​ยาว​ยี่สิบ​หลัก​เดิน​เข้า​มา​ทาง query parameter ใบ​เดียว ปลาย​ทาง​ทั้ง​สี่​รับ​สตริง​เส้น เดียวกัน แล้ว​ได้​ผล​สี่​แบบ​ตาม​ตาราง​ข้าง​บน

  • service ที่​เขียน​ด้วย Java ผ่าน Long.parseLong จบ request ด้วย exception และ​ไม่มี​แถว​ไหน​ถูก​แตะ
  • service ที่​เขียน​ด้วย Go แบบ​ทิ้ง error ทิ้ง ถือ 9223372036854775807 ไป​ค้น​ต่อ
  • service ที่​เขียน​ด้วย Node ผ่าน Number ถือ 12345678901234567000 ไป​ค้น​ต่อ
  • service ที่​เขียน​ด้วย Python ถือ 12345678901234567890 ซึ่ง​ตรง​กับ​ที่​ส่ง​มา

สอง​เส้น​กลาง​คือ​เส้น​ที่​เจ็บ ค่าที่​มัน​ถือ​เป็น​จำนวนเต็ม​บวก​ที่​มีหน้า​ตา​เหมือน order id ทุก​ประการ มัน​จึง​เดิน​ต่อ​เข้าไป​ใน​คำ​สั่ง​ค้น เข้าไป​ใน log และ​เข้าไป​ใน metric ได้​โดย​ไม่มี​ใคร​ทักท้วง

ค่าที่​ขอบ​เป็น​ค่า​เดียวกัน​ทุก​ครั้ง จึง​รวม​กัน​เป็นก​อง

9223372036854775807 ไม่​ได้​เป็น​ค่า​สุ่ม มัน​เป็น​ค่า​เดิม​ทุก​ครั้ง​ที่​สตริง​ของ​จำนวน​บวก​เกิน​ช่วง ออ​เด​อร์​คนละ​ใบ​ที่ id ยาว​เกิน​จึง​ตกลง​ไป​อยู่​ที่​ค่า​เดียวกัน​หมด

เอกสาร​เขียน​ว่า​ค่าที่​คืน​คือ​จำนวนเต็ม​ขนาด​มาก​ที่สุด of the appropriate bitSize and sign เครื่องหมาย​จึง​เป็น​ส่วน​หนึ่ง​ของ​กฎ สตริง​ของ​จำนวน​ลบ​ที่​เกิน​ช่วง​ได้​ค่าที่​ขอบ​ล่าง​ของ​ชนิด​แทน ไม่ใช่​ค่า​เดียวกัน​นี้ · ทรง​ที่ probe วัด​ไว้​ทั้ง​สอง​ทรง​เป็น​จำนวน​บวก บท​นี้​จึง​พิมพ์​ค่า​ของ​ฝั่ง​บวก​อย่าง​เดียว

ถ้า​ค่า​นั้น​บังเอิญ​ตรง​กับ​แถว​ที่​มี​อยู่​จริง​ใน database ทุ​กอ​อเด​อร์​ที่​เพี้ยน​จะ​ชี้​ไป​ที่​แถว​เดียวกัน และ​ถ้า​ไม่​ตรง คำ​สั่ง​ค้น​จะ​คืน​ศูนย์​แถว ซึ่ง​อ่าน​เหมือน “ไม่​พบ​ออ​เด​อร์” ไม่ใช่ “แปลง​ค่า​ไม่​สำเร็จ”

ช่อง​ยอด​รวม​เจ็บ​คนละ​แบบ สมมติ​ผู้​ใช้​วางข้อ​ความ​ที่​มี​อักขระ​ติด​มา​ด้วย​จน​กลาย​เป็น 12ab ฝั่ง Node ที่​ใช้ parseInt จะ​ได้ 12 แล้ว​บันทึก​ยอด​นั้น​ลง​ไป​เหมือน​ยอด​ปกติ

ฝั่ง Python ที่​ใช้ int จะยก ValueError ที่​บรรทัด​นั้น​เลย ส่วน​ฝั่ง Node ที่​ใช้ Number จะ​ได้ NaN ซึ่ง​ไม่​ได้​หยุด​โปรแกรม แต่​ทำให้​การ​คำนวณ​ทุก​ขั้น​ที่​ตาม​มา​ได้ NaN ต่อ​กัน​ไป

ความ​ต่าง​ของ​สาม​เส้น​ทาง​นี้​ไม่​ได้​อยู่​ที่​ว่า​ภาษา​ไหน​เข้ม​งวด​กว่า​กัน แต่​อยู่​ที่​ว่า​ความ​ผิด​พลาด​ถูก รายงาน​ที่​บรรทัด​ที่​เกิด หรือ​ถูก​เลื่อน​ไป​โผล่​ที่ไหน​สัก​แห่ง​ข้าง​หน้า​ใน​รูป​ของ​ตัวเลข​ที่​ดู​ปกติ

จุด​ที่​ห้า​ใน​ห้า​จุด · ครึ่ง​ที่​นโยบาย​ตอน​ล้ม​เหลว​ตัดสิน

หัวข้อ​ที่​มีชื่อ​ว่า “จุด​ที่​ห้า​ใน​ห้า​จุด · ครึ่ง​ที่​นโยบาย​ตอน​ล้ม​เหลว​ตัดสิน”

บท​ที่ 6 กับ​บท​นี้​อยู่​ที่​จุด​เดียวกัน แต่​วัด​คนละ​ครึ่ง​ของ​มัน และ​ครึ่ง​นี้​มี​ทั้ง​เส้น​ที่​หยุด​ทันที กับ​เส้น​ที่​เงียบ

ธี​สิ​ส​ของ​คอร์ส​นี้​คือ ค่า​จำนวนเต็ม​ค่า​หนึ่ง​เปลี่ยน​ได้​ห้า​จุด​ระหว่าง​ทาง​ผ่าน​ระบบ และ​ทุก​จุด​ใน​ห้า​จุด​นั้น เงียบ จุด​ที่​ห้า​คือ​พรมแดน​ของ​การ parse ซึ่ง​บท​ที่ 6 เปิด​ไว้​ด้วย​ตัวเลข​ที่​อยู่​ใน JSON

ครึ่ง​ที่​บท​ที่ 6 วัด คือ​ครึ่ง​ที่​ชนิด​ปลาย​ทาง​ตัดสิน อินพุต​เป็น​ตัวเลข​อยู่​แล้ว และ​สิ่ง​ที่​คุณ​เขียน​มี อย่าง​เดียว​คือ​ชนิด​ของ​ตัวแปร​ที่​รับ​ค่า

ครึ่ง​ที่​บท​นี้​วัด​เริ่ม​จาก​อินพุต​ที่​ไม่ใช่​ตัวเลข​ตั้งแต่​ต้น คุณ​จึง​ต้อง​เรียก​คำ​สั่ง​แปลง​ด้วย​ตัว​เอง และ สิ่ง​ที่​ตัดสิน​ไม่ใช่​ชนิด​ปลาย​ทาง​อย่าง​เดียว​แล้ว แต่​เป็น​นโยบาย​ของ​คำ​สั่ง​นั้น​เมื่อ​แปลง​ไม่​สำเร็จ

สี่​นโยบาย​ที่​บท​นี้​วัด สรุป​ได้​เป็น​สี่​บรรทัด​ตาม​สิ่ง​ที่​เหลือ​อยู่​ใน​มือ​ผู้​เรียก

  • Java · โยน — ไม่มี​ค่า​ออก​มา​ให้​ถือ​เลย​สัก​ค่า
  • Go · คืน​ค่า​พร้อม error — ค่าที่​คืน​เมื่อ​ค่า​เกิน​ช่วง​คือ​ขอบ​ของ​ชนิด และ error คือ​สิ่ง​เดียว​ที่​บอก​ว่า​มัน​คือ​ขอบ
  • Node · ไม่​ล้ม​เหลว​สัก​ทรง​ใน​ห้า​ทรง​ที่​วัด — คืน​ค่าที่​ปัด​แล้ว ค่าที่​ตัด​ท้าย​ทิ้ง​แล้ว หรือ NaN แล้ว​แต่​คำ​สั่ง​ที่​เรียก
  • Python · ตรง​เป๊ะ หรือ​ยก ValueError — ไม่มี​ความ​กว้าง​ให้​ล้น สิ่ง​ที่​ปฏิเสธ​ได้​คือ​รูป​ร่าง​ของ​สตริง กับ​ความ​ยาว​ของ​มัน

สอง​ใน​สี่​บรรทัด​นี้​ส่ง​จำนวนเต็ม​ที่​หน้าตา​ใช้​งาน​ได้​กลับ​มา​เมื่อ​แปลง​ไม่​สำเร็จ คือ​บรรทัด​ของ Go กับ บรรทัด​ของ Node และ​ทั้ง​สอง​ค่า​เดิน​ต่อ​เข้าไป​ใน​ระบบ​ได้​โดย​ไม่​ต้อง​ผ่าน​ด่าน​ไหน​อีก

สิ่ง​ที่​ตัดสิน​ว่า​คุณ​ได้​บรรทัด​ไหน​จึง​ไม่ใช่​ชื่อ​ภาษา แต่​เป็น​นโยบาย​ของ​คำ​สั่ง​ที่​คุณ​เรียก และ​ภาษา​เดียว มี​คำ​สั่ง​ที่​ให้​ผล​คนละ​แบบ​ได้ อย่าง​ที่ parseInt กับ Number ของ Node ตอบ 12ab คนละ​อย่าง

ทางออก​จึง​อยู่​ที่​การ​เลือก​คำ​สั่ง ไม่ใช่​การ​เลือก​ภาษา Node มี BigInt ที่​คืน​ยี่สิบ​หลัก​ครบ ส่วน Go มี err วาง​อยู่​ตรง​นั้น​แล้ว รอ​แค่​ให้​ผู้​เรียก​อ่าน​มัน​แทนที่​จะ​เขียน _ ทับ

บท​ที่ 8 เอา​ทั้ง​ห้า​จุด​มา​ต่อ​กัน​เป็น​ท่อ​เดียว แล้ว​วัด​ยอด​ที่​ออก​ปลาย​ทาง​ของ​ท่อ​สอง​เส้น​บน​อินพุต​ชุด เดียวกัน ไม่ใช่​บรรยาย​ว่า​เส้น​ไหน​ดี​กว่า


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • Integer (Java SE 25 & JDK 25) (เข้า​ถึง 2026-09-13) — parseInt(String) ระบุ​ว่า​โยน NumberFormatException เมื่อ​สตริง ไม่มี​จำนวนเต็ม​ที่ parse ได้​อยู่ · ส่วน parseInt(String, int) ไล่​เงื่อนไข​ของ​การ​โยน​ไว้​สี่​ข้อ และ​ข้อ​สุดท้าย​คือ The value represented by the string is not a value of type int ซึ่ง​เป็น ข้อ​ที่​แยก​การ​ล้น​ของ​ช่วง ออก​จาก​การ​ที่​สตริง​มี​อักขระ​ที่​ไม่ใช่​ตัวเลข​ของ​ฐาน​นั้น

  • Long (Java SE 25 & JDK 25) (เข้า​ถึง 2026-09-13) — parseLong(String) ระบุ​กฎ​ชุด​เดียวกัน​กับ​ปลาย​ทาง​เป็น long และ ระบุ​เพิ่ม​ว่า​อักษร L หรือ l ท้าย​สตริง​ไม่​ได้​รับ​อนุญาต ต่าง​จาก​ที่​เขียน​ได้​ใน source

  • Go package documentation — strconv (เข้า​ถึง 2026-09-13) — Atoi ระบุ​ว่า​เทียบ​เท่ากับ ParseInt(s, 10, 0) ที่​แปลง​เป็น​ชนิด int · ParseInt ระบุ​ว่า​เมื่อ​ค่าที่​สตริง​แทน​อยู่​ใส่​ลง​จำนวนเต็ม​มี​เครื่องหมาย​ขนาด​ที่​ขอ​ไม่​ได้ err.Err จะ​เป็น ErrRange และ​ค่าที่​คืน​คือ​จำนวนเต็ม​ที่​มี​ขนาด​มาก​ที่สุด​ของ bitSize กับ เครื่องหมาย​นั้น · ส่วน​ตัวแปร ErrRange เอง​นิยาม​ไว้​เป็น errors.New("value out of range")

  • parseInt() — MDN (เข้า​ถึง 2026-09-13) — ระบุ​ว่า​เมื่อ​เจอ​อักขระ​ที่​ไม่ใช่​ตัวเลข​ของ​ฐาน​ที่​ใช้ มัน​ข้าม​อักขระ​นั้น และ​ทุก​อักขระ​ที่​ตาม​มา แล้ว​คืน​ค่าที่ parse มา​ได้​ถึง​ตรง​นั้น · และ​ระบุ​ว่า​เพราะ​มัน​คืน number มัน​จึง​เสีย​ความ​แม่นยำ​ได้​เมื่อ​จำนวนเต็ม​นั้น​อยู่​นอก​ช่วง safe integer โดย​ชี้​ไป​ที่ BigInt()

  • Number — MDN (เข้า​ถึง 2026-09-13) — หัวข้อ Number coercion ระบุ​ว่า​สตริง​ถูก​แปลง​ด้วย​การ parse มัน​เสมือน เป็น number literal และ Parsing failure results in NaN · หัวข้อ​เดียวกัน​ระบุ​ว่า Number(x) ใช้​ขั้น​ตอน​ชุด​เดียวกัน​นี้ ต่าง​กัน​ที่ BigInt ไม่​โยน TypeError

  • Built-in Functions — Python documentation (เข้า​ถึง 2026-09-15) — class int(string, /, base=10) ระบุ​ว่า​สตริง​มี​เครื่องหมาย​นำ​หน้า โดย​ไม่​เว้น​วรรค ศูนย์​นำ​หน้า ช่อง​ว่าง​ล้อม​รอบ และ​ขีด​ล่าง​เดี่ยวๆ คั่น​ระหว่าง​ตัวเลข​ได้ · และ​ระบุ​ว่า ค่า​ฐาน​สิบ 0 ถึง 9 แทน​ด้วย​อักขระ​ตัวเลข​ของ Unicode ตัว​ใด​ก็ได้ · หมายเหตุ Changed in version 3.11 ของ​หน้า​เดียวกัน​ระบุ​ว่า​สตริง​ที่​ป้อน​เข้า int และ​ข้อความ​ที่ int เขียน​ออก​มา ถูก​จำกัด​ความ​ยาว​ได้ และ A ValueError is raised when the limit is exceeded while converting a string to an int · หน้า​นี้​ชี้​ต่อ​ไป​ที่​เอกสาร​ของ​ขีด​จำกัด​นั้น​แทน​การ​ระบุ​ขนาด​ของ​มัน​เอง

  • ตัวเลข​ทุก​ตัว​ใน​บท​นี้​มา​จาก scripts/integer-semantics/measured.json ซึ่ง scripts/integer-semantics/probe.py เขียน​ขึ้น​หลัง​ยิง​ทั้ง​ชุด​สาม​รอบ​ต่อ​การ​เรียก​หนึ่ง​ครั้ง · วัด​บน openjdk 25.0.4.1 go1.27.0 node v24.19.0 และ Python 3.14.6 บน darwin/arm64 เมื่อ 2026-09-12

เช็กความเข้าใจ — บทที่ 7

ข้อ 1 / 3

เขียน `n, _ := strconv.Atoi("12345678901234567890")` ใน Go แล้ว `n` ถือค่าอะไรอยู่