頡語(yǔ)言入門(mén) · 第二篇 第 6 課】)
【倉(cāng)頡語(yǔ)言入門(mén) · 第二篇 第 6 課】二進(jìn)制數(shù)據(jù)處理跟字節(jié)序死磕std.binary上一課哈希把字節(jié)攪碎成指紋用了異或、移位、與或非——這些位運(yùn)算只是操作單個(gè)數(shù)值內(nèi)部的比特。這一課的std.binary管的是另一件事一個(gè)數(shù)值在內(nèi)存/文件里到底占幾個(gè)字節(jié)、這些字節(jié)怎么排。為什么同一張 BMP 圖片Windows 和 Mac 讀出來(lái)一樣為什么網(wǎng)絡(luò)協(xié)議里寫(xiě)先傳高字節(jié)答案是同一個(gè)詞——字節(jié)序Endianness。本課把std.binary的三個(gè)接口大端、小端、字節(jié)反轉(zhuǎn)講透最后動(dòng)手解析一張 BMP 圖片的文件頭看看真實(shí)世界的二進(jìn)制格式長(zhǎng)什么樣。本文所有代碼、輸出與報(bào)錯(cuò)文案均在倉(cāng)頡 SDK 1.2.0 下逐行實(shí)測(cè)編譯運(yùn)行。目錄第二篇導(dǎo)航第一篇《倉(cāng)頡語(yǔ)言入門(mén)》共 30 課已完結(jié)環(huán)境搭建 → 語(yǔ)法 → 函數(shù) → 類型系統(tǒng) → 工程化 → 并發(fā) → 兩場(chǎng)實(shí)戰(zhàn)。第二篇在更高的起點(diǎn)上繼續(xù)共17 課、4 個(gè)模塊模塊課次內(nèi)容一、補(bǔ)遺篇0106正則、時(shí)間日期、命令行參數(shù)、隨機(jī)數(shù)、加密哈希、二進(jìn)制處理二、數(shù)據(jù)庫(kù)篇0710std.database.sql 入門(mén)與 CRUD、預(yù)編譯與事務(wù)、手寫(xiě) DAO、綜合小實(shí)戰(zhàn)三、進(jìn)階硬核篇1114反射、宏與 deriving、C 互操作 FFI、并發(fā)容器與對(duì)象池四、實(shí)戰(zhàn)項(xiàng)目篇1517HTTP 服務(wù)端、多線程日志分析器、簡(jiǎn)易爬蟲(chóng)正則表達(dá)式std.regex時(shí)間日期處理std.time命令行參數(shù)解析std.argopt隨機(jī)數(shù)與概率std.random加密與哈希std.crypto二進(jìn)制數(shù)據(jù)處理本文數(shù)據(jù)庫(kù)入門(mén)std.database.sql 與 CRUD預(yù)編譯、事務(wù)與防注入手寫(xiě) DAO 數(shù)據(jù)訪問(wèn)層數(shù)據(jù)庫(kù)綜合小實(shí)戰(zhàn)反射在運(yùn)行時(shí)打量類型宏與 deriving讓編譯器幫你寫(xiě)代碼與 C 互操作FFI并發(fā)容器與對(duì)象池實(shí)戰(zhàn)三HTTP 服務(wù)端實(shí)戰(zhàn)四多線程日志分析器實(shí)戰(zhàn)五簡(jiǎn)易爬蟲(chóng)一、為什么需要二進(jìn)制處理1.1 一個(gè)數(shù)占幾個(gè)字節(jié)倉(cāng)頡里Int32是 32 位整數(shù)——也就是 4 個(gè)字節(jié)。把0x12345678這個(gè)數(shù)存進(jìn)文件或發(fā)過(guò)網(wǎng)絡(luò)物理上就是 4 個(gè)字節(jié)依次排開(kāi)。但哪 4 個(gè)字節(jié)、什么順序0x12345678拆成 4 個(gè)字節(jié)0x12、0x34、0x56、0x78。其中0x12是最高位字節(jié)MSBMost Significant Byte0x78是最低位字節(jié)LSB。寫(xiě)進(jìn)內(nèi)存時(shí)誰(shuí)放低地址、誰(shuí)放高地址兩種主流約定字節(jié)序低地址 → 高地址誰(shuí)先用大端序Big-Endian高位在前12 34 56 78網(wǎng)絡(luò)協(xié)議TCP/IP、Java、PowerPC小端序Little-Endian低位在前78 56 34 12x86/x64 CPU、Windows、ARM多數(shù)模式名字來(lái)自《格列佛游記》里吃雞蛋先敲大頭還是小頭的荒唐爭(zhēng)論——沒(méi)有誰(shuí)對(duì)誰(shuí)錯(cuò)只是約定。麻煩在于不同約定的系統(tǒng)之間傳數(shù)據(jù)必須先講清楚用哪種字節(jié)序否則同一段字節(jié)讀出來(lái)是完全不同的數(shù)。1.2 不處理字節(jié)序會(huì)怎樣假設(shè)你用 x86 電腦小端把0x12345678寫(xiě)進(jìn)文件字節(jié)是78 56 34 12。另一個(gè)程序或另一臺(tái)機(jī)器按大端序讀回來(lái)得到的是0x78563412——同一個(gè)文件兩個(gè)數(shù)。這就是所有跨平臺(tái)二進(jìn)制格式圖片、視頻、網(wǎng)絡(luò)包必須在規(guī)范里寫(xiě)死字節(jié)序的原因。std.binary就是管這件事的給你一套統(tǒng)一接口顯式指定按大端還是小端讀寫(xiě)把約定寫(xiě)進(jìn)代碼而不是靠默契。1.3 先認(rèn)人std.binary 的家底std.binary只提供三個(gè)接口各司其職接口作用關(guān)鍵方法BigEndianOrderT大端序轉(zhuǎn)換readBigEndian/writeBigEndianLittleEndianOrderT小端序轉(zhuǎn)換readLittleEndian/writeLittleEndianSwapEndianOrderT翻轉(zhuǎn)字節(jié)順序swapBytes注意它的設(shè)計(jì)思路不是給你一個(gè)ByteBuffer類那在std.io里第 5 課用過(guò)而是用接口擴(kuò)展的方式直接給所有數(shù)值類型裝上讀/寫(xiě)字節(jié)序列的能力。Int32、Float64、Bool……統(tǒng)統(tǒng)自帶writeBigEndian。這種用接口擴(kuò)展內(nèi)建類型的寫(xiě)法本身就值得學(xué)。二、大端序BigEndianOrder2.1 接口形狀public interface BigEndianOrderT { func writeBigEndian(buffer: ArrayByte): Int64 static func readBigEndian(buffer: ArrayByte): T }writeBigEndian把當(dāng)前值按大端序?qū)戇M(jìn)buffer返回實(shí)際寫(xiě)入的字節(jié)數(shù)Int64 寫(xiě) 8 字節(jié)Int16 寫(xiě) 2 字節(jié)。readBigEndian從buffer開(kāi)頭按大端序讀出一個(gè) T 值。已實(shí)現(xiàn)的類型Bool、Int8/16/32/64、UInt8/16/32/64、Float16/32/64。import std.binary.*后直接可用。2.2 寫(xiě)值 → 字節(jié)數(shù)組import std.binary.* main(): Int64 { let buf ArrayByte(8, repeat: 0) // 分配 8 字節(jié)緩沖區(qū)桌面 SDK 用 repeat: let n 0x1234i16.writeBigEndian(buf) println(寫(xiě)入字節(jié)數(shù): ${n}) println(字節(jié)內(nèi)容: ${buf[..n]}) 0 }寫(xiě)入字節(jié)數(shù): 2 字節(jié)內(nèi)容: [18, 52]0x1234 十進(jìn)制 4660拆成大端序兩個(gè)字節(jié)就是0x12(18)、0x34(52)——高位在前和你腦子里讀十六進(jìn)制的順序一致??庸俜皆诰€文檔鴻蒙 V5 版里數(shù)組分配寫(xiě)的是ArrayByte(8, item: 0)桌面版 SDK 1.2.0 不支持item:命名參數(shù)實(shí)測(cè)報(bào)錯(cuò)unknown named argument prefix item:。桌面版統(tǒng)一用repeat:ArrayByte(8, repeat: 0)。本文所有代碼均用桌面版寫(xiě)法。2.3 讀字節(jié)數(shù)組 → 值let buf2: ArrayByte [0x12, 0x34] let v Int16.readBigEndian(buf2) println(讀回的值: ${v})讀回的值: 4660readBigEndian是靜態(tài)方法掛在類型上調(diào)用Int16.readBigEndian(...)不是在實(shí)例上。寫(xiě)出來(lái)的[18, 52]原樣讀回4660往返一致。2.4 多字節(jié)類型Int64 與 Float64Int64 占 8 字節(jié)Float64 也占 8 字節(jié)看大端序怎么排let val 0x0102030405060708i64 let beBuf ArrayByte(8, repeat: 0) val.writeBigEndian(beBuf) println(Int64 BE: ${beBuf}) let f 12.5f64 let fBuf ArrayByte(8, repeat: 0) f.writeBigEndian(fBuf) println(Float64 BE: ${fBuf}) println(Float64 讀回: ${Float64.readBigEndian(fBuf)})Int64 BE: [1, 2, 3, 4, 5, 6, 7, 8] Float64 BE: [64, 41, 0, 0, 0, 0, 0, 0] Float64 讀回: 12.500000Int64 的字節(jié)01 02 03 04 05 06 07 08一眼就能看出高位在前。Float64 的64 41 00 00 00 00 00 00是 IEEE 754 雙精度浮點(diǎn)的大端編碼——人腦算不出來(lái)沒(méi)關(guān)系只要讀寫(xiě)用同一種字節(jié)序就能無(wú)損還原這就是接口的價(jià)值。2.5 緩沖區(qū)不夠會(huì)怎樣Int32需要 4 字節(jié)只給 2 字節(jié)的緩沖區(qū)讀寫(xiě)都會(huì)拋異常實(shí)測(cè)原文let small: ArrayByte [0x12, 0x34] Int32.readBigEndian(small)Exception in thread main java.lang.IllegalArgumentException: Buffer size(2) is too small.寫(xiě)也是一樣的報(bào)錯(cuò)Buffer size(2) is too small.。所以分配緩沖區(qū)時(shí)大小要 ≥ 目標(biāo)類型的字節(jié)數(shù)Int32→4Int64→8Float32→4……。嫌算麻煩就直接開(kāi) 8 字節(jié)用buf[..n]切出實(shí)際寫(xiě)入的部分。三、小端序LittleEndianOrder3.1 接口形狀和大端序一模一樣只是方法名換了public interface LittleEndianOrderT { func writeLittleEndian(buffer: ArrayByte): Int64 static func readLittleEndian(buffer: ArrayByte): T }實(shí)現(xiàn)的類型也完全相同Bool、Int8/16/32/64、UInt8/16/32/64、Float16/32/64。3.2 小端序?qū)懭氲臀辉谇發(fā)et buf ArrayByte(8, repeat: 0) let n 0x12345678i32.writeLittleEndian(buf) println(Int32 LE 寫(xiě)入字節(jié): ${buf[..n]})Int32 LE 寫(xiě)入字節(jié): [120, 86, 52, 18]0x12345678的 4 個(gè)字節(jié)按小端序排列0x78(120)、0x56(86)、0x34(52)、0x12(18)——低位在前正好和大端序反過(guò)來(lái)。讀回來(lái)驗(yàn)證let buf2: ArrayByte [0x78, 0x56, 0x34, 0x12] let v Int32.readLittleEndian(buf2) println(讀回: ${v})讀回: 305419896305419896就是0x12345678。讀寫(xiě)配對(duì)順序一致值就對(duì)。3.3 大端 vs 小端同一張圖把同一個(gè)Int64值分別用大端、小端寫(xiě)出來(lái)對(duì)比一目了然let val 0x0102030405060708i64 let beBuf ArrayByte(8, repeat: 0) val.writeBigEndian(beBuf) let leBuf ArrayByte(8, repeat: 0) val.writeLittleEndian(leBuf) println(大端: ${beBuf}) println(小端: ${leBuf})大端: [1, 2, 3, 4, 5, 6, 7, 8] 小端: [8, 7, 6, 5, 4, 3, 2, 1]同樣 8 個(gè)字節(jié)順序完全相反。這就是字節(jié)序這三個(gè)字的全部含義。四、翻轉(zhuǎn)字節(jié)序SwapEndianOrder4.1 接口與用途public interface SwapEndianOrderT { func swapBytes(): T }swapBytes()返回一個(gè)字節(jié)順序完全翻轉(zhuǎn)的新值。實(shí)現(xiàn)的類型Int8/16/32/64、UInt8/16/32/64浮點(diǎn)和 Bool 沒(méi)有——翻轉(zhuǎn)浮點(diǎn)字節(jié)沒(méi)有意義。它的典型用途從文件里讀出來(lái)后才發(fā)現(xiàn)字節(jié)序不對(duì)就地翻轉(zhuǎn)一下。比如某格式文檔寫(xiě)的是大端序你誤用了小端序的readLittleEndian得到一個(gè)亂碼數(shù)值swapBytes()一下就救回來(lái)了——等價(jià)于換一種讀法重算。4.2 實(shí)測(cè)let val 0x0102030405060708i64 let swapped val.swapBytes() let swapBuf ArrayByte(8, repeat: 0) swapped.writeBigEndian(swapBuf) println(翻轉(zhuǎn)后大端字節(jié): ${swapBuf})翻轉(zhuǎn)后大端字節(jié): [8, 7, 6, 5, 4, 3, 2, 1]原值大端是[1,2,3,4,5,6,7,8]swapBytes()后再按大端寫(xiě)出就是[8,7,6,5,4,3,2,1]——正好等于原值的小端寫(xiě)法。換句話說(shuō)val.swapBytes()的大端表示 val的小端表示。UInt 類型也一樣let u 0x1234u16 println(0x1234 翻轉(zhuǎn)后: ${u.swapBytes()})0x1234 翻轉(zhuǎn)后: 133300x1234翻轉(zhuǎn)字節(jié)得到0x3412 13330。五、實(shí)戰(zhàn)解析 BMP 圖片文件頭光看接口太干來(lái)個(gè)真實(shí)格式。BMP 是 Windows 原生圖片格式文件頭全部用小端序因?yàn)?Windows 跑在 x86 上。我們用LittleEndianOrder把一個(gè) BMP 文件頭拆開(kāi)看看。5.1 BMP 文件頭結(jié)構(gòu)BMP 文件頭14 字節(jié) DIB 頭40 字節(jié)BITMAPINFOHEADER的關(guān)鍵字段偏移長(zhǎng)度字段類型說(shuō)明02signature字符“BM” 魔數(shù)24fileSizeUInt32 LE文件總字節(jié)數(shù)64reserved—保留通常 0104dataOffsetUInt32 LE像素?cái)?shù)據(jù)起始偏移144dibHeaderSizeUInt32 LEDIB 頭大小40184widthInt32 LE圖像寬度像素224heightInt32 LE圖像高度像素262planesUInt16 LE顏色平面數(shù)1282bitsPerPixelUInt16 LE每像素位數(shù)24真彩色5.2 構(gòu)造 解析為了不依賴外部圖片文件我們?cè)诖a里手動(dòng)構(gòu)造一個(gè) 54 字節(jié)的 BMP 頭再用LittleEndianOrder逐字段讀出來(lái)——寫(xiě)和讀都練到import std.binary.* func hexOf(bytes: ArrayByte): String { let hex [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f] var s for (b in bytes) { s s hex[Int64(b 4)] hex[Int64(b 0xF)] } return s } main(): Int64 { // 構(gòu)造 BMP 文件頭54 字節(jié) let bmp ArrayByte(54, repeat: 0) bmp[0] 0x42 // B bmp[1] 0x4D // M 54u32.writeLittleEndian(bmp[2..6]) // fileSize 54u32.writeLittleEndian(bmp[10..14]) // dataOffset 40u32.writeLittleEndian(bmp[14..18]) // dibHeaderSize 800i32.writeLittleEndian(bmp[18..22]) // width 600i32.writeLittleEndian(bmp[22..26]) // height 1u16.writeLittleEndian(bmp[26..28]) // planes 24u16.writeLittleEndian(bmp[28..30]) // bitsPerPixel println(原始頭 (前 30 字節(jié)): ${hexOf(bmp[..30])}) // 解析 let sig String.fromUtf8(bmp[0..2]) let fileSize UInt32.readLittleEndian(bmp[2..6]) let dataOffset UInt32.readLittleEndian(bmp[10..14]) let dibSize UInt32.readLittleEndian(bmp[14..18]) let width Int32.readLittleEndian(bmp[18..22]) let height Int32.readLittleEndian(bmp[22..26]) let bpp UInt16.readLittleEndian(bmp[28..30]) println(簽名: ${sig}) println(文件大小: ${fileSize} 字節(jié)) println(像素?cái)?shù)據(jù)偏移: ${dataOffset}) println(DIB 頭大小: ${dibSize}) println(寬度: ${width} px) println(高度: ${height} px) println(色深: ${bpp} bpp) 0 }原始頭 (前 30 字節(jié)): 424d36000000000000003600000028000000200300005802000001001800 簽名: BM 文件大小: 54 字節(jié) 像素?cái)?shù)據(jù)偏移: 54 DIB 頭大小: 40 寬度: 800 px 高度: 600 px 色深: 24 bpp幾個(gè)要點(diǎn)切片直接當(dāng)緩沖區(qū)bmp[2..6]是從第 2 字節(jié)開(kāi)始、長(zhǎng)度 4 的切片writeLittleEndian/readLittleEndian直接吃切片不用額外拷數(shù)組。這是處理固定偏移格式最順手的寫(xiě)法。魔數(shù)用字符串讀前兩字節(jié)0x42 0x4D是 ASCII 的BM用String.fromUtf8還原成人話。寬度 800 的小端字節(jié)是20 03 00 000x00000320 800小端序低位在前 →20 03 00 00??摧敵龅谌?..2003000058020000...20030000是寬 80058020000是高 6000x00000258 600。真實(shí)讀.bmp文件時(shí)把構(gòu)造那段換成File.readFrom(path)讀到bmp數(shù)組即可解析代碼完全不變。六、常用 API 速查API說(shuō)明val.writeBigEndian(buf)把val按大端序?qū)懭隻uf返回寫(xiě)入字節(jié)數(shù)Type.readBigEndian(buf)從buf按大端序讀出一個(gè)Type值靜態(tài)方法val.writeLittleEndian(buf)按小端序?qū)懭敕祷貙?xiě)入字節(jié)數(shù)Type.readLittleEndian(buf)按小端序讀出靜態(tài)方法val.swapBytes()返回字節(jié)順序翻轉(zhuǎn)后的新值僅整數(shù)類型ArrayByte(N, repeat: 0)分配 N 字節(jié)零填充緩沖區(qū)桌面 SDK 用repeat:一張表分清三個(gè)接口接口方向?qū)崿F(xiàn)的類型BigEndianOrderT高位在前Bool, Int8/16/32/64, UInt8/16/32/64, Float16/32/64LittleEndianOrderT低位在前同上SwapEndianOrderT翻轉(zhuǎn)Int8/16/32/64, UInt8/16/32/64無(wú)浮點(diǎn)、無(wú) Bool七、常見(jiàn)問(wèn)題 FAQQ1writeBigEndian返回的Int64是干嘛的為什么不直接返回字節(jié)數(shù)組返回的是實(shí)際寫(xiě)入的字節(jié)數(shù)。接口設(shè)計(jì)成調(diào)用方管緩沖區(qū)——你自己開(kāi)數(shù)組、自己決定大小方法只負(fù)責(zé)往里寫(xiě)并告訴你寫(xiě)了幾個(gè)字節(jié)。好處是可以復(fù)用同一個(gè)大緩沖區(qū)連續(xù)寫(xiě)多個(gè)字段像 BMP 頭那樣不用反復(fù)分配。要取實(shí)際寫(xiě)入的部分用buf[..n]切片。Q2readBigEndian為什么是靜態(tài)方法Int32.readBigEndian(...)而不是實(shí)例方法因?yàn)樽x之前你手里還沒(méi)有Int32實(shí)例——讀的目的就是造出一個(gè)實(shí)例。所以它必須掛在類型上作為靜態(tài)方法。寫(xiě)則相反你已經(jīng)有一個(gè)值了在值上調(diào)writeBigEndian。一讀一寫(xiě)一個(gè)靜態(tài)一個(gè)實(shí)例對(duì)稱得很。Q3為什么swapBytes()沒(méi)有 Float64/Float32 版本翻轉(zhuǎn)浮點(diǎn)數(shù)的字節(jié)在數(shù)學(xué)上沒(méi)有意義——IEEE 754 浮點(diǎn)的符號(hào)位、指數(shù)、尾數(shù)是按位劃分的翻轉(zhuǎn)字節(jié)會(huì)把它們?nèi)騺y得到的是一個(gè)幾乎隨機(jī)的浮點(diǎn)數(shù)沒(méi)有應(yīng)用場(chǎng)景。所以SwapEndianOrder只給整數(shù)類型實(shí)現(xiàn)。Q4緩沖區(qū)太小的報(bào)錯(cuò)文案是什么怎么避免實(shí)測(cè)報(bào)錯(cuò)Buffer size(N) is too small.N 是你給的實(shí)際大小。避免方法分配緩沖區(qū)時(shí)大小 ≥ 類型字節(jié)數(shù)Int8/UInt8→1Int16/UInt16→2Int32/UInt32/Float32→4Int64/UInt64/Float64→8。圖省事就統(tǒng)一開(kāi) 8 字節(jié)。Q5官方文檔里ArrayByte(8, item: 0)為什么我這里編譯不過(guò)官方在線文檔是鴻蒙OpenHarmony版 SDK 的寫(xiě)法桌面版 SDK 1.2.0 的數(shù)組構(gòu)造命名參數(shù)是repeat:不是item:。寫(xiě)ArrayByte(8, item: 0)實(shí)測(cè)報(bào)unknown named argument prefix item:。桌面版統(tǒng)一用ArrayByte(8, repeat: 0)。Q6std.binary有沒(méi)有 Base64、十六進(jìn)制編碼之類的功能1.2.0 的std.binary只做字節(jié)序轉(zhuǎn)換大端/小端/翻轉(zhuǎn)沒(méi)有 Base64、Hex 編碼。第 5 課的toHex是我們手寫(xiě)的十六進(jìn)制編碼目前需要自己實(shí)現(xiàn)或找第三方庫(kù)。Base64 同理。八、課后練習(xí)規(guī)矩不變前 4 題必做第 5 題選做。每題只考本課 12 個(gè)知識(shí)點(diǎn)題面給全函數(shù)簽名和 API 提示先看懂題再動(dòng)筆。第 1 題必做大端讀整數(shù)用Int32.readBigEndian從字節(jié)數(shù)組[0x00, 0x00, 0x01, 0xF4]讀出一個(gè) Int32 值并打印。提示直接Int32.readBigEndian(buf)0x000001F4 500。import std.binary.* main(): Int64 { let buf: ArrayByte [0x00, 0x00, 0x01, 0xF4] // 補(bǔ)全讀出并打印 0 }期望輸出500第 2 題必做小端寫(xiě) Int16把0x0100i16按小端序?qū)懭?2 字節(jié)緩沖區(qū)打印寫(xiě)入的字節(jié)內(nèi)容。提示writeLittleEndian返回寫(xiě)入字節(jié)數(shù)0x0100小端序是00 01低位0x00在前。import std.binary.* main(): Int64 { let buf ArrayByte(2, repeat: 0) // 補(bǔ)全寫(xiě)入并打印 buf 內(nèi)容 0 }期望輸出[0, 1]第 3 題必做翻轉(zhuǎn)字節(jié)序?qū)?x12345678u32調(diào)用swapBytes()打印結(jié)果的十進(jìn)制值。提示0x12345678翻轉(zhuǎn)后是0x78563412直接println(val.swapBytes())。import std.binary.* main(): Int64 { let v 0x12345678u32 // 補(bǔ)全打印翻轉(zhuǎn)后的值 0 }期望輸出2018915346第 4 題必做大端讀 Float32字節(jié)數(shù)組[0x41, 0x48, 0x00, 0x00]是12.5f32的大端編碼。用Float32.readBigEndian讀出來(lái)并打印。提示Float32 也是BigEndianOrder的實(shí)現(xiàn)類型調(diào)用方式和整數(shù)一樣。import std.binary.* main(): Int64 { let buf: ArrayByte [0x41, 0x48, 0x00, 0x00] // 補(bǔ)全讀出 Float32 并打印 0 }期望輸出12.500000第 5 題選做小端讀 BMP 寬度給定一個(gè) 22 字節(jié)的 BMP 頭片段用小端序讀出其中的寬度字段偏移 18長(zhǎng)度 4Int32。提示寬度在偏移 18 處用切片buf[18..22]讀Int32.readLittleEndian。import std.binary.* main(): Int64 { // 一個(gè) BMP 頭片段寬 10240x00000400高 7680x00000300 let buf: ArrayByte [ 0x42, 0x4D, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00 ] // 補(bǔ)全讀出寬度并打印 0 }期望輸出1024下節(jié)預(yù)告字節(jié)序解決了數(shù)值怎么變成字節(jié)的問(wèn)題但真實(shí)世界的數(shù)據(jù)格式遠(yuǎn)不止整數(shù)——數(shù)據(jù)庫(kù)里一行行的結(jié)構(gòu)化記錄怎么存、怎么查、怎么改下一課進(jìn)入數(shù)據(jù)庫(kù)篇從std.database.sql開(kāi)始把數(shù)據(jù)持久化從文件升級(jí)到真正的數(shù)據(jù)庫(kù)。系列說(shuō)明本系列基于 Windows 平臺(tái) CIDE 倉(cāng)頡 SDK1.2.0編寫(xiě)所有代碼均已實(shí)際編譯運(yùn)行通過(guò)。如遇 SDK 版本差異導(dǎo)致的細(xì)節(jié)出入以你本地版本為準(zhǔn)歡迎評(píng)論區(qū)交流。 遇到問(wèn)題掃碼聯(lián)系作者跟著課程練習(xí)時(shí)如果在 SDK 安裝、環(huán)境變量配置、編譯報(bào)錯(cuò)或調(diào)試上卡住歡迎掃碼加作者企業(yè)微信直接咨詢請(qǐng)備注倉(cāng)頡課程離線環(huán)境下圖片可能加載不出來(lái)也可以在 CIDE 菜單Help ? 聯(lián)系作者 / Contact中查看同一張二維碼應(yīng)用內(nèi)置兜底圖無(wú)需聯(lián)網(wǎng)。 工具下載本系列全程使用的倉(cāng)頡 IDE ——CIDE免費(fèi)開(kāi)源、社區(qū)版GitCode 倉(cāng)庫(kù) / 安裝包下載https://gitcode.com/wp_upala/cide打開(kāi)頁(yè)面后進(jìn)入發(fā)行版Releases兩種包任選其一安裝版下載CIDE-版本-x64-Setup.exe雙擊安裝適合日常長(zhǎng)期使用免安裝版Portable下載CIDE-版本-x64-Portable.zip解壓到任意目錄即用不寫(xiě)注冊(cè)表、不留安裝痕跡拷到 U 盤(pán)也能在別的電腦直接運(yùn)行包內(nèi)附《使用說(shuō)明.txt》。適合先試用、或在受限電腦上學(xué)習(xí)本系列課程。倉(cāng)頡 SDK 請(qǐng)前往倉(cāng)頡編程語(yǔ)言官網(wǎng)下載https://cangjie-lang.cn