UTF-8 คือการออกแบบที่ยอดเยี่ยม
(iamvishnu.com)- UTF-8 เป็นรูปแบบการเข้ารหัสแบบ ความยาวแปรผัน ที่สามารถแทนตัวอักษรได้หลายล้านตัว พร้อมทั้งยังคง ความเข้ากันได้ย้อนหลังกับ ASCII
- ช่วง 7 บิตเดียวกับ ASCII (
U+0000~U+007F) ใช้เป็น 1 ไบต์ ตามเดิม ทำให้ไฟล์ ASCII เป็นไฟล์ UTF-8 ที่ถูกต้องได้ในทันที - ตัวอักษรอื่น ๆ จะแทนด้วยลำดับ 2~4 ไบต์ โดย รูปแบบบิต ของไบต์นำหน้าจะกำหนดความยาว และไบต์ถัดไปจะขึ้นต้นด้วย
10เพื่อบ่งชี้ว่าเป็น continuation byte - ด้วยการออกแบบนี้ UTF-8 จึงรองรับชุดอักขระสากลได้ ขณะเดียวกันก็ เข้ากันได้อย่างสมบูรณ์ กับระบบ ASCII เดิม จนกลายเป็นการเข้ารหัสอักขระที่ใช้แพร่หลายที่สุด
- การเข้ารหัสยูนิโค้ดแบบอื่นอย่าง UTF-16 และ UTF-32 ไม่ได้ให้ความเข้ากันได้กับ ASCII ในลักษณะนี้
ความยอดเยี่ยมของการออกแบบ UTF-8
- เมื่อได้รู้จักการเข้ารหัส UTF-8 ครั้งแรก ผู้เขียนประทับใจมากกับโครงสร้างที่สามารถครอบคลุมอักขระ นับล้านแบบ จากภาษาและสัญลักษณ์ที่แตกต่างกันภายใต้ระบบเดียว ขณะเดียวกันก็ยัง เข้ากันได้กับ ASCII เดิม
- โดยพื้นฐานแล้ว UTF-8 ใช้งานได้สูงสุด 32 บิต แต่ ASCII ใช้เพียง 7 บิต
- หลักการออกแบบของ UTF-8 มีดังนี้
- ไฟล์ที่เข้ารหัสแบบ ASCII ทุกไฟล์เป็น ไฟล์ UTF-8 ที่ถูกต้อง
- ไฟล์ UTF-8 ที่มีเฉพาะอักขระ ASCII ทั้งหมดก็เป็น ไฟล์ ASCII ที่ถูกต้อง
- แนวคิดในการ ผสาน ระบบเก่าที่จำกัดอยู่เพียง 128 ตัวอักษรเข้ากับระบบที่ครอบคลุมตัวอักษรนับล้านนั้นล้ำหน้ามาก
แนวคิดพื้นฐานของ UTF-8
- UTF-8 คือ การเข้ารหัสอักขระแบบความยาวแปรผัน (variable-width encoding) ที่ออกแบบมาเพื่อแทนตัวอักษรทั้งหมดใน ชุดอักขระยูนิโค้ด
- เข้ารหัสอักขระแต่ละตัวด้วย 1~4 ไบต์
- อักขระ 128 ตัวแรก (
U+0000~U+007F) จะถูกเก็บเป็น ไบต์เดียว เพื่อให้เข้ากันได้ย้อนหลังกับ ASCII - อักขระอื่น ๆ จะถูกเข้ารหัสเป็น 2, 3 หรือ 4 ไบต์
- บิตนำหน้า ของไบต์ตัวแรกจะเป็นตัวกำหนดจำนวนไบต์ทั้งหมดที่ต้องใช้ในการเข้ารหัส
| รูปแบบ 1 ไบต์ | จำนวนไบต์ | รูปแบบลำดับไบต์ทั้งหมด |
|---|---|---|
| 0xxxxxxx | 1 | 0xxxxxxx (ASCII ทั่วไป) |
| 110xxxxx | 2 | 110xxxxx 10xxxxxx |
| 1110xxxx | 3 | 1110xxxx 10xxxxxx 10xxxxxx |
| 11110xxx | 4 | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
- ไบต์ลำดับที่ 2, 3 และ 4 ของลำดับหลายไบต์จะขึ้นต้นด้วย
10เสมอ ซึ่งช่วย ระบุอย่างชัดเจนว่าเป็น continuation byte - เมื่อนำบิตส่วนที่เหลือของไบต์หลักและ continuation byte มารวมกัน จะได้ code point หนึ่งค่า
- code point คือรหัสระบุอักขระยูนิโค้ดแบบไม่ซ้ำ ซึ่งเขียนด้วยคำนำหน้า "U+" และเลขฐานสิบหก
- ตัวอย่าง: code point ของ "A" คือ
U+0041
- ขั้นตอนการตีความอักขระจากไบต์ของ UTF-8 มีดังนี้
- 1. อ่านไบต์หนึ่งตัว ถ้าขึ้นต้นด้วย 0 ให้ถือว่าเป็นอักขระไบต์เดียว (ASCII) ใช้ 7 บิตที่เหลือแทนอักขระนั้น แล้วไปยังไบต์ถัดไป
- 2. ถ้าไม่ใช่ 0
- ถ้าเป็น 110 ให้ถือว่าเป็นอักขระ 2 ไบต์ แล้วอ่านไบต์ถัดไปเพิ่มอีก 1 ไบต์
- ถ้าเป็น 1110 ให้ถือว่าเป็นอักขระ 3 ไบต์ แล้วอ่านไบต์ถัดไปเพิ่มอีก 2 ไบต์
- ถ้าเป็น 11110 ให้ถือว่าเป็นอักขระ 4 ไบต์ แล้วอ่านไบต์ถัดไปเพิ่มอีก 3 ไบต์
- 3. นำ บิตที่ไม่ใช่บิตนำหน้า จากไบต์ที่กำหนดแล้วมารวมกัน เพื่อใช้เป็นค่าไบนารีของ code point
- 4. หา code point นั้นในชุดอักขระยูนิโค้ด แล้วแสดงผลบนหน้าจอ
- 5. ทำซ้ำกับไบต์ถัดไป
ตัวอย่าง: อักขระภาษาฮินดี "अ"
- การแทนแบบ UTF-8:
11100000 10100100 10000101(3 ไบต์) - ไบต์แรก (
11100000) → แสดงว่าเป็นอักขระ 3 ไบต์ - การรวมบิตที่มีความหมายของทั้งสามไบต์ →
00001001 00000101= เลขฐานสิบหก0x0905 - code point
U+0905หมายถึงอักษรเทวนาครี "अ"
ตัวอย่างไฟล์
-
1.
Hey👋 Buddy- มีทั้งหมด 13 ไบต์
- อักขระ ASCII (H, e, y, B, u, d, d, y, เว้นวรรค) → ตัวละ 1 ไบต์
- 👋 (U+1F44B) → 4 ไบต์
11110000 10011111 10010001 10001011
- ไฟล์นี้เป็น ไฟล์ UTF-8 ที่ถูกต้อง แต่เนื่องจากมีอักขระที่ไม่ใช่ ASCII (อีโมจิ) จึง ไม่ใช่ไฟล์ที่เข้ากันได้ย้อนหลังกับ ASCII
- มีทั้งหมด 13 ไบต์
-
2.
Hey Buddy- มีทั้งหมด 9 ไบต์ และอยู่ในช่วง ASCII ทั้งหมด
- ดังนั้นไฟล์นี้จึงเป็นทั้ง ไฟล์ ASCII ที่ถูกต้อง และ ไฟล์ UTF-8 ที่ถูกต้อง
เปรียบเทียบกับการเข้ารหัสอื่น
- มีการเข้ารหัสบางแบบที่ให้ความเข้ากันได้กับ ASCII เช่นกัน แต่ ไม่ได้ถูกใช้อย่างแพร่หลายเท่า UTF-8
- GB18030 (มาตรฐานของจีน) เป็นต้น ก็ให้ความเข้ากันได้กับ ASCII แต่ไม่ได้ใช้อย่างแพร่หลาย
- ตระกูล ISO/IEC 8859 เป็นการขยายแบบไบต์เดียว (สูงสุด 256 ตัวอักษร) จึงมีข้อจำกัด
- UTF-16/UTF-32 ไม่มีความเข้ากันได้กับ ASCII
- 'A' (U+0041): UTF-16 คือ
00 41, UTF-32 คือ00 00 00 41
- 'A' (U+0041): UTF-16 คือ
โบนัส: UTF-8 Playground
- เครื่องมือแบบอินเทอร์แอ็กทีฟ สำหรับสำรวจกระบวนการเข้ารหัส UTF-8 แบบมองเห็นภาพ
- https://utf8-playground.netlify.app/
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
ใน UTF-8 ไบต์ต่อเนื่องจะขึ้นต้นด้วย
10เสมอ ดังนั้นแม้จะกระโดดไปยังไบต์ใดแบบสุ่ม ก็ตรวจได้ทันทีว่าตำแหน่งนั้นเป็นจุดเริ่มของอักขระหรือเป็น continuation byte จึงหาจุดเริ่มของอักขระถัดไปหรือก่อนหน้าได้ง่าย หากเข้ารหัสแบบจำนวนเต็มความยาวแปรผันของ EBML (ที่สลับ 1/0 เพื่อคงความเข้ากันได้กับ ASCII แบบไบต์เดี่ยว) ก็จะรู้จุดเริ่มอักขระได้ยากกว่าจากตำแหน่งสุ่ม ดูรายละเอียดได้ที่ RFC8794 section 4.4ใช่ นี่เป็นข้อดีใหญ่ของ UTF-8 เลย คุณเลื่อนไปหน้าไปหลังในสตริง UTF-8 ได้โดยไม่จำเป็นต้องอ่านตั้งแต่ต้น ในกรณีของ Python เพื่อให้ทำดัชนีสตริงเป็นหน่วยอักขระได้ CPython จึงใช้ wide characters สมัยหนึ่งเคยเลือกได้ว่าจะใช้ 2 ไบต์หรือ 4 ไบต์ แล้วภายหลังก็สลับอัตโนมัติตอนรันไทม์ แต่ก็ยังเป็น wide character ไม่ใช่ UTF-8 ตัวอย่างเช่น แค่อีโมจิตัวเดียวก็อาจทำให้ขนาดสตริงโตขึ้นสี่เท่า ผมกลับเคยคิดว่าอยากใช้ UTF-8 ภายใน แล้วทำชนิดดัชนีให้เป็นอ็อบเจ็กต์ทึบแสง ที่สามารถบวกหรือลบด้วยจำนวนเต็มเล็ก ๆ เพื่อเลื่อนหน้า/หลังในสตริงได้ ส่วนการแปลงเป็นจำนวนเต็มจริง ๆ หรือการใช้ subscript โดยตรง ค่อยเป็นวิธีคำนวณตำแหน่งดัชนีของสตริง ในแนวทางแบบนี้ regex และอย่างอื่นก็จะใช้ดัชนีทึบแสงเพื่อทำงานกับตัวแทนแบบ UTF-8 ได้ดี
ผมคิดว่า LEB128/VLQ ดีกว่าวิธีจำนวนเต็มความยาวแปรผันของ EBML มันใช้ MSB (บิตสูงสุด) ในแต่ละไบต์เป็นตัวแบ่ง — ถ้าเป็น 0 แปลว่าจบลำดับแล้ว ไบต์ถัดไปคือลำดับใหม่ ถ้าเป็น 1 ก็ถอยกลับไปเรื่อย ๆ จนกว่าจะเจอ MSB เป็น 0 และยังมี implementation ที่มีประสิทธิภาพพร้อมการปรับแต่ง SIMD ด้วย ความต่างระหว่าง LEB128 กับ VLQ มีแค่เรื่อง endianness ASCII คือ
0xxxxxxxส่วนอักขระขยายก็เป็น1xxxxxxx 0xxxxxxx,1xxxxxxx 1xxxxxxx 0xxxxxxxเป็นต้น ทำให้เข้ารหัสได้สูงสุดถึง0x1FFFFFใน 3 ไบต์ ซึ่งมากเกินพอสำหรับ Unicode แม้จะไม่ self-synchronizing แต่บีบอัดได้ดีกว่า ASCII ก็ยังคงเป็น 1 ไบต์ และโค้ดพอยต์ต่ำกว่าU+3FFFเช่นสัญลักษณ์คณิตศาสตร์หรือภาษาญี่ปุ่น ก็แทนได้ใน 2 ไบต์ จึงช่วยลดขนาดโค้ดได้ผมว่ามันเป็นจริงได้ก็ต่อเมื่อข้อความไม่ได้เสียหายหรือถูกดัดแปลงอย่างจงใจเท่านั้น การ parse หรือ escape ลำดับ UTF-8 ที่ไม่ถูกต้องเคยก่อให้เกิดช่องโหว่ด้านความปลอดภัยมามากมาย ดูตัวอย่างได้จาก ปัญหา PostgreSQL CVE-2025-1094 และ รายการ CVE ที่เกี่ยวกับ UTF-8
มันไม่ได้ถูกเสมอไป ถ้าเป็น UTF-8 ที่ไม่ถูกต้อง อักขระอาจเปลี่ยนเป็น continuation byte ได้ เช่นถ้าเข้ามาเป็น
0b01100001 0b10000000 0b01100001ก็จะได้อักขระสามตัวเป็นa�aการจะรู้ว่าตำแหน่งที่แสดงผลออกมาเป็นจุดเริ่มของอักขระหรือไม่ ต้องดูไบต์ก่อนหน้า 1–3 ไบต์ถ้าขนาดหลายไบต์สูงสุดคือ 4 ไบต์ อย่างมากก็แค่ต้องมองย้อนกลับไป 3 ไบต์เพื่อดูว่าตำแหน่งปัจจุบันเป็น continuation byte หรือไม่ ถ้าไม่เจอ start byte ก็รู้ได้ว่าเป็นอักขระไบต์เดียว ผมเดาว่าการออกแบบแบบนี้มีไว้เพื่อการกู้คืน คือแม้ไลบรารีจะไม่รู้จัก UTF-8 อย่างถูกต้อง ก็ยังละทิ้งไบต์เสียที่หัวและท้ายของ slice แล้วดึงข้อความที่ยังพอสมเหตุสมผลออกมาได้
ผมคิดว่า UTF-8 ยอดเยี่ยมจริง ๆ แก่นสำคัญคือการที่ ASCII ใช้แค่ 7 บิต การเลือก 7 บิตนี้แม้แต่ในปี 1963 ก็ถือว่าค่อนข้างแปลกอยู่ เลยสงสัยว่านี่เป็นแค่ความบังเอิญทางประวัติศาสตร์หรือไม่ คนที่ออกแบบ ASCII เคยคิดจะใช้บิตเพิ่มอีกหนึ่งบิตเพื่อใส่สัญลักษณ์เพิ่มเติมหรือเปล่า หรือว่าเขาคิดถึง code page หรือความสามารถในการขยายตั้งแต่แรกอยู่แล้ว
ผมไม่รู้เหตุผลที่แน่ชัด แต่เมื่อก่อน 8 บิตไม่ได้มีให้ใช้เสมอไป 7 บิต + parity หรือบิตธงเป็นเรื่องปกติ (เพราะงั้น e-mail ถึงทุกวันนี้ก็ยังเข้ารหัส 8 บิตให้อยู่บน 7 บิตด้วย quoted-printable) การส่งข้อมูลที่ส่งครบทั้ง 8 บิตได้แบบตรง ๆ เรียกว่า 8-bit clean ในบริบทนี้ UTF-8 ก็เป็นตัวอย่างของการใช้บิตที่ 8 ที่เหลือจาก ASCII ได้อย่างชาญฉลาด ดูเพิ่มได้ที่ คำอธิบายเรื่อง 8-bit clean
ผมไม่ใช่ผู้เชี่ยวชาญ แต่เคยอ่านประวัติ ASCII มาก่อน ASCII มีรากมาจากรหัส teletypes (ซึ่งพัฒนามาจากรหัสโทรเลข) รหัสมอร์สมีความยาวแปรผัน จึงลำบากต่อการทำเป็นเครื่องจักร เลยมีรหัส Baudot 5 บิตขึ้นมา เป็นรหัสความยาวคงที่เพื่อลดความซับซ้อนของเครื่องจักร และช่วยลดความเหนื่อยล้าของโอเปอเรเตอร์ด้วย ทุกวันนี้หน่วย symbol rate ยังเรียกว่า baud ก็เพราะ Baudot code ต่อมาพอเริ่มใช้เครื่องพิมพ์ดีดเจาะเทปเป็นอินพุต ความยืดหยุ่นก็มากขึ้น จึงมีสัญลักษณ์พิเศษอย่าง Carriage Return และ Line Feed เพิ่มเข้ามา อุตสาหกรรมคอมพิวเตอร์ยุคแรกใช้บัตรเจาะรูเป็นอินพุต และ IBM ก็พัฒนาระบบ 8 บิตแบบใหม่เพื่อประมวลผลบัตรได้เร็วขึ้น ซึ่งต่อมากลายเป็นฐานสำคัญของ ASCII สุดท้ายแล้วมันคือประวัติการขยายรหัสเลขฐานสองตามพัฒนาการทางเทคโนโลยี ASCII เองก็เป็นผลผลิตของยุคเปลี่ยนผ่านก่อนที่ไบต์ 8 บิตจะกลายเป็นธรรมเนียม
จริง ๆ แล้วบิตที่เหลือนั้นมีไว้ใช้กับ parity นั่นเอง
ส่วนขยาย ASCII แบบ 8 บิต (ตระกูล ISO 8859-x) ถูกใช้อย่างแพร่หลายมาหลายสิบปี และยังใช้ใน code page มาตรฐานของ Windows อยู่ ถ้า ASCII เป็น 8 บิตมาตั้งแต่ต้น ผมก็คิดว่า UTF-8 ก็คงยังเหมาะอยู่ดี เพราะตัวอักษรหลักก็คงจะกระจุกอยู่ใน 128 ตัวแรกอยู่ดี ถ้าจะเรียกว่าความบังเอิญทางประวัติศาสตร์ ก็คงไม่ใช่เรื่องที่ ASCII เป็น 7 บิต แต่คือการที่การพัฒนาคอมพิวเตอร์ยุคนั้นเกิดขึ้นในโลกภาษาอังกฤษเป็นหลัก และภาษาอังกฤษนั้นใช้เพียง 7 บิตก็พอ
7 บิตไม่ได้แปลกอะไรนัก Baudot เป็น 5 บิต พอไม่พอจึงมีรหัส 6 บิต แล้วต่อมาจึงเกิด ASCII 7 บิตขึ้น IBM ทำให้ไบต์ 8 บิตเป็นมาตรฐานบน System/360 (ด้วยรหัส EBCDIC) แต่ผู้ผลิตคอมพิวเตอร์รายอื่นยังไม่ได้มีความยาวไบต์ตายตัว ดังนั้นแม้ 7 บิตจะดูแปลก แต่มันก็เป็นช่วงเวลาที่อักขระกับ system word ยังไม่ได้ลงตัวสวยงาม
ผมเห็นด้วยว่า UTF-8 ออกแบบได้ดีกว่าที่คาดไว้มาก แต่ Unicode เองมีปัญหาเรื่องขอบเขตการใช้งานที่กว้างเกินไป จนเกิดคำถามว่าควรใส่อะไรไว้ใน Unicode บ้าง ถ้ามองแบบสัญชาตญาณก็อาจคิดว่าเป็น “ตัวอักษรพิมพ์ได้ที่มนุษย์ใช้สื่อสารและแยกแยะกันได้ทั้งหมด” แต่ในความเป็นจริงไม่ใช่แบบนั้น
การแยกขอบเขตไม่ชัดเจน เพราะมีโค้ดพอยต์สำหรับ combining อยู่ด้วย
ไม่เฉพาะเจาะจง เพราะตัวอักษรหนึ่งตัวอาจเขียนได้หลายแบบ
แม้ภายนอกจะดูเหมือนตัวเดียวกัน ก็อาจใช้โค้ดพอยต์ต่างกันและมีความหมายต่างกัน
ไม่ได้มีแต่ตัวที่พิมพ์ได้ เพราะมี control character อยู่ด้วย ใส่มาส่วนหนึ่งเพื่อความเข้ากันได้กับ ASCII แต่ก็มี control character ของตัวเองเพิ่มขึ้นเรื่อย ๆ ดูเหมือนตอนนี้ยังไม่มี Unicode point แบบเคลื่อนไหว อย่างน้อยสิ่งที่พิมพ์ได้ก็น่าจะพิมพ์ลงกระดาษได้ แต่ก็ไม่แน่ใจว่าในอนาคตความคงที่นี้จะยังอยู่ไหม อีกอย่าง หนึ่งใน UTF encoding ที่ผู้เขียนไม่ได้พูดถึงคือ utf-7 มันคล้าย utf-8 แต่ถูกสร้างขึ้นภายใต้สมมติฐานว่าสภาพเครือข่ายยุค 80 ใช้บิตสุดท้ายได้ไม่ปลอดภัย ผมเคยได้รับอีเมลที่เข้ารหัสเป็น utf-7 โดยบังเอิญ และจนถึงทุกวันนี้ก็ยังไม่รู้ว่ามันถูกส่งมาได้อย่างไร
UTF-7 ถูกสร้างขึ้นมาเป็นหลักเพื่อสภาพแวดล้อมการส่งข้อมูลที่ไม่ใช่ 8-bit clean เช่นอีเมล ทุกวันนี้มันล้าสมัยแล้ว และยังเข้ารหัส supplementary plane ไม่ได้ด้วย (ทำได้ผ่าน UTF-16 surrogate pair เท่านั้น) ยังมี UTF-9 ด้วย แต่เป็นของล้อเลียนใน RFC วันเมษาหน้าโง่ (สำหรับสภาพแวดล้อม 36 บิตอย่าง PDP-10)
มีเรื่องหนึ่งที่ผมสงสัยมาตลอด: Unicode code point สามารถเข้ารหัสเป็นลำดับไบต์ที่ยาวเกินความจำเป็นได้ UTF-8 ห้ามสิ่งนี้ และยอมรับเฉพาะลำดับที่สั้นที่สุดเท่านั้น เช่น
00000001ก็ได้ และ11000000 10000001ก็สื่อความหมายเดียวกัน ถ้าอย่างนั้นจะออกแบบให้ไม่มี “การเข้ารหัสผิดกฎหมาย” เลยไม่ได้หรือ? เช่น ให้ต้นของลำดับ 2 ไบต์แทนค่าที่มีผลเป็นค่าสูงสุดตัวสุดท้าย แบบนั้น11000000 10000001ก็จะเป็น 128+1 และค่า 0-127 ก็ยังใช้ 1 ไบต์ตามเดิม อย่างนี้ก็จะไม่มีรหัสผิดกฎหมาย และใน edge case บางอย่างสตริงยังอาจสั้นลงเล็กน้อยด้วย เลยสงสัยว่าเป็นเพราะต้นทุนฮาร์ดแวร์ในยุคนั้นหรือไม่ที่ไม่ได้เลือกทางนี้ (อัปเดต: ลำดับบิตที่ถูกต้องควรเป็น10000001และได้แก้แล้ว)U+0080ถึงเป็นc2 80ไม่ใช่c0 80(ค่าตัวแรกหลัง7f) ผมคิดว่าเหตุผลคือดังนี้ a) ถ้ายอมให้มี overlong encoding มันจะกลายเป็นช่องโหว่ด้านความปลอดภัย เพราะบางที่ตรวจแค่ลำดับสั้น b) การเข้ารหัส/ถอดรหัส UTF-8 มาตรฐานทำได้ด้วยแค่ masking (bitmask) และ shifting (bitshift) เท่านั้น แต่แบบที่เสนอจะต้องมีการลบเพิ่มเข้ามาด้วย ในการถกเถียงทางอีเมลเมื่อปี 1992 ก็มีการพูดถึงประเด็นนี้ และ FSS-UTF ก็มี additive constants รวมอยู่ด้วย (ดูข้างล่าง)หัวใจสำคัญคือการคง self-synchronicity ของแพตเทิร์นไบต์ไว้ ถ้าไม่ทำให้ continuation byte ยังคงขึ้นต้นด้วย
10อย่าง11000000 10000001ก็จะเสียคุณสมบัติในการหาขอบเขตโค้ดพอยต์จากสตรีม UTF-8 ที่ถูกตัดทอน และถ้ายังต้องมีการบวก/ลบเพิ่มอีก ประสิทธิภาพของตัวถอดรหัสก็จะลดลง ปัจจุบันมันทำได้ด้วย bit operation อย่างเดียวอย่างที่คอมเมนต์ของ quectophoton บอก continuation byte ต้องขึ้นต้นด้วย
10เสมอ ตัว parser ถึงจะหาขอบเขต code point ได้จากทุกตำแหน่ง ซึ่งตอนออกแบบ UTF-8 ในช่วงต้นยุค 90 ก็พิจารณาไว้แล้วว่าในเวลานั้นยังมีสภาพแวดล้อมการส่งข้อมูลที่ไม่น่าเชื่อถืออยู่มากถ้าใช้แนวทางที่เสนอ การคำนวณตอน encode/decode จะซับซ้อนและช้าลง ทุกวันนี้แค่ shift บิตไม่กี่ครั้งก็พอ แต่ในยุค 90 ที่คอมพิวเตอร์ช้ากว่านี้มาก เรื่องนี้ถือว่าสำคัญ
ถ้าอยากอ่านเรื่องการออกแบบ UTF-8 เพิ่ม ดู one-pager ของ Russ Cox และ บทสรุปประวัติ ของ Rob Pike
UTF-8 ยอดเยี่ยม และคงดีมากถ้าถูกใช้ในทุกสภาพแวดล้อม (กำลังมองไปที่ JavaScript) แต่ข้อเสียอย่างเดียวคือมาตรฐานไม่ได้ระบุชัดเจนว่าจะต้องตีความลำดับไบต์ที่ไม่ถูกต้องอย่างไร ถ้าออกแบบให้ “ทุกลำดับไบต์ต้องมีวิธีตีความที่ระบุไว้เสมอ” ก็น่าจะสมบูรณ์แบบกว่า ผมคิดว่าแนวแบบสเปก HTML5 พิสูจน์แล้วว่านำไปใช้จริงได้สำเร็จ
ผมมีความรู้สึกรัก ๆ เกลียด ๆ กับ backward compatibility ผมไม่ชอบความสับสน แต่ก็ชอบแนวคิดการเดินหน้าแม้ต้องยอมทำของเดิมพังบ้าง ในขณะเดียวกัน ผมก็ชอบกรณีอย่าง UTF-8 หรือ EAN ที่รักษาความเข้ากันได้ไว้พร้อมกับออกแบบอย่างชาญฉลาด ตรงไปตรงมาเลยคือ UTF-8 ดูแทบไม่ได้เสียสละอะไรเพื่อความเข้ากันได้เลย
ถ้าจะเปลี่ยนจริง ๆ ผมอาจเอา control character บางตัวออกแล้วแทนด้วยอักขระที่ใช้บ่อยกว่าเพื่อประหยัดพื้นที่อีกนิด (ถ้าจะยอมทำลายความเข้ากันได้กับ Unicode ไปด้วย) แต่ถ้ามองในฐานะฟอร์แมตเข้ารหัสอักขระหลายไบต์เพียงอย่างเดียว มันก็เกือบจะเหมาะที่สุดแล้ว
ชอบลิงก์ UTF-8 playground (utf8-playground.netlify.app) มาก อยากให้ UI ป้อน code point โดยตรงได้ด้วย (ตอนนี้ทำได้ผ่าน URL อย่างเดียว) (อัปเดต: ตอนนี้ทำได้แล้ว เพราะ PR ถูกรวมเข้าไปแล้ว)
ถ้าอยากเจาะลึกหัวข้อนี้มากขึ้น และชอบอะไรแนว Advent of Code มี i18n-puzzles ที่รวมปริศนาเกี่ยวกับ text encoding หลายข้อไว้ ช่วยให้เข้าใจการทำงานของ UTF-8, UTF-16 และอื่น ๆ ได้อย่างฝังแน่น
ขอบคุณสำหรับบทความดี ๆ ผมเองก็แนะนำ UTF-8 เหมือนกัน แต่คิดว่ามันดีจริง ๆ ก็ต่อเมื่อใช้ร่วมกับ BOM เท่านั้น ไม่อย่างนั้นแอปพลิเคชันจะไม่รู้ว่าเป็น UTF-8 และอาจไม่รู้ด้วยว่าควรบันทึกเป็น UTF-8 ตัวอย่างเช่น บน Windows ถ้าสร้างเอกสารข้อความใหม่แล้วไฟล์ว่างเปล่ามีแค่ BOM แอปไหนก็ตามที่มาเปิดแก้ไข/บันทึกภายหลังก็จะรู้เองว่าต้องบันทึกเป็น UTF-8 ถ้าไม่มี BOM ต่อให้แอปพยายามตรวจจับ encoding อัตโนมัติ ก็ไม่มีทางเชื่อถือได้สมบูรณ์ และเมื่อเพิ่มอักขระพิเศษอย่างตัวมีสำเนียง ความสับสนก็จะยิ่งมากขึ้น (เช่น editor เดาภาษา/encoding ผิด หรือ Notepad เปลี่ยนค่าเริ่มต้นหลังอัปเดต) เพราะงั้นผมเห็นด้วยกับการใช้ UTF-8 แต่ BOM ควรเป็นค่าเริ่มต้นของ OS/แอปเสมอ