Mảng Và Chuỗi Trong Ngôn Ngữ D
Kỳ nghỉ Quốc khánh vừa qua, tôi đã hoàn thành cuốn sách “Ngôn ngữ lập trình D”. Quyển sách chứa rất nhiều nội dung thú vị mà tôi muốn chia sẻ một phần với các bạn.
Mảng, chuỗi và mảng kết hợp (bảng băm) là ba cấu trúc dữ liệu quan trọng nhất, gần như có thể dùng chúng mô phỏng mọi cấu trúc phức tạp hơn. Lua chính là ví dụ điển hình cho tư tưởng này, dù họ tích hợp mảng và mảng kết hợp thành một kiểu dữ liệu duy nhất gọi là table. Ngôn ngữ D hỗ trợ mạnh mẽ cả ba cấu trúc này với những định nghĩa rõ ràng và mạch lạc. Bài viết này sẽ tập trung vào mảng và chuỗi, phần mảng kết hợp xin phép được bỏ qua.
Mảng có thể hiểu là vùng nhớ liên tục chứa các phần tử cùng kiểu dữ liệu. Trong C, dù mảng và con trỏ là hai kiểu dữ liệu khác nhau nhưng trình biên dịch lại xử lý giống nhau. Về bản chất, mảng chính là con trỏ. Tuy nhiên, mảng mang theo thông tin về độ dài - xác định khoảng vùng nhớ quản lý. Một số mảng có kích thước cố định được biết trước ở thời điểm biên dịch, số khác cần mở rộng động và độ dài thay đổi theo thời gian thực. Việc truy xuất ngẫu nhiên trên mảng thiếu kiểm tra biên giới luôn tiềm ẩn rủi ro.
Là ngôn ngữ hướng đến cả hiệu năng lẫn an toàn, D có cách tiếp cận độc đáo về kiểm tra biên giới. Lập trình viên có thể đánh dấu đoạn mã là an toàn (safe), hệ thống (system) hoặc giao diện an toàn (interface safe). Trình biên dịch sẽ tự động chèn mã kiểm tra biên giới tương ứng. Thậm chí trong phiên bản debug, ngay cả mã hệ thống cũng có thêm kiểm tra kiểu assert.
Nhờ cơ chế GC quản lý bộ nhớ tập trung (và yêu cầu mọi dữ liệu có thể di chuyển), việc quản lý slice (các đoạn trích mảng) trở nên đơn giản. Các slice khác nhau có thể trỏ đến cùng một vùng nhớ mà không lo vấn đề tuổi thọ dữ liệu. Khi mở rộng mảng, hệ thống có thể cấp phát bộ nhớ mới hoặc mở rộng tại chỗ theo nhu cầu.
Không thể không nhắc đến cơ chế postblit khi nói về mở rộng mảng trong D. Trong khi C++ phải vật lộn với các giải pháp phức tạp như template đặc hóa cho POD hoặc move semantics qua rvalue reference để tối ưu hiệu năng khi vector mở rộng, D lại có cách tiếp cận đơn giản hơn. Thay vì constructor sao chép hay di chuyển phức tạp, D dùng cơ chế postblit: dữ liệu được sao chép nguyên khối (blit), sau đó phương thức postblit sẽ chỉnh sửa bản sao mới. Cách tiếp cận này không đụng đến dữ liệu gốc, vừa đơn giản vừa hiệu quả. Cơ chế này ngoài việc hỗ trợ mở rộng mảng còn giải quyết được vấn đề RVO (Return Value Optimization) mà C++ phải dùng thủ thuật.
Với mảng kích thước cố định, D xử lý theo kiểu giá trị (trái ngược với mảng động dùng kiểu tham chiếu). Các mảng cố định có độ dài khác nhau là những kiểu dữ liệu khác nhau, nhưng đều có thể chuyển đổi ngầm sang mảng động. Mảng cố định ngắn phù hợp với việc truyền tham trị nhờ hiệu năng cao và tương đồng các kiểu dữ liệu nguyên thủy. Với mảng dài, có thể dùng từ khóa ref để truyền tham chiếu.
Điều đặc biệt ở D là kiểu string thực chất là một mảng động không thay đổi (immutable), alias cho kiểu immutable(char)[]. Ngôn ngữ cũng hỗ trợ wstring (UTF-16) và dstring (UTF-32). Trong khi đó, nhiều ngôn ngữ C-like như C#, Java, Go hay C++ lại coi chuỗi là kiểu dữ liệu độc lập.
Trình biên dịch D còn cung cấp cú pháp mô tả chuỗi đa dạng, hỗ trợ nhị phân (0b10111), hex dễ đọc (x"7f 00 01"), chuỗi nguyên mẫu (raw string) không cần escape. Unicode được chọn làm tập ký tự chuẩn với các mã hóa mặc định UTF-8/UTF-16/UTF-32.
Ví dụ đặc biệt về chuỗi Unicode:
|
|
Với bản chất là kiểu tham chiếu (immutable(char)[]), việc truyền chuỗi rất nhẹ nhàng. Các biến immutable có thể chia sẻ an toàn giữa các luồng. Cơ chế GC đảm bảo tuổi thọ dữ liệu hợp lý.
Toán tử == trên mảng trong D đã được nạp để so sánh theo giá trị, do chuỗi là mảng nên == cũng làm việc đúng như mong đợi. Trong D, mảng/chuỗi có thể dùng trực tiếp trong câu lệnh switch - một tiện ích đáng giá. Nếu muốn so sánh hai chuỗi có phải cùng đối tượng (cùng vùng nhớ), D cung cấp toán tử is, tránh phải gọi hàm gián tiếp như Object.ReferenceEquals trong C#.
Dù mảng trong D đã tối ưu hiệu năng, ngôn ngữ vẫn cho phép dùng con trỏ thô thông qua thuộc tính .ptr. Tuy nhiên, khi dùng con trỏ theo cách này, mọi trách nhiệm an toàn bộ nhớ thuộc về lập trình viên. Để đảm bảo an toàn, D cho phép chọn subset SafeD để cấm hầu hết thao tác con trỏ trong quá trình biên dịch.