Ngôn ngữ lập trình
Ngôn ngữ lập trình (Programming Language) là một ngôn ngữ hình thức được tạo ra để con người mô tả công việc mà máy tính cần thực hiện và biểu diễn công việc đó...
| Phân loại | Ngôn ngữ hình thức của Khoa học máy tính, phương tiện phát triển của Kỹ thuật phần mềm |
|---|---|
| Mục đích | Biểu diễn chương trình máy tính và quy trình tính toán |
| Thành phần cốt lõi | Cú pháp, Ngữ nghĩa, Hệ thống kiểu, mô hình thực thi |
| Phương thức triển khai | Trình biên dịch, Trình thông dịch, Máy ảo |
| Mô hình chính | Lập trình mệnh lệnh, Lập trình hướng đối tượng, Lập trình hàm, Lập trình logic, Lập trình khai báo |
| Phân loại chính | Ngôn ngữ lập trình bậc thấp, Ngôn ngữ lập trình bậc cao, Ngôn ngữ lập trình đa dụng, Ngôn ngữ chuyên biệt miền |
| Tiêu chuẩn hóa | các tổ chức tiêu chuẩn hóa như ISO/IEC JTC 1/SC 22, Ecma International, v.v. |
| Lĩnh vực liên quan | Trình biên dịch, Lý thuyết ngôn ngữ lập trình, Ngữ nghĩa hình thức, Phân tích chương trình tĩnh |
Ngôn ngữ lập trình (Programming Language) là một ngôn ngữ hình thức được tạo ra để con người mô tả công việc mà máy tính cần thực hiện và biểu diễn công việc đó dưới dạng máy tính có thể hiểu và thi hành. Ngôn ngữ lập trình được thiết kế để biểu đạt nhiều mô hình tính toán như thuật toán, cấu trúc dữ liệu, luồng điều khiển, sự trừu tượng và xử lý song song; đây là công nghệ cốt lõi làm nền tảng cho phát triển phần mềm. Ngôn ngữ lập trình hiện đại được sử dụng trong gần như mọi lĩnh vực điện toán, gồm phần mềm hệ thống, ứng dụng web, ứng dụng di động, trí tuệ nhân tạo, hệ thống nhúng, phát triển trò chơi và tính toán khoa học. Mỗi ngôn ngữ có những đặc tính khác nhau về cú pháp, phương thức thực thi, quản lý bộ nhớ, hệ thống kiểu, hiệu năng và năng suất tùy theo mục tiêu cùng triết lý thiết kế. Ngôn ngữ lập trình thường được trình biên dịch hoặc trình thông dịch chuyển đổi thành mã máy hay biểu diễn trung gian để thực thi. Trong khoa học máy tính, chúng đã được nghiên cứu và phát triển từ nhiều góc độ như thiết kế ngôn ngữ, cú pháp (Syntax), ngữ nghĩa (Semantics), hệ thống kiểu (Type System), trình biên dịch, runtime (Runtime) và thư viện chuẩn.
Lịch sử
Lịch sử của ngôn ngữ lập trình không đột ngột bắt đầu sau sự ra đời của máy tính điện tử. Nguồn gốc của nó có thể được tìm thấy trong nỗ lực của con người nhằm thể hiện các quy trình tính toán bằng các ký hiệu và quy tắc nhất định và để máy móc tự động thực hiện các quy trình đó. Các ngôn ngữ lập trình ngày nay đã trở thành các ký hiệu để truyền tải hướng dẫn đến máy móc và đã phát triển thành các hệ thống chính thức để biểu diễn thuật toán, dữ liệu, trừu tượng hóa, thay đổi trạng thái, mối quan hệ logic và mô hình tính toán.
Tính toán cơ học và hình thành khái niệm chương trình
Máy dệt Jacquard, được phát triển bởi Joseph Marie Jacquard vào đầu thế kỷ 19, kiểm soát kiểu dệt bằng cách sắp xếp các lỗ trên thẻ đục lỗ. Mặc dù thẻ dệt Jacquard không chứa các lệnh tính toán số, nhưng nó được coi là tiền lệ quan trọng ảnh hưởng đến các phương pháp điều khiển chương trình sau này ở chỗ hoạt động của máy có thể được thay đổi theo các mẫu được ghi trong phương tiện bên ngoài.
Charles Babbage đã thiết kế Máy phân tích, một thiết bị tính toán cơ học đa năng, bắt đầu từ những năm 1830. Máy phân tích được thiết kế để bao gồm Mill, một thiết bị thực hiện các phép tính, Store, một thiết bị lưu trữ để lưu trữ số, thiết bị cho đầu vào và đầu ra cũng như các hướng dẫn tính toán và dữ liệu được gửi đến thẻ đục lỗ. Mặc dù chiếc máy thực tế chưa hoàn thiện nhưng nó có cấu trúc tương tự như một máy tính đa năng hiện đại ở chỗ nó phân biệt giữa đơn vị số học và đơn vị bộ nhớ và thực hiện một số phép tính theo các lệnh bên ngoài.
Năm 1843, Ada Lovelace đã dịch sang tiếng Anh bản mô tả Máy phân tích do Luigi Menabrea viết và bổ sung phần chú thích dài hơn cả nguyên bản. Trong đó, Chú thích G có một bảng trình bày thứ tự phép toán, sự dịch chuyển của biến, kết quả trung gian và quá trình lặp để tính số Bernoulli bằng Máy phân tích. Bảng này thường được giới thiệu là chương trình chi tiết đầu tiên được xuất bản dành cho một cỗ máy. Tuy nhiên, Babbage cũng từng viết quy trình tính toán cho Máy phân tích vào khoảng năm 1837. Vì vậy, thay vì khẳng định vô điều kiện rằng Lovelace là lập trình viên đầu tiên trong lịch sử, sẽ chính xác hơn khi xem bà là người đầu tiên trình bày một cách có hệ thống thuật toán dành cho máy được công bố rộng rãi.
Lovelace tin rằng máy phân tích có thể vượt xa các phép tính số và xử lý các đối tượng có thể được biểu thị bằng các quy tắc nhất định. Đây là quan điểm ban đầu về một chương trình như một thủ tục chung để thao tác các ký hiệu hơn là kết quả của các phép tính cụ thể. Tuy nhiên, do bản thân máy phân tích chưa được hoàn thiện nên không thể thực thi chương trình trên máy thực tế hoặc tạo thành một hệ thống lập trình có thể sử dụng nhiều lần vào thời điểm đó.
Cơ sở toán học của khả năng tính toán
Vào những năm 1930, trước khi máy tính thực tế trở nên phổ biến, nghiên cứu đã được tiến hành để xác định các phép tính và thuật toán về mặt toán học. Alonzo Church đã phát triển Phép tính lambda, một hệ thống tính toán trừu tượng hóa và áp dụng các hàm, đồng thời Alan Turing đã đề xuất Máy Turing trong một bài báo năm 1936 sử dụng một cuộn băng có ghi các ký hiệu trên đó và các chuyển đổi trạng thái hữu hạn.
Turing không chỉ xác định một máy thực hiện một phép tính cụ thể mà còn là Máy Turing phổ quát có thể bắt chước hoạt động của một máy Turing khác bằng cách nhận mô tả của nó. Điều này đã trở thành nền tảng lý thuyết cho một máy tính đa năng lưu trữ các hướng dẫn và dữ liệu trong cùng một thiết bị bộ nhớ và thực hiện các tác vụ khác nhau bằng cách thay thế các chương trình được lưu trữ. Phép tính lambda và máy Turing được cho là tương đương nhau về phạm vi các hàm tính toán, mặc dù biểu thức của chúng khác nhau. Mô hình tính toán này có ảnh hưởng lâu dài đến các lệnh gọi hàm, đệ quy, quy tắc đánh giá, lý thuyết loại và ngữ nghĩa hình thức trong các ngôn ngữ lập trình tiếp theo.
Máy tính điện tử sơ khai và ngôn ngữ máy
Những chiếc máy tính điện tử đầu thập niên 1940 còn lâu mới có thể viết và thực thi mã nguồn như ngày nay. Một số máy tính đã cấu hình quy trình tính toán của chúng bằng cách thay đổi kết nối cáp, công tắc và hệ thống dây điện cũng như thay đổi các chương trình yêu cầu cấu hình lại cấu hình vật lý của máy. Sau này, là phương pháp chương trình được lưu trữ, ghi lại các hướng dẫn ở dạng số trên thiết bị bộ nhớ hoặc băng đục lỗ, các chương trình và phần cứng dần dần được tách ra.
Các chương trình ban đầu được viết bằng Ngôn ngữ máy, là các hướng dẫn bằng số được bộ xử lý trung tâm giải thích trực tiếp. Các lập trình viên phải ghi thủ công mã hoạt động, địa chỉ bộ nhớ, vị trí nhánh bằng số và khi kiểu máy tính thay đổi, hầu hết hệ thống lệnh và chương trình đều phải viết lại. Ngay cả một phép tính địa chỉ nhỏ hoặc nhập sai một lệnh cũng có thể dẫn đến trục trặc cho toàn bộ chương trình, đồng thời việc đọc và duy trì cấu trúc chương trình cũng rất khó khăn.
Để giảm bớt vấn đề này, Hợp ngữ ra đời và gán những tên ký hiệu như ADD, LOAD và JMP cho lệnh máy. Assembler chuyển các lệnh ký hiệu này thành mã máy và tính toán địa chỉ bộ nhớ. Hợp ngữ dễ đọc hơn mã máy dạng số nhưng vẫn phụ thuộc chặt chẽ vào tập lệnh của một bộ xử lý cụ thể; logic chương trình vẫn phải được viết phù hợp với cấu trúc thanh ghi và bộ nhớ của máy.
Ngôn ngữ lập trình đầu tiên
Ngôn ngữ lập trình đầu tiên là gì phụ thuộc vào tiêu chí bạn sử dụng để xác định ngôn ngữ lập trình.
- Thuật toán chi tiết đầu tiên được xuất bản cho một máy cụ thể là quy trình tính toán của Ada Lovelace cho Máy phân tích.
- Plankalkül được nhắc đến là ngôn ngữ lập trình cấp cao đầu tiên được thiết kế.
- Short Code được đề cập như một hệ thống ký hiệu tiên tiến ban đầu được sử dụng trong máy tính điện tử thực tế.
- A-0 System được đề cập như một hệ thống biên dịch và lập trình tự động sớm.
- FORTRAN được đề cập là ngôn ngữ biên dịch cấp cao thực tế được sử dụng rộng rãi đầu tiên.
Konrad Zuse đã thiết kế Plankalkül từ năm 1942 đến năm 1945. Plankalkül được thiết kế để thể hiện không chỉ các con số mà còn cả dữ liệu phức tạp tương tự như mảng, giá trị logic và bản ghi cũng như để mô tả các câu lệnh có điều kiện và cấu trúc lặp đi lặp lại. Nó được coi là thiết kế ngôn ngữ lập trình cấp cao đầu tiên ở chỗ nó cố gắng diễn đạt các quy trình giải quyết vấn đề ở cấp độ cao hơn thay vì chỉ liệt kê các lệnh máy cụ thể. Tuy nhiên, do chiến tranh và môi trường hậu chiến, nó không được triển khai trên máy tính của Zuse vào thời điểm đó và nội dung của nó sau đó mới được biết đến.
Short Code là một trong những ngôn ngữ cấp cao đầu tiên, được đề xuất vào khoảng năm 1949 và được sử dụng vào đầu những năm 1950. Thay vì các lệnh máy, các biểu thức toán học có thể được biểu diễn dưới dạng ký hiệu, nhưng thay vì tạo ra toàn bộ ngôn ngữ máy bằng trình biên dịch theo nghĩa chung, một trình thông dịch sẽ diễn giải và thực thi các lệnh. Viết đơn giản hơn ngôn ngữ máy nhưng tốc độ thực thi chậm và phương thức biểu đạt còn hạn chế.
A-0 System, được phát triển bởi Grace Hopper từ năm 1951 đến năm 1952, là một hệ thống lập trình tự động tìm ra các quy trình ngôn ngữ máy cần thiết và kết hợp chúng thành một chương trình bằng cách chỉ định các chương trình con được viết sẵn bằng các ký hiệu. Vào thời điểm đó, nó được gọi là trình biên dịch và được giới thiệu rộng rãi như là trình biên dịch đầu tiên. Tuy nhiên, trong phân loại hiện đại, nó gần giống với một trình liên kết hoặc trình tải để đặt và kết nối các chương trình con hơn là một trình biên dịch dịch toàn bộ ngôn ngữ cấp cao. Tuy nhiên, đây là một bước quan trọng trong sự phát triển của các trình biên dịch sau này vì nó cho thấy máy móc có thể tự động hóa một phần quy trình viết chương trình.
Những năm 1950: Thực tế sử dụng ngôn ngữ cấp cao và trình biên dịch
Vào những năm 1950, các ngôn ngữ lập trình tiên tiến bắt đầu được đưa vào sử dụng thực tế, trong đó các lập trình viên viết chương trình theo các biểu thức gần với cấu trúc toán học và nghiệp vụ của vấn đề hơn là hệ thống lệnh của máy và người dịch chuyển chúng sang ngôn ngữ máy. Trong thời kỳ này, các họ ngôn ngữ chính được hình thành theo các nhu cầu khác nhau về tính toán khoa học, xử lý ký hiệu và xử lý dữ liệu kinh doanh.
FORTRAN
Năm 1954, nhóm nghiên cứu IBM do John Backus dẫn đầu bắt đầu phát triển FORTRAN (Dịch công thức), một hệ thống dịch công thức cho IBM 704. Vào thời điểm đó, để một ngôn ngữ cấp cao được chấp nhận rộng rãi, nó phải có hiệu năng thực thi gần giống với ngôn ngữ máy do con người viết. Theo đó, trong quá trình phát triển FORTRAN, việc thiết kế trình biên dịch tối ưu hóa được coi là quan trọng như ngữ pháp ngôn ngữ.
FORTRAN, được phân phối thương mại vào năm 1957, cho phép mô tả các tính toán khoa học và kỹ thuật bằng cách sử dụng các biểu thức toán học, biến, vòng lặp và các nhánh điều kiện. Các lập trình viên không phải trực tiếp quản lý phân bổ thanh ghi và lệnh máy chi tiết, đồng thời trình biên dịch đã phân tích các công thức bằng văn bản và tạo ra ngôn ngữ máy hiệu quả. Mặc dù FORTRAN không phải là ngôn ngữ cấp cao đầu tiên, nhưng nó được coi là ngôn ngữ biên dịch cấp cao đầu tiên được áp dụng rộng rãi trong các địa điểm nghiên cứu và công nghiệp thực tế. Thành công của FORTRAN đã chứng minh rằng các ngôn ngữ và trình biên dịch cấp cao có thể thực tế mà không phải hy sinh nhiều hiệu suất.
Sau đó, tên chính thức của ngôn ngữ này bắt đầu được viết là Fortran. Fortran đã được sửa đổi liên tục, kết hợp các hoạt động mảng, câu lệnh điều khiển có cấu trúc, mô-đun, kiểu dữ liệu do người dùng xác định, tính năng hướng đối tượng và khả năng xử lý song song. Mặc dù là một trong những ngôn ngữ ra đời sớm nhất nhưng nó vẫn tiếp tục được sử dụng trong điện toán hiệu năng cao hiện đại, phân tích số và mô phỏng khí tượng và vật lý.
FLOW-MATIC và COBOL
Không giống như các tính toán khoa học, trong công việc tính lương, kế toán, kiểm kê và hành chính của công ty, khả năng đọc và phân loại số lượng lớn hồ sơ và tạo báo cáo quan trọng hơn các công thức phức tạp. Grace Hopper lập luận rằng các nhà quản lý nên sử dụng các mệnh lệnh gần với từ tiếng Anh hơn là các ký hiệu toán học.
FLOW-MATIC, do nhóm của Hopper phát triển, là ngôn ngữ ban đầu thể hiện quy trình xử lý dữ liệu bằng các từ tiếng Anh như INPUT, OUTPUT và COMPARE. FLOW-MATIC cho thấy tiềm năng của một ngôn ngữ xử lý dữ liệu kinh doanh khác biệt với các ngôn ngữ thiên về toán học và ảnh hưởng đến phong trào tạo ra một ngôn ngữ kinh doanh chung có thể được sử dụng trên các máy tính của các nhà sản xuất khác nhau.
Năm 1959, chính phủ Hoa Kỳ cùng một số nhà sản xuất máy tính và tổ chức người dùng đã thành lập CODASYL để phát triển COBOL. Đặc tả COBOL 60 được công bố vào năm 1960 và COBOL đã sử dụng các từ khóa dài tương tự như câu tiếng Anh, định nghĩa dữ liệu rõ ràng và cấu trúc xử lý tệp theo định hướng bản ghi. Nó được dự định là ngôn ngữ kinh doanh thoát khỏi lệnh máy của một nhà sản xuất cụ thể và sau đó được sử dụng rộng rãi trong các hệ thống thông tin cốt lõi của tài chính, bảo hiểm, chính phủ và các tập đoàn lớn. COBOL trở thành cơ hội để chú trọng đến các thông số kỹ thuật của ngôn ngữ lập trình và việc tuân thủ tiêu chuẩn của trình biên dịch.
LISP
John McCarthy đã phát triển LISP, một ngôn ngữ dành cho trí tuệ nhân tạo và tính toán biểu tượng, vào cuối những năm 1950. Không giống như FORTRAN tập trung vào mảng số, LISP xử lý các ký hiệu và danh sách dưới dạng dữ liệu cơ bản. Nó tập trung vào các khái niệm như định nghĩa hàm, đệ quy, biểu thức điều kiện, cấu trúc dữ liệu động và truy xuất bộ nhớ tự động và bản thân mã chương trình có thể được biểu diễn dưới dạng dữ liệu dưới dạng danh sách.
Bài báo LISP xuất bản năm 1960 đã giải thích một cách có hệ thống cách xử lý các hàm đệ quy và các biểu thức ký hiệu trên máy tính. Phương pháp tính toán hướng hàm của LISP chịu ảnh hưởng của phép tính lambda và sau đó có tác động lâu dài đến các ngôn ngữ lập trình hàm, hệ thống macro, quản lý bộ nhớ tự động, môi trường phát triển tương tác và nghiên cứu trí tuệ nhân tạo.
ALGOL
Không giống như FORTRAN, bắt đầu trong môi trường cụ thể của IBM, các nhà nghiên cứu ở Châu Âu và Hoa Kỳ đã tìm cách tạo ra một ngôn ngữ ký hiệu thuật toán độc lập với cả máy móc và quốc gia. Kết quả là ALGOL 58 xuất hiện vào năm 1958 và báo cáo chính thức sửa đổi ALGOL 60 được xuất bản vào năm 1960.
ALGOL 60 cung cấp cấu trúc khối được chia thành begin và end, các biến cục bộ chỉ hợp lệ trong khối, quy trình đệ quy và cấu trúc điều khiển hệ thống. Cú pháp của ngôn ngữ được mô tả theo ngữ pháp chính thức của họ ký hiệu Backus-Naur, sau này trở thành phương pháp tiêu chuẩn cho các đặc tả ngôn ngữ lập trình và thiết kế trình phân tích cú pháp. Mặc dù bản thân việc sử dụng công nghiệp của ALGOL bị hạn chế hơn so với FORTRAN hoặc COBOL, nhưng nó ảnh hưởng đến ngữ pháp và cấu trúc của nhiều ngôn ngữ, bao gồm Pascal, C (ngôn ngữ lập trình) và Simula.
Thập niên 1960: Cấu trúc, giáo dục và sự xuất hiện của khái niệm đối tượng
Vào những năm 1960, phạm vi sử dụng máy tính đã mở rộng ra ngoài các viện nghiên cứu và cơ sở quân sự cho đến các công ty và trường đại học. Khi quy mô của chương trình phát triển, việc hiểu và sửa đổi chương trình cũng như cấu trúc nó để nhiều người có thể hợp tác trở nên quan trọng hơn việc chỉ tạo ra một ký hiệu để thay thế ngôn ngữ máy.
Cấu trúc khối và các biến cục bộ được họ ngôn ngữ ALGOL giới thiệu đã trở thành cơ sở để tách các phần của chương trình thành các đơn vị độc lập. Phong trào Lập trình có cấu trúc nhằm giảm các hướng dẫn phân nhánh bừa bãi và tổ chức các chương trình xoay quanh việc thực hiện tuần tự, lựa chọn, lặp lại và gọi thủ tục cũng ảnh hưởng đến thiết kế ngôn ngữ. Sau đó, các ngôn ngữ bắt đầu tập trung vào không chỉ cách diễn đạt ngữ pháp mà còn cả chức năng giới hạn và làm rõ cấu trúc của một chương trình.
BASIC và lập trình tương tác
Năm 1963, John Kemeny và Thomas Kurtz bắt đầu phát triển một môi trường điện toán chia sẻ thời gian và ngôn ngữ mà các sinh viên không chuyên tại Đại học Dartmouth có thể dễ dàng học được. Kết quả là vào ngày 1 tháng 5 năm 1964, chương trình BASIC đầu tiên đã chạy trên hệ thống chia sẻ thời gian của Dartmouth.
BASIC sử dụng các lệnh tiếng Anh đơn giản, số dòng và phương pháp thực thi tương tác. Thay vì gửi thẻ đục lỗ và chờ đợi kết quả lâu, người dùng có thể nhập chương trình vào thiết bị đầu cuối và xem kết quả ngay lập tức. Sau đó, khi nhiều loại trình thông dịch BASIC khác nhau được cung cấp làm tiêu chuẩn trong máy tính cá nhân vào những năm 1970 và 1980, BASIC đã trở thành ngôn ngữ lập trình tiêu biểu mà công chúng lần đầu tiên biết đến.
Simula và lập trình hướng đối tượng
Ole-Johan Dahl và Kristen Nygaard của Na Uy đã phát triển Simula để mô phỏng các hệ thống phức tạp trong thế giới thực. Ban đầu, Simula là ngôn ngữ mô phỏng dựa trên ALGOL, và sau đó Simula 67 đã hệ thống hóa các khái niệm về lớp, đối tượng, kế thừa, phương thức ảo và điều phối động.
Các đối tượng của Simula kết hợp dữ liệu và các thao tác xử lý dữ liệu đó thành một đơn vị. Các đối tượng đại diện cho các tác nhân hoặc thành phần hệ thống riêng lẻ trong thế giới thực và các lớp được sử dụng để xác định các cấu trúc chung. Mô hình này sau này trở thành nền tảng cốt lõi của các ngôn ngữ hướng đối tượng, bao gồm Smalltalk, C++, Java và C#. Simula thường được coi là ngôn ngữ lập trình hướng đối tượng đầu tiên.
Những năm 1970: Thiết lập các mô hình lập trình chính
Vào những năm 1970, một số mô hình lập trình vẫn tồn tại cho đến ngày nay đã được thiết lập dưới dạng ngôn ngữ độc lập. C nổi lên trong lập trình hệ thống, Pascal trong giáo dục và lập trình có cấu trúc, Smalltalk trong môi trường hướng đối tượng, Prolog trong lập trình logic và ML trong lập trình hàm tĩnh.
Pascal
Niklaus Wirth xuất bản Pascal vào năm 1970 dựa trên cấu trúc của ALGOL 60. Pascal thể hiện các nguyên tắc lập trình có cấu trúc với ngữ pháp rõ ràng và khả năng kiểm tra kiểu dữ liệu mạnh mẽ, đồng thời cung cấp các mảng, bản ghi, tập hợp, con trỏ và các kiểu dữ liệu do người dùng xác định.
Mặc dù Pascal đã được sử dụng rộng rãi trong giáo dục lập trình nhưng ban đầu nó không được thiết kế như một ngôn ngữ dành riêng cho giáo dục. Wirth tin rằng một ngôn ngữ cần có những nguyên tắc cơ bản đơn giản nhưng vẫn có thể viết được những chương trình lớn một cách hiệu quả. Pascal sau đó đã ảnh hưởng đến Modula-2, Oberon, Object Pascal và một số môi trường phát triển giáo dục và thương mại khác.
C và UNIX
Dennis Ritchie đã phát triển C (ngôn ngữ lập trình) tại Phòng thí nghiệm Bell từ năm 1971 đến năm 1973, chịu ảnh hưởng của BCPL và B (ngôn ngữ lập trình). C được thiết kế như một ngôn ngữ lập trình hệ thống để triển khai các hệ điều hành UNIX đời đầu.
Các hệ điều hành hiện tại chủ yếu được viết bằng hợp ngữ, nhưng phần lớn UNIX được viết lại bằng C, giúp việc chuyển hệ điều hành sang các kiến trúc máy tính khác dễ dàng hơn. C cung cấp con trỏ, truy cập bộ nhớ trực tiếp, hoạt động bit và mô hình thời gian chạy đơn giản, cho phép lập trình nâng cao bằng cách sử dụng các chức năng, kiểu dữ liệu và cấu trúc trong khi vẫn duy trì quyền kiểm soát gần với phần cứng.
C giữ ngôn ngữ ở mức nhỏ gọn và tách các chức năng như đầu vào/đầu ra, chuỗi và quản lý bộ nhớ thành các thư viện. Cấu trúc này phù hợp để triển khai hệ điều hành, trình biên dịch, cơ sở dữ liệu, phần mềm nhúng và một số thời gian chạy ngôn ngữ. Kể từ đó, cú pháp và mô hình thực thi của C đã ảnh hưởng trực tiếp hoặc gián tiếp đến nhiều ngôn ngữ, bao gồm C++, Objective-C, Java, C#, JavaScript, Go (ngôn ngữ lập trình), Rust, Wave (ngôn ngữ lập trình), v.v.
Smalltalk
Năm 1972, một nhóm nghiên cứu tại Xerox PARC, bao gồm Alan Kay, Dan Ingalls và Adele Goldberg, đã phát triển phiên bản đầu tiên của Smalltalk. Smalltalk không chỉ là một ngôn ngữ; đó là một môi trường tích hợp kết hợp ngôn ngữ hướng đối tượng, máy ảo, trình duyệt mã nguồn, trình gỡ lỗi và giao diện người dùng đồ họa.
Trong Smalltalk, hầu hết tất cả các phần tử, bao gồm số, chuỗi, lớp và môi trường thực thi, đều được coi là đối tượng và các phép tính được thể hiện bằng cách truyền thông điệp giữa các đối tượng. Các nhà phát triển có thể sửa đổi các đối tượng và mã trong hệ thống đang chạy và xem kết quả ngay lập tức. Mô hình đối tượng, máy ảo, phương pháp phát triển tương tác và môi trường đồ họa của Smalltalk có tác động đáng kể không chỉ đến các ngôn ngữ hướng đối tượng sau này mà còn đến các môi trường phát triển tích hợp hiện đại và giao diện người dùng.
Prolog
Năm 1972, tại Marseille, Pháp, Alain Colmerauer và nhóm nghiên cứu của ông đã phát triển triển khai đầu tiên của Prolog. Prolog thể hiện các chương trình dưới dạng sự kiện, quy tắc và truy vấn cần giải quyết, thay vì viết chúng theo thứ tự các lệnh sẽ được thực thi.
Trình thực thi Prolog sử dụng sự hợp nhất logic và quay lui để tìm kiếm giải pháp thỏa mãn các điều kiện. Các phương pháp này đã hình thành một mô hình riêng gọi là Lập trình logic và được sử dụng trong nghiên cứu xử lý ngôn ngữ tự nhiên, biểu diễn tri thức, hệ thống chuyên gia, hệ thống dựa trên quy tắc và giải quyết ràng buộc.
ML và ngôn ngữ hàm tĩnh
Robin Milner đã thiết kế ML như một ngôn ngữ kim loại để viết các thủ tục chứng minh trong hệ thống chứng minh định lý LCF. ML là ngôn ngữ hàm bậc cao coi các hàm là giá trị và cung cấp hệ thống kiểu tĩnh đa hình cho phép trình biên dịch suy ra hầu hết các kiểu dữ liệu mà không cần lập trình viên phải ghi chúng trực tiếp.
Nó đã chỉ ra rằng tính đa hình tham số và suy luận kiểu của ML có thể làm giảm độ dài của mã trong khi vẫn duy trì độ an toàn kiểu mạnh mẽ. Khớp mẫu, kiểu dữ liệu đại số và hệ thống mô-đun chịu ảnh hưởng sau này Haskell, OCaml, F#, Scala và các ngôn ngữ định kiểu tĩnh hiện đại.
SQL và ngôn ngữ khai báo
Khi cơ sở dữ liệu quan hệ phát triển, các ngôn ngữ khai báo riêng biệt để xử lý dữ liệu cũng xuất hiện. SEQUEL, được phát triển vào những năm 1970 bởi Donald Chamberlain và Raymond Boyce của IBM, sau đó được đổi tên thành SQL.
Người dùng SQL mô tả các điều kiện và dạng của dữ liệu mong muốn thay vì đưa ra hướng dẫn chi tiết về thứ tự tìm kiếm và kết hợp dữ liệu. Đường dẫn truy cập thực tế và kế hoạch thực hiện được xác định bởi hệ thống quản lý cơ sở dữ liệu. Mặc dù SQL có mục đích khác với các ngôn ngữ lập trình có mục đích chung, nhưng nó đã thiết lập một mô hình ngôn ngữ khai báo kết quả thay vì xử lý các thủ tục trong các hệ thống quy mô lớn thực tế.
Thập niên 1980: Máy tính cá nhân, sự phát triển của mô đun và hướng đối tượng
Vào những năm 1980, khi máy tính cá nhân trở nên phổ biến, cơ sở người dùng ngôn ngữ lập trình đã mở rộng đáng kể. BASIC được sử dụng rộng rãi trong các máy tính cá nhân đời đầu, đồng thời các trình biên dịch Pascal và C cũng được phổ biến cho giáo dục và phát triển phần mềm thương mại. Với sự ra đời của các môi trường phát triển cung cấp trình soạn thảo, trình biên dịch và trình gỡ lỗi tích hợp trong một sản phẩm, quá trình tạo và thực thi chương trình đã trở nên nhanh hơn.
Khi quy mô của chương trình tăng lên, việc tổ chức mã thành một tập hợp các thủ tục đơn giản trở nên không đủ. Khả năng chỉ định các giao diện chung giữa các mô-đun, ẩn các phần triển khai và kết hợp các thành phần được biên dịch riêng biệt một cách an toàn đã trở nên quan trọng. Birt's Modula-2, Oberon và một số ngôn ngữ dựa trên Pascal xử lý việc mô-đun hóa và kiểm tra giao diện ở cấp độ ngôn ngữ.
C++
Bjarne Stroustrup bắt đầu phát triển “C with Class” tại Bell Labs vào năm 1979. Ngôn ngữ này là một nỗ lực để kết hợp sự trừu tượng hóa đối tượng và lớp của Simula với hiệu suất và khả năng kiểm soát hệ thống của C. Khoảng năm 1983, tên được đổi thành C++ và cuốn sách ngôn ngữ và triển khai thương mại đầu tiên được phát hành vào năm 1985.
C++ dần dần bổ sung thêm các lớp và tính kế thừa cũng như nạp chồng hàm và toán tử, hàm tạo và hàm hủy, mẫu, xử lý ngoại lệ và lập trình chung. Nó đã phát triển thành một ngôn ngữ đa mô hình có thể kết hợp nhiều phương pháp lập trình trong khi vẫn duy trì khả năng tương thích đáng kể với C. Từ đó, nó được sử dụng rộng rãi trong các ứng dụng lớn, thành phần hệ điều hành, công cụ trò chơi, trình duyệt và phần mềm hiệu suất cao.
Ngôn ngữ Perl và script
Năm 1987, Larry Wall phát hành Perl để tự động hóa các tác vụ tạo báo cáo, xử lý văn bản và quản trị hệ thống trong môi trường UNIX. Perl kết hợp các tính năng của một số công cụ và ngôn ngữ, bao gồm C, shell, sed và awk.
Perl tích hợp sâu các biểu thức chính quy và chức năng xử lý chuỗi vào ngôn ngữ, cho phép các chương trình ngắn xử lý tệp và văn bản. Sau đó, nó đóng một vai trò quan trọng trong quá trình tự động hóa hệ thống và lập trình web dựa trên CGI ban đầu, đồng thời cũng ảnh hưởng đến mô hình xây dựng hệ sinh thái ngôn ngữ tập trung vào kho lưu trữ mô-đun mở rộng quy mô lớn.
Haskell
Vào những năm 1980, một số ngôn ngữ chức năng đánh giá lười đã được nghiên cứu, nhưng mỗi ngôn ngữ có ngữ pháp và chức năng khác nhau, gây khó khăn cho việc chia sẻ kết quả nghiên cứu và thư viện. Haskell được thiết kế vào cuối những năm 1980 thông qua hoạt động của ủy ban để giải quyết vấn đề này.
Các tính năng chính của Haskell là lập trình hàm thuần túy giúp phân tách các tác dụng phụ thông qua các loại và cấu trúc, đánh giá lười biếng, các kiểu dữ liệu đại số, khớp mẫu và các lớp loại. Haskell đã trở thành nền tảng để thử nghiệm các hệ thống kiểu mới và các tính năng trừu tượng trong nghiên cứu ngôn ngữ lập trình cũng như sử dụng thương mại.
Thập niên 1990: Sự phát triển của Internet, máy ảo và ngôn ngữ động
Vào những năm 1990, môi trường mà các ngôn ngữ lập trình phải đối mặt đã thay đổi đáng kể do những cải tiến về hiệu suất của máy tính cá nhân, hệ điều hành đồ họa và sự phổ biến của Internet. Khả năng phân phối các chương trình qua mạng, chạy cùng một mã trên các hệ điều hành khác nhau và phát triển các ứng dụng thay đổi nhanh chóng trong một khoảng thời gian ngắn đã trở nên quan trọng.
Python
Guido van Rossum đã thiết kế một ngôn ngữ tại CWI ở Hà Lan vào cuối những năm 1980, kế thừa những điểm mạnh của ngôn ngữ giáo dục ABC, nhưng có thể dễ dàng mở rộng sang môi trường UNIX và lập trình hệ thống thực. Việc triển khai bắt đầu vào cuối năm 1989 và Python được phát hành vào đầu những năm 1990.
Python thể hiện cấu trúc khối bằng cách sử dụng thụt lề và cung cấp các kiểu động, quản lý bộ nhớ tự động, xử lý ngoại lệ, mô-đun và các kiểu dữ liệu cơ bản phong phú. Cấu trúc cho phép kết nối dễ dàng với các module bên ngoài viết bằng C đã góp phần mở rộng phạm vi sử dụng từ các tập lệnh đơn giản đến các tính toán khoa học, máy chủ web, tự động hóa và trí tuệ nhân tạo.
Java
James Gosling và các nhà nghiên cứu tại Sun Microsystems đã nghiên cứu nền tảng phần mềm cho các thiết bị tiêu dùng nối mạng và hệ thống nhúng vào đầu những năm 1990. Ban đầu, C++ được sử dụng, nhưng một ngôn ngữ và môi trường thực thi mới đã được tạo ra để giải quyết các vấn đề về tính di động, tính ổn định, quản lý bộ nhớ và bảo mật.
Java, phát hành năm 1995, đã áp dụng cấu trúc biên dịch mã nguồn thành mã byte thay vì ngôn ngữ máy của một CPU cụ thể và thực thi nó trên Máy ảo Java. Bằng cách cung cấp khả năng quản lý bộ nhớ tự động, xác minh thời gian chạy và các thư viện tiêu chuẩn, tính độc lập của nền tảng khi thực thi một chương trình duy nhất trên nhiều hệ điều hành được nhấn mạnh. Java bắt đầu từ các ứng dụng web và mở rộng phạm vi sử dụng sang máy chủ, phần mềm doanh nghiệp, thiết bị di động và hệ thống phân tán quy mô lớn.
JavaScript
Năm 1995, Brendan Eich của Netscape đã thiết kế một ngôn ngữ để phản hồi thông tin đầu vào của người dùng và thay đổi linh hoạt nội dung màn hình trong các tài liệu web. Ngôn ngữ này ban đầu được phát triển dưới một tên khác, nhưng cuối cùng được đưa vào Netscape Navigator dưới tên JavaScript.
JavaScript có tên tương tự như Java, nhưng là một ngôn ngữ riêng biệt và sử dụng các kiểu động, đối tượng dựa trên nguyên mẫu và các hàm hạng nhất. Ban đầu, nó chủ yếu xử lý các tập lệnh ngắn được chèn vào các trang web, nhưng với những cải tiến về hiệu suất công cụ trình duyệt, tiêu chuẩn hóa ECMAScript và sự xuất hiện của Node.js cũng như hệ sinh thái mô-đun, nó đã phát triển thành một ngôn ngữ có mục đích chung xử lý cả giao diện người dùng và máy chủ.
PHP và Ruby
Rasmus Lerdorf đã tạo ra một công cụ CGI để quản lý lịch sử truy cập trang web của mình vào năm 1994, công cụ này đã phát triển thành PHP/FI và sau đó là PHP. PHP đã lan truyền nhanh chóng trong lĩnh vực phát triển trang web động thông qua phương pháp chèn mã phía máy chủ vào tài liệu HTML và cấu trúc triển khai đơn giản của nó.
Yukihiro Matsumoto đã kết hợp ảnh hưởng của Perl, Smalltalk và LISP để thiết kế Ruby, tập trung vào khả năng biểu đạt và sự tiện lợi khi viết của nhà phát triển. Ruby được phát triển bắt đầu từ năm 1993 và phát hành vào năm 1995, đồng thời cung cấp một mô hình đối tượng nhất quán coi tất cả các giá trị là đối tượng, cũng như các hàm lập trình khối và siêu lập trình. Sau khi Ruby on Rails xuất hiện, nó được biết đến rộng rãi trong phát triển ứng dụng web.
Những năm 2000: Nền tảng được quản lý và ngôn ngữ đa mô hình
Vào những năm 2000, máy ảo và thời gian chạy được quản lý đã trở thành công nghệ trung tâm để phát triển phần mềm quy mô lớn. Các ngôn ngữ đang bắt đầu được xem không chỉ dưới dạng ngữ pháp mã nguồn mà còn là toàn bộ hệ sinh thái bao gồm trình thu gom rác, thư viện chuẩn, quản lý gói, trình gỡ lỗi, trình biên dịch JIT và nền tảng triển khai.
C# và .NET
Microsoft đã phát triển C# cho nền tảng .NET, dẫn đầu bởi Anders Heilsberg. Việc triển khai rộng rãi đầu tiên được phát hành cùng với sáng kiến .NET vào năm 2000.
C# sử dụng cú pháp giống C và mô hình hướng đối tượng trong khi tích hợp quản lý bộ nhớ tự động và thông tin loại thời gian chạy, thuộc tính, đại biểu và sự kiện. Kể từ đó, các biểu thức generic, LINQ, lambda, hàm không đồng bộ, khớp mẫu và bản ghi đã được thêm vào và nó đã phát triển thành một ngôn ngữ hỗ trợ cả lập trình hướng đối tượng, lập trình hàm và khai báo.
Scala
Martin Odersky đã phát triển Scala tại EPFL từ năm 2001 và phát hành bản triển khai cho JVM vào năm 2004. Scala kết hợp lập trình hướng đối tượng và lập trình hàm trong một hệ thống kiểu tĩnh duy nhất trong khi vẫn duy trì khả năng tương tác với Máy ảo Java và các thư viện Java.
Scala cung cấp các hàm bậc cao hơn, khớp mẫu, cấu trúc dữ liệu bất biến, đặc điểm và suy luận kiểu mạnh mẽ. Chiến lược giới thiệu các tính năng ngôn ngữ mới đồng thời tận dụng hệ sinh thái của các nền tảng hiện có đã được lặp lại trong một số ngôn ngữ hiện đại nhắm vào JVM và .NET.
Thập niên 2010: Đồng thời, an toàn bộ nhớ và phát triển quy mô lớn
Kể từ cuối những năm 2000, việc phát triển bộ xử lý đã chuyển từ tập trung vào việc cải thiện tốc độ của một lõi sang tập trung vào nhiều lõi và xử lý song song. Đồng thời, khi quy mô dịch vụ Internet phát triển và cơ sở hạ tầng đám mây phát triển, tính đồng thời, triển khai đáng tin cậy, xây dựng nhanh và quản lý cơ sở mã lớn đã trở thành vấn đề chính trong thiết kế ngôn ngữ.
Go
Robert Griesemer, Rob Pike và Ken Thompson của Google bắt đầu thiết kế Go (ngôn ngữ lập trình) vào năm 2007 để giải quyết các vấn đề về thời gian biên dịch dài, sự phụ thuộc phức tạp và tính đồng thời phát sinh trong quá trình phát triển cơ sở hạ tầng phần mềm quy mô lớn. Dự án Go được phát hành vào năm 2009 và Go 1 được phát hành vào năm 2012.
Go cung cấp một cú pháp tương đối nhỏ, biên dịch nhanh, thu gom rác, hệ thống gói, goroutine và kênh. Thay vì giới hạn số lượng tính năng ngôn ngữ, chúng tôi chọn đơn giản hóa việc phát triển phần mềm mạng và máy chủ cho các nhóm bằng cách tích hợp các công cụ tiêu chuẩn, trình định dạng, kiểm tra, mô-đun và quy trình xây dựng.
Rust
Rust, được bắt đầu bởi Graydon Hoare và sau đó được Mozilla và cộng đồng công cộng phát triển, nhằm mục đích trở thành ngôn ngữ lập trình hệ thống cung cấp sự an toàn về bộ nhớ và an toàn đồng thời mà không cần trình thu gom rác. Khi Rust 1.0 được phát hành vào năm 2015, nó đi kèm với ngôn ngữ ổn định, thư viện tiêu chuẩn, gói Cargo và các công cụ xây dựng.
Rust kiểm tra các quy tắc sử dụng bộ nhớ tại thời điểm biên dịch thông qua phân tích quyền sở hữu, phép mượn và vòng đời. Chúng tôi đã cố gắng ngăn chặn nhiều loại lỗi khác nhau, chẳng hạn như chạy đua dữ liệu và truy cập bộ nhớ được giải phóng, ở giai đoạn biên dịch, đồng thời cung cấp khả năng kiểm soát chi tiết và hiệu suất gần giống với C và C++. Ngoài ra, phương pháp phát triển ngữ pháp ngôn ngữ từng bước đã được áp dụng thông qua hệ thống phiên bản trong khi vẫn duy trì khả năng tương thích với các mã hiện có.
Kotlin
JetBrains bắt đầu dự án Kotlin vào năm 2010 và phát hành vào năm 2011. Kotlin được thiết kế để tương thích với mã Java hiện có và hệ sinh thái JVM, đồng thời cung cấp các tính năng hiện đại như an toàn rỗng, suy luận kiểu, hàm mở rộng, lớp dữ liệu và coroutine.
Kotlin 1.0 được phát hành vào năm 2016 và kể từ đó đã mở rộng sang ngôn ngữ đa nền tảng không chỉ nhắm mục tiêu vào JVM mà còn cả JavaScript, mã gốc và WebAssembly.
Swift
Apple đã phát triển Swift để hiện đại hóa môi trường phát triển dựa trên Objective-C hiện có và phát hành nó vào năm 2014. Swift cung cấp kiểu suy luận kiểu và kiểu tĩnh, kiểu giá trị, giá trị tùy chọn, khái quát và trừu tượng hóa theo giao thức.
Ban đầu nó được sử dụng chủ yếu để phát triển ứng dụng iOS và macOS, nhưng sau đó trở thành nguồn mở và mở rộng sang nhiều nền tảng, bao gồm Linux và Windows, máy chủ và các khu vực nhúng.
TypeScript
Khi các ứng dụng web phát triển, nhu cầu kiểm tra cấu trúc và lỗi của mã JavaScript được định kiểu động trong giai đoạn phát triển cũng tăng lên. TypeScript, được Microsoft phát hành vào năm 2012, được thiết kế dưới dạng siêu bộ JavaScript, cho phép bổ sung ký hiệu kiểu tĩnh tùy chọn trong khi vẫn duy trì mã hiện có.
TypeScript sử dụng hệ thống kiểu cấu trúc và phân tích luồng điều khiển, đồng thời tạo ra JavaScript đơn giản nhờ quá trình biên dịch. Điều này đã trở thành một câu chuyện thành công tiêu biểu của một hệ thống kiểu tăng dần bổ sung lớp phân tích tĩnh bên trên môi trường thực thi và hệ sinh thái lớn hiện có mà không thay thế nó.
WebAssembly
Năm 2019, World Wide Web Consortium đã áp dụng đặc tả cốt lõi WebAssembly làm tiêu chuẩn web. WebAssembly là định dạng lệnh máy ảo và mã byte cấp thấp an toàn và di động được tạo bằng các ngôn ngữ như C, C++ và Rust, thay vì ngôn ngữ cấp cao thông thường do con người viết.
Do đó, các trình duyệt web hiện có một mục tiêu chung là có thể thực thi hiệu quả các kết quả biên dịch không chỉ từ JavaScript mà còn từ nhiều ngôn ngữ. WebAssembly sau đó được sử dụng trong các máy chủ bên ngoài trình duyệt, plug-in, sandbox và môi trường thực thi di động, một lần nữa mở rộng mối quan hệ giữa ngôn ngữ lập trình và nền tảng thực thi.
Từ thập niên 2020 trở đi
Các ngôn ngữ lập trình trong những năm 2020 đang trở nên khó phân loại thành một mô hình hoặc phương thức thực thi. Các ngôn ngữ hiện đại cung cấp các mô hình thủ tục, hướng đối tượng, chức năng, khai báo và đồng thời cùng nhau khi cần thiết. Ngôn ngữ kiểu tĩnh nâng cao các công cụ tương tác và suy luận kiểu, còn ngôn ngữ động cũng giới thiệu các tính năng kiểm tra kiểu và phân tích tĩnh tùy chọn.
Các hướng quan trọng trong thiết kế ngôn ngữ như sau.
- An toàn bộ nhớ và giảm hành vi không xác định
- Thực thi không đồng bộ và đồng thời có cấu trúc
- Xử lý lỗi và null rõ ràng
- Kiểu suy luận và hệ thống kiểu lũy tiến
- Mở rộng dữ liệu bất biến và các biểu thức chức năng
- Tiêu chuẩn hóa trình quản lý gói, xây dựng hệ thống và định dạng
- Chạy trên nhiều nền tảng, hỗ trợ nhiều hệ điều hành và kiến trúc CPU
- Khả năng tương tác với các hệ sinh thái C ABI, JVM, .NET, JavaScript và WebAssembly hiện có
- Điện toán không đồng nhất xử lý CPU, GPU và bộ tăng tốc cùng nhau
- Phát triển mở và quản trị cộng đồng về đặc tả và triển khai ngôn ngữ
Ngay cả trong các cấu trúc trình biên dịch, việc sử dụng nhiều cấp độ biểu diễn trung gian đang được mở rộng thay vì luồng đơn giản là dịch trực tiếp một ngôn ngữ nguồn sang một ngôn ngữ máy. Các công nghệ dựa trên trình biên dịch như LLVM và MLIR cho phép tái sử dụng giao diện ngôn ngữ, trình tối ưu hóa cũng như phần phụ trợ CPU và GPU, đồng thời nhằm mục đích giảm chi phí phát triển Ngôn ngữ chuyên biệt miền cho phần cứng hoặc miền tính toán cụ thể.
Mặt khác, sự xuất hiện của một ngôn ngữ mới không có nghĩa là ngôn ngữ hiện có sẽ biến mất ngay lập tức. Fortran và COBOL tiếp tục được sử dụng trong các hệ thống khoa học và kinh doanh trong nhiều thập kỷ, trong khi C và C++ duy trì một hệ sinh thái rộng lớn gồm các hệ điều hành và phần mềm hiệu suất cao. Java và C# tiếp tục bổ sung các tính năng tập trung vào nền tảng được quản lý, đồng thời Python và JavaScript cũng duy trì hệ sinh thái mã và gói hiện có, đồng thời nâng cao hiệu suất và khả năng phân tích kiểu của ngôn ngữ và trình thực thi.
Do đó, lịch sử của các ngôn ngữ lập trình đã trình bày các mô hình tính toán và phương pháp trừu tượng khác nhau để đáp ứng hiệu suất và cấu trúc của máy tính vào thời điểm đó, phạm vi người dùng, quy mô của phần mềm sẽ được phát triển cũng như độ an toàn và năng suất cần thiết. Các ngôn ngữ lập trình hiện đại đang phát triển bằng cách kết hợp các phả hệ lịch sử này và các cấp độ ngôn ngữ khác nhau được sử dụng bổ sung cho nhau, từ ngôn ngữ cấp thấp gần với điều khiển máy đến ngôn ngữ khai báo và ngôn ngữ dành riêng cho miền gần với biểu hiện vấn đề của con người.
Phân loại và thành phần
Ngôn ngữ lập trình không được phân biệt rõ ràng chỉ bằng một tiêu chí. Một ngôn ngữ vừa là ngôn ngữ cấp cao vừa là ngôn ngữ được định kiểu tĩnh, hỗ trợ lập trình hướng đối tượng, thủ tục và chức năng, đồng thời có thể nhắm mục tiêu cả mã gốc và máy ảo để thực thi. Do đó, thay vì một danh sách loại trừ lẫn nhau, việc phân loại ngôn ngữ lập trình nên được hiểu là một hệ thống đa chiều giải thích mức độ trừu tượng, phương thức thực thi, hệ thống kiểu, mô hình lập trình, mục đích sử dụng và phương pháp quản lý tài nguyên của ngôn ngữ từ các góc độ khác nhau. Hệ thống phân loại điện toán của ACM cũng sử dụng cấu trúc cho phép áp dụng nhiều phân loại cho một đối tượng nghiên cứu duy nhất, với các ngôn ngữ chức năng, hướng đối tượng, logic/ràng buộc, luồng dữ liệu, song song/phân phối và đa mô hình làm các mục phân loại riêng biệt.
Các tiêu chí phân loại chính có thể được tóm tắt như sau.
| tiêu chuẩn | Ví dụ phân loại | Mục tiêu danh mục |
|---|---|---|
| mức độ trừu tượng | Ngôn ngữ máy, hợp ngữ, ngôn ngữ cấp cao | Nó thể hiện trực tiếp cấu trúc phần cứng chi tiết như thế nào? |
| Nó chạy như thế nào | Biên dịch trước, giải thích, mã byte, JIT | Cách chuyển đổi mã nguồn thành dạng thực thi |
| hệ thống kiểu | Các kiểu tĩnh/động, các loại rõ ràng/suy ra | Khi nào và làm thế nào để kiểm tra tính hợp lệ của các giá trị và phép toán |
| loại mối quan hệ | loại danh nghĩa, loại kết cấu | Cách đánh giá tính tương thích của các loại khác nhau |
| Mô hình | Luồng dữ liệu bắt buộc, hướng đối tượng, chức năng, logic, | Cách diễn đạt các phép tính và cấu trúc chương trình |
| Mục đích sử dụng | Ngôn ngữ có mục đích chung, ngôn ngữ dành riêng cho miền | Phạm vi các lĩnh vực vấn đề mà ngôn ngữ được thiết kế để giải quyết |
| Quản lý tài nguyên | Quản lý thủ công, thu gom rác, quản lý dựa trên quyền sở hữu | Cách quản lý vòng đời của bộ nhớ và tài nguyên bên ngoài |
| mô hình đồng thời | Chủ đề, bộ nhớ dùng chung, truyền tin nhắn, kênh, coroutine | Một cách thể hiện việc thực thi và đồng bộ hóa nhiều tác vụ |
Phân loại theo mức độ trừu tượng
Ngôn ngữ máy là biểu diễn nhị phân của các hướng dẫn mà bộ xử lý diễn giải trực tiếp. Mỗi lệnh biểu thị mã hoạt động và toán hạng, thanh ghi hoặc địa chỉ bộ nhớ của một kiến trúc tập lệnh cụ thể. Ngôn ngữ máy có thể điều khiển trực tiếp phần cứng nhất, nhưng con người rất khó đọc và viết, và ngay cả đối với cùng một chương trình, nếu bộ xử lý đích thay đổi thì biểu thức lệnh phải được tổ chức lại.
Hợp ngữ thể hiện lệnh máy bằng cách sử dụng các ký hiệu như MOV, ADD và JMP thay vì số. Nó dễ đọc và quản lý hơn ngôn ngữ máy vì nó có thể sử dụng nhãn, hằng và macro, nhưng nó được phân loại là ngôn ngữ cấp thấp ở chỗ nó xử lý trực tiếp cấu trúc của kiến trúc đích, chẳng hạn như thanh ghi, quy ước gọi, phương pháp đánh địa chỉ bộ nhớ và tập lệnh.
Ngôn ngữ lập trình bậc cao thể hiện các chương trình dựa trên các khái niệm gần với việc giải quyết vấn đề hơn, chẳng hạn như thuật toán, cấu trúc dữ liệu, hàm, đối tượng và mô-đun, thay vì hướng dẫn phần cứng. Mức độ trừu tượng là khác nhau, từ các ngôn ngữ có thể xử lý trực tiếp các địa chỉ và con trỏ bộ nhớ, chẳng hạn như C, đến các ngôn ngữ để lại nhiều môi trường thực thi và biểu diễn dữ liệu cho việc triển khai ngôn ngữ, chẳng hạn như Python hoặc Haskell. Do đó, sự khác biệt giữa ngôn ngữ cấp thấp và ngôn ngữ cấp cao gần với sự phân biệt liên tục dựa trên sự phụ thuộc phần cứng và mức độ trừu tượng hơn là một quy trình hai bước tuyệt đối.
Cụm từ “nâng cao” ở đây không có nghĩa là chất lượng hay độ khó của ngôn ngữ. Nó cho biết mức độ trừu tượng của hoạt động cụ thể của phần cứng và ngôn ngữ cấp cao không nhất thiết có nghĩa là nó dễ sử dụng hoặc có tốc độ thực thi chậm. Trình biên dịch có thể chuyển đổi cấu trúc của ngôn ngữ cấp cao thành ngôn ngữ máy hiệu quả thông qua các bước biểu thức và tối ưu hóa trung gian. LLVM cung cấp cấu trúc phân tích và tối ưu hóa các biểu diễn trung gian do giao diện ngôn ngữ tạo ra, sau đó chuyển đổi chúng thành ngôn ngữ máy cho nhiều kiến trúc mục tiêu.
Phân loại theo phương pháp thực hiện
Ngôn ngữ lập trình thường được chia thành ngôn ngữ biên dịch và ngôn ngữ thông dịch, nhưng sự khác biệt này gần với cách triển khai cụ thể thực thi một chương trình hơn là thuộc tính tuyệt đối của chính ngôn ngữ đó. Tùy thuộc vào cách triển khai, cùng một ngôn ngữ có thể được biên dịch trước thành mã gốc, chuyển đổi thành mã byte cho máy ảo, được biên dịch sang ngôn ngữ máy trong khi thực thi hoặc cấu trúc cú pháp được diễn giải trực tiếp. Chỉ riêng trong Python, có nhiều cách triển khai khác nhau như CPython, PyPy, Jython và IronPython. PyPy sử dụng trình biên dịch JIT, trong khi Jython và IronPython lần lượt sử dụng môi trường thực thi JVM và .NET.
Biên dịch trước hoặc biên dịch AOT là phương pháp chuyển đổi mã nguồn sang ngôn ngữ máy hoặc định dạng thực thi khác của hệ thống đích trước khi thực thi chương trình. Trong cách triển khai C và C++ điển hình, một tệp hoặc thư viện thực thi được tạo thông qua quá trình tiền xử lý, phân tích cú pháp và ngữ nghĩa, tạo biểu diễn trung gian, tối ưu hóa, tạo mã dành riêng cho mục tiêu cũng như các quy trình lắp ráp và liên kết. Tuy nhiên, trong các công cụ thực tế, nhiều bước có thể được kết hợp thành một lệnh hoặc việc tạo một số tệp trung gian có thể bị bỏ qua.
Trình thông dịch đọc biểu thức của chương trình tại thời điểm thực thi và thực hiện các hành động theo ý nghĩa của nó. Trình thông dịch không nhất thiết phải trực tiếp thực thi từng dòng mã nguồn gốc. Mã nguồn trước tiên có thể được chuyển đổi thành mã thông báo, cây cú pháp, hướng dẫn nội bộ hoặc mã byte và sau đó có thể diễn giải các định dạng trung gian.
Trong phương pháp sử dụng Mã byte và Máy ảo, mã nguồn được chuyển đổi thành một tập hợp các lệnh ảo thay vì các lệnh CPU thực tế. Mã nguồn Java thường được biên dịch thành các tệp lớp JVM và mã byte, đồng thời JVM xác định cấu trúc thực thi và tập lệnh, chẳng hạn như ngăn xếp, vùng heap, vùng phương thức và nhóm hằng số. Cách tiếp cận này đặt một lớp thực thi chung giữa ngôn ngữ và phần cứng vật lý, cho phép sử dụng cùng một định dạng chương trình trên các hệ điều hành và bộ xử lý khác nhau.
Biên dịch JIT chuyển đổi các biểu diễn trung gian hoặc mã được thực thi thường xuyên sang ngôn ngữ máy của hệ thống hiện tại trong quá trình thực thi chương trình. Dựa trên thông tin được thu thập trong quá trình thực thi, có thể thực hiện tối ưu hóa phù hợp với mối quan hệ cuộc gọi thực tế, kiểu giá trị và tần suất lặp lại, nhưng thời gian và bộ nhớ cần thiết để biên dịch sẽ được đưa vào quy trình thực thi. Việc triển khai ngôn ngữ hiện đại thường sử dụng các trình thông dịch, trình biên dịch JIT và trình biên dịch AOT cùng nhau, vì vậy việc biên dịch và thông dịch là các chiến lược triển khai có thể được kết hợp trong một hệ thống thực thi thay vì tách biệt hoàn toàn với nhau.
Phân loại theo hệ thống kiểu
Hệ thống kiểu là một hệ thống quy tắc xác định các kiểu giá trị có thể được sử dụng trong chương trình, các thao tác được phép cho từng giá trị và mối quan hệ giữa các giá trị. Ví dụ: các phép toán được phép cho số nguyên và chuỗi là khác nhau và các điều kiện loại cụ thể cũng có thể áp dụng cho tham số hàm và giá trị trả về. Đặc tả Go mô tả một loại là một tập hợp các giá trị cụ thể cũng như một tập hợp các thao tác và phương thức được áp dụng cho các giá trị đó, trong khi ở Haskell, các biểu thức biểu thị các giá trị và có một kiểu tĩnh.
Kiểu tĩnh Ngôn ngữ kiểm tra mối quan hệ kiểu của biểu thức, biến và lệnh gọi hàm trước khi thực hiện chương trình. C, C++, Rust, Java, Haskell, Go, v.v. tập trung vào kiểm tra kiểu tĩnh. Kiểm tra kiểu tĩnh phát hiện một số lỗi có thể xảy ra trước khi thực thi và cho phép trình biên dịch sử dụng thông tin về biểu diễn giá trị và cấu trúc lệnh gọi để tối ưu hóa. Tuy nhiên, ngay cả trong các ngôn ngữ được nhập tĩnh, các điều kiện chỉ được xác định trong quá trình thực thi, chẳng hạn như phạm vi mảng, đầu vào bên ngoài và tải động, có thể cần phải được kiểm tra riêng.
Trong ngôn ngữ Kiểu động, các giá trị có thông tin loại trong khi thực thi và tính hợp lệ thường được kiểm tra khi áp dụng các thao tác cho giá trị thực. Các ví dụ tiêu biểu bao gồm Python, Ruby và JavaScript. Kiểu động cho phép bạn kết nối tuần tự các kiểu giá trị khác nhau với một tên duy nhất hoặc xử lý một cách tự nhiên các cấu trúc được tạo trong quá trình thực thi, nhưng một số lỗi loại nhất định có thể không được tiết lộ cho đến khi đường dẫn mã thực sự được thực thi. Trong mô hình dữ liệu của Python, tất cả dữ liệu trong chương trình được thể hiện dưới dạng đối tượng hoặc mối quan hệ giữa các đối tượng và mỗi đối tượng có một danh tính, loại và giá trị.
Sự khác biệt giữa kiểu tĩnh và kiểu động không phụ thuộc vào việc ký hiệu kiểu có được viết trực tiếp hay không. Ngôn ngữ kiểu tĩnh hỗ trợ suy luận kiểu cho phép trình biên dịch xác định kiểu bằng cách phân tích các biểu thức và mối quan hệ sử dụng ngay cả khi lập trình viên không chỉ định đầy đủ kiểu. Haskell, ML, Rust, Kotlin, Swift, v.v. tích cực sử dụng suy luận kiểu. Ngược lại, ngay cả trong các ngôn ngữ động, bạn có thể thêm lớp phân tích tĩnh không trực tiếp thay đổi hành vi thực thi, như ký hiệu kiểu của TypeScript hoặc gợi ý kiểu của Python.
Khả năng tương thích về loại cũng có thể được chia thành Kiểu định danh và kiểu cấu trúc. Hệ thống kiểu danh nghĩa xác định tính tương thích dựa trên tên của khai báo kiểu hoặc các mối quan hệ kế thừa/triển khai rõ ràng. Hệ thống kiểu cấu trúc so sánh cấu trúc của các trường và phương thức thực tế hơn là tên kiểu. TypeScript sử dụng phân nhóm cấu trúc, xác định tính tương thích của loại dựa trên tư cách thành viên của đối tượng.
Hệ thống kiểu cũng được chia thành một số đặc điểm khác, chẳng hạn như tổng quát, đa hình tham số, phân nhóm, kiểu tổng và sản phẩm, kiểu dữ liệu đại số, kiểu phụ thuộc và kiểu tăng dần. Các biểu thức thường được sử dụng là kiểu mạnh và kiểu yếu được sử dụng với các ý nghĩa khác nhau, chẳng hạn như chuyển đổi tiềm ẩn, an toàn bộ nhớ, kiểm tra thời gian chạy và khả năng bỏ qua loại, tùy thuộc vào tài liệu và người dùng. Do đó, khi cần phân loại chính xác, việc mô tả trực tiếp các đặc điểm cụ thể như kiểm tra tĩnh/động, chuyển đổi rõ ràng/ẩn và an toàn bộ nhớ là phù hợp. Các loại và tính đa hình không phải là một mối quan hệ mạnh/yếu đơn lẻ mà bao gồm một số quy tắc độc lập xác định sự phân loại, tính trừu tượng và khả năng tương thích của các giá trị.
Phân loại theo mô hình lập trình
Mô hình lập trình chỉ ra những khái niệm và cấu trúc nào được sử dụng để diễn đạt các phép tính. Hầu hết các ngôn ngữ hiện đại là ngôn ngữ đa mô hình không tuân thủ nghiêm ngặt một mô hình mà hỗ trợ nhiều phương thức cùng nhau. Ví dụ: C++ hỗ trợ các biểu thức thủ tục, hướng đối tượng, lập trình chung và hàm, Python và JavaScript cũng có thể sử dụng các đối tượng, hàm và cấu trúc điều khiển mệnh lệnh. Phân loại ngôn ngữ của ACM cũng xử lý riêng biệt các ngôn ngữ chức năng, hướng đối tượng, logic/ràng buộc, luồng dữ liệu, đồng thời/phân phối và đa mô hình.
Lập trình mệnh lệnh thể hiện các phép tính dưới dạng một chuỗi hướng dẫn thay đổi trạng thái của chương trình. Gán giá trị cho các biến và kiểm soát luồng thực thi thông qua các câu lệnh điều kiện, câu lệnh vòng lặp và lệnh gọi hàm. Lập trình thủ tục là phương pháp chia chương trình mệnh lệnh thành các đơn vị có thể gọi được như hàm, thủ tục và chương trình con. C, Pascal, FORTRAN, v.v. được phân loại là ngôn ngữ thủ tục đại diện.
Lập trình hướng đối tượng tổ chức dữ liệu và các thao tác để xử lý dữ liệu đó thành các đơn vị gọi là đối tượng. Trong các ngôn ngữ dựa trên lớp, các lớp xác định cấu trúc và hành vi của các đối tượng và thể hiện mối quan hệ giữa các đối tượng thông qua đóng gói, kế thừa, đa hình và gửi động. Các ví dụ tiêu biểu bao gồm Java, C#, C++ và Smalltalk, nhưng mỗi ngôn ngữ có các cách xác định đối tượng, lớp và kế thừa khác nhau. Một số ngôn ngữ, như JavaScript, cung cấp các khai báo lớp nhưng sử dụng nội bộ mô hình đối tượng dựa trên nguyên mẫu. Đặc tả ECMAScript xác định tính toán và thao tác đối tượng của ngôn ngữ, nhưng tách các chức năng đầu vào và đầu ra, chẳng hạn như tệp và mạng, thành các vai trò cho môi trường máy chủ.
Lập trình hàm coi các hàm là đơn vị trung tâm và giá trị của phép tính, đồng thời xây dựng các chương trình thông qua sự kết hợp của các hàm, đệ quy, hàm bậc cao hơn và cấu trúc dữ liệu bất biến. Các ngôn ngữ chức năng thuần túy nhấn mạnh các chức năng trả về cùng một kết quả cho cùng một đầu vào và kiểm soát các tác dụng phụ. Haskell xây dựng ngôn ngữ của mình xung quanh các biểu thức, hàm và kiểu tĩnh, trong khi LISP, ML, F# và nhiều ngôn ngữ hiện đại cũng hỗ trợ các khái niệm chính về lập trình hàm. Lập trình hàm đã được nghiên cứu như một mô hình tính toán thay thế hoặc bổ sung cho lập trình von Neumann truyền thống tập trung vào các lệnh và trạng thái lưu trữ.
Lập trình logic là một phương pháp trong đó các sự kiện, quy tắc và truy vấn được mô tả và người thực thi tìm kiếm giải pháp thỏa mãn các điều kiện thông qua suy luận logic. Prolog là một ví dụ điển hình và nó sử dụng các công nghệ thống nhất, quay lui và định hướng mối quan hệ. Lập trình ràng buộc mô tả các điều kiện mà một biến phải đáp ứng và trình giải ràng buộc sẽ khám phá các giá trị có thể có. Cả hai phương pháp đều có đặc điểm là khai báo các mối quan hệ, điều kiện phải thiết lập hơn là trực tiếp viết một chuỗi lệnh cụ thể.
Lập trình khai báo là một phương pháp rộng để mô tả các kết quả hoặc mối quan hệ mong muốn thay vì chỉ định các quy trình tính toán trong các hướng dẫn chi tiết. Các ngôn ngữ chức năng và logic cũng được bao gồm trong danh mục loại khai báo và các ngôn ngữ như SQL mô tả các điều kiện và dạng dữ liệu cần thiết, đồng thời để lại trình tự truy cập thực tế và kế hoạch thực hiện cho hệ thống cũng được phân loại là ngôn ngữ khai báo.
Lập trình luồng dữ liệu thể hiện sự phụ thuộc dữ liệu giữa các thao tác dưới dạng biểu đồ và thực hiện các thao tác tương ứng khi các đầu vào cần thiết đã sẵn sàng. Nó thường được sử dụng trong môi trường lập trình trực quan và trong xử lý tín hiệu, tính toán song song và hệ thống đồ thị học máy. Lập trình phản ứng xây dựng các chương trình dựa trên các giá trị thay đổi theo thời gian, luồng sự kiện và mối quan hệ lan truyền giữa các giá trị.
Lập trình tương tranh xử lý cấu trúc trong đó nhiều phép tính được thực hiện chồng chéo cũng như sự liên lạc và đồng bộ hóa giữa chúng. Tùy thuộc vào ngôn ngữ, nó cung cấp các luồng hệ điều hành và bộ nhớ dùng chung, các tác nhân và truyền thông điệp, coroutine và các chức năng không đồng bộ cũng như giao tiếp dựa trên kênh. Go xác định giao tiếp kênh là cơ chế đồng bộ hóa chính giữa các goroutine và chỉ định mối quan hệ thứ tự giữa kênh gửi và nhận tương ứng trong mô hình bộ nhớ.
Phân loại theo mục đích sử dụng
Ngôn ngữ lập trình đa dụng được thiết kế để cho phép viết nhiều loại phần mềm mà không bị giới hạn ở một vấn đề cụ thể. Chúng bao gồm C, C++, Java, Python, Rust, Go, v.v. Tuy nhiên, các ngôn ngữ có mục đích chung có thể được sử dụng rộng rãi hơn trong các lĩnh vực cụ thể như phần mềm hệ thống, ứng dụng doanh nghiệp, máy chủ web và tự động hóa, tùy thuộc vào mục đích thiết kế và hệ sinh thái.
Ngôn ngữ chuyên biệt miền được thiết kế để thể hiện các lĩnh vực vấn đề cụ thể một cách chính xác và chính xác. SQL thể hiện các truy vấn và thao tác dữ liệu quan hệ, biểu thức chính quy thể hiện các mẫu chuỗi, ngôn ngữ đổ bóng thể hiện các hoạt động đồ họa GPU và ngôn ngữ mô tả phần cứng thể hiện các mạch kỹ thuật số. Các ngôn ngữ dành riêng cho miền có thể chạy dưới dạng chương trình độc lập hoặc được bao gồm trong chuỗi, thư viện, macro hoặc ngữ pháp tích hợp của các ngôn ngữ khác.
Tùy thuộc vào lĩnh vực sử dụng, nó được phân loại thành ngôn ngữ lập trình hệ thống, ngôn ngữ script, ngôn ngữ shell, ngôn ngữ truy vấn dữ liệu, ngôn ngữ tính toán khoa học, ngôn ngữ giáo dục, ngôn ngữ mô tả phần cứng, ngôn ngữ đổ bóng và ngôn ngữ lập trình đồng thời/song song. Những tên này cho biết mục đích thiết kế chính và cách sử dụng thay vì toàn bộ các tính năng có sẵn cho ngôn ngữ, cho phép một ngôn ngữ thuộc về nhiều lĩnh vực.
Thành phần cốt lõi của ngôn ngữ
Một ngôn ngữ lập trình không chỉ được tạo thành từ ngữ pháp. Để một ngôn ngữ hoạt động như một hệ thống tính toán hoàn chỉnh, các ký tự và mã thông báo tạo nên mã nguồn, cấu trúc của câu, các thao tác mà mỗi câu ngụ ý, giá trị và loại, phạm vi tên, luồng điều khiển, phương tiện trừu tượng, bộ nhớ và phương thức xử lý lỗi phải được xác định cùng nhau. Hệ thống phân loại của ACM trình bày các kiểu dữ liệu và cấu trúc dữ liệu, cấu trúc điều khiển, chức năng và thủ tục, lớp và đối tượng, mô-đun và gói, đa hình, đệ quy, cấu trúc đồng thời và quản lý lưu trữ động là các thành phần chính của ngôn ngữ.
Cấu trúc từ vựng và mã thông báo
cấu trúc từ vựng xác định đơn vị ngữ pháp nhỏ nhất tạo nên mã nguồn. Điều này bao gồm các quy tắc xử lý cho mã định danh, từ khóa, ký tự, toán tử, dấu phân cách, nhận xét, dấu cách và ngắt dòng. Trình phân tích từ vựng đọc chuỗi ký tự đầu vào và chuyển đổi nó thành chuỗi mã thông báo mà trình phân tích cú pháp có thể xử lý.
Ý nghĩa của khoảng trắng thay đổi tùy theo ngôn ngữ. Trong các ngôn ngữ dựa trên C, hầu hết các khoảng trắng chỉ phục vụ cho các mã thông báo riêng biệt, nhưng trong Python, những thay đổi về thụt lề được chuyển đổi thành các mã thông báo INDENT và DEDENT, biểu thị cấu trúc khối. Tài liệu tham khảo Python phân biệt giữa phân tích từ vựng, xử lý đầu vào ký tự và phân tích cú pháp, xử lý mã thông báo kết quả.
Cú pháp
Cú pháp xác định thứ tự và cấu trúc trong đó các mã thông báo phải được kết hợp để tạo thành một chương trình chính xác. Các biểu mẫu như khai báo biến, định nghĩa hàm, biểu thức phép toán, câu lệnh điều kiện, câu lệnh vòng lặp, khai báo kiểu và khai báo mô-đun đều được bao gồm trong các quy tắc cú pháp. Đặc tả ngôn ngữ thường mô tả cú pháp theo ký hiệu ngữ pháp như BNF, EBNF hoặc PEG hoặc định dạng tương đương.
Các quy tắc cú pháp chỉ xác định cấu trúc bên ngoài của chương trình. Ví dụ: ngay cả khi a + b là biểu thức đúng ngữ pháp, các quy tắc loại và ngữ nghĩa sẽ xác định liệu phép cộng có thể được áp dụng cho a và b hay không và kết quả của thao tác là gì.
Ngữ nghĩa
Ngữ nghĩa xác định ý nghĩa của một chương trình đúng ngữ pháp và cách nó hoạt động. Các tiêu chuẩn ngôn ngữ thường chỉ định ngữ nghĩa cùng với cú pháp và các tiêu chuẩn COBOL cũng chỉ định cú pháp và ngữ nghĩa của ngôn ngữ cho các chương trình độc lập với máy.
Ngữ nghĩa tĩnh xử lý các quy tắc có thể được kiểm tra trước khi thực hiện chương trình. Điều này bao gồm việc tên có được khai báo hay không, phạm vi của biến có đúng hay không, các đối số và kiểu trả về của lệnh gọi hàm có đúng hay không và liệu câu lệnh điều khiển có ở vị trí được phép hay không. Kiểm tra kiểu và quy tắc phân giải tên khó diễn đạt chỉ bằng cú pháp thuộc về ngữ nghĩa tĩnh.
Ngữ nghĩa động xác định trạng thái nào thay đổi và giá trị nào được tính toán theo từng biểu thức và lệnh khi chương trình được thực thi. Thứ tự đánh giá các toán tử, lệnh gọi hàm, sự xuất hiện ngoại lệ, tạo đối tượng, truy cập bộ nhớ và thực thi đồng thời đều được bao gồm trong ngữ nghĩa động. Ngữ nghĩa có thể được giải thích bằng ngôn ngữ tự nhiên hoặc được xác định thông qua các phương pháp toán học như ngữ nghĩa hoạt động, ngữ nghĩa biểu thị và ngữ nghĩa tiên đề.
Giá trị và mô hình dữ liệu
Mô hình dữ liệu xác định giá trị trong ngôn ngữ là gì và kiểu giá trị nào có thể được tạo và kết hợp. Nó có thể bao gồm các giá trị cơ bản như số nguyên, số thực, giá trị logic, ký tự và chuỗi, cũng như mảng, bộ dữ liệu, bản ghi, cấu trúc, danh sách, bộ, bản đồ, hàm, lớp và đối tượng.
Mối quan hệ giữa giá trị và đối tượng ở mỗi ngôn ngữ là khác nhau. Python mô hình hóa tất cả dữ liệu dưới dạng đối tượng hoặc mối quan hệ giữa các đối tượng và xác định rằng mỗi đối tượng có một danh tính, loại và giá trị. Haskell cho phép các biểu thức biểu diễn các giá trị và gán cho mỗi biểu thức một kiểu tĩnh. C xác định các đối tượng, giá trị và vùng lưu trữ trong mô hình bộ nhớ gần với phần cứng. Sự khác biệt trong các mô hình dữ liệu này ảnh hưởng đến việc gán và sao chép, so sánh đẳng thức, khả năng thay đổi và bố cục bộ nhớ.
Tên, ràng buộc và phạm vi
Mã định danh của chương trình đề cập đến các đối tượng như biến, hàm, loại và mô-đun. Kết nối giữa mã định danh và đối tượng thực tế được gọi là Liên kết tên. Các ràng buộc có thể được hình thành tại thời điểm biên dịch, thời gian liên kết, thời gian tải chương trình hoặc trong khi thực thi.
Phạm vi là vùng mã nguồn có thể được tham chiếu bằng một tên cụ thể và Tuổi thọ là phạm vi thời gian thực hiện trong đó giá trị, đối tượng hoặc không gian lưu trữ thực sự tồn tại. Hai khái niệm có liên quan nhưng không giống nhau. Một đối tượng nằm ngoài phạm vi có thể tiếp tục tồn tại thông qua các tham chiếu khác và ngược lại, ngay cả khi tên nằm trong phạm vi, tài nguyên mà nó trỏ tới có thể không còn giá trị.
Một ngôn ngữ có thể cung cấp phạm vi toàn cục, phạm vi mô-đun, phạm vi hàm, phạm vi khối, phạm vi lớp, v.v. Trong một hàm lồng nhau, closure có thể được tạo để nắm bắt các biến cục bộ của hàm bên ngoài và thứ tự tìm thấy tên cũng như phương thức thu thập phụ thuộc vào mô hình thực thi của ngôn ngữ.
Biểu thức và câu lệnh
Biểu thức là thành phần ngôn ngữ tính toán hoặc biểu thị một giá trị duy nhất. Biểu thức có thể bao gồm hằng số, tham chiếu biến, ứng dụng toán tử, lệnh gọi hàm, biểu thức điều kiện và tạo đối tượng. Câu lệnh hoặc câu lệnh biểu thị các hoạt động ảnh hưởng đến luồng thực thi hoặc trạng thái của chương trình, chẳng hạn như gán, lặp lại, phân nhánh, trả về và xử lý ngoại lệ.
Trong một số ngôn ngữ, ngay cả các câu lệnh và khối điều kiện cũng được coi là biểu thức tạo ra giá trị, trong khi ở các ngôn ngữ khác, có sự phân biệt rõ ràng giữa các biểu thức tạo ra giá trị và câu lệnh chỉ thực hiện việc thực thi. Tham chiếu ngôn ngữ Python cũng xác định riêng biệt cú pháp và ngữ nghĩa của các biểu thức, trong khi Haskell coi biểu thức là đơn vị tính toán trung tâm của ngôn ngữ.
Luồng điều khiển
Luồng điều khiển xác định phần nào của chương trình được thực thi và theo thứ tự nào. Thực thi tuần tự, phân nhánh có điều kiện, lặp lại, gọi và trả về hàm, đệ quy, xử lý ngoại lệ, v.v. là các cấu trúc điều khiển cơ bản.
Tùy thuộc vào ngôn ngữ, nó có thể cung cấp các cấu trúc điều khiển nâng cao như khớp mẫu, coroutine, trình tạo, hàm không đồng bộ, phần tiếp theo và lựa chọn không xác định. Những cấu trúc này không chỉ đơn giản là các hàm thuận tiện về mặt cú pháp; họ xác định trạng thái chương trình, ngăn xếp cuộc gọi, thứ tự đánh giá các giá trị và phương pháp lan truyền lỗi.
Hàm và sự trừu tượng
Các hàm, thủ tục và chương trình con tách các phép tính lặp lại thành các đơn vị được đặt tên. Tham số, giá trị trả về, biến cục bộ, quy ước gọi và phương thức truyền giá trị là các thành phần chính của hệ thống hàm. Nếu một hàm có thể là đối số hoặc giá trị trả về của một hàm khác thì nó được cho là hỗ trợ hàm cấp một và một hàm nhận hoặc trả về một hàm khác làm đối số được gọi là hàm bậc cao hơn.
Ngoài các hàm, ngôn ngữ còn cung cấp khả năng trừu tượng hóa dữ liệu và hoạt động thông qua các lớp, giao diện, đặc điểm, giao thức, lớp kiểu và kiểu dữ liệu trừu tượng. Đa hình cho phép một giao diện hoặc mã được áp dụng cho các giá trị thuộc nhiều loại và được chia thành quá tải, đa hình kiểu con và đa hình tham số.
Mô-đun và gói
Mô-đun nhóm các khai báo về các giá trị, hàm, kiểu, lớp, v.v. liên quan vào một không gian tên và đơn vị triển khai. Một mô-đun phân biệt giữa các mục được hiển thị bên ngoài và bên trong triển khai, đồng thời cung cấp cấu trúc để nhập các chức năng từ các mô-đun khác. Các mô-đun của Haskell xác định các giá trị, kiểu dữ liệu, kiểu từ đồng nghĩa, lớp, v.v., xuất một số mục ra bên ngoài và bao gồm các mục từ các mô-đun khác trong phạm vi thông qua khai báo nhập.
Một gói thường được sử dụng như một đơn vị triển khai lớn hơn bao gồm một hoặc nhiều mô-đun, tài nguyên và siêu dữ liệu. Tuy nhiên, ý nghĩa chính xác và mối quan hệ giữa các mô-đun và gói khác nhau tùy thuộc vào ngôn ngữ và hệ thống công cụ.
Quản lý bộ nhớ và tài nguyên
Các chương trình phải phân bổ bộ nhớ để lưu trữ các giá trị và đối tượng và lấy lại bộ nhớ khi không còn cần thiết. Trong C và C++, lập trình viên trực tiếp phân bổ và giải phóng bộ nhớ hoặc sử dụng các quy tắc sống của đối tượng. Java, C#, Go, Python, v.v. chủ yếu sử dụng Thu gom rác, nơi môi trường thực thi tìm và truy xuất các đối tượng không thể truy cập.
Rust sử dụng phương pháp cung cấp sự an toàn cho bộ nhớ mà không cần trình thu gom rác thông qua các quy tắc sở hữu, mượn và vòng đời được trình biên dịch kiểm tra. Tài nguyên được dọn sạch khi chủ sở hữu của giá trị nằm ngoài phạm vi và các tình huống trong đó các tham chiếu tồn tại lâu hơn các tham chiếu có thể thay đổi ban đầu hoặc xung đột được tạo sẽ bị giới hạn tại thời gian biên dịch.
Ngoài bộ nhớ, còn có các tài nguyên yêu cầu giải phóng rõ ràng, chẳng hạn như tệp, kết nối mạng, khóa và tài nguyên GPU. Ngôn ngữ quản lý vòng đời của các tài nguyên này bằng cách sử dụng cú pháp hủy và dọn dẹp, trình quản lý bối cảnh, thực thi từng phần và các phương thức kết hợp việc thu thập và khởi tạo tài nguyên.
Xử lý lỗi và ngoại lệ
Ngôn ngữ cung cấp một cách để thể hiện và truyền đạt sự thất bại. Bạn có thể sử dụng mã trả về truyền thống và trạng thái lỗi chung hoặc bạn có thể đưa ra một ngoại lệ để hủy bỏ luồng cuộc gọi hiện tại và chuyển nó đến trình xử lý cấp cao hơn. Một số ngôn ngữ định kiểu tĩnh thể hiện giá trị thành công và lỗi dưới dạng kiểu tổng hoặc kiểu kết quả, cho phép người gọi xử lý khả năng xảy ra lỗi thông qua các loại.
Việc xử lý lỗi ảnh hưởng đến kiểu trả về của hàm, luồng điều khiển, dọn dẹp tài nguyên và độ ổn định của chương trình. Ngay cả những ngôn ngữ cung cấp ngoại lệ cũng có thể xử lý các lỗi có thể phục hồi, lỗi lập trình và sự cố ngừng hoạt động của hệ thống theo những cách khác nhau.
Thực thi đồng thời và không đồng bộ
Mô hình đồng thời xác định cách tạo nhiều tác vụ, trao đổi dữ liệu giữa chúng và điều phối thứ tự thực hiện của chúng. Trong các ngôn ngữ sử dụng luồng và bộ nhớ dùng chung, việc khóa, thao tác nguyên tử và quy tắc sắp xếp bộ nhớ rất quan trọng. Trong mô hình truyền thông điệp hoặc kênh, quá trình giao tiếp qua đó các tác vụ trao đổi giá trị sẽ trở thành trung tâm của sự đồng bộ hóa.
Coroutine và async·await cho phép tác vụ trả về đặc quyền thực thi trong khi chờ I/O hoặc các hoạt động không đồng bộ khác, sau đó tiếp tục thực thi từ nơi tác vụ đã dừng lại. Những tính năng này không nhất thiết hàm ý việc thực thi song song thực sự; nhiều tác vụ có thể được thực thi luân phiên trên một luồng hoặc kết hợp với nhiều luồng.
Thư viện chuẩn và môi trường thực thi
Thư viện tiêu chuẩn cung cấp các chức năng cần thiết thường xuyên như chuỗi, bộ sưu tập, tệp, toán học, thời gian, mạng và đồng thời thông qua một giao diện chung. Mặc dù đặc tả cốt lõi của ngôn ngữ và thư viện tiêu chuẩn có liên quan chặt chẽ với nhau nhưng chúng không giống nhau. Tham chiếu ngôn ngữ Python cũng phân biệt giữa các quy tắc của chính ngôn ngữ đó và các chức năng của các mô-đun tiêu chuẩn và tích hợp.
Runtime là lớp phần mềm hỗ trợ các chức năng ngôn ngữ trong quá trình thực thi chương trình. Nó có thể chịu trách nhiệm phân bổ bộ nhớ, thu thập rác, kiểm tra kiểu động, xử lý ngoại lệ, quản lý luồng, gọi phương thức ảo, phản chiếu, v.v. Có thể có một máy ảo độc lập chỉ định bộ nhớ thực thi và một tập hợp các lệnh ảo, như JVM, hoặc có một cấu trúc sử dụng thư viện hệ thống và thời gian chạy trình biên dịch tương đối nhỏ, như C.
Trong một số trường hợp, ngôn ngữ không trực tiếp xác định các chức năng đầu vào/đầu ra và hệ điều hành mà để chúng trong môi trường thực thi. Đặc tả ECMAScript xác định mô hình đối tượng và tính toán của ngôn ngữ, nhưng dữ liệu đầu vào và kết quả đầu ra bên ngoài được cung cấp bởi môi trường máy chủ, chẳng hạn như thời gian chạy của trình duyệt hoặc máy chủ. Do đó, môi trường phát triển thực tế bao gồm sự kết hợp của các đặc tả ngôn ngữ, thời gian chạy, thư viện chuẩn và API máy chủ.
Bộ xử lý ngôn ngữ và công cụ
Hệ thống xử lý ngôn ngữ có thể bao gồm từ vựng, trình phân tích cú pháp, trình phân tích ngữ nghĩa, trình biên dịch, trình thông dịch, trình tối ưu hóa, trình tạo mã, trình biên dịch, trình liên kết và trình tải. Chuỗi công cụ biên dịch thực tế phân tích mã nguồn để tạo cây cú pháp trừu tượng, hạ thấp nó thành biểu diễn trung gian, thực hiện tối ưu hóa và tạo mã dành riêng cho mục tiêu, đồng thời liên kết các tệp đối tượng và thư viện được tạo để tạo chương trình thực thi.
Môi trường phát triển bao gồm trình gỡ lỗi, trình phân tích tĩnh, trình định dạng, trình nói dối, công cụ kiểm tra, trình tạo tài liệu, hệ thống xây dựng và trình quản lý gói. Mặc dù các công cụ này không phải là một phần của cú pháp ngôn ngữ, nhưng chúng có tác động đáng kể đến khả năng sử dụng thực tế, tính ổn định của hệ sinh thái cũng như khả năng di chuyển và tái tạo mã.
Phân biệt giữa ngôn ngữ, đặc tả và cách triển khai
Ngôn ngữ lập trình cần được hiểu bằng cách chia chúng thành các đặc tả ngôn ngữ, cách triển khai, thư viện tiêu chuẩn và hệ sinh thái phát triển. Đặc tả ngôn ngữ xác định chương trình nào đúng và ý nghĩa của từng chương trình. Bản triển khai là phần mềm như trình biên dịch, trình thông dịch và máy ảo được tạo để thực sự thực thi đặc tả.
Một ngôn ngữ có thể có nhiều cách triển khai và mỗi cách triển khai có thể cho thấy sự khác biệt về cấu trúc bên trong, hiệu suất, mục tiêu thực thi và các chức năng bổ sung. Ngoài CPython, Python còn có một số triển khai, bao gồm Jython cho JVM, IronPython cho .NET và PyPy, sử dụng trình biên dịch JIT. Ngược lại, một môi trường thực thi có thể hỗ trợ nhiều ngôn ngữ. Ngoài Java, JVM có thể chạy Kotlin, Scala và Clojure, đồng thời trong .NET, C#, F# và Visual Basic có thể chạy trên ngôn ngữ trung gian và thời gian chạy chung.
Do đó, khi mô tả các đặc điểm của một ngôn ngữ lập trình cụ thể, thay vì chỉ dán nhãn nó là “ngôn ngữ được biên dịch”, “ngôn ngữ hướng đối tượng” hoặc “ngôn ngữ định kiểu tĩnh”, bạn cũng nên xem xét ngữ pháp, ngữ nghĩa, hệ thống kiểu, mô hình, mô hình thực thi, quản lý bộ nhớ, cấu trúc đồng thời và cách triển khai chính do ngôn ngữ đó cung cấp. Sự kết hợp của các yếu tố này xác định loại chương trình có thể được thể hiện bằng ngôn ngữ, phương pháp phát triển, độ an toàn, hiệu suất và lĩnh vực sử dụng.
Mô hình lập trình
Mô hình lập trình đề cập đến quan điểm và phương pháp cơ bản để xem và thể hiện các chương trình và tính toán. Một số mô hình mô tả chương trình như một chuỗi các hướng dẫn thay đổi trạng thái bộ nhớ, trong khi những mô hình khác mô tả nó như việc đánh giá các chức năng, trao đổi thông báo giữa các đối tượng, mối quan hệ logic, phụ thuộc dữ liệu hoặc luồng sự kiện. Mô hình là một mô hình thiết kế xác định đơn vị nào sẽ chia chương trình thành, vị trí đặt trạng thái và luồng điều khiển cũng như cách trừu tượng hóa vấn đề.
Ngôn ngữ lập trình và mô hình lập trình không tương ứng một-một. Một ngôn ngữ có thể hỗ trợ nhiều mô hình và cùng một mô hình có thể được triển khai với các mô hình thực thi và ngữ pháp khác nhau cho mỗi ngôn ngữ. C chủ yếu được sử dụng làm ngôn ngữ thủ tục/mệnh lệnh, nhưng cũng có thể sử dụng các phương pháp cấu hình khác sử dụng con trỏ hàm và kiểu dữ liệu trừu tượng. C++ hỗ trợ các biểu thức thủ tục, hướng đối tượng, lập trình chung và hàm, đồng thời Python và JavaScript cũng có thể sử dụng mã mệnh lệnh, đối tượng, hàm hạng nhất và hàm bậc cao hơn. Do đó, thay vì xác định một ngôn ngữ cụ thể chỉ có một mô hình, sẽ chính xác hơn khi xem xét nó được thiết kế xung quanh mô hình nào và mỗi mô hình được hỗ trợ ở cấp độ nào.
Các mô hình lập trình chính có thể được tóm tắt như sau.
| Mô hình | Một quan điểm trung tâm về tính toán | Thành phần đại diện |
|---|---|---|
| lập trình mệnh lệnh | Chuỗi lệnh thay đổi trạng thái | Biến, phép gán, câu lệnh điều kiện, câu lệnh vòng lặp |
| Lập trình thủ tục | Phân tách hướng dẫn thành các thủ tục có thể gọi được | Hàm, thủ tục, chương trình con |
| Lập trình hướng đối tượng | Sự hợp tác của đối tượng với trạng thái và hành vi | Đối tượng, thông điệp, lớp, giao diện |
| Lập trình hàm | Đánh giá và kết hợp các chức năng | hàm thuần túy, hàm bậc cao, giá trị bất biến |
| lập trình logic | Suy ra kết quả từ sự kiện và quy tắc | Mối quan hệ, quy tắc, truy vấn, thống nhất |
| Lập trình ràng buộc | Mô tả các điều kiện mà giá trị phải thỏa mãn | Biến, miền, ràng buộc, tìm kiếm lời giải |
| lập trình luồng dữ liệu | Thực hiện các thao tác theo phụ thuộc dữ liệu | Nút, kết nối, mã thông báo, biểu đồ dữ liệu |
| lập trình phản ứng | Tuyên truyền những thay đổi xảy ra theo thời gian | Luồng sự kiện, Tín hiệu, Đăng ký, Tuyên truyền |
| Lập trình tương tranh | Tiến triển và tương tác của nhiều phép tính | thread, thông điệp, channel, actor |
| lập trình tổng quát | Thuật toán độc lập với loại và cách thực hiện | Loại tham số, ràng buộc, khái niệm |
| Lập trình hướng theo khía cạnh | Tách biệt các mối quan tâm trên nhiều mô-đun | Quan điểm, Điểm nối, Lời khuyên, Dệt may |
Những loại này không loại trừ lẫn nhau. Một chương trình chức năng có thể vừa là chương trình khai báo vừa là chương trình đồng thời, đồng thời nó cũng có thể sử dụng các thuật toán thủ tục và cấu trúc dữ liệu tổng quát trong ngôn ngữ hướng đối tượng. Các phương thức như luồng dữ liệu, khả năng phản hồi và mô hình tác nhân đôi khi được triển khai làm mô hình cho toàn bộ ngôn ngữ, nhưng cũng được sử dụng làm mô hình lập trình một phần do các thư viện hoặc khung cụ thể cung cấp.
Lập trình mệnh lệnh
Lập trình mệnh lệnh thể hiện một chương trình dưới dạng một chuỗi các hướng dẫn thay đổi trạng thái của máy tính. Chương trình đọc trạng thái hiện tại, ghi kết quả tính toán vào các biến hoặc bộ nhớ và chọn lệnh tiếp theo để thực hiện dựa trên các điều kiện. Một chương trình mệnh lệnh điển hình bao gồm các biến, câu lệnh gán, câu lệnh điều kiện, câu lệnh vòng lặp, lệnh gọi hàm và lệnh đầu vào/đầu ra.
Mô hình cơ bản của lập trình mệnh lệnh được kết nối chặt chẽ với cấu trúc của một máy tính chương trình được lưu trữ truyền thống, đọc các giá trị từ bộ nhớ, thực hiện các thao tác và ghi chúng trở lại bộ nhớ. Trong bài giảng về Giải thưởng Turing năm 1977, John Backus đã phân tích rằng các ngôn ngữ mệnh lệnh hiện có phản ánh mạnh mẽ mô hình tính toán kiểu von Neumann thay đổi từng giá trị lưu trữ một lần và đề xuất một phương pháp lập trình thay thế tập trung vào sự kết hợp của các hàm. Cuộc thảo luận này đã trở thành nền tảng lý thuyết tiêu biểu giải thích sự khác biệt giữa lập trình mệnh lệnh và lập trình hàm.
Đoạn mã sau đây hiển thị cấu trúc điển hình để tính tổng theo cách bắt buộc.
int total = 0;
for (int i = 0; i < count; ++i) {
total += values[i];
}
Trong mã này, giá trị của total và i tiếp tục thay đổi trong quá trình thực thi. Để hiểu kết quả của một chương trình, bạn cần theo dõi trạng thái của các biến thay đổi như thế nào sau khi mỗi lệnh được thực thi.
Lập trình mệnh lệnh tương ứng một cách tự nhiên với hành vi của phần cứng và cho phép kiểm soát chi tiết thứ tự thực hiện, mức sử dụng bộ nhớ và các tác dụng phụ. Nó được sử dụng rộng rãi trong các lĩnh vực mà quy trình thực thi và quản lý tài nguyên là quan trọng, chẳng hạn như hệ điều hành, trình điều khiển thiết bị, công cụ trò chơi, hệ thống nhúng và phần mềm hiệu suất cao. Mặt khác, việc thay đổi trạng thái chia sẻ ở nhiều nơi có thể gây khó khăn cho việc suy ra hành vi của mã và trong môi trường thực thi đồng thời, các điều kiện tương tranh và sự cố đồng bộ hóa cho trạng thái có thể thay đổi có thể xảy ra.
Lập trình thủ tục
Lập trình thủ tục là phương pháp chia chương trình mệnh lệnh thành các đơn vị có thể gọi được gọi là hàm, thủ tục hoặc chương trình con. Mỗi thủ tục thực hiện một nhiệm vụ cụ thể, nhận các giá trị làm đầu vào thông qua các tham số và trả về kết quả hoặc thay đổi trạng thái của chương trình.
Lập trình thủ tục chia các vấn đề lớn thành nhiều thủ tục nhỏ hơn. Ví dụ: một chương trình xử lý tệp có thể được chia thành các quy trình mở tệp, đọc tiêu đề, giải thích dữ liệu, lưu kết quả và xử lý lỗi. Mỗi thủ tục chỉ có thể cung cấp giao diện cần thiết cho người gọi và ẩn các chi tiết triển khai nội bộ của nó.
C, Pascal, FORTRAN và BASIC được phân loại là ngôn ngữ thủ tục đại diện. Ngay cả trong các ngôn ngữ hướng đối tượng và đa mô hình hiện đại, các thuật toán trong các phương thức hoặc hàm chung thường được viết theo cách thức thủ tục.
Cốt lõi của lập trình thủ tục không chỉ đơn giản là sử dụng các hàm. Mục tiêu là tổ chức luồng thực thi của chương trình thành một mối quan hệ gọi thủ tục rõ ràng và tách biệt các tác vụ cũng như đầu vào/đầu ra của từng thủ tục. Việc phân tách thủ tục phù hợp sẽ tăng khả năng sử dụng lại và kiểm tra mã, nhưng việc phụ thuộc quá nhiều vào trạng thái có thể thay đổi được chia sẻ trong toàn bộ chương trình có thể làm tăng khả năng ghép nối giữa các thủ tục.
Lập trình có cấu trúc
Lập trình có cấu trúc là một cách tiếp cận tổ chức luồng điều khiển của chương trình thành một cấu trúc rõ ràng như thực hiện tuần tự, lựa chọn và lặp lại, đồng thời giảm bớt các nhánh không có cấu trúc di chuyển đến các vị trí tùy ý. Mục đích là làm cho việc hiểu và xác minh hoạt động của mã cục bộ trở nên dễ dàng hơn bằng cách thể hiện luồng chương trình trong các khối và thủ tục có thể lồng nhau.
Các nhà nghiên cứu bao gồm Etzher Dijkstra tin rằng khi quy mô của chương trình tăng lên, cấu trúc cho phép con người suy luận chính xác đường dẫn thực thi sẽ trở nên quan trọng hơn. Ghi chú về lập trình có cấu trúc của Dijkstra là một văn bản chính hệ thống hóa cách phân rã chương trình từng bước và tổ chức nó thành một cấu trúc điều khiển giới hạn.
Lập trình có cấu trúc thường được mô tả như một cách tránh sử dụng câu lệnh goto, nhưng mục đích không phải là cấm một số ngữ pháp nhất định. Bản chất là chia chương trình thành các đơn vị nhỏ có thể hiểu được tại một thời điểm và làm rõ các điểm vào và ra, các điều kiện tiên quyết và kết quả của từng đơn vị. Các khối, hàm, điều kiện, vòng lặp và cấu trúc xử lý ngoại lệ của các ngôn ngữ hiện đại chịu ảnh hưởng phần lớn bởi lập trình có cấu trúc.
Lập trình hướng đối tượng
Lập trình hướng đối tượng là một mô hình xây dựng một chương trình dưới dạng một tập hợp các đối tượng có trạng thái và hành vi cũng như sự tương tác giữa các đối tượng. Một đối tượng kết hợp dữ liệu mà nó quản lý và các thao tác áp dụng cho dữ liệu đó thành một đơn vị. Các đối tượng khác tương tác với đối tượng đó thông qua các giao diện hoặc thông báo công cộng và biểu diễn cụ thể bên trong đối tượng có thể được cấu hình để thế giới bên ngoài không cần biết trực tiếp về nó.
Nguồn gốc của lập trình hướng đối tượng nằm ở Simula, được phát triển vào những năm 1960. Simula giới thiệu các khái niệm về đối tượng, lớp và tính kế thừa để thể hiện các tác nhân và quy trình trong thế giới thực trong mô phỏng sự kiện rời rạc. Simula 67 đã hệ thống hóa các khái niệm mà sau này trở thành trung tâm của các ngôn ngữ hướng đối tượng, chẳng hạn như các lớp, đối tượng và thủ tục ảo.
Smalltalk vào những năm 1970 đã mở rộng việc truyền đối tượng và thông điệp thành nguyên tắc trung tâm của toàn bộ chương trình. Trong Smalltalk, hầu hết mọi thứ, bao gồm số, bộ sưu tập, lớp và chính môi trường phát triển, đều được coi là một đối tượng và các tính toán được thể hiện bằng các đối tượng gửi tin nhắn cho nhau. Smalltalk đã có tác động đáng kể không chỉ đến các ngôn ngữ hướng đối tượng mà còn đến sự phát triển giao diện người dùng đồ họa và môi trường phát triển tương tác.
Trong lập trình hướng đối tượng, các khái niệm sau thường được coi là quan trọng.
- Đối tượng là một đơn vị trong chương trình có danh tính, trạng thái và hành vi.
- Đóng gói tách biệt trạng thái bên trong và cách triển khai của một đối tượng khỏi giao diện bên ngoài của nó.
- Truyền tin nhắn hoặc gọi phương thức là cách để một đối tượng yêu cầu một hành động từ một đối tượng khác.
- Đa hình cho phép sử dụng các cách triển khai khác nhau thông qua cùng một giao diện.
- Kế thừa là một phương pháp xác định một kiểu mới dựa trên các đặc điểm của một kiểu hoặc lớp hiện có.
- Thành phần là một phương pháp xây dựng các hoạt động lớn hơn bằng cách kết hợp nhiều đối tượng.
Không phải tất cả các ngôn ngữ hướng đối tượng đều cung cấp các yếu tố này theo cách giống nhau. Java và C# tập trung vào các lớp và giao diện rõ ràng, trong khi C++ sử dụng các lớp, kiểu giá trị và mẫu cùng nhau. Mô hình đối tượng của JavaScript theo truyền thống dựa trên liên kết nguyên mẫu và cú pháp class của ngôn ngữ cũng được xây dựng nội bộ dựa trên hành vi dựa trên nguyên mẫu. Go không cung cấp tính kế thừa lớp truyền thống nhưng hỗ trợ thiết kế hướng đối tượng thông qua các phương thức, giao diện và thành phần.
Trong thiết kế hướng đối tượng, tính kế thừa chỉ là một công cụ chứ không phải là yếu tố trung tâm thiết yếu. Mục đích cơ bản hơn là phân chia rõ ràng trách nhiệm của các đối tượng, cung cấp giao diện ổn định với thế giới bên ngoài và quản lý các mối quan hệ phụ thuộc giữa các đối tượng. Bởi vì hệ thống phân cấp kế thừa quá sâu và việc chia sẻ rộng rãi trạng thái có thể thay đổi có thể làm tăng khả năng ghép nối chương trình, nên thiết kế hướng đối tượng hiện đại thường sử dụng các giao diện, thành phần, đối tượng bất biến và nội xạ phụ thuộc cùng nhau.
Lập trình hàm
Lập trình hàm là một mô hình thể hiện các phép tính dưới dạng đánh giá và kết hợp các hàm. Hàm không chỉ đơn giản là các thủ tục chứa các lệnh mà được coi là các giá trị hạng nhất có thể được lưu trữ trong các biến, được truyền dưới dạng đối số và được trả về dưới dạng kết quả giống như bất kỳ giá trị nào khác.
Nền tảng lý thuyết của lập trình hàm là Phép tính lambda do Alonzo Church thiết lập. LISP ban đầu đã giới thiệu các khái niệm chính về lập trình hàm, tập trung vào các hàm, đệ quy và xử lý danh sách, sang các ngôn ngữ lập trình thực tế. Sau đó, các ngôn ngữ dựa trên ML đã phát triển các kiểu dữ liệu đại số và suy luận kiểu tĩnh, đồng thời Haskell đã thiết lập một mô hình đại diện cho các ngôn ngữ hàm thuần túy tập trung vào các hàm thuần túy, đánh giá lười biếng và các lớp kiểu. Haskell bắt nguồn từ những năm 1980 từ các cuộc thảo luận về sự cần thiết của một ngôn ngữ mở chung cho các ngôn ngữ chức năng đánh giá lười biếng khác nhau.
Các khái niệm quan trọng trong lập trình hàm như sau.
- Hàm hạng nhất cho phép các hàm được lưu trữ và truyền đi như bất kỳ giá trị nào khác.
- Hàm bậc cao hơn lấy một hàm làm đối số hoặc kết quả trả về chính hàm đó.
- Hàm thuần túy không thay đổi trạng thái bên ngoài và trả về cùng một kết quả cho cùng một dữ liệu đầu vào.
- Tính minh bạch tham chiếu là thuộc tính mà ý nghĩa của chương trình không thay đổi ngay cả khi một biểu thức được thay thế bằng giá trị kết quả của nó.
- Tính bất biến sử dụng phương pháp tạo giá trị mới thay vì thay đổi các giá trị đã tạo.
- Đệ quy thể hiện các phép tính lặp đi lặp lại trong một cấu trúc trong đó hàm gọi chính nó.
- Thành phần hàm kết hợp các hàm nhỏ để tạo ra các phép tính phức tạp hơn.
- Khớp mẫu phân nhánh phép tính dựa trên cấu trúc của giá trị và trích xuất các phần tử bên trong.
Sau đây là một biểu thức hàm điển hình xử lý các tập hợp thông qua sự kết hợp của các hàm.
sumOfSquares =
sum
. map (\x -> x * x)
. filter even
Biểu thức này không mô tả trực tiếp thứ tự duyệt bộ sưu tập và giá trị nào sẽ được gán cho các biến tạm thời. Nó thể hiện mối quan hệ chuyển đổi của việc chọn một số chẵn, bình phương từng giá trị và sau đó tính tổng kết quả.
Việc tổ chức một chương trình xung quanh các hàm thuần túy và các giá trị bất biến giúp giảm tác động của các hàm lên trạng thái bên ngoài, giúp việc suy luận cục bộ và kiểm tra độc lập các kết quả của mã trở nên dễ dàng hơn. Vì nó làm giảm số lượng trạng thái có thể thay đổi được chia sẻ nên rất thuận lợi cho việc hạn chế các cuộc chạy đua dữ liệu ngay cả trong các chương trình đồng thời.
Tuy nhiên, không phải tất cả các ngôn ngữ chức năng chỉ cho phép các hàm thuần túy. LISP, OCaml, F# và Scala cung cấp các chức năng chức năng, trạng thái có thể thay đổi và cấu trúc điều khiển mệnh lệnh. Haskell cũng có thể biểu diễn các thay đổi đầu vào/đầu ra và trạng thái, nhưng các kiểu và cấu trúc tính toán phân biệt những hiệu ứng này với các biểu thức thuần túy. Lập trình hàm có thể hiểu là một cách kiểm soát rõ ràng vị trí và mối quan hệ xảy ra tác dụng phụ hơn là loại bỏ hoàn toàn tác dụng phụ.
Các ngôn ngữ chức năng cũng khác nhau tùy thuộc vào chiến lược đánh giá của chúng. Đánh giá nghiêm ngặt đánh giá các đối số của hàm trước khi gọi nó, trong khi đánh giá không nghiêm ngặt hoặc lười biếng có thể trì hoãn việc đánh giá cho đến khi kết quả thực tế yêu cầu. Đánh giá lười biếng có thể thể hiện sự kết hợp vô hạn của cấu trúc dữ liệu và phép tính một cách tự nhiên, nhưng trong một số trường hợp, rất khó để dự đoán thời gian đánh giá thực tế và mức sử dụng bộ nhớ.
Lập trình khai báo
Lập trình khai báo bao gồm một cách tiếp cận mô tả các kết quả, mối quan hệ hoặc ràng buộc mong muốn thay vì liệt kê trực tiếp chuỗi hướng dẫn cụ thể để thực hiện tính toán. Trong khi các chương trình mệnh lệnh tập trung vào “thứ tự tính toán” thì các chương trình khai báo lại tập trung vào “những kết quả cần đạt được”.
Lập trình khai báo không trỏ đến một mô hình thực thi duy nhất. Lập trình hàm, lập trình logic, lập trình ràng buộc, lập trình dựa trên quy tắc và ngôn ngữ truy vấn dữ liệu có thể được đưa vào mô hình khai báo rộng. Tuy nhiên, các hàm mệnh lệnh có thể được sử dụng ngay cả trong các ngôn ngữ hàm và người thực thi ngôn ngữ khai báo sử dụng thuật toán mệnh lệnh phức tạp bên trong, do đó ranh giới giữa mệnh lệnh và khai báo không phải lúc nào cũng rõ ràng.
Truy vấn SQL là một ví dụ điển hình của lập trình khai báo.
SELECT name
FROM users
WHERE active = TRUE
ORDER BY name;
Truy vấn này không chỉ định thứ tự các tệp dữ liệu sẽ được đọc, chỉ mục nào sẽ sử dụng và thuật toán nào để thực hiện sắp xếp và lọc. Người dùng mô tả kiểu kết quả mong muốn và trình tối ưu hóa truy vấn của hệ thống quản lý cơ sở dữ liệu sẽ xác định kế hoạch thực hiện thực tế.
Các chương trình khai báo đơn giản hóa biểu thức bằng cách để lại chi tiết về cách thực thi cho hệ thống và cho phép người thực thi chọn các chiến lược tối ưu hóa khác nhau tùy theo tình huống. Mặt khác, do quá trình thực hiện được xác định bởi quá trình triển khai nên có thể cần phải hiểu riêng kế hoạch thực hiện nội bộ và các quy tắc tối ưu hóa khi phân tích các vấn đề về hiệu suất.
Lập trình logic
Lập trình logic thể hiện một chương trình dưới dạng một tập hợp các sự kiện, quy tắc và truy vấn. Lập trình viên mô tả mối quan hệ nào là đúng và người thực thi sử dụng lý luận logic để tìm ra giải pháp thỏa mãn truy vấn.
Robert Kowalski giải thích các thuật toán là sự kết hợp giữa logic và điều khiển. Logic thể hiện các mối quan hệ và ý nghĩa phải được thiết lập trong một vấn đề, và sự kiểm soát sẽ xác định thứ tự và phương pháp mà các mối quan hệ đó sẽ được khám phá. Sự khác biệt này đã trở thành nền tảng quan trọng để hiểu ý nghĩa của vấn đề và chiến lược thực hiện riêng biệt trong lập trình logic.
Prolog là một ngôn ngữ lập trình logic đại diện. Chương trình Prolog xác định các sự kiện và quy tắc rồi thực hiện các truy vấn.
parent(alice, bob).
parent(bob, carol).
ancestor(X, Y) :-
parent(X, Y).
ancestor(X, Y) :-
parent(X, Z),
ancestor(Z, Y).
Ở đây, quy tắc ancestor xác định mối quan hệ giữa cha mẹ và tổ tiên, thay vì viết một thứ tự tính toán cụ thể dưới dạng một tuyên bố chung. Người thực thi sử dụng sự thống nhất và quay lui để tìm kiếm các giá trị của các biến thỏa mãn điều kiện.
Lập trình logic đã được sử dụng trong xử lý ngôn ngữ tự nhiên, biểu diễn tri thức, chứng minh định lý, hệ thống chuyên gia, phân tích chương trình và hệ thống dựa trên quy tắc. Các ngôn ngữ logic giới hạn như Datalog cũng được sử dụng cho các truy vấn cơ sở dữ liệu, phân tích tĩnh, chính sách kiểm soát truy cập và biểu hiện mối quan hệ trong các hệ thống phân tán.
Lập trình ràng buộc
Lập trình ràng buộc là một mô hình mô tả các điều kiện mà các biến phải đáp ứng và cho phép người giải ràng buộc tìm kiếm các giải pháp khả thi, thay vì viết các thủ tục tính toán trực tiếp giá trị của các biến.
Ví dụ, trong bài toán lập kế hoạch, thời gian bắt đầu và thời gian kết thúc của mỗi nhiệm vụ có thể được đặt dưới dạng các biến và có thể xác định các ràng buộc sau.
- Nhiệm vụ A phải được hoàn thành trước nhiệm vụ B.
- Hai tác vụ không thể sử dụng cùng một tài nguyên cùng một lúc.
- Mọi công việc phải hoàn thành trước thời hạn quy định.
- Một số nhiệm vụ nhất định chỉ có thể được thực hiện trong khoảng thời gian cho phép.
Bộ giải ràng buộc làm giảm miền giá trị có thể có của từng biến, truyền bá các ràng buộc, chọn ứng viên nếu cần và thực hiện tìm kiếm ngược. Lập trình ràng buộc phù hợp cho việc lập kế hoạch và phân bổ tài nguyên, tối ưu hóa tổ hợp, thiết kế mạch, các vấn đề về cấu hình và xác minh, v.v.
Trong lập trình logic ràng buộc, kết hợp lập trình logic và lập trình ràng buộc, các mối quan hệ logic, số, tập hợp và ràng buộc thời gian có thể được biểu diễn cùng nhau trong một chương trình.
Lập trình dựa trên quy tắc
Lập trình dựa trên quy tắc thể hiện hành động sẽ được thực hiện khi một điều kiện cụ thể được đáp ứng theo quy tắc. Quy tắc chung bao gồm các điều kiện và kết luận.
Điều kiện:
Tài khoản người dùng đang bị khóa
và đã 30 phút trôi qua kể từ lần xác thực thất bại gần nhất.
Hành động:
Mở khóa tài khoản.
Công cụ quy tắc kiểm tra các sự kiện và điều kiện hiện tại để tìm các quy tắc có thể áp dụng và thực thi các quy tắc theo mức độ ưu tiên và chính sách giải quyết xung đột. Các phương pháp dựa trên quy tắc được sử dụng trong các hệ thống chuyên gia, chính sách kinh doanh, kiểm soát truy cập, xử lý sự kiện và hệ thống hành vi trò chơi.
Lập trình dựa trên quy tắc có vẻ tương tự như lập trình logic, nhưng không nhất thiết phải sử dụng mô hình lý luận của logic hình thức. Một số hệ thống quy tắc bao gồm các hành động bắt buộc trực tiếp thay đổi trạng thái và thứ tự thực thi các quy tắc có thể ảnh hưởng đến kết quả.
Lập trình tổng quát
Lập trình tổng quát là một mô hình tạo ra các thuật toán và cấu trúc dữ liệu không phụ thuộc vào các kiểu dữ liệu hoặc cách triển khai cụ thể, đồng thời chỉ định các hoạt động và thuộc tính mà thuật toán yêu cầu.
Ví dụ: một thuật toán sắp xếp có thể được viết không chỉ cho mảng số nguyên mà còn được áp dụng cho nhiều kiểu dữ liệu có thể so sánh và trao đổi. Điều mà thuật toán yêu cầu không phải là tên loại cụ thể mà là điều kiện để các phần tử có thể được so sánh và di chuyển.
Lập trình tổng quát được hệ thống hóa thông qua nghiên cứu của David Musser và Alexander Stepanov vào những năm 1980. Kể từ đó, Thư viện mẫu chuẩn C++ đã cung cấp một hệ thống lập trình chung tiêu biểu giúp phân tách các trình vòng lặp, vùng chứa và thuật toán, đồng thời cho phép chúng được kết hợp thông qua các tham số kiểu và tính đa hình tại thời điểm biên dịch. Trong lập trình tổng quát, điều quan trọng là phải xem xét liệu một thuật toán trừu tượng có thể chuyên biệt cho các loại cụ thể mà không phải trả chi phí thực hiện không cần thiết hay không.
Cách thể hiện lập trình chung khác nhau tùy theo ngôn ngữ.
- C++ sử dụng các mẫu và khái niệm.
- Java và C# sử dụng các kiểu chung và các ràng buộc.
- Rust sử dụng các giới hạn chung và đặc điểm.
- Haskell sử dụng tính đa hình tham số và các lớp kiểu.
- Go sử dụng các tham số kiểu và ràng buộc giao diện.
- Các ngôn ngữ họ ML sử dụng hệ thống mô-đun và hàm đa hình.
Lập trình tổng quát không chỉ đơn giản là kỹ thuật thay thế tên kiểu bằng các tham số trong mã. Đó là một phương pháp thiết kế nhằm khám phá các khái niệm chung giữa các loại và thuật toán khác nhau và thể hiện các khái niệm đó dưới dạng yêu cầu tối thiểu.
Lập trình luồng dữ liệu
Lập trình luồng dữ liệu thể hiện chương trình dưới dạng biểu đồ gồm các nút tính toán và các phụ thuộc dữ liệu kết nối chúng. Mỗi thao tác được thực thi khi các đầu vào cần thiết đã sẵn sàng và kết quả tính toán được chuyển sang thao tác được kết nối tiếp theo.
Các chương trình mệnh lệnh truyền thống thực hiện các lệnh do bộ đếm chương trình chỉ định theo thứ tự, nhưng trong mô hình luồng dữ liệu, tính sẵn sàng của dữ liệu sẽ quyết định tính khả thi của việc thực thi. Bởi vì các phép tính không phụ thuộc lẫn nhau có thể được thực hiện đồng thời nên các mô hình luồng dữ liệu là cơ sở tự nhiên để biểu diễn tính toán song song. Nghiên cứu của Jack Dennis về kiến trúc luồng dữ liệu đã chỉ định một mô hình tính toán kích hoạt các hoạt động dựa trên sự phụ thuộc dữ liệu thay vì thứ tự lệnh.
Một chương trình luồng dữ liệu có thể được xem dưới dạng sau:
Đầu vào A ─┐
├─ Phép nhân ─┐
Đầu vào B ─┘ │
├─ Phép cộng ─ Kết quả
Đầu vào C ───────────────┘
Phép nhân được thực hiện khi cả A và B đã sẵn sàng và phép cộng được thực hiện khi kết quả nhân và C đã sẵn sàng. Luồng điều khiển của chương trình không được chỉ định riêng và bắt nguồn từ sự phụ thuộc dữ liệu.
Phương pháp luồng dữ liệu được sử dụng rộng rãi trong môi trường lập trình đồ họa, xử lý tín hiệu số, hệ thống truyền phát, hệ thống xây dựng, bảng tính, đồ thị tính toán GPU và khung học máy. Tuy nhiên, để thể hiện sự lặp lại, phân nhánh có điều kiện và trạng thái có thể thay đổi trong biểu đồ luồng dữ liệu, cần có các nút, mã thông báo, phụ thuộc điều khiển hoặc cấu trúc quản lý trạng thái riêng biệt.
Lập trình dựa trên sự kiện
Lập trình dựa trên sự kiện là phương pháp trong đó luồng thực thi của chương trình được xác định bởi các sự kiện như đầu vào của người dùng, thông báo mạng, bộ hẹn giờ, thay đổi tệp và tín hiệu cảm biến. Một chương trình đăng ký một trình xử lý sự kiện hoặc lệnh gọi lại tương ứng với một sự kiện cụ thể và môi trường thực thi sẽ gọi trình xử lý đó khi sự kiện xảy ra.
Giao diện người dùng đồ họa là một hệ thống dựa trên sự kiện đại diện. Chương trình không trực tiếp điều khiển khi người dùng nhấn nút; nó xác định hành động sẽ thực thi khi xảy ra sự kiện nhấn nút.
button.addEventListener("click", () => {
saveDocument();
});
Các chương trình dựa trên sự kiện phù hợp với máy chủ, giao diện người dùng, trò chơi, hệ thống nhúng và dịch vụ phân tán đáp ứng đầu vào bên ngoài. Tuy nhiên, khi các lệnh gọi lại được lồng vào nhiều giai đoạn và mỗi giai đoạn thay đổi trạng thái chia sẻ, việc hiểu thứ tự thực hiện và lan truyền lỗi có thể trở nên khó khăn. Lời hứa, tương lai, coroutine và async·await được dùng để thể hiện sự kết nối của các sự kiện không đồng bộ và phân phối kết quả một cách có cấu trúc hơn.
Lập trình dựa trên sự kiện và lập trình phản ứng có liên quan chặt chẽ với nhau, nhưng chúng không phải là một khái niệm giống nhau. Các chương trình dựa trên sự kiện thường thực thi các trình xử lý đã đăng ký một cách bắt buộc khi một sự kiện xảy ra. Các chương trình đáp ứng chú trọng nhiều hơn đến việc khai báo sự phụ thuộc giữa các giá trị và sự kiện thay đổi theo thời gian, đồng thời định cấu hình các giá trị để truyền bá theo các mối quan hệ đó khi thay đổi xảy ra.
Lập trình phản ứng
Lập trình phản ứng là một mô hình thể hiện luồng giá trị và sự kiện xảy ra theo thời gian và định cấu hình nó để những thay đổi trong đầu vào được tự động truyền đến các phép tính liên quan. Những thay đổi liên tục về giá trị hoặc sự kiện không đồng bộ được mô hình hóa dưới dạng luồng hoặc tín hiệu và các luồng mới được tạo thông qua các hoạt động như lọc, chuyển đổi và kết hợp.
Bảng tính có thể được coi là một ví dụ đơn giản về tính toán đáp ứng. Khi giá trị của một ô thay đổi, kết quả của các công thức tham chiếu đến ô đó sẽ tự động được tính toán lại. Thay vì trực tiếp viết chuỗi lệnh sẽ cập nhật từng ô, lập trình viên sẽ xác định sự phụ thuộc giữa các giá trị.
Lập trình phản ứng chức năng kết hợp lập trình hàm và tính toán phản ứng. Trong nghiên cứu FRP ban đầu, các hành động thay đổi liên tục theo thời gian và các sự kiện xảy ra không liên tục được thể hiện dưới dạng trừu tượng có thể tổng hợp được. Kể từ đó, FRP đã phát triển thành một phương pháp cấu hình khai báo giao diện người dùng, hoạt ảnh, điều khiển robot và hệ thống tương tác.
Các thư viện phản ứng hiện đại thường cung cấp các hoạt động sau:
mapchuyển đổi từng sự kiện hoặc giá trị thành giá trị khác.filterchỉ vượt qua các sự kiện thỏa mãn điều kiện.mergekết hợp nhiều luồng thành một.combinetạo một giá trị mới dựa trên một số giá trị gần đây.debouncegiảm các sự kiện lặp lại trong một khoảng thời gian ngắn xuống còn một.bufferthu thập và xử lý một khoảng thời gian hoặc số lượng sự kiện nhất định.subscribekết nối trình xử lý sẽ nhận kết quả của luồng.
Trong chương trình phản ứng, khi nhà sản xuất tạo ra dữ liệu nhanh hơn người tiêu dùng, áp lực ngược kiểm soát tốc độ xử lý, thời gian tồn tại của đăng ký, lan truyền lỗi và bộ lập lịch thực thi phải được quản lý cùng nhau.
Lập trình tương tranh
Lập trình tương tranh là mô hình để xây dựng một chương trình trong đó hai hoặc nhiều phép tính tiến hành độc lập trong cùng một khoảng thời gian. Nhiều tác vụ không thực sự phải chạy cùng lúc và tính đồng thời tồn tại nếu chúng có vòng đời chồng chéo ngay cả khi chúng chạy luân phiên trên một bộ xử lý.
Lập trình song song tập trung vào việc tăng thông lượng hoặc tốc độ tính toán bằng cách thực hiện đồng thời hai hoặc nhiều phép tính. Lập trình phân tán xử lý các hệ thống trong đó nhiều máy tính, mỗi máy có bộ nhớ độc lập và vùng lỗi, hợp tác thông qua mạng.
Ba khái niệm có liên quan nhưng không giống nhau.
| khái niệm | vấn đề trọng tâm |
|---|---|
| đồng thời | Cách tổ chức và điều phối hợp lý nhiều nhiệm vụ |
| Song song | Cách thực thi đồng thời bằng nhiều tài nguyên tính toán |
| Khả năng phân tán | Cách phối hợp các mạng và nhiều hệ thống khi có lỗi cục bộ |
Các mô hình đồng thời được chia thành nhiều loại tùy thuộc vào việc chúng có chia sẻ trạng thái hay không, cách chúng giao tiếp và cách chúng xác định thứ tự thực hiện các tác vụ.
Bộ nhớ chia sẻ và thread
Trong mô hình bộ nhớ dùng chung, nhiều luồng truy cập các giá trị trong cùng một bộ nhớ. Giao tiếp giữa các luồng được thực hiện thông qua các biến và cấu trúc dữ liệu được chia sẻ, đồng thời các khóa, ngữ nghĩa, biến điều kiện và hoạt động nguyên tử được sử dụng để ngăn chặn các cuộc chạy đua dữ liệu xảy ra khi sửa đổi cùng một trạng thái cùng một lúc.
Bộ nhớ dùng chung cho phép giao tiếp hiệu quả vì dữ liệu không cần phải truyền trực tiếp nhưng kết quả của chương trình có thể khác nhau tùy thuộc vào thứ tự thực hiện chi tiết. Để tránh bế tắc, khóa trực tiếp, đảo ngược mức độ ưu tiên, chạy đua dữ liệu và các vấn đề về khả năng hiển thị bộ nhớ, bạn phải hiểu mô hình bộ nhớ được xác định bởi ngôn ngữ và môi trường thực thi.
Communicating Sequential Processes
Giao tiếp các quy trình tuần tự là một mô hình trong đó các quy trình tuần tự chạy độc lập tương tác thông qua giao tiếp rõ ràng. Trong một bài báo năm 1978, Tony Hoare đề xuất giao tiếp đồng bộ giữa đầu vào, đầu ra và các quy trình như một thành phần cơ bản của các chương trình song song.
Trong CSP, mỗi tiến trình quản lý trạng thái cục bộ của chính nó và trao đổi giá trị thông qua các kênh. Giao tiếp không chỉ thực hiện việc truyền dữ liệu đơn giản mà còn đóng vai trò đồng bộ hóa giữa các quy trình.
Các goroutine và kênh của Go là các mô hình lập trình tiêu biểu chịu ảnh hưởng của CSP. Tuy nhiên, mô hình và kênh thực thi của Go không trực tiếp triển khai dạng CSP ban đầu mà cung cấp các chức năng ngôn ngữ và lập lịch thời gian chạy phù hợp với lập trình hệ thống thực tế.
Mô hình actor
Mô hình tác nhân thể hiện một hệ thống đồng thời là một tập hợp các tác nhân độc lập với nhau. Khi mỗi tác nhân nhận được một tin nhắn, nó có thể thực hiện các hành động sau:
- Thay đổi trạng thái nội bộ của bạn.
- Gửi tin nhắn cho actor khác.
- Tạo một actor mới.
- Xác định hành động xử lý tin nhắn tiếp theo.
Các tác nhân giao tiếp thông qua các tin nhắn không đồng bộ mà không có quyền truy cập trực tiếp vào trạng thái nội bộ của các tác nhân khác. Mô hình tác nhân có nguồn gốc từ mô hình tính toán đồng thời do Carl Hewitt, Peter Bishop và Richard Steiger đề xuất năm 1973.
Các mô hình quy trình của Erlang và Elixir, Akka và một số khung tác nhân phân tán đã bị ảnh hưởng bởi khái niệm tác nhân. Trạng thái cô lập của các tác nhân và việc truyền thông điệp làm giảm sự tranh chấp bộ nhớ dùng chung và phù hợp để biểu diễn các thành phần của hệ thống phân tán.
Tuy nhiên, phải xử lý sự chậm trễ và trình tự gửi tin nhắn, trùng lặp, mất mát và lỗi tác nhân, đồng thời các thay đổi trạng thái nhất quán giữa nhiều tác nhân yêu cầu các giao thức riêng biệt. Việc sử dụng các tác nhân không tự động giải quyết được các vấn đề về lỗi và tính nhất quán trong hệ thống phân tán.
Coroutine và tương tranh có cấu trúc
coroutine là một đơn vị tính toán có thể tạm dừng quá trình thực thi và sau đó tiếp tục lại từ cùng một vị trí. Nó được sử dụng để triển khai các trình tạo, các hàm không đồng bộ và các tác vụ hợp tác và thường được cung cấp dưới dạng một đơn vị thực thi nhẹ hơn một luồng của hệ điều hành.
async và await thể hiện các hoạt động không đồng bộ trong cấu trúc tương tự như mã tuần tự chung. Trong khi một hàm đang chờ kết quả đầu vào/đầu ra, nó có thể tạm dừng việc thực thi và trả lại quyền điều khiển cho bộ thực thi hoặc vòng lặp sự kiện.
Structured Concurrency là một cách tiếp cận cố gắng gắn kết thời gian tồn tại của các hoạt động đồng thời được tạo với các khối và cấu trúc lệnh gọi của mã. Bằng cách để tác vụ gốc quản lý việc hoàn thành, thất bại và hủy bỏ các tác vụ con của nó, nó sẽ giảm bớt công việc nằm ngoài chương trình và việc thực thi không đồng bộ khó theo dõi.
Bản thân các coroutine không đảm bảo tính song song. Nhiều coroutine có thể chạy phối hợp trên một luồng hoặc các coroutine có thể chạy song song trên nhiều luồng hoặc trình thực thi tác vụ.
Lập trình hướng khía cạnh
Lập trình hướng khía cạnh là một mô hình cố gắng tách các mối quan tâm xuyên suốt liên tục mở rộng nhiều mô-đun và lớp thành các đơn vị riêng biệt. Các hoạt động thường được chèn vào các phần khác nhau của chương trình, chẳng hạn như ghi nhật ký, kiểm tra quyền, giao dịch, đo lường hiệu suất và khôi phục lỗi, là những mối quan tâm xuyên suốt điển hình.
Trong kiến trúc hướng đối tượng hoặc thủ tục điển hình, chức năng này có thể được lặp lại trên nhiều hàm và lớp, xen kẽ với logic nghiệp vụ cốt lõi. Lập trình hướng theo khía cạnh xác định các mối quan tâm xuyên suốt là các quan điểm và áp dụng hành động của chúng vào các điểm chung cụ thể trong chương trình. Nghiên cứu của Gregor Kiczales et al. lập trình hướng theo khía cạnh được hệ thống hóa như một phương pháp để cải thiện việc phân tách các mối quan tâm.
Các khái niệm chính như sau:
- Điểm liên kết là một điểm trong chương trình nơi có thể áp dụng các thao tác bổ sung, chẳng hạn như gọi hàm hoặc tạo đối tượng.
- Pointcut chỉ định điểm nối nào cần chọn.
- Lời khuyên là hành động được thực hiện trước, sau hoặc xung quanh điểm đã chọn.
- Dệt là quá trình kết hợp hành vi của một quan điểm vào một chương trình mục tiêu.
Các phối cảnh có thể tách biệt logic cốt lõi khỏi những tiếng chuông và tiếng huýt sáo lặp đi lặp lại, nhưng chúng cũng có thể làm cho luồng điều khiển thực tế khó theo dõi vì các hành động không xuất hiện trực tiếp trong mã nguồn sẽ được chèn vào nơi khác. Thay vì sử dụng toàn bộ ngôn ngữ hướng theo khía cạnh, các ngôn ngữ và khung công tác hiện đại thường cung cấp chức năng tương tự thông qua các thuộc tính, chú thích, trang trí, proxy, phần mềm trung gian và plugin trình biên dịch.
Lập trình meta
Metaprogramming là phương pháp viết chương trình phân tích, tạo và chuyển đổi các chương trình như dữ liệu. Nó thường được xem như một kỹ thuật cấu thành chương trình có thể được sử dụng trong nhiều mô hình hơn là một mô hình cốt lõi độc lập.
Lập trình meta xuất hiện dưới các dạng sau:
- Macro tiền xử lý thay thế hoặc tạo mã nguồn.
- Cú pháp macro biến đổi cấu trúc cú pháp trừu tượng.
- Mẫu tạo mã dựa trên loại và hằng số.
- Reflection kiểm tra kiểu và cấu trúc của chương trình khi nó đang chạy.
- Trình tạo mã tạo mã nguồn từ các định nghĩa lược đồ hoặc giao diện.
- Plug-in trình biên dịch phân tích hoặc sửa đổi các khai báo và biểu thức.
- Chuyên môn hóa chương trình bằng cách tính toán trước các đầu vào có đánh giá từng phần đã biết.
Macro trong các ngôn ngữ dựa trên LISP cho phép cấu trúc cú pháp của chương trình được coi là cấu trúc dữ liệu của chính ngôn ngữ đó. Các mẫu trong C++ không chỉ được sử dụng cho lập trình chung mà còn được sử dụng để tính toán và tạo mã tại thời điểm biên dịch. Các macro của Rust cung cấp các macro khai báo dựa trên mẫu và các macro thủ tục xử lý cây cú pháp.
Siêu lập trình có thể giảm bớt mã lặp lại và thêm các phần trừu tượng mới vào ngôn ngữ, nhưng mã được tạo và luồng thực thi thực tế có thể không hiển thị rõ ràng từ nguồn ban đầu. Do đó, các thông báo lỗi, gỡ lỗi, hỗ trợ công cụ, thời gian xây dựng và khả năng đọc mã phải được xem xét cùng nhau.
Lập trình đa mô hình
Các ngôn ngữ lập trình đa năng hiện đại thường hỗ trợ nhiều mô hình cùng nhau. Ngay cả trong một ứng dụng, các mô hình khác nhau có thể được kết hợp tùy thuộc vào bản chất của vấn đề.
Ví dụ: công cụ trò chơi có thể được định cấu hình theo cách sau:
- Lập trình mệnh lệnh được sử dụng để tạo các lệnh kết xuất và thiết bị điều khiển.
- Sử dụng thiết kế hướng đối tượng hoặc hướng dữ liệu cho các thành phần của hệ thống và công cụ trò chơi.
- Các hoạt động chức năng được sử dụng để chuyển đổi bộ sưu tập và xử lý tài sản.
- Bộ lập lịch tác vụ sử dụng luồng dữ liệu và mô hình đồng thời.
- Shader Graph sử dụng lập trình luồng dữ liệu.
- Giao diện soạn thảo sử dụng lập trình phản ứng và dựa trên sự kiện.
- Sử dụng siêu lập trình để tuần tự hóa và tạo mã.
Mục tiêu của lập trình đa mô hình không phải là sử dụng mọi phương pháp có thể có trong một chương trình. Mục tiêu là chọn ra mô hình thể hiện rõ ràng nhất cấu trúc của từng vấn đề và làm rõ ranh giới giữa các mô hình khác nhau.
Khi kết hợp các mô hình, quyền sở hữu trạng thái và dữ liệu, việc truyền lỗi, ranh giới đồng thời, hướng cuộc gọi và mức độ trừu tượng phải được giữ nhất quán. Bạn có thể sử dụng các phép biến đổi chức năng trong lớp hướng đối tượng, nhưng bạn sẽ mất đi lợi ích của cả hai mô hình nếu bạn thay đổi trạng thái bên trong của đối tượng theo cách không thể đoán trước. Ngay cả khi kết hợp các luồng phản ứng với các lệnh gọi lại bắt buộc, việc tạo và phát hành đăng ký, thứ tự thực hiện và quy tắc xử lý lỗi phải được xác định rõ ràng.
Mô hình và thiết kế phần mềm
Một mô hình lập trình không chỉ là vấn đề chọn một ngữ pháp cụ thể. Nó ảnh hưởng đến cách chương trình được phân tách thành các đơn vị và cách xác định mối quan hệ giữa mỗi đơn vị.
Trong mô hình mệnh lệnh, những thay đổi trạng thái và trình tự lệnh là trung tâm. Trong mô hình hướng đối tượng, trách nhiệm và sự hợp tác của các đối tượng là trung tâm. Trong mô hình chức năng, việc chuyển đổi giá trị và thành phần chức năng là trung tâm. Trong mô hình logic/ràng buộc, các mối quan hệ và điều kiện phải được thiết lập là trung tâm. Trong mô hình luồng dữ liệu, sự phụ thuộc giữa các hoạt động là trung tâm, trong khi ở mô hình đồng thời, giao tiếp và đồng bộ hóa giữa các tác vụ độc lập là trung tâm.
Cùng một vấn đề có thể được diễn đạt khác nhau tùy thuộc vào mô hình. Thao tác chuyển đổi và cộng các giá trị thỏa mãn điều kiện trong một tập hợp có thể xem như sau.
Mệnh lệnh:
Duyệt qua các phần tử, kiểm tra điều kiện
và cập nhật biến.
Hàm:
Kết hợp filter, map và reduce.
Hướng đối tượng:
Yêu cầu đối tượng collection thực hiện thao tác biến đổi và tổng hợp.
Logic:
Mô tả quan hệ và điều kiện của các phần tử cần có trong kết quả.
Luồng dữ liệu:
Kết nối các nút lọc, biến đổi và tổng hợp.
Song song:
Chia đầu vào, tính toán trong nhiều tác vụ rồi kết hợp kết quả.
Không có biểu hiện nào luôn vượt trội. Phương pháp thích hợp sẽ khác nhau tùy thuộc vào kích thước và tần suất thay đổi của đầu vào, yêu cầu về hiệu suất, xử lý lỗi, kinh nghiệm của nhóm, mã hiện có và môi trường thực thi.
Các mô hình lập trình không phải là sự tiến triển từng bước thay thế hoàn toàn lẫn nhau. Ngôn ngữ thủ tục không biến mất cùng với sự ra đời của ngôn ngữ hướng đối tượng và lập trình hàm không thay thế hoàn toàn lập trình mệnh lệnh. Mô hình mới cho phép giải quyết các vấn đề khó diễn đạt theo cách hiện có từ một góc nhìn khác và khi kết hợp với mô hình hiện có, nó sẽ được hấp thụ vào các ngôn ngữ hiện đại và thiết kế phần mềm.
Vì vậy, việc hiểu mô hình có ý nghĩa rộng hơn việc ghi nhớ ngữ pháp của từng ngôn ngữ. Chúng ta cần có khả năng mô hình hóa cùng một tính toán theo nhiều cách và so sánh cách biểu diễn trạng thái, điều khiển, dữ liệu, trừu tượng và đồng thời theo từng cách. Sự hiểu biết này trở thành cơ sở để lựa chọn cấu trúc chương trình và chức năng ngôn ngữ phù hợp với vấn đề mà không bị phụ thuộc vào một ngôn ngữ cụ thể.
Thực thi và triển khai
Mã nguồn được viết bằng ngôn ngữ lập trình bản thân nó không phải là một lệnh mà bộ xử lý trung tâm có thể thực thi. Việc triển khai ngôn ngữ sẽ đọc mã nguồn, phân tích ngữ pháp và ý nghĩa, sau đó chuyển đổi chương trình thành ngôn ngữ máy, mã byte, biểu diễn trung gian hoặc cấu trúc bên trong mà trình thông dịch có thể xử lý. Chương trình được chuyển đổi thực sự được thực thi với sự trợ giúp của trình tải hệ điều hành, máy ảo hoặc thời gian chạy ngôn ngữ.
Quá trình này thay đổi rất nhiều tùy thuộc vào ngôn ngữ và cách thực hiện. Cách triển khai điển hình của C và C++ biên dịch mã nguồn thành các tệp đối tượng và liên kết chúng để tạo ra một tệp thực thi gốc. Java và Kotlin thường tạo mã byte JVM, trong khi C# và F# sử dụng ngôn ngữ trung gian phổ biến trong .NET. CPython chuyển đổi mã nguồn Python thành một đối tượng mã chứa mã byte bên trong và chạy nó trên máy ảo. Các công cụ JavaScript thường sử dụng trình thông dịch và trình biên dịch JIT cùng nhau sau khi phân tích mã nguồn. Do đó, quy trình từ mã nguồn đến thực thi không phải là một quy trình cố định mà là một hệ thống triển khai trong đó có thể chọn và kết hợp nhiều bước.
Quá trình biên dịch bản địa truyền thống có thể được tóm tắt như sau.
Mã nguồn
↓
Tiền xử lý
↓
Phân tích từ vựng
↓
Token
↓
Phân tích cú pháp
↓
Cây cú pháp trừu tượng
↓
Phân tích ngữ nghĩa và kiểm tra kiểu
↓
Biểu diễn trung gian
↓
Tối ưu hóa
↓
Sinh mã đích
↓
Hợp ngữ hoặc mã máy
↓
Tệp đối tượng
↓
Liên kết
↓
Tệp thực thi hoặc thư viện
↓
Trình nạp của hệ điều hành và runtime
↓
Thực thi
Không phải tất cả quá trình triển khai đều tách biệt rõ ràng các bước này theo cùng một thứ tự. Một số ngôn ngữ không có bộ tiền xử lý, một số cách triển khai tạo mã byte trực tiếp từ cây cú pháp trừu tượng và một số trình biên dịch phân tích mã nguồn trong khi thực thi hoặc chia các biểu diễn trung gian thành nhiều bước. Trình điều khiển của Clang cũng phân biệt về mặt khái niệm giữa tiền xử lý, biên dịch, lắp ráp và liên kết, nhưng trong quá trình thực thi thực tế, một số bước có thể được tích hợp vào một quy trình hoặc được liên kết thông qua các đường dẫn và tệp tạm thời.
Mã nguồn và đơn vị dịch
Mã nguồn là văn bản gốc của chương trình được lập trình viên viết theo ngữ pháp của một ngôn ngữ lập trình cụ thể. Nó thường bao gồm một hoặc nhiều tệp, mỗi tệp được xử lý bằng mã hóa ký tự, ngắt dòng, đường dẫn tệp và cấu trúc mô-đun hoặc gói.
Trình biên dịch không nhất thiết phải đọc toàn bộ dự án cùng một lúc. Trong C và C++, phương pháp truyền thống xử lý một tệp nguồn được xử lý trước làm đơn vị dịch được sử dụng rộng rãi. Các tệp đối tượng được tạo từ nhiều đơn vị dịch sau đó được kết hợp thành một tệp thực thi hoặc thư viện duy nhất bằng trình liên kết. Trong Java, tệp class thường chứa biểu diễn nhị phân của một lớp hoặc giao diện và JVM có thể tải riêng từng lớp được yêu cầu trong khi thực thi.
Hệ thống mô-đun của các ngôn ngữ hiện đại không nhất thiết phải xử lý các tệp nguồn và đơn vị chương trình logic một cách giống hệt nhau. Một mô-đun có thể bao gồm nhiều tệp và một tệp có thể chứa nhiều loại và khai báo. Trình biên dịch có thể xác định thứ tự xử lý bằng cách phân tích sự phụ thuộc giữa các mô-đun hoặc chỉ lưu các giao diện chung dưới dạng tệp trung gian riêng biệt để giảm chi phí phân tích lại các mô-đun khác.
Tiền xử lý
Tiền xử lý là bước chuyển đổi một phần mã nguồn trước khi phân tích cú pháp ở quy mô đầy đủ. Không phải tất cả các ngôn ngữ đều sử dụng bộ tiền xử lý riêng biệt, nhưng trong C và C++, các chỉ thị và macro tiền xử lý chiếm một vị trí quan trọng trong quá trình xử lý ngôn ngữ.
Bộ tiền xử lý dòng C thường thực hiện các tác vụ sau:
- Bao gồm nội dung của tệp được chỉ định bằng lệnh
#include. - Mở rộng macro được xác định là
#define. - Bao gồm hoặc loại trừ một số mã tùy thuộc vào chỉ thị biên dịch có điều kiện.
- Thay thế các macro được xác định trước do quá trình triển khai cung cấp.
- Theo dõi vị trí nguồn hợp lý và thông tin chẩn đoán.
Kết quả tiền xử lý có thể được xem dưới dạng một loạt mã thông báo trong đó nguồn và tiêu đề, ban đầu được chia thành nhiều tệp, được kết hợp và macro được mở rộng. Sau đó, trình phân tích cú pháp và phân tích ngữ nghĩa của trình biên dịch sẽ xử lý kết quả này. Trên thực tế, Clang có thể kết hợp chặt chẽ giữa tiền xử lý và phân tích cú pháp bên trong nên không nhất thiết phải tạo một tệp kết quả tiền xử lý hoàn chỉnh trên đĩa rồi thực hiện bước tiếp theo.
Các macro tiền xử lý chuyển đổi mã thông báo mà không hiểu cấu trúc cú pháp chung của ngôn ngữ. Do đó, phạm vi, loại và thứ tự đánh giá của mã được tạo từ macro có thể xuất hiện khác với mong đợi. Thay vì bộ tiền xử lý, các ngôn ngữ hiện đại thường cung cấp chức năng tương tự bằng cách hiểu các cấu trúc ngôn ngữ như mô-đun, biểu thức hằng số, tổng quát, thuộc tính, macro cú pháp và trình cắm trình biên dịch.
Phân tích từ vựng
Phân tích từ vựng là quá trình chuyển đổi các ký tự của mã nguồn thành một chuỗi mã thông báo mà ngôn ngữ có thể mang lại ý nghĩa. Thành phần chịu trách nhiệm phân tích từ vựng được gọi là lexer hoặc tokenizer.
Chúng ta có thể giả sử rằng chúng ta có mã nguồn sau:
int result = value + 10;
Lexer đại khái chia số tiền này thành các mã thông báo sau:
Từ khóa(int)
Định danh(result)
Toán tử(=)
Định danh(value)
Toán tử(+)
Literal số nguyên(10)
Dấu phân cách(;)
Từ vựng phân biệt giữa mã định danh và từ khóa, ký tự số và chuỗi ký tự, toán tử, dấu phân cách và nhận xét. Nó cũng có thể diễn giải mã hóa ký tự của mã nguồn và xử lý các chuỗi thoát, nối dòng và ý nghĩa của khoảng trắng. Trong Python, mức tăng hoặc giảm thụt lề được chuyển đổi thành mã thông báo INDENT và DEDENT để biểu thị cấu trúc khối, do đó khoảng trắng có ý nghĩa ngữ pháp. Tài liệu chính thức của Python cũng giải thích rằng lexer chuyển đổi đầu vào thành luồng mã thông báo trước khi chương trình được chuyển tới trình phân tích cú pháp.
Mã thông báo có thể lưu trữ thông tin như tên tệp, hàng và cột cũng như vị trí bắt đầu/kết thúc cũng như loại và văn bản gốc. Thông tin vị trí này được sử dụng để tạo thông báo lỗi và điều hướng mã, tái cấu trúc và thông tin gỡ lỗi.
Phân tích cú pháp
Phân tích cú pháp là quá trình kiểm tra xem việc sắp xếp các mã thông báo có phù hợp với ngữ pháp của ngôn ngữ hay không và xây dựng mối quan hệ cấu trúc giữa các mã thông báo. Thành phần thực hiện việc này được gọi là parser.
Ví dụ: các biểu thức sau có cấu trúc khác nhau tùy thuộc vào độ ưu tiên của toán tử.
a + b * c
Trình phân tích cú pháp không chỉ đơn giản xem đây là năm mã thông báo được liệt kê từ bên trái mà còn diễn giải nó dưới dạng cấu trúc phân cấp sau.
Phép cộng
├── a
└── Phép nhân
├── b
└── c
Quy tắc nhân được thực hiện trước phép cộng được phản ánh trong cấu trúc cú pháp. Các hướng kết hợp dấu ngoặc đơn và toán tử, lệnh gọi hàm, câu lệnh điều kiện, câu lệnh vòng lặp, câu lệnh khai báo và biểu thức kiểu cũng được trình phân tích cú pháp cấu trúc theo ngữ pháp.
Trình phân tích cú pháp có thể được triển khai theo nhiều cách, bao gồm phân tích cú pháp gốc đệ quy, LR, LALR, LL và PEG. Một số trình phân tích cú pháp được tạo tự động từ các định nghĩa ngữ pháp, trong khi các trình phân tích cú pháp khác được người triển khai viết trực tiếp. Trình phân tích cú pháp của Clang nhận mã thông báo từ từ vựng, kiểm tra cấu trúc ngữ pháp của nguồn và tạo AST cùng với phân tích ngữ nghĩa.
Nếu nguồn không đúng ngữ pháp, trình phân tích cú pháp sẽ báo lỗi cú pháp. Một trình phân tích cú pháp thực tế sẽ cố gắng gắn cờ nhiều lỗi trong một lần biên dịch, bằng cách đoán lỗi xảy ra ở đâu và khôi phục câu lệnh hoặc khai báo tiếp theo, thay vì dừng ngay ở lỗi đầu tiên. Tuy nhiên, do cấu trúc sau mã thông báo không chính xác có thể được ước tính không hoàn hảo nên lỗi đầu tiên có thể là nguyên nhân của một số chẩn đoán tiếp theo.
Cây cú pháp trừu tượng
Cây cú pháp trừu tượng là biểu diễn bên trong thể hiện cấu trúc ngữ pháp của chương trình dưới dạng cây. Nó thường được viết tắt là AST.
AST khác với cây cú pháp cụ thể ở chỗ nó bảo toàn tất cả các ký tự từ nguồn ban đầu. Một số thành phần xác định cấu trúc, chẳng hạn như dấu ngoặc đơn hoặc dấu chấm phẩy, nhưng không cần thiết trực tiếp cho ý nghĩa thực thi sẽ bị bỏ qua và biểu thức tập trung vào mối quan hệ có ý nghĩa giữa các khai báo, biểu thức, loại và câu lệnh.
Một ví dụ là đoạn mã sau:
int square(int value) {
return value * value;
}
AST có thể có cấu trúc gần đúng như sau.
Khai báo hàm: square
├── Kiểu trả về: int
├── Tham số
│ └── value: int
└── Thân hàm
└── return
└── Phép nhân
├── Tham chiếu value
└── Tham chiếu value
AST được sử dụng làm cơ sở để kiểm tra kiểu, phân giải tên, phân tích tĩnh, tạo mã, tái cấu trúc và khám phá mã. Trình biên dịch có thể thêm thông tin như kiểu được diễn giải và tham chiếu khai báo, vị trí nguồn, giá trị không đổi và quyền truy cập vào nút AST. Clang tạo AST, một biểu diễn trừu tượng của nguồn, là kết quả của phân tích cú pháp và ngữ nghĩa, đồng thời quản lý các nút AST tồn tại lâu dài, bao gồm các loại và khai báo, thông qua ASTContext.
Một số công cụ phát triển cần duy trì nhận xét, dấu cách và vị trí mã thông báo chính xác trong văn bản gốc, để chúng bảo toàn thông tin về cây cú pháp hoặc mã thông báo cụ thể tách biệt với AST. Điều này là do các trình định dạng tự động và trình chuyển đổi mã nguồn phải xử lý không chỉ ý nghĩa thực thi mà còn cả ký hiệu gốc và vị trí nhận xét.
Phân tích ngữ nghĩa
Ngay cả khi mã đúng về mặt cú pháp, điều đó không có nghĩa là ý nghĩa là đúng. Phân tích ngữ nghĩa kiểm tra xem chương trình có đáp ứng các quy tắc ngữ nghĩa tĩnh của ngôn ngữ dựa trên AST hay không và tính toán thông tin cần thiết cho các bước tiếp theo.
Phân tích ngữ nghĩa thường bao gồm các nhiệm vụ sau:
- Độ phân giải tên liên kết tên và khai báo
- Kiểm tra phạm vi của các biến, chức năng và loại
- Xác định các kiểu biểu thức và khai báo
- Kiểm tra chuyển đổi loại ngầm định
- Phân tích quá tải chức năng
- Ràng buộc kiểm tra generic hoặc mẫu
- Kiểm tra kiểm soát truy cập
- Kiểm tra mối quan hệ kế thừa và giao diện
- Xác thực các câu lệnh trả về và luồng điều khiển
- Đánh giá các biểu thức hằng
- Kiểm tra thứ tự khởi tạo và quy tắc vòng đời
- Chẩn đoán các cấu trúc bị cấm trong ngôn ngữ
Ví dụ: đoạn mã sau đây về mặt ngữ pháp là một biểu thức gọi hàm thông thường, nhưng nếu các tham số của hàm chỉ cho phép số nguyên thì có thể xảy ra lỗi trong phân tích ngữ nghĩa.
calculate("text")
Bộ phân tích ngữ nghĩa xác định tính hợp lệ của cuộc gọi bằng cách so sánh loại "text" với loại tham số calculate. Trong các ngôn ngữ hỗ trợ nạp chồng, chẳng hạn như C++, bạn phải chọn hàm phù hợp nhất với loại đối số và quy tắc chuyển đổi trong số một số hàm có cùng tên.
Clang gọi phần chịu trách nhiệm phân tích ngữ nghĩa là Sema, phần này xác định xem nguồn có đúng về mặt ngữ nghĩa hay không và tạo ra hầu hết các chẩn đoán biên dịch. Bởi vì trình phân tích cú pháp và Sema phối hợp chặt chẽ với nhau nên tên và kiểu có thể được diễn giải cùng nhau trong quá trình tạo cấu trúc cú pháp.
AST đã hoàn thành phân tích ngữ nghĩa chứa nhiều thông tin hơn một cây ngữ pháp đơn giản. Giá trị nhận dạng được liên kết với các khai báo thực tế, các biểu thức được cung cấp các loại và các mục tiêu cuộc gọi và chuyển đổi tiềm ẩn được chỉ định. Dựa trên thông tin này, trình biên dịch có thể dịch hành vi của chương trình sang biểu diễn cấp thấp hơn.
Phân tích và chẩn đoán tĩnh
Trình biên dịch có thể phân tích không chỉ các lỗi vi phạm quy tắc ngôn ngữ mà còn cả mã có thể thực thi được nhưng có khả năng bị lỗi. Phân tích tĩnh là phương pháp tìm kiếm các vấn đề có thể xảy ra bằng cách kiểm tra mã nguồn hoặc các biểu diễn trung gian mà không trực tiếp thực thi chương trình.
Phân tích tĩnh có thể phát hiện các vấn đề như:
- Sử dụng các giá trị chưa được khởi tạo
- mã không thể truy cập
- Điều kiện luôn đúng hoặc luôn sai
- Có sẵn bộ nhớ giải phóng hoặc tham chiếu null
- Rò rỉ tài nguyên
- Khả năng đi ra khỏi phạm vi mảng
- Thứ tự khóa không chính xác
- Khả năng cạnh tranh dữ liệu
- Bỏ qua giá trị trả về
- Không khớp giữa chuỗi định dạng và đối số
- Các hoạt động có thể dẫn đến hành vi không xác định
Máy phân tích tĩnh có thể sử dụng các phương pháp như biểu đồ luồng điều khiển và phân tích luồng dữ liệu, thực thi ký hiệu và diễn giải trừu tượng. Bởi vì nhìn chung không thể dự đoán hoàn toàn tất cả các đầu vào thực tế và đường dẫn thực thi nên kết quả phân tích có thể bao gồm các cảnh báo không phải là lỗi thực tế hoặc đường dẫn mà máy phân tích chưa chứng minh được.
Chẩn đoán trình biên dịch có thể được chia thành các lỗi, cảnh báo và thông tin tham khảo. Một lỗi cho biết sự cố ngăn cản việc tạo chương trình chính xác và phải dừng quá trình biên dịch, trong khi cảnh báo cho biết cấu trúc đáng ngờ, ngay cả khi được cho phép bằng ngôn ngữ. Một hệ thống chẩn đoán tốt không chỉ hiển thị vị trí của vấn đề mà còn hiển thị các khai báo liên quan và các cách khắc phục có thể.
Biểu diễn trung gian
Biểu diễn trung gian là định dạng nội bộ thể hiện chương trình giữa AST của ngôn ngữ nguồn và hướng dẫn thực tế của máy đích. Nó thường được viết tắt là IR.
Lý do trình biên dịch sử dụng các biểu diễn trung gian là để giảm độ phức tạp phát sinh khi liên kết trực tiếp ngôn ngữ nguồn và kiến trúc đích. Các frontend của nhiều ngôn ngữ có thể tạo ra một IR chung, một trình tối ưu hóa chung sẽ xử lý IR đó và sau đó nhiều backend có thể tạo ra CPU hoặc mã máy ảo của riêng chúng.
C ─────────┐
C++ ───────┤
Rust ──────┼─ Biểu diễn trung gian chung ─┬─ x86-64
Fortran ───┤ ├─ AArch64
Swift ─────┘ ├─ RISC-V
└─ WebAssembly
Không cần chỉ có một biểu diễn trung gian. Các trình biên dịch hiện đại có thể sử dụng nhiều IR theo từng giai đoạn với các mức độ trừu tượng khác nhau.
- IR cấp cao duy trì một lượng tương đối lớn cấu trúc của ngôn ngữ nguồn, bao gồm các hàm, đối tượng, mảng, ngoại lệ và coroutine.
- IR cấp trung thể hiện luồng điều khiển, luồng dữ liệu và truy cập bộ nhớ ở dạng dễ phân tích.
- IR cấp thấp sử dụng các thanh ghi và cấu trúc gần với hoạt động của máy nhưng có thể chưa phụ thuộc vào CPU cụ thể.
- IR máy chứa thông tin dành riêng cho mục tiêu để lựa chọn lệnh thực tế và phân bổ thanh ghi.
LLVM IR là một biểu diễn trung gian cấp thấp được định kiểu và được thiết kế xung quanh một biểu mẫu gán đơn tĩnh. Nó được sử dụng như một biểu diễn mã chung trong suốt chiến lược biên dịch của LLVM và hoạt động như một ranh giới giữa frontend, bộ tối ưu hóa và backend.
Dạng gán một lần tĩnh
Dạng gán một lần tĩnh là một phương thức biểu diễn trung gian chỉ gán giá trị cho một tên biến một lần và được viết tắt là SSA.
Chúng ta có thể giả sử rằng chúng ta có mã mệnh lệnh sau:
x = 10
x = x + 1
x = x * 2
SSA đặt cho mỗi nhiệm vụ một tên mới.
x1 = 10
x2 = x1 + 1
x3 = x2 * 2
Cấu trúc này làm rõ mỗi giá trị được xác định ở đâu và được sử dụng ở đâu. Trình biên dịch có thể dễ dàng theo dõi luồng giá trị và thực hiện các tối ưu hóa như truyền liên tục, loại bỏ mã không sử dụng và loại bỏ các biểu thức con phổ biến.
Tại thời điểm hợp nhất các luồng điều khiển, một cấu trúc tương ứng với hàm ϕ có thể được sử dụng để chọn một trong các giá trị từ nhiều đường dẫn.
if condition:
x1 = 10
else:
x2 = 20
x3 = φ(x1, x2)
Các quy tắc hình dạng và chuyển đổi của LLVM IR thực tế được xác định chặt chẽ hơn và trình biên dịch duy trì cấu trúc SSA trong quá trình tối ưu hóa hoặc giảm nó thành biểu diễn máy dành riêng cho mục tiêu.
Hạ mức
Hạ mức là quá trình chuyển đổi cấu trúc ngôn ngữ cấp cao thành các hoạt động cấp thấp hơn, đơn giản hơn. Nó là một biểu thức bắt nguồn từ từ tiếng Anh hạ thấp.
Ví dụ: các tính năng sau trong ngôn ngữ cấp cao có thể không có một lệnh nào tương ứng trực tiếp với máy mục tiêu:
- Đối tượng và phương thức ảo
- đóng cửa
- Khớp mẫu
- Xử lý ngoại lệ
- Coroutine và
async·await - Nội suy chuỗi
- Vòng lặp và vòng lặp dựa trên phạm vi
- Tạo đối tượng bằng cách sử dụng bộ sưu tập rác
- Chức năng chung
- Hoạt động kiểu động
Trình biên dịch chuyển các cấu trúc này thành các hoạt động cơ bản hơn, chẳng hạn như lệnh gọi hàm và nhánh, cấp phát bộ nhớ, máy trạng thái, con trỏ hàm, bảng nhảy và kiểm tra thời gian chạy.
Một coroutine có thể được biến thành một máy trạng thái lưu trữ trạng thái trong các biến cục bộ và tại các điểm dừng. Một bao đóng có thể được chuyển đổi thành sự kết hợp của một đối tượng môi trường chứa các biến được ghi lại và một hàm có thể gọi được. Các cuộc gọi phương thức ảo có thể được hạ cấp xuống cấu trúc cuộc gọi gián tiếp bằng cách tìm địa chỉ hàm trong thông tin loại của đối tượng.
Việc hạ thấp không nhất thiết phải xảy ra cùng một lúc. Bạn có thể tiến hành nhiều lần từ IR gần với ngôn ngữ nguồn, tới IR cấp cao thông thường, đến IR cấp thấp và quay lại IR máy cụ thể theo mục tiêu. IR từng bước cho phép mỗi lần tối ưu hóa và chuyển đổi sử dụng mức độ thông tin phù hợp với nó.
Tối ưu hóa
Tối ưu hóa trình biên dịch là một phép biến đổi nhằm cải thiện các đặc điểm như thời gian thực thi, kích thước mã, quyền truy cập bộ nhớ và mức tiêu thụ điện năng trong khi vẫn duy trì ý nghĩa có thể quan sát được của chương trình.
Trình tối ưu hóa phân tích những gì chương trình đang tính toán và xác định liệu có thể đạt được kết quả tương tự với ít thao tác hơn hoặc hướng dẫn hiệu quả hơn hay không. Tuy nhiên, chuyển đổi chỉ nên được thực hiện trong phạm vi hành vi có thể quan sát, mô hình bộ nhớ, quy tắc dấu phẩy động, ngoại lệ và hành vi không xác định được ngôn ngữ cho phép.
Tối ưu hóa đại diện như sau.
- Gấp liên tục tính toán trước các thao tác đã biết tại thời điểm biên dịch.
- Truyền bá liên tục chuyển một giá trị được xác định là hằng số đến vị trí sử dụng tiếp theo.
- Xóa mã không sử dụng xóa các thao tác không ảnh hưởng đến kết quả và mã không thể truy cập được.
- Xóa các biểu thức con phổ biến sử dụng lại các kết quả trước đó khi lặp lại phép tính tương tự.
- Nội tuyến hàm thay thế lệnh gọi hàm bằng mã trong nội dung hàm.
- Chuyển động mã bất biến của vòng lặp di chuyển các thao tác tạo ra cùng một kết quả cho mỗi lần lặp lại ra khỏi vòng lặp.
- Mở rộng vòng lặp giảm số lượng nhánh bằng cách sao chép phần thân vòng lặp nhiều lần.
- Vectorization kết hợp cùng một thao tác trên nhiều phần tử thành các lệnh SIMD.
- Phân tích thoát tối ưu hóa vị trí phân bổ bằng cách phân tích xem một đối tượng có được truyền ra khỏi hàm hoặc luồng hay không.
- Thay thế vô hướng phân tách một đối tượng nhỏ hoặc tập hợp các giá trị thành các giá trị riêng lẻ.
- Tối ưu hóa lệnh gọi đuôi sử dụng lại khung lệnh gọi hiện tại trong lệnh gọi hàm cụ thể.
- Đơn giản hóa nhánh loại bỏ hoặc kết hợp các điều kiện và bước nhảy không cần thiết.
Tối ưu hóa không kết thúc trong một bước. Bởi vì một chuyển đổi có thể tạo cơ hội cho các tối ưu hóa khác, hãy lặp lại nhiều bước tối ưu hóa hoặc chạy chúng theo thứ tự đã đặt. Ví dụ: nội tuyến hàm có thể loại bỏ ranh giới cuộc gọi, cho phép truyền liên tục và loại bỏ mã không sử dụng.
Phân tích bí danh
Trong các ngôn ngữ sử dụng con trỏ hoặc tham chiếu, điều quan trọng là xác định xem hai biểu thức khác nhau có trỏ đến cùng một vị trí bộ nhớ hay không. Điều này được gọi là phân tích bí danh.
Nếu trình biên dịch không thể loại trừ khả năng hai con trỏ trỏ đến cùng một giá trị, thì nó phải thừa nhận rằng việc ghi qua một con trỏ có thể thay đổi kết quả của việc đọc từ con trỏ kia. Điều này hạn chế việc di chuyển và vector hóa các hoạt động và thanh ghi lưu trữ các giá trị.
Thông tin như hệ thống kiểu ngôn ngữ và quy tắc quyền sở hữu, các bộ hạn định như restrict và các tham chiếu không thể thay đổi có thể làm giảm khả năng xảy ra bí danh và cung cấp thêm thông tin để tối ưu hóa. Ngược lại, các lệnh gọi số học và hàm bên ngoài con trỏ tùy ý có thể làm cho hiệu ứng bộ nhớ khó suy ra.
Tối ưu hóa dựa trên hồ sơ
Tối ưu hóa dựa trên hồ sơ là phương pháp sử dụng thông tin được thu thập từ quá trình thực thi chương trình thực tế để biên dịch và được viết tắt là PGO.
Quá trình chung như sau.
Build chương trình đã được đo đạc
↓
Thực thi với đầu vào đại diện
↓
Thu thập tần suất gọi và xác suất rẽ nhánh
↓
Biên dịch lại bằng profile
Bằng cách phân biệt giữa các hàm và nhánh được thực hiện thường xuyên cũng như các đường dẫn lỗi hiếm khi được thực hiện, trình biên dịch có thể điều chỉnh nội tuyến, vị trí mã, thứ tự nhánh và cách sử dụng thanh ghi. Nếu cấu hình không đại diện cho kiểu sử dụng thực tế thì hiệu quả dự kiến có thể bị giảm hoặc hiệu suất có thể kém đối với một số tác vụ nhất định.
Tối ưu hóa thời gian liên kết
Tối ưu hóa thời gian liên kết là phương pháp biên dịch các đơn vị dịch riêng lẻ, sau đó phân tích các biểu diễn trung gian của một số mô-đun với nhau ở giai đoạn liên kết và được viết tắt là LTO.
Trong quá trình biên dịch tách biệt điển hình, trình biên dịch của một đơn vị dịch thuật không thể biết nội dung của hàm được xác định trong một tệp khác. LTO cho phép trình liên kết hoặc plugin liên kết kết hợp IR từ nhiều mô-đun hoặc trao đổi thông tin tóm tắt để thực hiện nội tuyến giữa các mô-đun, loại bỏ mã chung không được sử dụng và phân tích toàn bộ chương trình.
Việc xử lý toàn bộ IR cùng một lúc có thể sử dụng nhiều bộ nhớ và thời gian, do đó, cấu trúc LTO phân tán hoặc mỏng sử dụng tóm tắt mô-đun và phần phụ trợ song song có thể được sử dụng trong các chương trình lớn.
Sinh mã đích
Biểu diễn trung gian được tối ưu hóa được chuyển đổi thành hướng dẫn cho bộ xử lý đích hoặc máy ảo. Điều này được gọi là tạo mã.
Trình tạo mã gốc thực hiện các tác vụ sau:
- Chọn thao tác của biểu diễn trung gian làm lệnh đích.
- Xác định phương pháp đánh địa chỉ được bộ xử lý đích hỗ trợ.
- Đặt các thanh ghi ảo vào các thanh ghi vật lý thực tế.
- Các giá trị thiếu thanh ghi sẽ được gửi vào ngăn xếp hoặc bộ nhớ.
- Điều chỉnh thứ tự thực hiện các lệnh cho phù hợp với bộ xử lý đích.
- Sắp xếp các đối số và giá trị trả về theo quy ước gọi hàm.
- Xây dựng khung ngăn xếp.
- Xác định vị trí của các nhánh, hằng số và dữ liệu toàn cục.
- Tạo thông tin bổ sung cần thiết để xử lý và gỡ lỗi ngoại lệ.
Lựa chọn lệnh
Lựa chọn lệnh là quá trình chuyển đổi các hoạt động IR thành các lệnh thực tế của bộ xử lý đích.
Ngay cả đối với cùng một thao tác, các hướng dẫn có thể được sử dụng cũng khác nhau tùy thuộc vào bộ xử lý. Một số bộ xử lý có thể đọc và thao tác trên một giá trị từ bộ nhớ bằng một lệnh duy nhất, trong khi các bộ xử lý khác trước tiên có thể cần tải giá trị đó vào một thanh ghi rồi sử dụng một lệnh thao tác riêng.
Trình tạo mã chọn tổ hợp lệnh thích hợp bằng cách xem xét chi phí và chức năng sẵn có của lệnh đích. Nếu CPU hỗ trợ SIMD, các thao tác bit đặc biệt hoặc các lệnh mã hóa thì các lệnh này có thể được sử dụng thay vì kết hợp các thao tác thông thường.
Phân bổ thanh ghi
Các biểu diễn trung gian có thể sử dụng nhiều thanh ghi ảo hơn bộ xử lý thực tế. Phân bổ thanh ghi là quá trình đặt các giá trị ảo vào một số lượng thanh ghi vật lý giới hạn.
Nếu số lượng giá trị tồn tại cùng lúc vượt quá số thanh ghi có sẵn, một số giá trị sẽ được lưu tạm thời trong bộ nhớ ngăn xếp. Điều này được gọi là tràn thanh ghi. Truy cập thanh ghi thường nhanh hơn truy cập bộ nhớ, vì vậy điều quan trọng là phải giữ các giá trị được sử dụng thường xuyên trong các thanh ghi và giảm việc lưu trữ và tải không cần thiết.
Quy ước gọi hàm xác định những thanh ghi nào sẽ được người gọi bảo quản và những thanh ghi nào sẽ được người gọi bảo quản. Trình tạo mã đặt các thanh ghi theo các quy tắc này.
Lập lịch lệnh
Các bộ xử lý hiện đại có thể thực thi nhiều lệnh chồng chéo lẫn nhau, nhưng sự phụ thuộc dữ liệu giữa các lệnh và giới hạn của đơn vị thực thi sẽ ảnh hưởng đến hiệu suất. Lập lịch lệnh là quá trình giảm tình trạng ngừng hoạt động trong quy trình và xung đột tài nguyên bằng cách điều chỉnh thứ tự các hướng dẫn trong khi vẫn duy trì ý nghĩa của chúng.
Ngay cả khi CPU mục tiêu tự động điều chỉnh thứ tự lệnh trong khi thực thi, phạm vi lệnh mà bộ xử lý có thể xử lý song song có thể được mở rộng nếu trình biên dịch đặt mã theo thứ tự thích hợp.
Hợp dịch và tệp đối tượng
Nếu trình tạo mã xuất ra ngôn ngữ hợp ngữ ở dạng văn bản, thì trình biên dịch mã sẽ chuyển nó sang ngôn ngữ máy. Trình biên dịch cũng có thể sử dụng trình biên dịch nội bộ của nó để tạo các tệp đối tượng trực tiếp mà không cần các tệp hợp ngữ trung gian.
Tệp đối tượng không phải là tệp thực thi hoàn chỉnh mà là tệp nhị phân trung gian chứa ngôn ngữ máy, dữ liệu, ký hiệu, thông tin di chuyển và nhiều siêu dữ liệu khác nhau.
Một tệp đối tượng điển hình chứa các nội dung sau:
- Mã máy thực thi được
- Hằng số chỉ đọc
- Dữ liệu toàn cầu được khởi tạo
- Thông tin kích thước của dữ liệu toàn cầu chưa được khởi tạo
- Các biểu tượng được tiết lộ hoặc tham chiếu ra bên ngoài
- Thông tin di dời cho các địa điểm chưa xác định được địa chỉ thực tế
- Xử lý ngoại lệ và thông tin giải phóng ngăn xếp
- Thông tin gỡ lỗi
- Thông tin về nền tảng và kiến trúc
Tùy thuộc vào nền tảng, các định dạng đối tượng và tệp thực thi như ELF, PE/COFF và Mach-O được sử dụng. Mỗi định dạng chia mã và dữ liệu thành các phần hoặc phân đoạn và cung cấp thông tin mà trình liên kết và trình tải cần.
Ký hiệu và tái định vị
Symbol đại diện cho tên và thuộc tính của các đối tượng cần được xác định ở giai đoạn liên kết, chẳng hạn như hàm, biến toàn cục và dữ liệu tĩnh. Tệp đối tượng ghi lại các ký hiệu mà nó xác định và các ký hiệu phải được nhập từ bên ngoài.
Bạn có thể gọi một hàm bên ngoài từ một tệp như sau:
extern int calculate(int value);
int run(void) {
return calculate(10);
}
Địa chỉ bộ nhớ cuối cùng của calculate có thể không được biết tại thời điểm biên dịch tệp này. Trình biên dịch và trình biên dịch để lại các giá trị tạm thời và các mục tái định vị tại vị trí cuộc gọi. Trình liên kết tìm kiếm các tệp đối tượng hoặc thư viện khác xác định calculate và phản ánh địa chỉ thực tế hoặc phương pháp tính địa chỉ.
Tái định vị là quá trình sửa đổi các vị trí phụ thuộc vào địa chỉ trong mã và dữ liệu để khớp với vị trí cuối cùng. Có nhiều phương pháp tái định vị khác nhau, bao gồm địa chỉ tuyệt đối, địa chỉ tương đối dựa trên vị trí lệnh hiện tại, bảng offset chung và bảng liên kết thủ tục.
Trình liên kết
Linker kết hợp nhiều tệp đối tượng và thư viện để tạo một tệp thực thi hoặc thư viện dùng chung.
Trình liên kết thường thực hiện các nhiệm vụ sau:
- Thu thập và đặt các phần của tệp đối tượng đầu vào.
- Liên kết các định nghĩa ký hiệu và tài liệu tham khảo có cùng tên.
- Tìm các ký hiệu không xác định trong thư viện.
- Chẩn đoán các định nghĩa trùng lặp hoặc xung đột.
- Xác định địa chỉ cuối cùng của từng mã và dữ liệu.
- Áp dụng di dời.
- Những phần không sử dụng có thể được gỡ bỏ.
- Cấu hình thứ tự các chức năng khởi tạo và kết thúc.
- Tạo thông tin để trình liên kết động sử dụng.
- Ghi lại điểm vào và tiêu đề chương trình của tệp thực thi.
Liên kết tĩnh
Liên kết tĩnh là một phương pháp bao gồm mã thư viện cần thiết cho một chương trình trong một tệp thực thi. Việc phân phối có thể được đơn giản hóa vì tệp thư viện tương ứng không cần phải tải riêng khi chạy, nhưng kích thước của tệp thực thi tăng lên và nhiều chương trình có thể chứa cùng một mã thư viện.
Thư viện tĩnh nói chung là một tập hợp nhiều tệp đối tượng vào một tệp lưu trữ. Trình liên kết chọn các đối tượng cung cấp các ký hiệu thực sự được chương trình tham chiếu và đưa chúng vào đầu ra.
Liên kết động
Liên kết động là phương pháp liên kết với thư viện bên ngoài khi tải chương trình hoặc sử dụng ký hiệu lần đầu tiên mà không cần sao chép toàn bộ mã và dữ liệu của thư viện dùng chung vào tệp thực thi.
Trong hệ thống dựa trên ELF, các tệp thực thi tham gia liên kết động có thể chỉ định trình liên kết động thông qua thông tin trình thông dịch chương trình. Hệ điều hành tải một trình liên kết động vào không gian địa chỉ tiến trình cùng với tệp thực thi và trình liên kết động sẽ tìm các đối tượng được chia sẻ cần thiết, diễn giải các ký hiệu và xử lý việc di chuyển.
Liên kết động cho phép nhiều quy trình chia sẻ mã thư viện chỉ đọc và các thư viện tương thích có thể được thay thế mà không cần tạo lại tệp thực thi. Mặt khác, phiên bản chính xác của thư viện phải tồn tại trong môi trường thực thi và phải quản lý các phiên bản ký hiệu, đường dẫn tìm kiếm và khả năng tương thích ABI.
ABI và quy ước gọi
Giao diện nhị phân ứng dụng là các quy tắc dành cho mã được biên dịch riêng để tương tác ở cấp độ nhị phân, viết tắt là ABI.
Một ABI thường bao gồm những điều sau đây:
- Cách đặt đối số hàm vào thanh ghi và ngăn xếp
- Cách chuyển giá trị trả về
- Các thanh ghi phải được bảo quản trước và sau khi gọi
- Căn chỉnh ngăn xếp và cấu trúc khung ngăn xếp
- Kích thước và căn chỉnh các loại cơ bản
- Vị trí bộ nhớ của cấu trúc và lớp
- Cách chuyển đổi tên ký hiệu thành tên nhị phân
- Xử lý ngoại lệ và quy tắc gỡ bỏ ngăn xếp
- Cấu trúc bảng hàm ảo
- Tệp đối tượng và định dạng tệp thực thi
- Cuộc gọi hệ thống và giao diện hệ điều hành
Ngay cả khi khai báo hàm trong mã nguồn giống nhau, nếu sử dụng các ABI khác nhau, các đối số và giá trị trả về có thể được diễn giải khác nhau, khiến không thể gọi chúng một cách bình thường.
C++ sử dụng Biến đổi tên để thể hiện thông tin như nạp chồng hàm, lớp, không gian tên và mẫu trong ký hiệu. Itanium C++ ABI xác định các giao diện gọi hàm, xử lý ngoại lệ, tên chung và quy ước mã đối tượng, cho phép mã C++ được biên dịch độc lập hoạt động cùng nhau.
ABI không giống như đặc tả ngôn ngữ. Nếu đặc tả ngôn ngữ xác định ý nghĩa của chương trình thì ABI sẽ xác định cấu trúc nhị phân trong đó ý nghĩa đó sẽ được thể hiện trên một nền tảng và cách triển khai cụ thể. Ngay cả đối với cùng một ngôn ngữ, ABI có thể khác nhau tùy thuộc vào hệ điều hành, kiến trúc CPU và dòng trình biên dịch.
Tệp thực thi và nạp
Loader là một thành phần hệ điều hành đọc các tệp thực thi, xây dựng không gian địa chỉ của một quy trình mới và bắt đầu thực thi.
Quá trình tải chung như sau.
- Kiểm tra tiêu đề của tệp thực thi.
- Kiểm tra kiến trúc đích và định dạng thực thi.
- Mã ánh xạ, dữ liệu chỉ đọc và dữ liệu có thể ghi vào bộ nhớ.
- Chuẩn bị vùng dữ liệu chưa được khởi tạo.
- Tạo ngăn xếp và trạng thái quy trình ban đầu.
- Nếu cần có liên kết động, hãy tải trình liên kết động.
- Lập bản đồ các thư viện dùng chung và áp dụng di dời.
- Khởi tạo thời gian chạy ngôn ngữ và các đối tượng chung.
- Chuyển điều khiển đến điểm vào của chương trình.
Điểm vào mà hệ điều hành gọi đầu tiên không nhất thiết phải là hàm main do người dùng viết. Trong các chương trình C và C++, mã khởi động nhận các đối số và thông tin môi trường từ hệ điều hành, khởi tạo thời gian chạy và gọi main. Khi chương trình kết thúc, giá trị trả về sẽ được chuyển đến hệ điều hành và việc xử lý chấm dứt đã đăng ký cũng như dọn dẹp các đối tượng tĩnh có thể được thực hiện.
Máy ảo Java phân biệt giữa tải, tìm biểu diễn nhị phân của một lớp, liên kết, liên kết lớp đó với trạng thái đang chạy và khởi tạo, chuẩn bị trạng thái tĩnh. Trong JVM, các lớp mới có thể được tải thông qua trình nạp lớp do người dùng định nghĩa ngay cả khi đang chạy.
Hệ thống thời gian chạy
Hệ thống thời gian chạy là lớp phần mềm hỗ trợ chức năng của ngôn ngữ lập trình trong khi chương trình được biên dịch đang chạy. Kích thước và vai trò của thời gian chạy rất khác nhau tùy theo ngôn ngữ.
Một môi trường thực thi điển hình trong C có thể sử dụng thời gian chạy tương đối hạn chế, chẳng hạn như mã khởi động, thư viện chuẩn, giải phóng ngăn xếp và các hàm phụ trợ số học. Java và .NET sử dụng thời gian chạy được quản lý lớn bao gồm máy ảo, trình biên dịch JIT, trình thu gom rác, hệ thống kiểu, trình nạp lớp, xử lý ngoại lệ và quản lý luồng.
Thời gian chạy có thể cung cấp các chức năng sau:
- Khởi tạo chương trình và thread
- Quản lý ngăn xếp lệnh gọi hàm và khung thực thi
- Cấp phát bộ nhớ heap
- Thu gom rác
- Quản lý siêu dữ liệu đối tượng và loại
- Công văn phương thức động
- Ném một ngoại lệ và tua lại ngăn xếp
- phản ánh
- Mô-đun động và tải lớp
- Lập lịch thread và tác vụ
- Coroutines và đầu vào/đầu ra không đồng bộ
- Kiểm tra phạm vi của chuỗi và mảng
- Hoạt động kiểu động
- Khả năng tương tác với mã gốc
- Thu thập thông tin lập hồ sơ và tối ưu hóa JIT
Môi trường thực thi được quản lý của .NET cung cấp các dịch vụ như biên dịch JIT và thu thập rác, khả năng tương tác với mã không được quản lý, gỡ lỗi đa ngôn ngữ cũng như quản lý phiên bản và triển khai.
Ngăn xếp thực thi và khung lệnh gọi
Khi một hàm được gọi, khung cuộc gọi sẽ được tạo, thường lưu trữ trạng thái cần thiết cho cuộc gọi đó. Khung cuộc gọi còn được gọi là khung ngăn xếp.
Khung cuộc gọi có thể bao gồm các thông tin sau:
- Địa chỉ trả lại
- Đối số của hàm
- Biến cục bộ
- Kết quả tính toán tạm thời
- Thông tin trỏ về khung trước đó
- Đăng ký các giá trị phải được bảo tồn
- Xử lý ngoại lệ và thông tin giải phóng ngăn xếp
Khi một hàm gọi một hàm khác, một khung mới sẽ được thêm vào và khi hàm quay trở lại, khung đó sẽ bị xóa. Các cuộc gọi đệ quy tạo ra các khung khác nhau cho cùng một chức năng liên tiếp.
JVM có một ngăn xếp JVM chuyên dụng cho từng luồng và tạo một khung chứa một mảng biến cục bộ và ngăn xếp toán hạng khi một phương thức được gọi. Vùng heap của JVM được chia sẻ bởi tất cả các luồng JVM, đồng thời các thể hiện và mảng của lớp được phân bổ.
Không phải mọi giá trị đều nhất thiết được lưu trong bộ nhớ stack vật lý. Trong mã bản địa đã tối ưu hóa, biến cục bộ có thể chỉ tồn tại trong thanh ghi hoặc bị loại bỏ hoàn toàn. Ngay cả trong bản triển khai máy ảo, call frame có thể được lưu trong một stack, vùng nhớ bản địa liên tục hoặc một cấu trúc nội bộ riêng.
Phân bổ vùng heap và bộ nhớ
Heap là vùng bộ nhớ lưu trữ các đối tượng và dữ liệu có kích thước và tuổi thọ được xác định động trong quá trình thực thi. Các đối tượng phải tồn tại độc lập với cấu trúc gọi của ngăn xếp thường được phân bổ trên heap.
Bộ cấp phát heap tìm và trả về một khối bộ nhớ có kích thước được yêu cầu, đồng thời sử dụng lại vùng giải phóng cho các yêu cầu tiếp theo. Trình cấp phát hiệu quả có thể sử dụng danh sách miễn phí dựa trên kích thước, phân bổ dựa trên khu vực, bộ nhớ đệm theo luồng và quản lý cấp trang.
Việc sử dụng heap có thể gây ra các vấn đề sau:
- Rò rỉ do không giải phóng bộ nhớ được phân bổ
- Lỗi truy cập bộ nhớ đã được giải phóng
- Lỗi giải phóng cùng một bộ nhớ hai lần
- Phân mảnh, làm giảm diện tích tiếp giáp có thể sử dụng.
- Chi phí đồng bộ hóa phát sinh khi nhiều luồng truy cập vào bộ cấp phát
- Vấn đề về hiệu suất bộ nhớ đệm thấp do các đối tượng được đặt cách xa nhau
Ngôn ngữ và thời gian chạy quản lý vòng đời của các đối tượng heap theo nhiều cách, bao gồm giải phóng thủ công, đếm tham chiếu, thu thập rác được theo dõi, quản lý dựa trên khu vực và phân tích quyền sở hữu.
Thu gom rác
Thu gom rác là một phương thức trong đó bộ thực thi tự động lấy lại bộ nhớ của các đối tượng mà chương trình không thể truy cập được nữa.
Trình thu thập rác theo dõi thường tìm thấy một tập hợp các gốc bắt đầu từ các biến toàn cục, ngăn xếp luồng, thanh ghi CPU, v.v. và đánh dấu các đối tượng có thể truy cập bằng cách theo dõi các tham chiếu từ gốc. Các đối tượng không thể truy cập không thể được sử dụng lại trong chương trình và do đó có thể bị thu hồi. Trình thu gom rác của .NET cũng kiểm tra biểu đồ đối tượng có thể truy cập dựa trên thông tin gốc do trình biên dịch và thời gian chạy JIT quản lý.
Có một số triển khai thu gom rác.
- Phương pháp đánh dấu và quét đánh dấu các đối tượng có thể tiếp cận và sau đó truy xuất các đối tượng không được đánh dấu.
- Phương pháp sao chép di chuyển các đối tượng sống sang khu vực khác và làm trống khu vực hiện có cùng một lúc.
- Phương pháp thế hệ quản lý các đối tượng mới được tạo và các đối tượng tồn tại lâu dài ở các khu vực khác nhau.
- Cách tiếp cận gia tăng chia nhiệm vụ thu thập thành nhiều bước nhỏ.
- Phương thức đồng thời thực thi đồng thời luồng ứng dụng và một phần của hoạt động thu thập.
- Phương pháp nén thu thập các vật thể sống để giảm sự phân mảnh.
Thu thập rác tự động giải phóng bộ nhớ nhưng không tự động và kịp thời dọn sạch các tài nguyên bên ngoài phải được trả lại ngay lập tức, chẳng hạn như tệp, ổ cắm, khóa và đối tượng GPU. Các tài nguyên này có thể sử dụng cú pháp dọn dẹp rõ ràng riêng biệt, quản lý dựa trên phạm vi, phương pháp chấm dứt, v.v.
Xử lý ngoại lệ
Xử lý ngoại lệ là cấu trúc điều khiển chuyển lỗi hoặc các tình huống đặc biệt khó xử lý bằng luồng trả về thông thường tới lệnh gọi phía trên.
Khi một ngoại lệ xảy ra, bộ thực thi sẽ tìm kiếm một trình xử lý thích hợp trong hàm hiện tại. Nếu không có trình xử lý, nó sẽ di chuyển đến khung của hàm gọi và đi lên ngăn xếp cuộc gọi cho đến khi tìm thấy trình xử lý. Trong quá trình này, các câu lệnh hủy hoặc dọn dẹp của các đối tượng cục bộ đã được tạo có thể được thực thi. Điều này được gọi là Tháo cuộn ngăn xếp.
Việc xử lý ngoại lệ có thể được cấu hình theo những cách sau, tùy thuộc vào cách triển khai:
- Liên tục kiểm tra xem có xảy ra ngoại lệ trong mỗi lệnh gọi và thao tác chức năng hay không.
- Không có nhánh bổ sung nào được thêm vào trong quá trình thực thi thông thường và khi xảy ra ngoại lệ, vị trí xử lý sẽ được tìm thấy bằng bảng.
- Thời gian chạy quản lý các đối tượng ngoại lệ và loại thông tin.
- Kết hợp với hệ thống báo hiệu hoặc xử lý ngoại lệ có cấu trúc của hệ điều hành.
C++ ABI có thể xác định các giao diện xử lý ngoại lệ và quy tắc nhị phân để giải phóng ngăn xếp, dọn dẹp đối tượng và điều hướng trình xử lý. Việc chuyển các ngoại lệ giữa các trình biên dịch hoặc thời gian chạy ngôn ngữ khác nhau yêu cầu các ABI và mô hình ngoại lệ tương thích.
Ngoại lệ là một cách thể hiện lỗi ở cấp độ ngôn ngữ và không phải lúc nào cũng giống với ngoại lệ phần cứng trong hệ điều hành. Một số triển khai chuyển đổi các lỗi phần cứng hoặc hệ điều hành, chẳng hạn như chia cho 0 và truy cập bộ nhớ không hợp lệ, thành ngoại lệ ngôn ngữ, trong khi một số khác chấm dứt chương trình ngay lập tức.
Bytecode và máy ảo
Mã byte là định dạng lệnh nhắm mục tiêu vào máy ảo thay vì CPU vật lý cụ thể. Việc chuyển đổi mã nguồn thành mã byte cho phép chúng ta có một lớp thực thi chung giữa giao diện ngôn ngữ và phần cứng thực tế.
Máy ảo xác định hướng dẫn mã byte, mô hình bộ nhớ, quy ước gọi, xác thực loại và định dạng mô-đun. Mã byte có thể được thực thi trực tiếp bởi trình thông dịch hoặc được trình biên dịch JIT chuyển đổi thành mã gốc.
JVM
Mã nguồn Java thường được biên dịch sang định dạng class, chứa các hướng dẫn JVM. JVM không trực tiếp biết ngôn ngữ Java và các ngôn ngữ khác có thể tạo ra các định dạng class hợp lệ cũng có thể chạy trên JVM. Tệp JVM class chứa mã byte, một nhóm hằng số đóng vai trò là bảng ký hiệu, các trường và phương thức cũng như một số thuộc tính.
JVM có thể xác thực cấu trúc và mã byte của tệp lớp trước khi thực thi nó. Quá trình xác minh kiểm tra xem mối quan hệ kiểu của lệnh, sử dụng ngăn xếp, mục tiêu nhánh, v.v. có đáp ứng các quy tắc JVM hay không, từ đó hạn chế các tệp nhị phân không chính xác vi phạm các quy tắc nội bộ của thời gian chạy.
CPython
CPython chuyển đổi mã nguồn Python thành các đối tượng mã nội bộ và mã byte. Đối tượng mã là một đối tượng bất biến chứa mã thực thi được biên dịch và siêu dữ liệu liên quan, đồng thời đối tượng hàm liên kết đối tượng mã với ngữ cảnh thực thi, chẳng hạn như không gian tên chung và các đối số mặc định.
Khi nhập mô-đun, CPython có thể lưu kết quả biên dịch vào tệp bộ đệm .pyc. Trong các lần thực thi tiếp theo, nếu bộ nhớ đệm khớp với nguồn hiện tại thì công việc chuyển đổi nguồn trở lại mã byte có thể được lưu lại. Hệ thống nhập của Python có thể xác thực bộ đệm bằng cách sử dụng dấu thời gian và kích thước tệp hoặc hàm băm nguồn.
.NET
Trình biên dịch ngôn ngữ .NET thường tạo ra các tập hợp có chứa siêu dữ liệu và ngôn ngữ trung gian phổ biến. CLR tải tập hợp, diễn giải thông tin về loại và phương thức, đồng thời có thể biên dịch JIT phương thức sẽ được thực thi hoặc sử dụng mã được biên dịch trước.
Một hệ thống kiểu chung và siêu dữ liệu cho phép các ngôn ngữ khác nhau, chẳng hạn như C# và F#, sử dụng cùng thời gian chạy và thư viện. Tuy nhiên, không phải tất cả các chức năng riêng biệt của mỗi ngôn ngữ đều được thể hiện dưới dạng giống nhau đối với các ngôn ngữ khác và phải sử dụng giao diện chung có thể được thể hiện bằng các quy tắc chung để có khả năng tương tác.
WebAssembly
WebAssembly là định dạng lệnh ảo cấp thấp, di động. Đặc tả cốt lõi xác định tập lệnh và các loại, mô-đun, mã hóa nhị phân, xác thực và ngữ nghĩa thực thi. Các phương thức tương tác với một hệ điều hành hoặc API trình duyệt cụ thể được tách biệt khỏi ngữ nghĩa thực thi cốt lõi và được cung cấp bởi máy chủ hoặc một lớp giao diện riêng biệt.
Các mô-đun WebAssembly được xác minh theo quy tắc cấu trúc và loại trước khi thực thi. Khi một mô-đun được khởi tạo, các phiên bản thời gian chạy như hàm, bộ nhớ, bảng và biến toàn cục sẽ được tạo, phần dữ liệu được khởi tạo và chức năng khởi động được chỉ định có thể được thực thi.
Trình thông dịch
Trình thông dịch đọc biểu thức bên trong của chương trình và trực tiếp thực hiện các hành động phù hợp với ý nghĩa của nó.
Trình thông dịch truyền tải cây đơn giản nhất truy cập các nút AST theo cách đệ quy.
Thực thi nút phép cộng:
Đánh giá nút con bên trái.
Đánh giá nút con bên phải.
Cộng hai kết quả.
Phương pháp duyệt cây dễ thực hiện và có sự tương ứng rõ ràng giữa cấu trúc nguồn và việc thực thi, nhưng chi phí thực hiện có thể tăng do loại nút phải được xác định cho mỗi thao tác và phải tuân theo con trỏ.
Trình thông dịch mã byte chuyển đổi AST thành mảng lệnh dày đặc hơn và sau đó thực hiện các lệnh bằng cách di chuyển bộ đếm chương trình ảo.
LOAD_CONST 10
LOAD_LOCAL value
ADD
RETURN
Trình thông dịch mã byte sử dụng các ngăn xếp lệnh và toán hạng hoặc các thanh ghi ảo. Máy ảo có thể sử dụng cấu trúc dựa trên ngăn xếp hoặc dựa trên thanh ghi và có các đặc điểm khác nhau về kích thước lệnh, chi phí giải mã và khả năng tối ưu hóa.
Trình thông dịch có thời gian biên dịch ngắn và có thể thay đổi linh hoạt cấu trúc chương trình trong quá trình thực thi. Mặt khác, do chi phí diễn giải mỗi thao tác được lặp lại nên nó có thể chậm hơn mã gốc trong các phép tính chạy dài. Thời gian chạy hiện đại có thể sử dụng phương pháp bắt đầu thực thi nhanh chóng bằng trình thông dịch và sau đó JIT chỉ biên dịch những phần được thực thi thường xuyên.
Biên dịch JIT
Biên dịch JIT là phương pháp chuyển đổi mã byte hoặc biểu diễn trung gian sang ngôn ngữ máy của hệ thống hiện tại trong khi chương trình đang chạy.
Trình biên dịch JIT có thể thu thập các thông tin sau trong quá trình thực thi:
- Các hàm thường được gọi
- Số lần thực hiện câu lệnh lặp lại
- Tỷ lệ thực tế của các nhánh có điều kiện
- Loại đối tượng được quan sát tại một vị trí cuộc gọi cụ thể
- Phương thức ảo thực sự được gọi là gì
- Kích thước mảng và phạm vi giá trị
- Đường dẫn thường xuyên xảy ra ngoại lệ
- Các kiểu giá trị mà hàm đối số lặp đi lặp lại có
Thông tin này cho phép trình biên dịch trước chuyên biệt hóa mã dựa trên các điều kiện khó đảm bảo an toàn. Ví dụ: nếu chỉ quan sát thấy một loại đối tượng tại một vị trí cuộc gọi cụ thể, bạn có thể tối ưu hóa để bỏ qua việc điều hướng phương thức ảo và gọi trực tiếp hàm tương ứng.
Tuy nhiên, trong quá trình thực thi, các loại mới có thể xuất hiện và các giả định hiện có có thể bị phá vỡ. Trong trường hợp này, bộ thực thi phải loại bỏ mã được tối ưu hóa và quay lại mã chung hơn hoặc trạng thái trình thông dịch. Điều này được gọi là tối ưu hóa ngược.
Việc biên dịch JIT cũng có các chi phí sau:
- Thời gian CPU được sử dụng để biên dịch trong quá trình thực thi.
- Cần có bộ nhớ để lưu trữ ngôn ngữ máy được tạo.
- Không có đủ thông tin hồ sơ ngay sau khi bắt đầu chương trình.
- Hiệu suất có thể thay đổi trước và sau khi hoàn tất tối ưu hóa.
- Vì bộ nhớ thực thi được tạo động nên nó có thể bị ảnh hưởng bởi các chính sách bảo mật và ký mã.
API ORC JIT của LLVM được thiết kế để tạo cấu trúc dựa trên JIT chuyển đổi LLVM IR thành mã gốc và quản lý các ký hiệu trong quá trình thực thi.
Biên dịch phân tầng
Biên dịch theo lớp là phương pháp sử dụng nhiều lớp thực thi với tốc độ thực thi và chi phí biên dịch khác nhau.
Trình thông dịch nhanh
↓ Được thực thi thường xuyên
Biên dịch JIT chi phí thấp
↓ Được thực thi rất thường xuyên
Biên dịch JIT tối ưu hóa chi phí cao
Chương trình lần đầu tiên được thực thi ngay lập tức bằng trình thông dịch hoặc trình biên dịch nhanh. Bộ thực thi quan sát tần suất thực thi và biên dịch lại mã được thực thi thường xuyên lên mức tối ưu hóa cao hơn. Mã chạy không thường xuyên không thực hiện các tối ưu hóa tốn kém, điều này có thể làm giảm thời gian khởi động và mức sử dụng bộ nhớ.
Biên dịch AOT
Biên dịch AOT là phương pháp chuyển đổi chương trình thành mã đích trước khi thực thi nó. Ngoài việc biên dịch gốc thông thường, quá trình biên dịch AOT cũng có thể liên quan đến việc chuyển đổi các định dạng trung gian cho máy ảo thành hình ảnh gốc trước khi triển khai.
Các đặc điểm chính của quá trình biên dịch AOT là:
- Thấp hoặc không có chi phí biên dịch trong quá trình thực hiện.
- Dễ dàng dự đoán thời gian khởi động và hiệu suất ban đầu.
- Không cần cho phép tạo mã động trong môi trường thực thi.
- Mã phù hợp với kiến trúc đích phải được tạo trước.
- Các loại và thông tin cuộc gọi chỉ có thể biết trong quá trình thực thi chỉ có thể được sử dụng ở một mức độ hạn chế.
- Thông tin bổ sung có thể được yêu cầu để duy trì các tính năng phản chiếu, tải động và tạo mã thời gian chạy.
Các nền tảng hiện đại không lựa chọn một cách cứng nhắc giữa AOT và JIT mà thường kết hợp mã gốc được biên dịch trước với tối ưu hóa dựa trên hồ sơ trong quá trình thực thi.
Khả năng tương tác bản địa
Giao diện hàm ngoại là một hệ thống gọi các chức năng và trao đổi dữ liệu giữa các ngôn ngữ lập trình và thời gian chạy khác nhau và được viết tắt là FFI.
FFI phải giải quyết các vấn đề sau:
- Cách kết hợp các loại của hai ngôn ngữ với nhau
- Quy ước gọi hàm
- Mã hóa chuỗi và biểu diễn bộ nhớ
- Vị trí của cấu trúc và mảng
- Tuổi thọ của đối tượng và tài liệu tham khảo
- Chủ sở hữu bộ nhớ và chịu trách nhiệm giải phóng nó
- Truyền chức năng gọi lại
- Chuyển đổi các ngoại lệ và lỗi
- Kết nối giữa các luồng và trạng thái thời gian chạy
- Di chuyển và cố định các đối tượng được quản lý bởi người thu gom rác
Bởi vì C ABI tương đối đơn giản và được hỗ trợ trên nhiều ngôn ngữ nên nó được sử dụng rộng rãi như một ranh giới về khả năng tương tác chung. Bởi vì các lớp, mẫu, ngoại lệ và đối tượng thư viện chuẩn của C++ phụ thuộc nhiều hơn vào ABI và phiên bản trình biên dịch, nên các giao diện bên ngoài thường sử dụng quy ước gọi C, các tay cầm mờ và các hàm tạo/hủy rõ ràng.
Khi gọi một hàm gốc từ thời gian chạy được quản lý, có thể cần phải có quy trình sắp xếp thứ tự để chuyển đổi đối tượng được quản lý sang biểu diễn bộ nhớ riêng của nó. Ngược lại, nếu mã gốc tham chiếu đến một đối tượng được quản lý thì cần có một bộ xử lý riêng để đảm bảo rằng trình thu gom rác nhận ra tham chiếu đó và duy trì vòng đời của đối tượng.
Biên dịch chéo
Biên dịch chéo là một phương pháp biên dịch trong đó hệ thống máy chủ nơi trình biên dịch chạy và hệ thống đích nơi chương trình được tạo ra được thực thi là khác nhau.
Ví dụ: bạn có thể tạo chương trình cho AArch64 Linux hoặc hệ điều hành nhúng trên máy tính Linux x86-64.
Môi trường biên dịch chéo thường yêu cầu các yếu tố sau:
- Phần phụ trợ tạo mã hỗ trợ kiến trúc đích
- Nhắm mục tiêu ABI và quy ước gọi
- Tiêu đề của hệ điều hành mục tiêu
- Thư viện tiêu chuẩn mục tiêu và thư viện hệ thống
- Trình biên dịch mục tiêu và trình liên kết hoặc các công cụ nhúng chúng
- sysroot chỉ ra hệ thống tập tin đích
- Thiết bị thực hoặc trình mô phỏng để chạy và thử nghiệm
Việc trình biên dịch có thể tạo ngôn ngữ máy cho CPU mục tiêu là chưa đủ. Các cài đặt thư viện, mã khởi động và trình liên kết mà chương trình sử dụng cũng phải phù hợp với môi trường đích. Clang có thể tạo mã cho các kiến trúc khác nhau bằng cách chỉ định target và đường dẫn CPU, ABI, sysroot, tiêu đề và thư viện.
Biên dịch không đồng nhất
Các chương trình hiện đại có thể không chỉ nhắm mục tiêu vào một CPU. Khi sử dụng cùng nhau các thiết bị thực thi khác nhau như GPU, DSP và bộ tăng tốc mạng thần kinh, mã máy chủ và mã thiết bị phải được biên dịch riêng biệt từ một nguồn hoặc mô-đun.
Một mã nguồn
├── Mã host CPU → x86-64 hoặc AArch64
└── Mã thiết bị GPU → GPU ISA hoặc SPIR-V
Chuỗi công cụ có thể biên dịch cùng một đơn vị dịch cho nhiều mục tiêu và bao gồm mã thiết bị được tạo trong tệp thực thi trên máy chủ hoặc đóng gói dưới dạng tệp nhị phân riêng biệt. Chuỗi công cụ giảm tải của Clang cũng có thể tạo các đối tượng mã riêng biệt cho máy chủ và một hoặc nhiều thiết bị rồi kết hợp chúng thành một gói duy nhất.
Thông tin gỡ lỗi
Ngôn ngữ máy được tối ưu hóa không duy trì các biến, câu và cấu trúc kiểu của mã nguồn gốc. Thông tin gỡ lỗi là siêu dữ liệu cho phép trình gỡ lỗi kết nối lại trạng thái thực thi ngôn ngữ máy với các khái niệm cấp mã nguồn.
Thông tin gỡ lỗi có thể bao gồm:
- Sự tương ứng giữa địa chỉ ngôn ngữ máy và tệp nguồn, hàng và cột
- Chức năng và thông tin cuộc gọi nội tuyến
- Các loại biến và tham số
- Vị trí thanh ghi hoặc bộ nhớ nơi giá trị được lưu trữ tại vị trí thực thi cụ thể.
- Vị trí của các trường trong cấu trúc và lớp
- Không gian tên và mô-đun
- Thông tin khôi phục khung cuộc gọi
Định dạng DWARF được sử dụng rộng rãi trong môi trường ELF dựa trên Unix và thông tin dựa trên CodeView và PDB được sử dụng trong môi trường Windows PE/COFF. DWARF là một định dạng thông tin được tiêu chuẩn hóa để hỗ trợ gỡ lỗi cấp nguồn trong một số loại ngôn ngữ được biên dịch.
Trong một chương trình được tối ưu hóa, các giá trị tương ứng với biến nguồn có thể bị xóa hoặc di chuyển đến nhiều vị trí khác nhau, các hàm có thể được nội tuyến và thứ tự thực hiện các câu lệnh có thể bị thay đổi. Do đó, vị trí thực thi và các giá trị biến được trình gỡ lỗi hiển thị có thể không hoàn toàn khớp với thứ tự trực quan của nguồn.
Biên dịch và lưu vào bộ nhớ đệm tăng dần
Biên dịch một chương trình lớn từ đầu mỗi lần sẽ làm tăng thời gian phát triển. Biên dịch tăng dần là phương pháp chỉ xử lý lại các phần đã thay đổi và các thành phần phụ thuộc bị ảnh hưởng bởi các thay đổi.
Trình biên dịch tăng dần có thể lưu trữ các thông tin sau:
- Nguồn băm của từng mô-đun
- Giao diện công cộng và thông tin loại
- Sự phụ thuộc giữa các module
- Kết quả phân tích cú pháp và ngữ nghĩa trước đó
- Tạo đại diện trung gian
- Kết quả tối ưu hóa và tệp đối tượng
- Tùy chọn biên dịch và môi trường đích
Nếu chỉ việc triển khai nội bộ của một chức năng thay đổi và giao diện chung được duy trì thì chỉ có thể tạo lại mô-đun chứa chức năng đó. Mặt khác, nếu các kiểu công khai, mẫu và hàm nội tuyến thay đổi thì các mô-đun khác tham chiếu đến chúng có thể cần phải được biên dịch lại.
Bộ đệm xây dựng sử dụng lại các kết quả trước đó bằng cách sử dụng nội dung nguồn, phiên bản trình biên dịch, tùy chọn, tiêu đề và tệp phụ thuộc làm khóa. Việc không định cấu hình khóa bộ đệm chính xác có thể dẫn đến việc sử dụng không chính xác các kết quả cũ hoặc làm mất hiệu lực bộ đệm nhiều hơn mức cần thiết.
Bootstrapping và tự lưu trữ
Nếu trình biên dịch của một ngôn ngữ lập trình được viết bằng cùng một ngôn ngữ thì nó được gọi là trình biên dịch self-hosted. Vì một ngôn ngữ mới chưa có trình biên dịch riêng nên ban đầu nó có thể được triển khai bằng ngôn ngữ khác hoặc bắt đầu bằng một trình biên dịch nhỏ xử lý một tập hợp con của ngôn ngữ hiện có.
Quá trình khởi động chung như sau.
Viết trình biên dịch ban đầu bằng một ngôn ngữ hiện có
↓
Biên dịch ngôn ngữ mới bằng trình biên dịch ban đầu
↓
Viết lại trình biên dịch bằng ngôn ngữ mới
↓
Build trình biên dịch mới bằng trình biên dịch trước đó
↓
Trình biên dịch mới biên dịch chính mã nguồn của nó
Tự lưu trữ cho thấy ngôn ngữ này có khả năng viết phần mềm thực, quy mô lớn và cho phép các nhà phát triển trình biên dịch sử dụng trực tiếp ngôn ngữ và hệ thống công cụ của riêng họ. Tuy nhiên, có một vấn đề về độ tin cậy của bootstrap yêu cầu xác minh xem mã nhị phân và nguồn của trình biên dịch có đáng tin cậy hay không cũng như các bước xây dựng trước đó ảnh hưởng đến kết quả như thế nào.
Sự phù hợp giữa đặc tả và bản triển khai ngôn ngữ
Việc triển khai ngôn ngữ phải triển khai chính xác ý nghĩa của chương trình được xác định bởi đặc tả ngôn ngữ. Ngay cả khi trình biên dịch tạo ra mã nhanh hơn, nó cũng không được tự ý thay đổi hành vi có thể quan sát được của chương trình.
Tính đúng đắn của việc thực hiện có thể được kiểm tra theo cách sau.
- Kiểm tra sự phù hợp để kiểm tra từng tính năng của tiêu chuẩn ngôn ngữ
- Kiểm tra đơn vị và kiểm tra tích hợp của chính trình biên dịch
- Thử nghiệm khác biệt bằng cách chạy cùng một chương trình trên nhiều lần triển khai
- Làm mờ để tạo các chương trình ngẫu nhiên
- Xác minh các biểu diễn trung gian và các bước tối ưu hóa
- So sánh hoạt động của chương trình trước và sau khi biên dịch
- Sử dụng phân tích tĩnh và kiểm tra thời gian chạy
- Sử dụng trình biên dịch đã được xác minh kiểu hoặc phép chuyển đổi được xác thực
Trong các ngôn ngữ cho phép hành vi không xác định hoặc do triển khai xác định, không phải tất cả các hoạt động triển khai đều cần tạo ra kết quả giống nhau. Trong trường hợp này, đặc tả phân biệt giữa phạm vi mà việc triển khai phải tuân thủ và phạm vi có thể được chọn.
Các tệp nhị phân dành cho máy ảo có thể trải qua một bước xác minh riêng trước khi thực thi. WebAssembly xác minh rằng các lệnh và mô-đun đáp ứng các quy tắc cấu trúc và kiểu tĩnh, đồng thời JVM cũng áp dụng các ràng buộc cấu trúc cho các tệp lớp và mã byte.
Toàn bộ luồng thực thi
Quá trình thực thi của một ngôn ngữ lập trình khác nhau tùy thuộc vào ngôn ngữ và cách triển khai, nhưng mỗi bước có thể được tổ chức thành các vai trò sau.
| bước | Vai trò chính | Kết quả đại diện |
|---|---|---|
| Tiền xử lý | Bao gồm các tệp và macro, xử lý mã có điều kiện | Mã thông báo được xử lý trước |
| Phân tích từ vựng | Tách các ký tự thành các đơn vị có ý nghĩa | Mã thông báo |
| phân tích | Sắp xếp các mã thông báo thành các cấu trúc ngữ pháp | Cây cú pháp hoặc AST |
| phân tích ngữ nghĩa | Kiểm tra tên, loại, phạm vi và quy tắc ngôn ngữ | AST có thêm thông tin ngữ nghĩa |
| Hạ | Chuyển đổi các hàm cấp cao thành các thao tác cơ bản | AST hoặc IR cấp thấp |
| Tạo biểu diễn trung gian | Cấu hình định dạng với tối ưu hóa và chuyển đổi mục tiêu | IR |
| Tối Ưu Hóa | Cải thiện mã trong khi vẫn duy trì ý nghĩa | IR được tối ưu hóa |
| Sinh mã | Xác định lệnh đích, thanh ghi và quy ước gọi | Hợp ngữ hoặc mã máy |
| Lắp ráp | Tổ chức mã máy và siêu dữ liệu | tập tin đối tượng |
| Liên kết | Giải thích và tái định vị ký hiệu, kết hợp nhị phân | tập tin hoặc thư viện thực thi |
| đang tải | Đặt chương trình và thư viện vào bộ nhớ | Tình trạng thực hiện quy trình |
| Runtime | Hỗ trợ các tính năng ngôn ngữ như bộ nhớ, kiểu, ngoại lệ và thread | Chương trình đang chạy |
Khi triển khai bằng mã byte và máy ảo, một số bước sẽ khác nhau:
Mã nguồn
↓
Phân tích frontend
↓
Bytecode hoặc ngôn ngữ trung gian chung
↓
Kiểm chứng và nạp mô-đun
↓
Trình thông dịch hoặc trình biên dịch JIT
↓
Thực thi lệnh bản địa
Việc triển khai một ngôn ngữ lập trình không chỉ đơn giản là nhiệm vụ dịch thuật ngữ pháp sang ngôn ngữ máy. Trình biên dịch và trình thông dịch phải triển khai ngữ nghĩa ngôn ngữ, hệ thống kiểu, mô hình bộ nhớ, ngoại lệ và quy tắc tương tranh trên phần cứng và hệ điều hành thực tế. Trình liên kết, trình tải, ABI, thời gian chạy và thư viện chuẩn cũng là một phần của hệ sinh thái ngôn ngữ quyết định kết quả thực thi của chương trình.
Việc triển khai hiện đại kết hợp việc biên dịch, giải thích, mã byte và biên dịch JIT khi cần, đồng thời tối ưu hóa chương trình bằng cách sử dụng nhiều cấp độ biểu diễn trung gian và thông tin hồ sơ. Do đó, hiệu suất thực tế, mức sử dụng bộ nhớ, thời gian khởi động, tính di động và đặc điểm gỡ lỗi của ngôn ngữ lập trình không chỉ được xác định bởi cú pháp ngôn ngữ mà bởi toàn bộ hệ thống thực thi, kết hợp trình biên dịch, thời gian chạy, hệ điều hành, ABI, thư viện và phần cứng đích.
Hệ thống kiểu
Hệ thống kiểu là một hệ thống quy tắc phân loại các giá trị và biểu thức được sử dụng trong chương trình thành các danh mục nhất định và xác định mối quan hệ tương thích giữa các thao tác và giá trị được phép trong mỗi danh mục. Một kiểu không chỉ biểu thị định dạng trong đó một giá trị được lưu trữ trong bộ nhớ mà còn biểu thị các thao tác có thể được thực hiện với giá trị đó, các điều kiện có thể được chuyển sang các giá trị khác, đầu vào và đầu ra của hàm và ranh giới trừu tượng của chương trình.
Ví dụ: số nguyên và chuỗi đều là giá trị được lưu trữ trong bộ nhớ, nhưng các phép toán và ý nghĩa được phép của chúng là khác nhau. Các phép toán số học có thể được áp dụng cho số nguyên và các phép toán như nối và tính độ dài có thể được áp dụng cho chuỗi. Kiểu hàm cho biết giá trị nào hàm nhận được làm đầu vào và giá trị nào nó trả về, còn kiểu đối tượng có thể chỉ ra các trường và phương thức có thể được sử dụng hoặc giao diện mà đối tượng đáp ứng.
Hệ thống kiểu không phải là một công cụ loại bỏ tất cả các lỗi trong chương trình. Các vấn đề khó xác định chỉ dựa vào loại, chẳng hạn như các tình huống trong đó tệp không tồn tại, lỗi kết nối mạng, dữ liệu nhập của người dùng không chính xác và lỗi logic trong thuật toán, vẫn có thể xảy ra trong quá trình thực thi. Vai trò của hệ thống kiểu là phát hiện sự không nhất quán về giá trị và hoạt động trong phạm vi do ngôn ngữ đặt, chỉ định cấu trúc và mục đích của chương trình, đồng thời cung cấp thông tin cho phép trình biên dịch và công cụ phát triển phân tích chương trình. Cardelli và Wegner coi các kiểu là khái niệm trung tâm tổ chức việc phân loại, trừu tượng hóa và đa hình của các biểu thức và giá trị chương trình.
Các phân loại chính của hệ thống kiểu có thể được tóm tắt như sau.
| Tiêu chí | Phân loại chính | Mục tiêu phân loại |
|---|---|---|
| Thời điểm kiểm tra | Kiểu tĩnh, kiểu động | Quy tắc kiểu được kiểm tra trước hay trong khi thực thi |
| Ký hiệu kiểu | Kiểu tường minh, kiểu suy luận | Lập trình viên và trình biên dịch xác định thông tin kiểu như thế nào |
| Quan hệ kiểu | Kiểu định danh, kiểu cấu trúc | Khả năng tương thích kiểu được đánh giá theo tên hay cấu trúc |
| Đa hình | Đa hình tham số, đa hình kiểu con, đa hình tùy biến | Một đoạn mã xử lý nhiều kiểu như thế nào |
| Kết hợp giá trị | Kiểu tích, kiểu tổng, kiểu hàm | Cấu trúc nào được dùng để biểu diễn giá trị phức hợp |
| Sử dụng tài nguyên | Kiểu tuyến tính, kiểu affine, kiểu sở hữu | Một giá trị có thể được dùng bao nhiêu lần và do ai quản lý |
| Độ chính xác | Kiểu thông thường, kiểu tinh chỉnh, kiểu phụ thuộc | Các điều kiện về giá trị được đưa vào kiểu cụ thể đến mức nào? |
| Hiệu ứng | Kiểu thông thường, kiểu hiệu ứng | Hiệu ứng bên ngoài của hàm có được theo dõi trong kiểu hay không |
| Phương thức áp dụng | Kiểu tĩnh toàn phần, định kiểu dần dần | Thông tin kiểu tĩnh được áp dụng đến mức nào trong chương trình |
Những phân loại này không loại trừ lẫn nhau. Một ngôn ngữ duy nhất có thể cung cấp tính năng định kiểu tĩnh, suy luận kiểu, gõ danh nghĩa, khái quát, phân tích tính vô hiệu và kiểm tra quyền sở hữu cùng nhau. Ngược lại, các ngôn ngữ được định kiểu động cũng có thể áp dụng kiểm tra kiểu cho một số mã bằng cách thêm ký hiệu loại tùy chọn và bộ phân tích tĩnh.
Kiểu và phán đoán kiểu
Kiểm tra kiểu là quá trình gán một loại cụ thể cho biểu thức của chương trình và kiểm tra xem loại đó có tương thích với ngữ cảnh mà nó được sử dụng hay không. Một hệ thống kiểu chính thức sử dụng các phán đoán loại tương tự như sau:
Γ ⊢ expression : Type
Điều này có nghĩa là expression trong môi trường Γ có loại Type. Môi trường chứa thông tin về các biến, hàm, tham số kiểu và kiểu của chúng có sẵn trong phạm vi hiện tại.
Ví dụ: giả sử bạn có đoạn mã sau:
x: Integer
y: Integer
x + y
Trình kiểm tra kiểu gán một loại số nguyên cho x + y bằng cách áp dụng quy tắc rằng x và y là số nguyên và kết quả của phép cộng số nguyên cũng là một số nguyên. Ngược lại, nếu bạn cố gắng thêm các giá trị mà quy tắc bổ sung không được xác định, chẳng hạn như chuỗi và đối tượng tệp, sẽ xảy ra lỗi loại.
Các cuộc gọi chức năng cũng được kiểm tra theo cách tương tự.
parse: String → Integer
Nếu parse là hàm lấy một chuỗi làm đầu vào và trả về một số nguyên, thì một biểu thức chuỗi có thể được truyền dưới dạng đối số, nhưng có thể không được phép truyền một đối tượng tùy ý không thể chuyển đổi thành chuỗi.
Xác định loại không chỉ đơn giản là quá trình so sánh tên loại để xem chúng có giống nhau hay không. Các mối quan hệ kiểu con, chuyển đổi tiềm ẩn, ràng buộc chung, giải thích quá tải, thời gian tồn tại, tính vô hiệu, kết quả suy luận tham số loại, v.v. có thể được xem xét cùng nhau.
An toàn kiểu và tính vững
An toàn kiểu là thuộc tính hạn chế chương trình sử dụng các giá trị theo cách bị cấm theo quy tắc loại. Ví dụ: một hệ thống kiểu có thể chặn các hành động như gọi một giá trị số nguyên làm con trỏ hàm, diễn giải một chuỗi dưới dạng bố cục đối tượng tùy ý hoặc gọi một phương thức không tồn tại.
Trong lý thuyết ngôn ngữ hình thức, hai thuộc tính bảo toàn và tiến triển thường được dùng để giải thích Tính vững của kiểu.
- Bảo quản là thuộc tính mà một biểu thức được gõ đúng sẽ duy trì các quy tắc loại của nó ngay cả sau một bước thực thi.
- Tiến trình là thuộc tính mà biểu thức được nhập chính xác đã là giá trị kết quả hoặc có thể tiến tới bước thực hiện tiếp theo được xác định bởi ngôn ngữ.
Việc chứng minh hai thuộc tính này cùng nhau cung cấp bằng chứng cho thấy một chương trình vượt qua kiểm tra kiểu sẽ không rơi vào trạng thái mà hệ thống kiểu không được xác định trong quá trình thực thi. Wright và Felleisen đã hệ thống hóa cách tiếp cận cú pháp để chứng minh tính đúng đắn của kiểu chữ theo cách này.
Độ ổn định của loại không giống như độ an toàn của bộ nhớ. Nếu ngôn ngữ cho phép các hoạt động con trỏ không an toàn và chuyển đổi loại tùy ý hoặc gọi đến mã gốc bên ngoài thì lỗi bộ nhớ có thể xảy ra ngay cả trong mã đã vượt qua quá trình kiểm tra kiểu tĩnh. Một số ngôn ngữ đặt các hoạt động này trong một ranh giới riêng biệt, chẳng hạn như khu vực unsafe, để phân định rõ ràng các đảm bảo được duy trì trong mã an toàn. Rust cũng có thể sử dụng mã unsafe bên trong phần trừu tượng an toàn, nhưng tác giả của mã đó phải tự đảm bảo con trỏ, bí danh và quy tắc vòng đời.
Những gì hệ thống kiểu đảm bảo thay đổi tùy theo ngôn ngữ. Một số ngôn ngữ kiểm tra phạm vi của một mảng dưới dạng một loại, một số ngôn ngữ khác kiểm tra nó trong khi thực thi và một số ngôn ngữ coi tràn số nguyên là một lỗi hoặc coi nó như một chu kỳ giá trị. Do đó, chỉ riêng cụm từ “ngôn ngữ an toàn loại” không nên được hiểu là đảm bảo tất cả sự an toàn khi thực thi.
Kiểu tĩnh và kiểu động
Hệ thống Kiểu tĩnh phân tích mối quan hệ kiểu giữa các biến, biểu thức và lệnh gọi hàm trước khi thực hiện chương trình. Trong Java, kiểu của tất cả các biến và biểu thức được xác định tại thời điểm biên dịch và các kiểu giới hạn phạm vi giá trị cũng như các thao tác được phép.
Trình kiểm tra kiểu tĩnh có thể tìm thấy các vấn đề sau trước khi thực thi:
- Sử dụng các trường hoặc phương thức không tồn tại
- Không khớp giữa các đối số của hàm và loại tham số
- Kiểu trả về không hợp lệ
- Không được phép chuyển đổi loại
- Vi phạm ràng buộc kiểu chung
- Các bộ phận của các nhánh mẫu và loại chưa được xử lý
- Vi phạm quy tắc vô hiệu
- Sử dụng thời gian tham chiếu và quyền sở hữu không chính xác
- Một số loại hiệu ứng và vi phạm quy tắc ngoại lệ
Định kiểu tĩnh không phải là phương pháp tìm ra giá trị thực của toàn bộ chương trình trước khi thực hiện. Mặc dù đã biết rằng loại biến là số nguyên, nhưng trước khi thực thi, người ta thường không biết giá trị thực tế là 1 hay 100. Một kiểu thể hiện một cách trừu tượng một tập hợp các giá trị có thể có và một phạm vi hoạt động.
Trong hệ thống Kiểu động, kiểu giá trị và tính hợp lệ của thao tác chủ yếu được kiểm tra trong quá trình thực thi. Thay vì gán một kiểu cố định cho chính tên biến, sẽ chính xác hơn khi giải thích rằng giá trị hiện được trỏ đến bởi tên có thông tin về kiểu.
value = 10
value = "text"
Trong ngôn ngữ được định kiểu động, cùng một tên có thể đề cập đến một số nguyên và một chuỗi tại các thời điểm khác nhau. Tuy nhiên, sự khác biệt giữa số nguyên và chuỗi không biến mất. Người thi hành kiểm tra kiểu giá trị hiện tại để xác định xem thao tác có được phép hay không.
Kiểu động phù hợp để viết chương trình nhanh và xử lý linh hoạt các cấu trúc cũng như nhiều kiểu dữ liệu khác nhau được tạo ra trong quá trình thực thi. Mặt khác, một số loại lỗi nhất định có thể không được phát hiện cho đến khi chúng thực sự đi qua đường dẫn thực thi tương ứng. Để bổ sung cho điều này, các bài kiểm tra, kiểm tra thời gian chạy, ký hiệu loại tùy chọn và máy phân tích tĩnh bên ngoài được sử dụng cùng nhau.
Ngay cả các ngôn ngữ được định kiểu tĩnh cũng có thể kiểm tra kiểu thời gian chạy. Downcast, tải mô-đun động, phản chiếu, đối tượng giao diện, giải tuần tự hóa dữ liệu ngoài, v.v. yêu cầu kiểm tra giá trị thực thi thực tế. Ngược lại, các ngôn ngữ được định kiểu động cũng cho phép trình biên dịch chẩn đoán trước các lỗi rõ ràng hoặc suy ra các loại để tối ưu hóa. Do đó, việc định kiểu tĩnh và động không có nghĩa là tất cả các kiểm tra đều tồn tại tại một thời điểm, mà chỉ ra rằng tại thời điểm nào ngôn ngữ đặt trách nhiệm chính về tính đúng đắn của kiểu.
Kiểu rõ ràng và suy luận kiểu
Ký hiệu kiểu là khi lập trình viên ghi trực tiếp các kiểu cho biến, hàm, tham số, giá trị trả về, v.v.
fn add(a: i32, b: i32) -> i32 {
a + b
}
Suy luận kiểu là một quá trình trong đó trình biên dịch xác định các phần trong đó loại không được chỉ định trong mã nguồn bằng cách phân tích các biểu thức xung quanh và mối quan hệ sử dụng.
let value = 10;
Trong mã này, trình biên dịch có thể suy ra loại value dựa trên ngữ cảnh sử dụng và ngữ cảnh sử dụng. Việc sử dụng suy luận kiểu không thay đổi ngôn ngữ thành kiểu định kiểu động. Loại suy luận được sử dụng để kiểm tra kiểu tĩnh và có thể xảy ra lỗi nếu sau đó gán một giá trị không tương thích cho biến.
Kiểu suy luận sử dụng thông tin sau:
- Kiểu chữ cơ bản
- Loại toán hạng do người vận hành yêu cầu
- Tham số hàm và kiểu trả về
- Mối quan hệ thay thế của các biến
- Hạn chế về tham số loại chung
- Mối quan hệ loại phụ và chuyển đổi
- Loại mục tiêu bắt buộc trong bối cảnh xung quanh
- Thu hẹp các loại dựa trên luồng điều khiển
Chuỗi hệ thống kiểu Hindley–Milner cung cấp một hệ thống để suy ra các kiểu chính phổ biến nhất ngay cả trong các chương trình chức năng có ít ký hiệu kiểu rõ ràng. Công trình của Damas và Milner đã thiết lập một hệ thống kiểu chính trong đó các loại khác nhau áp dụng cho một chương trình cụ thể được biểu diễn dưới dạng các thể hiện của loại chính.
Suy luận kiểu trong các ngôn ngữ hiện đại không hoàn toàn giống với Hindley–Milner. Khi các tính năng như phân nhóm, nạp chồng, đối tượng, tổng quát, tuổi thọ và khả năng thay đổi được kết hợp, cần có các biểu thức ràng buộc phức tạp hơn. Suy luận kiểu của Java cũng chuyển đổi tính tương thích của biểu thức và các mối quan hệ kiểu con thành các biểu thức ràng buộc và sau đó tính toán các ranh giới có thể có của các biến kiểu.
Kiểu suy luận có phạm vi sau:
- Suy luận kiểu cục bộ chỉ suy ra một số loại bên trong hàm hoặc biểu thức.
- Suy luận kiểu toàn cục phân tích một loạt các chương trình vượt ra ngoài ranh giới chức năng.
- Suy luận một chiều đánh giá loại từ chính biểu thức.
- Suy luận dựa trên ngữ cảnh chuyển loại được yêu cầu từ khu vực xung quanh vào một biểu thức.
- Kiểm tra kiểu hai chiều kết hợp các loại của một số biểu thức và kiểm tra các biểu thức khác để đảm bảo chúng khớp với loại mục tiêu nhất định.
Kiểm tra kiểu hai chiều phân biệt giữa tổng hợp kiểu và kiểm tra kiểu và được sử dụng như một cách để kiểm soát số lượng ký hiệu kiểu và độ phức tạp kiểm tra được yêu cầu ngay cả trong các hệ thống kiểu có khả năng biểu đạt cao.
Không phải lúc nào cũng mong muốn tự động suy ra tất cả các loại. Việc chỉ định loại chức năng công khai giúp hợp đồng API và thông báo lỗi rõ ràng, đồng thời giảm những thay đổi ngoài ý muốn trong kết quả suy luận do thay đổi triển khai.
Kiểu cơ bản và kiểu phức tạp
Ngôn ngữ lập trình cung cấp nhiều loại khác nhau để thể hiện các kiểu giá trị khác nhau. Phương pháp phân loại khác nhau tùy thuộc vào ngôn ngữ, nhưng nhìn chung có thể thấy các loại sau.
- Số nguyên và số thực
- Giá trị logic
- Ký tự và chuỗi
- Con trỏ và tài liệu tham khảo
- Mảng và lát
- Bộ dữ liệu và hồ sơ
- Cấu trúc và lớp học
- Kiểu liệt kê
- Loại chức năng
- Giao diện và đặc điểm
- Loại chung
- Kiểu tổng và loại giao lộ
- Kiểu đệ quy
- Loại Nullable
- Kiểu tham số
Java chia các kiểu thành kiểu cơ bản và kiểu tham chiếu. Các kiểu cơ bản bao gồm các giá trị logic và kiểu số, còn kiểu tham chiếu bao gồm các lớp, giao diện và kiểu mảng.
Sự phân loại này không áp dụng như nhau cho tất cả các ngôn ngữ. Trong một số ngôn ngữ, số nguyên cũng là đối tượng và trong các ngôn ngữ khác, số nguyên và đối tượng có cách biểu diễn bộ nhớ và quy ước gọi khác nhau. Sự khác biệt giữa kiểu giá trị và kiểu tham chiếu không được định nghĩa đơn giản là sự khác biệt giữa ngăn xếp và đống. Điều quan trọng là cách các giá trị được sao chép, so sánh và chia sẻ cũng như cách ngôn ngữ xác định danh tính của các giá trị đó.
Kiểu tích
Kiểu tích là loại phức tạp chứa nhiều giá trị cùng một lúc. Bộ dữ liệu, bản ghi và cấu trúc là những ví dụ tiêu biểu.
Point = {
x: Float,
y: Float
}
Giá trị Point phải có cả x và y. Nó được gọi là kiểu tích vì tất cả các kết hợp có thể có của giá trị x và y tạo thành tập hợp tổng giá trị Point.
Các bộ dữ liệu thường được chia thành các thành phần theo vị trí trường và các bản ghi được phân tách theo tên.
(Integer, String)
{
id: Integer,
name: String
}
Bộ dữ liệu trong Haskell cũng được định nghĩa là kiểu dữ liệu đại số với một hàm tạo.
Kiểu tổng
Kiểu tổng là loại có một trong nhiều dạng có thể. Còn được gọi là biến thể, liên kết được gắn thẻ hoặc kiểu tổng riêng biệt.
enum Result<T, E> {
Ok(T),
Err(E),
}
Giá trị Result<T, E> không có cả Ok và Err mà có một trong hai dạng. Mỗi biểu mẫu có thể chứa dữ liệu khác nhau và biểu mẫu hiện tại được phân biệt thông qua các thẻ hoặc hàm tạo.
Kiểu tổng rất hữu ích để thể hiện rõ ràng các trạng thái sau:
- Thành công hay thất bại
- Có hoặc không có giá trị
- Một số loại lệnh
- Các nút cây cú pháp khác nhau
- Loại tin nhắn mạng
- Trạng thái hiện tại của máy trạng thái
enum của Rust tạo ra một kiểu liệt kê danh nghĩa và từng biến thể, đồng thời xác định chúng để có thể sử dụng chúng để khớp mẫu.
Kiểu dữ liệu đại số
Kiểu dữ liệu đại số là kiểu dữ liệu được xác định bằng cách kết hợp kiểu tích và kiểu tổng. Haskell cung cấp các kiểu dữ liệu đại số do người dùng xác định và khớp mẫu dưới dạng các tính năng ngôn ngữ cốt lõi.
Sau đây là kiểu dữ liệu đại số đệ quy biểu diễn cây nhị phân.
data Tree a
= Empty
| Node (Tree a) a (Tree a)
Tree a là Empty hoặc Node, bao gồm cây bên trái và các giá trị cũng như cây bên phải. Nó cũng là một kiểu đệ quy vì định nghĩa kiểu đề cập đến chính nó.
So khớp mẫu sẽ kiểm tra hàm tạo hiện tại và trích xuất các giá trị mà nó chứa.
size Empty = 0
size (Node left _ right) =
1 + size left + size right
Trình biên dịch có thể kiểm tra xem tất cả các dạng tổng đóng đã được xử lý chưa. Điều này cho phép bạn tìm mã bị ảnh hưởng khi một biến thể mới được thêm vào hoặc chẩn đoán các tình trạng chưa được xử lý.
Kiểu hàm
Kiểu hàm biểu thị đầu vào mà hàm nhận được và đầu ra mà nó trả về.
String → Integer
Đây là một loại hàm nhận vào một chuỗi và trả về một số nguyên.
Nhiều tham số có thể được xem dưới dạng bộ dữ liệu.
(Integer, Integer) → Integer
Trong các ngôn ngữ sử dụng Currying, nó có thể được biểu thị dưới dạng chấp nhận một đối số và trả về một hàm như sau.
Integer → Integer → Integer
Các kiểu hàm được sử dụng làm biến, trường, tham số hàm và kiểu trả về trong các ngôn ngữ hỗ trợ các hàm hạng nhất.
Khi phân nhóm các loại hàm, hướng đầu vào và đầu ra hoạt động khác nhau. Có thể an toàn khi cung cấp loại cụ thể hơn cho kiểu trả về, nhưng loại tham số phải đủ rộng để chấp nhận bất kỳ giá trị nào mà người gọi có thể chuyển. Mối quan hệ này dẫn đến hiệp phương sai và contravariance.
Kiểu hợp và kiểu giao
Kiểu hợp chỉ ra rằng giá trị có thể là một trong một số loại.
string | number
Giá trị của loại này là chuỗi hoặc số. Loại nào phải được thu hẹp thông qua đường dẫn thực thi hoặc kiểm tra điều kiện và chỉ có thể sử dụng các thao tác an toàn trên tất cả các loại thành phần cho đến khi loại được xác nhận. TypeScript sử dụng luồng điều khiển và kiểm tra kiểu để thu hẹp các kiểu kết hợp thành các kiểu cụ thể hơn.
Kiểu giao là loại thỏa mãn điều kiện của nhiều loại cùng một lúc.
Serializable & Loggable
Loại này phải cung cấp tất cả các thành phần theo yêu cầu của Serializable và Loggable. Các kiểu chéo trong TypeScript kết hợp các thành viên của nhiều loại thành một loại.
Các loại liên kết và các loại giao lộ có thể được hiểu theo các tập hợp giá trị.
A | Blà tập hợp các giá trị thuộc A hoặc B.A & Blà tập hợp các giá trị thỏa mãn cả điều kiện A và B.
Các kiểu đặc biệt
Trong hệ thống kiểu, ngoài các kiểu dữ liệu chung, có thể còn có các kiểu đặc biệt thể hiện cấu trúc tính toán.
Kiểu đỉnh
Kiểu đỉnh là loại rộng nhất bao gồm tất cả các giá trị trong hệ thống kiểu. Tùy thuộc vào ngôn ngữ, các tên như Object, Any và unknown có thể được sử dụng.
Các hoạt động chỉ tồn tại trong các loại cụ thể không thể được áp dụng trực tiếp cho các giá trị thuộc loại cao nhất. Để sử dụng một giá trị, bạn cần thu hẹp giá trị đó thành một loại cụ thể hơn thông qua kiểm tra kiểu, khớp mẫu, hạ thấp, v.v.
any trong một số ngôn ngữ có chức năng bỏ qua mạnh hơn các kiểu đỉnh đơn giản. Chúng có thể vô hiệu hóa việc kiểm tra kiểu hoặc cho phép hầu hết các thao tác, vì vậy chúng không nên được xử lý thay thế cho nhau bằng các kiểu cấp cao nhất theo lý thuyết kiểu.
Kiểu đáy
Kiểu đáy là loại không có giá trị nào có thể. Nó có thể biểu thị các trường hợp hàm không trả về bình thường hoặc luôn ném ra một ngoại lệ.
Loại ! của Rust biểu thị một kết quả không có giá trị và quá trình tính toán chưa hoàn tất và có thể bị ép buộc sang loại khác.
fn terminate() -> ! {
panic!("terminated");
}
Loại thấp nhất không thực sự tạo ra bất kỳ giá trị nào, vì vậy nhiều loại có thể phù hợp một cách hợp lý khi được yêu cầu.
let value = if condition {
10
} else {
terminate()
};
terminate() không trả về giá trị, do đó nó không xung đột với các kiểu số nguyên từ các nhánh khác.
Kiểu đơn vị
Kiểu đơn vị là loại có một giá trị duy nhất không có thông tin có ý nghĩa. Nó có thể thể hiện một thực tế là một hàm không trả về kết quả tính toán mà chỉ thực hiện các tác dụng phụ.
Unit
Loại đơn vị khác với loại thấp nhất ở chỗ nó không có giá trị.
- Một loại đơn vị có một giá trị.
- Loại thấp nhất không có giá trị.
Kiểu null
Trong một số ngôn ngữ, null hoặc giá trị tương đương được bao gồm trong kiểu tham chiếu. Trong các ngôn ngữ khác, tính chất vô hiệu được xác định bởi chính loại đó.
Việc ngầm bao gồm tính chất vô hiệu trong một kiểu tham chiếu chung có nghĩa là tất cả việc sử dụng tham chiếu đó phải tính đến tính chất vô hiệu. Để giảm bớt điều này, các ngôn ngữ hiện đại phân tách sự tồn tại của một giá trị thành các loại, chẳng hạn như T, T?, Option<T> và Maybe T.
Kiểu danh nghĩa và kiểu cấu trúc
Hệ thống Kiểu định danh xác định tính tương thích dựa trên tên khai báo của loại và mối quan hệ rõ ràng.
class UserId
class OrderId
Ngay cả khi biểu thức bên trong của cả hai loại giống nhau như một số nguyên, nếu chúng được khai báo riêng biệt thì chúng có thể được coi là các loại khác nhau. Để một kiểu trở thành kiểu con của kiểu khác, cần có mối quan hệ rõ ràng, chẳng hạn như kế thừa hoặc triển khai giao diện.
Các loại danh nghĩa có lợi cho việc phân biệt các khái niệm miền và ngăn chặn sự trộn lẫn các loại không chủ ý. Ngay cả khi sử dụng các số thực có cùng độ dài và khối lượng, việc thay thế sai có thể được ngăn chặn bằng cách xác định chúng là các kiểu khác nhau.
Hệ thống Kiểu cấu trúc xác định tính tương thích dựa trên cấu trúc của các trường và phương thức được cung cấp thực tế thay vì tên của loại.
type Named = {
name: string;
};
Giá trị có trường name: string có thể tương thích với Named mà không cần phải khai báo rõ ràng rằng nó triển khai loại đó. TypeScript sử dụng hệ thống kiểu cấu trúc phản ánh việc sử dụng rộng rãi các đối tượng ẩn danh và nghĩa đen đối tượng trong JavaScript.
Các loại danh nghĩa và cấu trúc đều có ưu điểm và nhược điểm.
| Danh mục | loại danh nghĩa | Kiểu kết cấu |
|---|---|---|
| Tiêu chuẩn tương thích | tên khai báo và mối quan hệ rõ ràng | Cấu trúc trường và phương thức |
| Tuyên bố về mối quan hệ | Nói chung là bắt buộc | Có thể bỏ qua nếu cấu trúc đúng |
| Phân loại tên miền | mạnh mẽ | Các cấu trúc giống nhau có thể được trộn lẫn |
| Tái sử dụng các đối tượng hiện có | Có thể yêu cầu triển khai rõ ràng | Tương thích mà không cần sửa đổi |
| Tiến hóa API | Tuyên bố mối quan hệ rõ ràng | Tác động của những thay đổi cơ cấu có thể sâu rộng |
Một số ngôn ngữ sử dụng các loại danh nghĩa và các thành phần cấu trúc cùng nhau. Trong khi cung cấp loại lớp danh nghĩa, nó có thể hỗ trợ các loại hàm có cấu trúc hoặc bản ghi ẩn danh.
Quan hệ kiểu con
Quan hệ kiểu con là mối quan hệ cho phép sử dụng các giá trị của một loại một cách an toàn ở những vị trí yêu cầu loại khác.
Dog <: Animal
Điều này có nghĩa là Dog là kiểu con của Animal. Nếu Dog đáp ứng tất cả các điều kiện mà mã yêu cầu Animal mong đợi thì giá trị Dog có thể được chuyển đến vị trí tương ứng.
Phân nhóm không chỉ có nghĩa là có nhiều trường hơn. Đầu vào và đầu ra của phương thức, các trường biến, ngoại lệ, quy tắc thay đổi trạng thái, v.v. phải được xem xét cùng nhau. Cardelli và Wegner đã phân loại đa hình bao gồm dựa trên kiểu con như một dạng đa hình phổ quát.
Kế thừa và phân nhóm có liên quan chặt chẽ với nhau, nhưng chúng không phải là khái niệm giống nhau.
- Kế thừa là một cấu trúc ngôn ngữ tái sử dụng hoặc mở rộng việc triển khai và khai báo.
- Phân nhóm là mối quan hệ tương thích cho phép một giá trị nhập vào một vị trí yêu cầu loại khác.
Tùy thuộc vào ngôn ngữ, phân nhóm giao diện có thể được cung cấp mà không có tính kế thừa hoặc có thể có các tính năng sử dụng lại việc triển khai nhưng không tạo mối quan hệ gán an toàn.
Rust không sử dụng phân nhóm phân cấp lớp chung, giới hạn phân nhóm chủ yếu ở các mối quan hệ vòng đời và vòng đời bậc cao hơn. Nếu thời gian tồn tại bị loại bỏ, hầu hết các mối quan hệ kiểu đều được coi là đẳng thức kiểu.
Hiệp biến, nghịch biến và bất biến
Tính biến thiên cho biết tham số kiểu bên trong một hàm tạo kiểu bảo toàn quan hệ kiểu con như thế nào.
Giả sử Dog <: Animal, điều quan trọng là mối quan hệ giữa Container<Dog> và Container<Animal>.
Hiệp biến
Các mối quan hệ loại là hiệp biến nếu chúng vẫn cùng hướng.
Dog <: Animal
Producer<Dog> <: Producer<Animal>
Bạn có thể sử dụng nhà sản xuất chỉ tạo ra Dog làm nhà sản xuất tạo ra Animal. Người gọi mong đợi một con vật và không có vấn đề gì nếu con chó thực sự được trả lại.
Nghịch biến
Nếu mối quan hệ kiểu thay đổi theo hướng ngược lại thì đó là trái ngược nhau.
Dog <: Animal
Consumer<Animal> <: Consumer<Dog>
Người tiêu dùng có thể xử lý tất cả Động vật cũng có thể xử lý Chó, vì vậy người tiêu dùng Chó có thể được sử dụng khi cần thiết.
Tính bất biến
Các mối quan hệ kiểu con là bất biến nếu chúng không được chuyển giữa các kiểu chung.
Không có quan hệ kiểu con tự động
giữa Box<Dog> và Box<Animal>
Các thùng chứa có thể thay đổi cho phép cả đọc và ghi thường không thể thay đổi để đảm bảo an toàn. Nếu Box<Dog> có thể được coi là Box<Animal> thì ai đó có thể chèn Cat và giả định trong mã được sử dụng ban đầu Box<Dog> có thể bị phá vỡ.
Trong các giao diện và đại biểu chung của .NET, hiệp phương sai và contravariance có thể được chỉ định và các đối số loại bất biến yêu cầu chính xác cùng một loại. Kiểu trả về của vai trò nhà sản xuất có thể được sử dụng theo kiểu hiệp biến và kiểu tham số của vai trò người tiêu dùng có thể được sử dụng trái ngược nhau.
Trong các loại hàm, các tham số thường trái ngược nhau và giá trị trả về là hiệp biến.
Animal → Dog
Hàm này chấp nhận Animal và trả về Dog. Nếu bạn thay thế nó bằng một hàm chỉ nhận đầu vào hẹp hơn, nó có thể không xử lý được Animal đơn giản được người gọi truyền vào. Ngược lại, việc trả lại một Con chó cụ thể hơn sẽ an toàn hơn cho những người gọi mong đợi một Con vật.
Chuyển đổi kiểu và ép kiểu
Chuyển đổi kiểu là quá trình coi giá trị của một loại là giá trị của loại khác hoặc thay đổi biểu thức thực tế.
Chuyển đổi loại có thể được chia như sau:
- Chuyển đổi ngầm
- Chuyển đổi rõ ràng
- Chuyển đổi số để thay đổi biểu thức
- Tiểu loại upcast
- Downcast cần kiểm tra trong quá trình thực hiện
- Quyền anh và mở hộp
- Diễn giải lại con trỏ
- Chuyển đổi tùy chỉnh
Ép kiểu ngầm hoặc ép buộc là một chuyển đổi loại mà ngôn ngữ tự động thực hiện trong các ngữ cảnh nhất định. Chuyển đổi cưỡng bức trong Rust cũng chỉ tự động xảy ra ở một số vị trí nhất định và các mối quan hệ loại hạn chế.
Upcast thường coi một kiểu con là siêu kiểu của nó mà không làm mất thông tin.
Dog → Animal
Downcasting có thể yêu cầu kiểm tra trong quá trình thực thi xem giá trị thực của siêu kiểu có phải là một kiểu con cụ thể hay không.
Animal → Dog
Đặc tả Java cũng giải thích rằng có thể chuyển đổi từ Thread sang Object mà không cần kiểm tra bổ sung, nhưng phải kiểm tra chuyển đổi từ Object sang Thread trong quá trình thực thi để xem liệu đối tượng thực tế có thuộc loại tương ứng hay không.
Trong chuyển đổi số, phạm vi và độ chính xác của giá trị có thể khác nhau. Chuyển đổi mở rộng một số nguyên nhỏ thành loại số nguyên lớn có thể giữ nguyên giá trị, nhưng chuyển đổi số thực thành số nguyên hoặc số nguyên lớn thành số nguyên nhỏ có thể dẫn đến mất thông tin.
Chuyển đổi loại phải phân biệt giữa chuyển đổi làm thay đổi ý nghĩa của giá trị và diễn giải lại chỉ thay đổi cách diễn giải trong khi vẫn giữ nguyên biểu diễn bit. Việc diễn giải lại tùy ý có thể vi phạm căn chỉnh, mẫu bit hợp lệ và các quy tắc bí danh, dẫn đến hành vi không xác định.
Đa hình
Đa hình là thuộc tính mà một hàm, loại hoặc giao diện có thể được áp dụng cho một số kiểu giá trị. Cardelli và Wegner phần lớn chia đa hình thành đa hình phổ quát và đa hình tạm thời, đồng thời chia đa hình phổ quát thành đa hình tham số và đa hình bao hàm.
Đa hình tham số
Đa hình tham số là một phương pháp áp dụng cùng một cách triển khai cho nhiều loại bằng cách trừu tượng hóa một kiểu cụ thể thành một tham số kiểu.
identity<T>(value: T) -> T
identity có thể được áp dụng theo cùng cấu trúc cho số nguyên, chuỗi và đối tượng do người dùng xác định. Việc triển khai chỉ sử dụng các thao tác thường được phép cho tất cả các loại thay vì danh tính cụ thể của T.
Tính đa hình tham số làm tăng tính tổng quát và khả năng sử dụng lại của mã trong đó việc triển khai không thay đổi tùy ý tùy theo loại.
Đa hình nhúng
Đa hình bao gồm là một phương pháp sử dụng các giá trị kiểu con thông qua giao diện siêu kiểu.
draw(shape: Shape)
Nếu Circle và Rectangle là kiểu con của Shape thì chúng có thể được chuyển đến cùng một hàm. Việc triển khai phương thức thực tế có thể được chọn dựa trên loại thời gian chạy thông qua công văn động.
Đa hình tạm thời
Đa hình tạm thời là một cách để một tên hoặc thao tác duy nhất có các cách triển khai khác nhau cho từng loại. Các ví dụ tiêu biểu bao gồm nạp chồng hàm và nạp chồng toán tử.
add(Integer, Integer)
add(Float, Float)
add(Vector, Vector)
Mặc dù nó sử dụng cùng tên add nhưng việc triển khai thực tế có thể khác nhau tùy thuộc vào loại.
Lớp kiểu
Lớp kiểu hệ thống hóa đa hình tùy biến bằng cách khai báo các phép toán mà một kiểu phải cung cấp và định nghĩa riêng bản triển khai cho từng kiểu.
class Eq a where
(==) :: a -> a -> Bool
Các loại cung cấp phiên bản của Eq có thể sử dụng phép toán đẳng thức. Wadler và Blott đã đề xuất một cách thể hiện tính đa hình tạm thời, chẳng hạn như nạp chồng toán tử, dưới dạng một lớp kiểu.
Các đặc điểm của Rust cũng xác định các giao diện trừu tượng mà các kiểu có thể triển khai và có thể bao gồm các hàm, các kiểu liên kết và các hằng số liên quan. Các hàm chung sử dụng giới hạn đặc điểm để giới hạn các loại hành vi mà chúng cho phép.
Generic
Generics là một tính năng ngôn ngữ cho phép tái sử dụng cho nhiều loại bằng cách xác định một phần của loại hoặc hàm làm tham số loại.
struct Pair<T> {
first: T,
second: T,
}
Các ràng buộc có thể được chỉ định trên các tham số loại chung.
fn maximum<T: Ord>(values: &[T]) -> &T
Hàm này không chấp nhận tất cả các loại một cách vô điều kiện mà chỉ chấp nhận các loại cung cấp so sánh theo thứ tự.
Các ràng buộc chung có dạng sau tùy thuộc vào ngôn ngữ:
- Giới hạn trên của lớp hoặc giao diện
- trait bound
- Loại ràng buộc lớp
- Tập phương pháp
- Hạn chế về tuổi thọ
- Điều kiện tồn tại của hàm tạo
- Kiểu giá trị hoặc điều kiện kiểu tham chiếu
- Bộ loại và điều kiện vận hành
Mặc dù các generic trông giống nhau ở cấp độ nguồn, nhưng cách chúng được triển khai trong mã thực thi có thể khác nhau.
Xóa kiểu
Xóa kiểu là một phương pháp chuyển đổi một số hoặc tất cả các đối số loại chung thành một biểu thức chung trong quá trình biên dịch. Các khái quát của Java sử dụng tính năng xóa để duy trì khả năng tương thích ngược và không phải tất cả các đối số kiểu đều có thể được tra cứu trực tiếp trong thời gian chạy. Đặc tả Java xác định các quy tắc để xóa các kiểu chung và các biến kiểu.
Xóa kiểu cho phép nhiều phiên bản loại chia sẻ cùng một mã thực thi, nhưng có thể tạo ra các hạn chế đối với các hoạt động yêu cầu thông tin tham số loại trong khi thực thi.
Đơn hình hóa
Hạ mức là phương pháp tạo mã chuyên dụng cho từng loại cụ thể sử dụng mã chung. Rust kiểm tra vị trí sử dụng chung tại thời điểm biên dịch và tạo mã phù hợp với loại cụ thể.
Tính đơn hình có lợi cho việc tối ưu hóa theo loại cụ thể và gửi tĩnh, nhưng có thể tăng kích thước mã được tạo và thời gian biên dịch khi sử dụng nhiều kết hợp loại.
Bảo toàn kiểu tại runtime
Một số thời gian chạy cũng bảo toàn các đối số kiểu chung trong thời gian chạy. Generics trong .NET duy trì thông tin loại thời gian chạy và không sử dụng tính năng xóa kiểu như Java.
Điều này có thể cung cấp thêm thông tin để kiểm tra kiểu phản ánh và thời gian chạy, nhưng yêu cầu thời gian chạy và siêu dữ liệu để hỗ trợ nó.
Việc triển khai thực tế có thể không hoàn toàn thuộc một trong ba cách tiếp cận này. Các kiểu tham chiếu có thể chia sẻ mã và các kiểu giá trị có thể chuyên biệt hoặc bạn có thể tạo mã chung và chỉ chuyển một số thao tác cho các hàm phụ trợ dành riêng cho từng loại.
Khả năng null và kiểu tùy chọn
null được sử dụng để chỉ ra rằng giá trị không tồn tại hoặc tham chiếu không trỏ đến mục tiêu. Nếu null được ngầm bao gồm trong tất cả các kiểu tham chiếu thì rất khó để xác định mức độ an toàn của đoạn mã sau chỉ dựa vào loại.
user.name
Cần phải có một bước kiểm tra riêng để xác định xem user là vật thật hay null.
Các hệ thống kiểu hỗ trợ an toàn null thường phân biệt các loại như sau:
User
User?
User chỉ cho phép các giá trị khác null và User? cho phép User hoặc null. Kotlin phân biệt khả năng null trong hệ thống kiểu và hạn chế các thao tác truy cập trực tiếp vào các giá trị có thể rỗng.
Kiểu tham chiếu có thể rỗng của C# được triển khai với các chú thích tại thời điểm biên dịch và phân tích luồng cho các kiểu tham chiếu hiện có. string và string? không phải là các loại lớp khác nhau trong thời gian chạy và được trình biên dịch sử dụng để cảnh báo về khả năng sử dụng tham chiếu null.
Các ngôn ngữ hàm và Rust chủ yếu sử dụng phương pháp biểu thị sự tồn tại của một giá trị dưới dạng kiểu tổng.
enum Option<T> {
None,
Some(T),
}
Trong cấu trúc này, trạng thái không có giá trị là một biến thể rõ ràng, do đó cả hai trường hợp phải được xử lý thông qua các hàm kết hợp hoặc khớp mẫu.
Ngay cả khi khả năng null được biểu thị dưới dạng một loại, lỗi null không hoàn toàn biến mất do khả năng tương tác với mã bên ngoài, các vấn đề về xác nhận bắt buộc, phản ánh và thứ tự khởi tạo. Cần phải phân biệt giữa các ranh giới nơi áp dụng các đảm bảo về hệ thống kiểu và các chức năng bỏ qua.
Kiểu nhạy theo luồng và thu hẹp kiểu
Phân tích kiểu theo luồng là phương pháp điều chỉnh cụ thể hơn thông tin loại của cùng một biến theo luồng điều khiển của chương trình.
function length(value: string | string[]) {
if (typeof value === "string") {
return value.length;
}
return value.length;
}
Trong câu lệnh điều kiện, value được thu hẹp thành một chuỗi theo kiểm tra typeof value === "string". Ở các nhánh khác, nó có thể được thu hẹp thành một chuỗi các chuỗi.
TypeScript sử dụng typeof, instanceof, kiểm tra thuộc tính, vị từ loại do người dùng xác định và luồng điều khiển để thu hẹp các loại kết hợp.
Một hệ thống tương tự còn được gọi là Kiểu xuất hiện. Typed Scheme đã đưa vào một hệ thống kiểu xuất hiện có khả năng tinh chỉnh kiểu của biến dựa trên câu lệnh điều kiện và phép kiểm tra kiểu.
Phân tích nhạy cảm với dòng chảy cũng phải xem xét liệu giá trị của các biến có thể thay đổi sau khi thử nghiệm hay không. Các biến có thể thay đổi được chia sẻ, lệnh gọi lại không đồng bộ và trạng thái có thể được thay đổi từ các luồng khác nhau có thể phá vỡ tính hợp lệ của các lần kiểm tra kiểu trước đó. Do đó, trình biên dịch chỉ có thể duy trì các kiểu hẹp trong các biến cục bộ bất biến hoặc phạm vi có thể phân tích được.
Kiểu dần dần
Định kiểu dần dần là một phương pháp cho phép sử dụng cùng nhau các phần được định kiểu tĩnh và các phần được kiểm tra động trong một chương trình.
Thay vì thay đổi cơ sở mã kiểu động hiện có thành kiểu tĩnh cùng một lúc, thông tin loại có thể được thêm vào trên cơ sở mô-đun, hàm hoặc biểu thức. Các bộ phận có thông tin loại cụ thể có thể được kiểm tra tĩnh và kiểm tra thời gian chạy có thể được chèn khi loại vượt qua ranh giới không xác định.
Việc định kiểu dần dần có ý nghĩa chặt chẽ hơn việc chỉ đơn giản cung cấp các kiểm tra tĩnh và động song song. Mối quan hệ giữa các loại chính xác và không xác định, kiểm tra thời gian chạy ở ranh giới và trách nhiệm đối với các lỗi loại phải được xác định một cách có hệ thống. Nghiên cứu định kiểu dần dần được phát triển để cho phép các lập trình viên kiểm soát mức độ kiểm tra tĩnh được áp dụng cho từng phần của chương trình.
TypeScript là một ví dụ điển hình về một ví dụ thực tế về việc thêm phân tích kiểu tĩnh vào mã JavaScript, nhưng không phải tất cả các tính năng của TypeScript đều có thể được xem là hoàn toàn tương đương với một mô hình cụ thể của kiểu gõ lũy tiến chính thức. Điều này là do các hàm bỏ qua phần lớn việc kiểm tra kiểu, chẳng hạn như any, kiểu cấu trúc và ký hiệu loại có thể xóa được tồn tại cùng nhau.
Ưu điểm của các kiểu lũy tiến là phạm vi ứng dụng kiểu có thể được mở rộng trong khi vẫn duy trì mã hiện có và các thư viện bên ngoài. Mặt khác, chi phí kiểm tra và độ phức tạp theo dõi lỗi có thể phát sinh ở ranh giới giữa vùng tĩnh và vùng động, đồng thời cần có các quy tắc ranh giới để đảm bảo rằng các giá trị không được gõ không phá vỡ giả định của vùng tĩnh.
Kiểu tinh chỉnh
Kiểu tinh chỉnh là loại thể hiện phạm vi giá trị chính xác hơn bằng cách thêm các điều kiện logic vào loại hiện có.
PositiveInteger = {
value: Integer | value > 0
}
Các loại số nguyên chung cho phép tất cả các số nguyên, nhưng PositiveInteger chỉ cho phép các số nguyên lớn hơn 0.
Các loại chức năng cũng có thể bao gồm các điều kiện.
divide:
x: Integer
y: Integer where y != 0
→ Integer
Loại này thể hiện điều kiện tiên quyết là giá trị chia không bằng 0.
Trình kiểm tra kiểu cải tiến có thể chuyển các điều kiện logic được tạo trong chương trình sang một công cụ chứng minh tự động như bộ giải SMT. Dòng Liquid Type sử dụng sơ đồ sàng lọc có giới hạn cho phép bạn tự động kiểm tra trong khi thêm các vị từ logic vào các loại. Loại cải tiến có thể xác minh các thuộc tính chương trình chính xác hơn loại chung.
Ví dụ về những gì có thể được thể hiện như một loại cải tiến như sau.
- Chỉ số mảng nằm trong phạm vi
- Số nguyên nằm trong một phạm vi nhất định
- Độ dài của mảng kết quả giống với độ dài đầu vào.
- Mẫu số không bằng 0
- Điều kiện là nó là một bộ sưu tập được sắp xếp
- Quyền truy cập và mức độ bảo mật
- Tràn số học không xảy ra
Khi các điều kiện logic trở nên phức tạp hơn, chi phí kiểm tra kiểu và khó khăn trong việc giải thích lỗi có thể tăng lên. Một phương pháp được sử dụng để giới hạn phạm vi logic mà bộ chuẩn tự động có thể đánh giá hoặc cho phép người dùng cung cấp bằng chứng phụ trợ khi cần thiết.
Kiểu phụ thuộc
Kiểu phụ thuộc là một hệ thống kiểu trong đó loại có thể phụ thuộc vào giá trị. Nếu một kiểu chung chung nhận một kiểu khác làm tham số thì kiểu phụ thuộc cũng có thể sử dụng các giá trị như số tự nhiên, giá trị logic và chuỗi để xác định kiểu.
Ví dụ đại diện là một vectơ có độ dài được bao gồm trong loại.
Vector<Element, 3>
Vector<Element, 4>
Một vectơ có độ dài bằng 3 và một vectơ có độ dài bằng 4 là các loại khác nhau. Hàm kết nối vectơ có thể biểu thị mối quan hệ độ dài dưới dạng các loại.
append:
Vector<T, n>
→ Vector<T, m>
→ Vector<T, n + m>
Tài liệu Agda mô tả kiểu phụ thuộc là họ các loại được lập chỉ mục bởi các đối tượng thuộc loại khác và trích dẫn Vec n làm ví dụ về loại vectơ được lập chỉ mục theo số tự nhiên n.
Trong các loại hàm phụ thuộc, kiểu trả về có thể khác nhau tùy thuộc vào giá trị đầu vào.
isSingleton:
Bool → Type
Có thể tính toán ở cấp độ loại để trả về loại số nguyên nếu giá trị đầu vào là true và loại danh sách số nguyên nếu nó là false. Idris coi các loại là đối tượng hạng nhất, cho phép chúng được chuyển đến các hàm, trả về hoặc đánh giá.
Kiểu phụ thuộc có thể bao gồm các thuộc tính sau trong loại.
- Kích thước của mảng và ma trận
- Trạng thái hiện tại của giao thức
- Thực tế là cây cú pháp thỏa mãn các quy tắc kiểu.
- Độ dài và cấu trúc của kết quả tính toán
- Chứng minh chương trình thỏa mãn một định lý nào đó
- Bằng chứng cho thấy quyền truy cập chỉ mục là hợp lệ
Theo thư từ Curry–Howard, một loại có thể được hiểu là một mệnh đề và giá trị của loại đó có thể được hiểu là bằng chứng của mệnh đề. Việc thực hiện một hàm trở thành một thuật ngữ chứng minh mệnh đề được thể hiện bằng loại. Cấu trúc này cho phép chương trình và bằng chứng được viết bằng cùng một ngôn ngữ.
Các kiểu phụ thuộc cung cấp các hợp đồng rất chính xác nhưng yêu cầu tính toán và chuẩn hóa mức giá trị trong quá trình kiểm tra kiểu. Nếu việc suy luận tự động gặp khó khăn thì loại và thuật ngữ chứng minh phải được viết thủ công và những thay đổi trong việc thực hiện chương trình có thể dẫn đến việc sửa đổi chứng minh.
Kiểu tuyến tính và affine
Trong một hệ thống kiểu điển hình, một giá trị có thể được sử dụng nhiều lần hoặc không sử dụng lần nào. Kiểu tuyến tính hạn chế việc sử dụng một giá trị cụ thể chính xác một lần. Kiểu affine cho phép một giá trị được sử dụng nhiều nhất một lần và cho phép loại bỏ nó mà không cần sử dụng.
LinearResource
Nếu những tài nguyên này không thể được sao chép hoặc sử dụng hai lần, thì những tài nguyên phải có một chủ sở hữu duy nhất, chẳng hạn như bộ xử lý tệp, bộ đệm bộ nhớ, kênh liên lạc và bộ đệm lệnh GPU, có thể được quản lý dưới dạng loại.
Các kiểu tuyến tính kiểm soát việc sử dụng trùng lặp và thiếu sót tài nguyên bằng cách giới hạn số lần sử dụng một giá trị. Các kiểu Affine buộc một số giá trị không bao giờ được sử dụng nhiều lần.
Ví dụ về việc sử dụng như sau:
- Giải phóng bộ nhớ đúng một lần
- Đóng file sau khi sử dụng
- Không sử dụng lại sau khi mở khóa
- Chỉ gửi tin nhắn một lần
- Quản lý tài nguyên phần cứng không thể sao chép
- Sự sao chép hạn chế của các trạng thái lượng tử
- Giới hạn bí danh cho các chương trình đồng thời
Mặc dù Rust không giống như ngôn ngữ gõ tuyến tính truyền thống, nhưng nó kết hợp quyền sở hữu, di chuyển, mượn và thời gian tồn tại với việc kiểm tra kiểu. Quy tắc sở hữu cho phép trình biên dịch kiểm tra các mối quan hệ quản lý bộ nhớ và là cơ sở để cung cấp sự an toàn cho bộ nhớ mà không cần trình thu gom rác.
Quy tắc tham chiếu của Rust cho phép một tham chiếu có thể thay đổi hoặc nhiều tham chiếu bất biến cùng một lúc và hạn chế các tham chiếu tồn tại ngoài mục tiêu hợp lệ của chúng.
Quyền sở hữu và tính tuyến tính cho thấy rằng một hệ thống kiểu có thể theo dõi không chỉ hình thức bên ngoài của một giá trị mà còn cả quyền sử dụng và thời gian tồn tại của nó.
Kiểu hiệu ứng
Một loại hàm điển hình chỉ đại diện cho đầu vào và đầu ra.
String → Integer
Tuy nhiên, ngay cả đối với các chức năng có cùng loại đầu vào và đầu ra, hoạt động thực tế có thể khác nhau rất nhiều.
- Phân tích thuần túy các chuỗi
- Đọc tập tin
- Gửi yêu cầu mạng
- Thay đổi trạng thái toàn cầu
- Đưa ra một ngoại lệ
- Bắt đầu hoạt động không đồng bộ
Kiểu hiệu ứng theo dõi các tác dụng phụ mà một chức năng có thể thực hiện cùng với loại của nó.
readConfig:
Path → Config
effects { FileRead, IOError }
Hệ thống hiệu ứng có thể thể hiện:
- Đầu vào/Đầu ra
- Đọc và thay đổi trạng thái
*Ngoại lệ
- Thực hiện không đồng bộ
- Hiệu ứng tiếp tục và kiểm soát
- Truy cập vùng bộ nhớ
- Tạo nhiệm vụ đồng thời
- Quyền bảo mật
- Độ tinh khiết của chức năng
Lucassen và Gifford đã đề xuất một hệ thống hiệu ứng theo dõi đa hình các hiệu ứng mà một biểu thức có thể thực hiện và phân tích các ràng buộc khi thực hiện song song.
Loại hiệu ứng bao gồm bản chất của quá trình thực hiện cũng như kiểu kết quả thực hiện của chương trình trong giao diện. Điều này có thể ngăn các hàm thuần túy truyền các hàm thay đổi trạng thái khi cần thiết hoặc yêu cầu người gọi xử lý một số trường hợp ngoại lệ nhất định.
Các ngoại lệ được kiểm tra của Java cũng có thể được coi là một dạng theo dõi hiệu ứng hạn chế, nhưng một hệ thống hiệu ứng chung có thể kết hợp và suy luận về các loại tác dụng phụ cũng như ngoại lệ khác nhau.
Kiểu phiên
Kiểu phiên thể hiện các quy tắc về thứ tự, hướng, phân nhánh và kết thúc của các tin nhắn được trao đổi qua kênh liên lạc dưới dạng một loại.
Client:
Send<Request>
Receive<Response>
Close
Loại này đại diện cho một giao thức yêu cầu máy khách gửi yêu cầu và đóng kênh sau khi nhận được phản hồi.
Các loại phía máy chủ thể hiện giao tiếp theo hướng ngược lại.
Server:
Receive<Request>
Send<Response>
Close
Các loại phiên có thể được sử dụng để ngăn chặn các lỗi sau:
- Tin nhắn gửi sai thứ tự
- Nhận được loại tin nhắn không mong muốn
- Nhánh truyền thông không khớp
- Kênh chỉ có một bên bị chấm dứt
*Bỏ qua một số bước trong giao thức
- Vai trò không phù hợp giữa các đối tác truyền thông
Loại phiên nhiều bên có thể xác định toàn bộ giao thức liên lạc giữa nhiều vai trò ngoài hai người tham gia và chiếu điều này vào loại cục bộ của mỗi người tham gia. Nghiên cứu về loại phiên đã được phát triển dựa trên các kiểu cấu trúc các chương trình định hướng giao tiếp.
Các loại phiên thường được kết hợp với các loại tuyến tính. Nếu một trạng thái kênh được sử dụng tùy ý ở nhiều nơi thì tiến trình của giao thức có thể khác nhau, do đó quyền sở hữu kênh và số lần sử dụng có thể bị giới hạn để đảm bảo rằng bước tiếp theo là duy nhất.
Biểu diễn kiểu tại runtime
Việc các loại chỉ tồn tại ở mã nguồn và thời gian biên dịch hay được giữ nguyên trong quá trình thực thi, sẽ khác nhau tùy thuộc vào ngôn ngữ và cách triển khai.
Kiểu được cụ thể hóa duy trì thông tin loại ngay cả trong khi thực thi và có thể được sử dụng để phản ánh, kiểm tra kiểu, tuần tự hóa, v.v. Các đối tượng lớp, bộ mô tả loại thời gian chạy, bảng phương thức ảo, v.v. có thể chứa thông tin này.
Trong Kiểu bị xóa, một số thông tin loại sẽ bị xóa khỏi mã thực thi sau khi kiểm tra kiểu. Các biến loại chung, bí danh loại, thông tin vô hiệu chỉ tĩnh, v.v. có thể bị xóa.
Xóa kiểu không có nghĩa là kiểu đó không ảnh hưởng đến kết quả thực hiện. Thông tin loại có thể đã được sử dụng để xác định:
- Chức năng gọi và quá tải
- Vị trí bộ nhớ
- Ngôn ngữ máy để tạo
- Kiểm tra thời gian chạy để chèn
- Chuyên môn chung
- Phương pháp hủy và sao chép
- ABI và quy ước gọi
- Khả năng tối ưu hóa
Giống như kiểu tham chiếu có thể rỗng của C#, biểu thức thời gian chạy giống nhau, nhưng cũng có thông tin về kiểu chỉ được sử dụng để phân tích luồng trình biên dịch.
Kiểu mạnh và kiểu yếu
“Được gõ mạnh” và “được gõ yếu” là những cách diễn đạt được sử dụng rộng rãi mà không có tiêu chuẩn chính thức nào được thống nhất. Tùy thuộc vào ngữ cảnh, nó có thể đề cập đến các đặc điểm khác nhau, chẳng hạn như:
- Phạm vi chuyển đổi kiểu ẩn
- Khả năng trộn các loại khác nhau
- Khả năng diễn giải lại bộ nhớ
- Có kiểm tra kiểu thời gian chạy hay không
- Khả năng bỏ qua quy tắc loại
- Tự động chuyển đổi giữa chuỗi và số
- Số học con trỏ và hành vi không xác định
- Khi xảy ra lỗi kiểu
Ví dụ: rất khó để so sánh một ngôn ngữ được nhập tĩnh cung cấp nhiều chuyển đổi số ẩn với ngôn ngữ động cung cấp ít chuyển đổi tự động nhưng kiểm tra các loại trong thời gian chạy theo thứ tự từ mạnh đến yếu đơn giản.
Khi cần giải thích chính xác, tốt hơn nên mô tả trực tiếp các đặc tính cụ thể như sau.
- Sử dụng kiểm tra kiểu tĩnh.
- Cho phép chuyển đổi ngầm giữa chuỗi và số.
- Downcast yêu cầu kiểm tra thời gian chạy.
- Con trỏ có thể được diễn giải lại thành các loại tùy ý.
- Đối số kiểu chung được giữ nguyên trong quá trình thực thi.
- Nếu xảy ra lỗi loại, một ngoại lệ sẽ được đưa ra.
- Không cho phép diễn giải lại bộ nhớ trong mã bảo mật.
Đặc tả Java mô tả Java là một ngôn ngữ tĩnh và được định kiểu mạnh, nhưng đây là bối cảnh trong đó nó mô tả các kiểu và hạn chế hoạt động mà Java xác định. Nó không xác định thang điểm mạnh/điểm yếu phổ quát bao gồm các ngôn ngữ khác.
Bỏ qua và ranh giới của hệ thống kiểu
Các ngôn ngữ thực tế cung cấp cách giải quyết cho các trường hợp khó diễn đạt trong hệ thống kiểu hoặc yêu cầu khả năng tương tác với các hệ thống bên ngoài.
Ví dụ đại diện như sau:
- ép kiểu tường minh
- Kiểu động
- Kiểm tra các loại loại trừ như
any - Con trỏ thô
- Khối
unsafe - phản ánh
- Giao diện chức năng bên ngoài
- Tuần tự hóa và giải tuần tự hóa
- Plugin gốc
- Kiểu khẳng định
- Chức năng tích hợp trong trình biên dịch
Những khả năng này có thể cần thiết để lập trình hệ thống, tối ưu hóa hiệu suất và kết nối API cấp thấp. Tuy nhiên, lập trình viên hoặc mã bên ngoài chịu trách nhiệm trực tiếp về các điều kiện mà trình kiểm tra kiểu đang chứng minh.
Để tạo ranh giới an toàn, bạn nên giới hạn chức năng bỏ qua trong phạm vi triển khai nội bộ nhỏ và cung cấp giao diện bên ngoài đáp ứng các quy tắc loại. Trong Rust, phương thức gói triển khai unsafe bên trong một hàm an toàn cũng tương ứng với cấu trúc này.
Dữ liệu bên ngoài cũng nằm ngoài ranh giới tin cậy của hệ thống kiểu. Các gói, tệp và hàng cơ sở dữ liệu JSON và mạng không tự động tuân theo loại mà chương trình của bạn mong đợi. Trong bước giải tuần tự hóa, cấu trúc và phạm vi giá trị phải được xác minh và sau đó được chuyển đổi thành loại nội bộ.
Lựa chọn thiết kế cho hệ thống kiểu
Hệ thống chữ không được thiết kế chỉ để tối đa hóa tính biểu cảm và an toàn. Các nhà thiết kế ngôn ngữ lựa chọn sự cân bằng giữa các yếu tố sau:
- Độ chính xác của việc kiểm tra kiểu
- Số lượng ký hiệu loại
- Khả năng suy luận kiểu
- Thời gian biên dịch
- Dễ hiểu các thông báo lỗi
- Khả năng tương thích với mã hiện có
- Chức năng động và phản ánh
- Hiệu suất thực thi và kích thước mã
- Tách biệt ranh giới biên dịch và mô-đun
- Khả năng tương tác với các ngôn ngữ bên ngoài
- Khả năng phân tích của các công cụ và IDE
- Độ phức tạp của đặc tả và triển khai ngôn ngữ
Một hệ thống kiểu chính xác hơn có thể phát hiện nhiều lỗi hơn trước khi thực thi và ghi lại bản chất của chương trình. Tuy nhiên, vấn đề kiểm tra kiểu có thể trở nên phức tạp hơn và ký hiệu kiểu cũng như mã chứng minh có thể tăng lên. Ngược lại, một hệ thống kiểu đơn giản có thể có ngôn ngữ và cách triển khai dễ dàng hơn nhưng có thể yêu cầu nhiều bất biến hơn trong chương trình để được kiểm tra hoặc kiểm tra trong thời gian chạy.
Các ngôn ngữ hiện đại kết hợp nhiều cấp độ kiểm tra thay vì chỉ chọn một cách tiếp cận. Nó cung cấp kiểu suy luận kiểu và kiểu tĩnh cơ bản, đồng thời bạn có thể thêm kiểu tổng, khớp mẫu, ràng buộc chung, tính vô hiệu, quyền sở hữu, kiểu nâng cao hoặc máy phân tích tĩnh bên ngoài khi cần.
Mục đích của hệ thống kiểu không phải là chấp nhận mọi chương trình có thể. Vai trò của hệ thống kiểu cũng là từ chối các chương trình mà ngôn ngữ không thể xác định là an toàn. Ngay cả khi một chương trình thực sự an toàn, nó vẫn có thể bị từ chối nếu trình kiểm tra kiểu thiếu thông tin để chứng minh rằng nó an toàn. Trong trường hợp này, bạn có thể thay đổi cấu trúc của chương trình, thêm ký hiệu loại và bằng chứng, trì hoãn việc kiểm tra theo thời gian thực hiện hoặc sử dụng các ranh giới không an toàn có giới hạn.
Do đó, hệ thống kiểu không phải là một chức năng để gắn nhãn các giá trị, mà là một hệ thống chính thức thể hiện cấu trúc của chương trình, phạm vi tính toán, tính trừu tượng và quyền sở hữu tài nguyên, lỗi và tác dụng phụ và thậm chí cả các giao thức truyền thông. Bắt đầu từ các chức năng cơ bản bao gồm kiểm tra tĩnh và động, suy luận kiểu, phân kiểu, đa hình, tổng quát và kiểu dữ liệu đại số, nó mở rộng sang các kiểu phụ thuộc, kiểu tuyến tính, kiểu hiệu ứng và kiểu phiên, đồng thời xác định chương trình nào mà ngôn ngữ lập trình có thể diễn đạt và lỗi nào có thể được ngăn chặn trước.
Thiết kế và tiêu chuẩn hóa ngôn ngữ
Thiết kế ngôn ngữ lập trình không chỉ giới hạn ở việc tạo từ khóa và ngữ pháp mới. Nhiệm vụ của nó là xác định vấn đề và môi trường sử dụng chính mà ngôn ngữ đang cố gắng giải quyết và sắp xếp các giá trị, loại, thay đổi trạng thái, lệnh gọi hàm, quản lý bộ nhớ, xử lý lỗi, đồng thời, mô-đun, phương thức thực thi và quy tắc tương tác với các hệ thống bên ngoài vào một hệ thống tính toán nhất quán duy nhất. Thông số ngôn ngữ, cách triển khai, thư viện tiêu chuẩn, công cụ phát triển, quy trình thay đổi và chính sách tương thích phải được kết hợp để tạo thành một hệ sinh thái ngôn ngữ lập trình có thể sử dụng lâu dài.
Tiêu chuẩn hóa ngôn ngữ lập trình là quá trình xác định cú pháp và ngữ nghĩa của ngôn ngữ trong một tài liệu chung và thiết lập các tiêu chuẩn để các cách triển khai khác nhau xử lý cùng một chương trình một cách nhất quán nhất có thể. Tiêu chuẩn hóa nhằm mục đích tăng tính di động của các chương trình và thư viện và hình thành một hệ sinh thái ngôn ngữ không phụ thuộc vào việc triển khai cụ thể. SC 22, theo ISO/IEC JTC 1, là bộ phận tiêu chuẩn hóa quốc tế chịu trách nhiệm về các ngôn ngữ lập trình, môi trường thực thi của chúng và giao diện phần mềm hệ thống, với mục tiêu chính là tính di động.
Không phải tất cả các ngôn ngữ lập trình đều có tiêu chuẩn quốc tế. C (ngôn ngữ lập trình) và C++ được tiêu chuẩn hóa bởi nhóm làm việc ISO/IEC, nhưng ECMAScript, đặc tả cơ sở cho JavaScript, được phát triển bởi TC39 của Ecma International. Python thảo luận về các thay đổi thông qua PEP, Rust thông qua RFC, Go (ngôn ngữ lập trình) thông qua các quy trình đề xuất, Kotlin thông qua KEEP và Swift thông qua Swift Evolution. Do đó, việc phát triển ngôn ngữ lập trình có thể được dẫn dắt theo nhiều cách khác nhau bởi các tổ chức tiêu chuẩn hóa quốc tế, tập đoàn công nghiệp, tập đoàn, dự án phần mềm mở và cộng đồng độc lập.
Mục tiêu thiết kế và lĩnh vực ứng dụng
Khi thiết kế một ngôn ngữ lập trình mới, trước tiên bạn phải quyết định vấn đề mà ngôn ngữ đó dự định giải quyết và đối tượng người dùng chính. Rất khó để đạt được hiệu suất thực thi cao nhất, cú pháp đơn giản nhất, độ an toàn mạnh nhất, biên dịch nhanh nhất, tính di động cao nhất và khả năng tương thích ngược hoàn toàn trong tất cả các trường cùng một lúc. Thiết kế ngôn ngữ là quá trình lựa chọn mức độ ưu tiên giữa các mục tiêu có thể xung đột với nhau.
Mục tiêu thiết kế chính có thể bao gồm:
- Bạn có thể kiểm soát chi tiết phần cứng và tài nguyên hệ thống không?
- Có thể ngăn chặn được lỗi bộ nhớ và lỗi đánh máy ở mức độ nào?
- Có thể dự đoán được chi phí vận hành và thực hiện chương trình không?
- Mã có thể được trình bày ngắn gọn và rõ ràng không?
- Quá trình biên dịch và khởi động chương trình có nhanh không?
- Hiệu suất của các chương trình chạy trong thời gian dài có cao không?
- Có dễ dàng chuyển sang nhiều hệ điều hành và bộ xử lý không?
- Nó có hỗ trợ cơ sở mã lớn và sự cộng tác giữa nhiều nhà phát triển không?
- Có thể sử dụng lại các ngôn ngữ, thư viện và API hệ điều hành hiện có không?
- Tính đồng thời và song song có thể được thể hiện một cách an toàn không?
- Trình biên dịch, thời gian chạy và các công cụ phát triển có dễ triển khai không?
- Khả năng tương thích giữa mã nguồn và thư viện có thể được duy trì lâu dài không?
- Các thông báo lỗi và trải nghiệm gỡ lỗi có dễ hiểu không?
Các ngôn ngữ lập trình hệ thống thường tập trung vào vị trí và tuổi thọ bộ nhớ, quy ước gọi, giao diện chức năng bên ngoài cũng như khả năng dự đoán về độ trễ và chi phí thực thi. Các ngôn ngữ chủ yếu được sử dụng để tự động hóa hoặc phân tích dữ liệu có thể ưu tiên thực thi tương tác, biểu thức ngắn gọn, cấu trúc dữ liệu động và khả năng truy cập thư viện. Ngôn ngữ giáo dục nhấn mạnh vào sự rõ ràng về khái niệm, ngữ pháp nhất quán và thông báo lỗi dễ hiểu, trong khi các ngôn ngữ dành riêng cho miền tập trung vào việc diễn đạt các khái niệm trong một lĩnh vực cụ thể một cách ngắn gọn và chính xác.
Môi trường thực thi mà ngôn ngữ hướng tới cũng ảnh hưởng trực tiếp đến thiết kế của nó. Khi nhắm mục tiêu các hệ điều hành và hệ thống nhúng, các tệp thực thi độc lập, thời gian chạy nhỏ, ABI rõ ràng và hoạt động với nguồn lực hạn chế trở nên quan trọng. Đối với các ngôn ngữ chạy trên trình duyệt web, hộp cát, tải mã lũy tiến và kết nối với môi trường máy chủ là rất quan trọng. Các hoạt động nhẹ, cách ly lỗi, giao tiếp mạng, khả năng quan sát và các công cụ triển khai có thể là những yếu tố chính trong ngôn ngữ dành cho các dịch vụ phân tán.
Nguyên tắc thiết kế
Thay vì thêm các tính năng riêng lẻ một cách độc lập, các nhà thiết kế ngôn ngữ nên thiết lập các nguyên tắc cơ bản sẽ áp dụng cho toàn bộ ngôn ngữ. Việc sử dụng các quy tắc khác nhau cho từng tính năng khiến ngôn ngữ khó học và triển khai, đồng thời có thể dẫn đến hành vi không thể đoán trước khi kết hợp nhiều tính năng.
Nguyên tắc thiết kế đại diện như sau.
- Tính nhất quán đảm bảo rằng các khái niệm tương tự có ngữ pháp và ý nghĩa giống nhau.
- Tính trực giao cho phép một số lượng nhỏ các hàm độc lập được kết hợp theo nhiều cách.
- Tính rõ ràng tiết lộ những thay đổi trạng thái quan trọng, khả năng xảy ra lỗi và chi phí trong mã nguồn.
- Tính trừu tượng bảo toàn các đặc tính hiệu suất và ngữ nghĩa cần thiết trong khi ẩn các chi tiết triển khai.
- Khả năng suy luận cục bộ giảm thiểu tình huống bạn phải kiểm tra toàn bộ chương trình để hiểu một đoạn mã.
- Mở rộng thận trọng đảm bảo rằng các tính năng mới không làm thay đổi ngữ nghĩa của các chương trình hiện có một cách không cần thiết.
- Khả năng triển khai cho phép chức năng do ngôn ngữ xác định được triển khai bằng trình biên dịch và thời gian chạy thực tế.
- Thân thiện với công cụ cho phép trình phân tích cú pháp, trình định dạng, trình gỡ lỗi, trình phân tích tĩnh và IDE phân tích mã của bạn một cách đáng tin cậy.
- Khả năng tương tác cho phép liên kết với hệ điều hành, thư viện bên ngoài, ngôn ngữ khác và định dạng dữ liệu hiện có.
- Đơn giản giữ cho các khái niệm và quy tắc ngoại lệ của ngôn ngữ ở mức nhỏ nhất có thể.
Tính trực giao cao không có nghĩa là tất cả các chức năng phải được kết hợp mà không có bất kỳ hạn chế nào. Ngay cả khi các chức năng riêng lẻ đơn giản thì việc kết hợp hai chức năng có thể tạo ra ý nghĩa mơ hồ hoặc việc triển khai quá phức tạp. Việc cung cấp các thông tin tổng quát, nạp chồng, suy luận kiểu, chuyển đổi tiềm ẩn và phân kiểu con cùng nhau rất hữu ích nhưng nó làm tăng đáng kể độ phức tạp của việc kiểm tra kiểu và chẩn đoán lỗi. Vì vậy, trong thiết kế ngôn ngữ, không chỉ phải xem xét số lượng chức năng mà còn phải xem xét sự tương tác giữa các chức năng.
Sự đơn giản của ngôn ngữ cũng không chỉ có nghĩa là có ít từ khóa hơn. Ngay cả khi ngữ pháp ngắn, ngữ nghĩa thực tế có thể phức tạp nếu có nhiều hành động ngầm ẩn và quy tắc ngoại lệ. Ngược lại, ngay cả khi có một vài thành phần ngữ pháp, nếu vai trò và quy tắc kết hợp của từng thành phần rõ ràng thì hành vi của chương trình có thể được dự đoán dễ dàng hơn.
Mô hình tính toán cốt lõi
Ngôn ngữ phải quyết định mô hình tính toán nào để thể hiện chương trình. Ngôn ngữ mệnh lệnh tập trung vào việc thay đổi trạng thái lưu trữ và thứ tự thực hiện lệnh, trong khi ngôn ngữ chức năng tập trung vào việc đánh giá hàm và chuyển đổi giá trị. Ngôn ngữ hướng đối tượng tập trung vào đối tượng và thông điệp hoặc lệnh gọi phương thức, trong khi ngôn ngữ logic tập trung vào sự kiện, quy tắc và truy vấn.
Các ngôn ngữ lập trình hiện đại thường hỗ trợ nhiều mô hình, nhưng không phải tất cả các mô hình đều có trọng lượng như nhau trong ngôn ngữ. Ngay cả khi hàm là giá trị hạng nhất, nếu cấu trúc cơ bản của chương trình và thư viện chuẩn được thiết kế xung quanh các đối tượng có thể thay đổi và luồng điều khiển mệnh lệnh, thì ngôn ngữ có thể được sử dụng chủ yếu theo cách mệnh lệnh và hướng đối tượng.
Khi quyết định mô hình tính toán phải thiết kế đồng thời các yếu tố sau:
- Giá trị và đối tượng là gì?
- Liệu giá trị có nhận dạng và có thể thay đổi hay không
- Cách gọi hàm và phương thức
- Biểu thức được đánh giá theo thứ tự nào?
- Nơi lưu trữ trạng thái chương trình và cách thay đổi nó
- Cách thể hiện tác dụng phụ và đầu vào/đầu ra
- Các lỗi và lỗi được truyền đạt như thế nào?
- Cách thức chia sẻ hoặc trao đổi trạng thái của các tác vụ đồng thời
- Bạn phân rã và kết hợp chương trình thành những đơn vị nào?
Điều quan trọng là phải tổ chức mô hình tính toán về mặt ngữ nghĩa trước ngữ pháp. Cùng một mô hình tính toán có thể được biểu diễn bằng các ngữ pháp khác nhau, nhưng nếu ngữ pháp được quyết định trước trong khi ý nghĩa chưa rõ ràng, các quy tắc ngoại lệ và hành động tiềm ẩn có thể tăng lên khi các tính năng được thêm vào sau này.
Giá trị và mô hình dữ liệu
Thiết kế ngôn ngữ yêu cầu xác định những giá trị mà chương trình xử lý. Ngoài các giá trị nguyên thủy như số nguyên, số thực, giá trị logic, ký tự và chuỗi, mảng, bộ dữ liệu, bản ghi, hàm, đối tượng, con trỏ, tham chiếu, kiểu tổng và mô-đun cũng có thể được đưa vào mô hình dữ liệu của ngôn ngữ.
Một mô hình dữ liệu phải trả lời các câu hỏi sau:
- Các giá trị được sao chép trực tiếp hay được chia sẻ thông qua tham chiếu?
- Giá trị có nhận dạng duy nhất không?
- Giá trị có thể thay đổi sau khi được tạo không?
- Cách phân biệt giữa bản sắc và bình đẳng
- Giá trị đó có biểu hiện gì trong bộ nhớ?
- Ai quản lý tuổi thọ của giá trị?
- Bạn có cho phép tham chiếu vòng tròn không?
- Các hàm và kiểu có thể được coi là giá trị không?
- Thông tin loại có được bảo toàn trong thời gian chạy không?
Sự khác biệt giữa kiểu giá trị và kiểu tham chiếu không được định nghĩa đơn giản là sự khác biệt giữa ngăn xếp và đống. Điều quan trọng là liệu các phép gán và đối số có truyền các giá trị sao chép hay chia sẻ tham chiếu hay không và liệu các thay đổi có được quan sát thấy trong các tham chiếu khác hay không. Trong quá trình triển khai được tối ưu hóa, các kiểu giá trị có thể được đặt trên vùng heap hoặc các đối tượng kiểu tham chiếu có thể được đặt trên ngăn xếp, do đó ngữ nghĩa ngôn ngữ và vị trí bộ nhớ cụ thể phải được phân biệt.
Trạng thái và khả năng thay đổi
Trạng thái chương trình có thể được thay đổi hay không và ở mức độ nào cũng là một vấn đề thiết kế quan trọng. Các biến và đối tượng có thể thay đổi dễ dàng thể hiện trực tiếp trạng thái của một hệ thống thực tế, nhưng việc chia sẻ cùng một trạng thái trong nhiều phần có thể gây khó khăn cho việc suy ra hành vi của chương trình.
Một ngôn ngữ có thể kiểm soát khả năng thay đổi theo những cách sau:
- Tất cả các biến đều có thể thay đổi theo mặc định.
- Tách các biến bất biến và biến đổi thành các phần khai báo riêng biệt.
- Phân biệt giữa khả năng thay đổi của chính đối tượng và khả năng thay đổi của tham chiếu.
- Biểu thị theo loại liệu hàm có thay đổi trạng thái bên ngoài hay không.
- Giới hạn thay đổi giá trị chỉ cho một chủ sở hữu.
- Các giá trị được chia sẻ chỉ có thể được truy cập một cách bất biến.
- Giới hạn các thay đổi trạng thái bên trong của một mô-đun hoặc đối tượng cụ thể.
Tính bất biến không phải là một khái niệm loại bỏ hoàn toàn trạng thái của một chương trình. Đây là phương pháp kiểm soát vị trí và phạm vi thay đổi bằng cách tạo một giá trị mới hoặc chuyển trạng thái dưới dạng đối số rõ ràng và giá trị trả về. Ngược lại, ngay cả trong các ngôn ngữ cung cấp khả năng thay đổi, các thay đổi trạng thái có thể được bản địa hóa thông qua các hạn chế đóng gói, quyền sở hữu và quyền truy cập.
Thiết kế hệ thống kiểu
Hệ thống kiểu xác định mối quan hệ giữa các giá trị và thao tác mà ngôn ngữ sẽ cho phép. Các nhà thiết kế ngôn ngữ phải quyết định cách kết hợp kiểm tra kiểu tĩnh và động, suy luận kiểu, phân kiểu, tổng quát, quá tải, chuyển đổi kiểu và thông tin kiểu thời gian chạy.
Việc thiết kế một hệ thống kiểu bao gồm các lựa chọn sau:
- Loại nên được kiểm tra trước khi thực hiện hoặc trong khi thực hiện?
- Người lập trình nên chỉ ra trực tiếp các loại ở mức độ nào?
- Tên loại hoặc cấu trúc nào sẽ được sử dụng làm tiêu chuẩn tương thích?
- Việc chuyển đổi loại tiềm ẩn sẽ được cho phép ở mức độ nào?
- Cách thể hiện và triển khai mã chung
- Có nên đưa các điều kiện về tính vô hiệu và lỗi vào loại này không?
- Trình kiểm tra kiểu có theo dõi quyền sở hữu và thời gian tồn tại của giá trị không?
- Các tác dụng phụ và ngoại lệ của hàm có được thể hiện dưới dạng loại không?
- Các hoạt động không an toàn sẽ được phép ở ranh giới nào?
Việc tăng tính biểu cảm của hệ thống kiểu cho phép kiểm tra nhiều lỗi và bất biến hơn tại thời điểm biên dịch, nhưng cũng có thể làm phức tạp việc kiểm tra kiểu và thông báo lỗi. Ngược lại, một hệ thống kiểu đơn giản giúp ngôn ngữ và cách triển khai dễ hiểu hơn nhưng lại có nhiều điều kiện hơn để kiểm tra và kiểm tra trong thời gian chạy.
Suy luận kiểu không nên được thiết kế chỉ để bỏ qua ký hiệu. Bằng cách suy ra các loại biến cục bộ rõ ràng nhưng làm cho các loại đó rõ ràng ở ranh giới mô-đun và hàm công khai, bạn có thể giảm độ dài của mã trong khi vẫn duy trì hợp đồng API rõ ràng.
Luồng điều khiển và quy tắc đánh giá
Luồng điều khiển xác định phần nào của chương trình được thực hiện và theo thứ tự nào. Ngôn ngữ phải xác định các quy tắc để thực thi tuần tự, phân nhánh có điều kiện, lặp, gọi hàm, đệ quy, xử lý ngoại lệ, khớp mẫu, coroutine và thực thi không đồng bộ.
Nếu thứ tự đánh giá không rõ ràng, các biểu thức có tác dụng phụ có thể tạo ra các kết quả khác nhau trong quá trình triển khai. Do đó, đặc tả ngôn ngữ phải chỉ định những điều sau:
- Các đối số của hàm được đánh giá theo thứ tự nào?
- Toán tử logic có thực hiện đánh giá ngắn mạch không?
- Ưu tiên và phương hướng kết hợp nhiệm vụ và hoạt động như thế nào?
- Khi nào các giá trị tạm thời và biến cục bộ được tạo và hủy?
- Hoạt động dọn dẹp nào được thực hiện khi xảy ra ngoại lệ?
- Tại thời điểm nào hàm không đồng bộ dừng và tiếp tục thực thi?
- Các hoạt động bộ nhớ được quan sát trên nhiều luồng theo thứ tự nào?
Nếu ngôn ngữ không chỉ định một số thứ tự đánh giá, việc triển khai có thể chọn nhiều thứ tự cho mục đích tối ưu hóa. Tuy nhiên, nếu người lập trình dựa vào thứ tự không xác định thì kết quả có thể khác nhau tùy thuộc vào cách triển khai và mức độ tối ưu hóa. Khi cho phép quyền tự do này, đặc tả phải phân biệt rõ ràng giữa điều gì được đảm bảo và điều gì không.
Hàm và mô hình gọi
Hàm là đơn vị cơ bản để phân tách và tái sử dụng tính toán trong hầu hết các ngôn ngữ lập trình. Thiết kế hàm vượt xa cú pháp khai báo đơn giản và bao gồm các quy ước truyền và trả về tham số, thu thập, nạp chồng, đa hình và gọi.
Ngôn ngữ phải xác định những điều sau:
- Các đối số được truyền theo giá trị hay theo tham chiếu?
- Nó có hỗ trợ phân phối theo tên hoặc đánh giá lười biếng không?
- Nó có chấp nhận các đối số mặc định và biến đổi không?
- Các hàm có thể được coi là giá trị hạng nhất không?
- Cách bao đóng nắm bắt các biến bên ngoài
- Có được phép nạp chồng hàm và nạp chồng toán tử không?
- Bạn có sử dụng công văn động cho các cuộc gọi phương thức không?
- Việc tối ưu hóa cuộc gọi đuôi có được đảm bảo không?
- Cách thể hiện lỗi và tác động mà hàm có thể trả về
- ABI nào được sử dụng khi gọi bằng mã bên ngoài?
Việc một bao đóng sao chép một biến bên ngoài theo giá trị hay ghi lại nó bằng tham chiếu và cách nó kéo dài tuổi thọ của biến được ghi lại, đều gắn chặt với mô hình bộ nhớ của ngôn ngữ. Mô hình gọi hàm cũng phải xem xét hệ thống kiểu, mô hình đối tượng và việc triển khai thời gian chạy.
Quản lý bộ nhớ và tài nguyên
Các nhà thiết kế ngôn ngữ phải quyết định cách quản lý vòng đời của các đối tượng, bộ nhớ, tệp, kết nối mạng, khóa, tài nguyên GPU, v.v.
Các phương pháp đại diện như sau.
- Phân bổ và phân bổ trực tiếp bởi người lập trình.
- Tính toán số lượng tài liệu tham khảo đến đối tượng và tự động giải phóng nó.
- Sử dụng bộ sưu tập rác để theo dõi các đối tượng có thể tiếp cận.
- Quản lý đối tượng theo khu vực hoặc đấu trường.
- Kiểm tra tĩnh quyền sở hữu và vòng đời.
- Tự động thực thi chức năng hủy hoặc dọn dẹp khi phạm vi kết thúc.
- Sử dụng nhiều phương pháp cùng nhau.
Quản lý bộ nhớ tự động không có nghĩa là các tài nguyên bên ngoài được tự động sắp xếp vào thời điểm thích hợp. Ngay cả trong các ngôn ngữ có trình thu gom rác, các tài nguyên phải được trả về ngay lập tức, chẳng hạn như tệp, ổ cắm và khóa, đều yêu cầu một cấu trúc tổ chức riêng.
Thực tiễn quản lý bộ nhớ ảnh hưởng đến hiệu suất thực thi, độ trễ, độ an toàn, ABI, khả năng tương tác với các ngôn ngữ bên ngoài và cấu trúc chương trình của ngôn ngữ. Quản lý bộ nhớ thủ công cung cấp khả năng kiểm soát chi tiết về chi phí thực thi nhưng yêu cầu người lập trình phải quản lý các rủi ro như sử dụng sau khi rảnh, trống gấp đôi và rò rỉ. Việc thu gom rác tự động hóa vòng đời của đối tượng nhưng có thể phải chịu chi phí thu thập, thời gian ngừng hoạt động và độ phức tạp trong thời gian chạy. Các phương pháp tiếp cận dựa trên quyền sở hữu có thể chặn nhiều lỗi một cách tĩnh, nhưng yêu cầu các ràng buộc bổ sung về mối quan hệ tham chiếu và thời gian tồn tại.
Xử lý lỗi
Ngôn ngữ phải xác định cách thể hiện và truyền đạt thất bại. Mã lỗi có thể được trả về hoặc có thể đưa ra một ngoại lệ, đồng thời thành công và thất bại cũng có thể được biểu thị dưới dạng kiểu tổng hoặc kiểu kết quả.
Những điều cần cân nhắc khi thiết kế xử lý lỗi là:
- Phân biệt giữa lỗi có thể phục hồi và lỗi chương trình?
- Các lỗi có được chỉ định trong loại hàm không?
- Buộc người gọi xử lý lỗi
- Các ngoại lệ di chuyển ranh giới hàm như thế nào
- Cách sắp xếp tài nguyên khi tua lại ngăn xếp
- Có thể bảo tồn nhiều lỗi cùng một lúc không?
- Các lỗi trong hoạt động không đồng bộ và đồng thời xảy ra ở đâu?
- Cách chuyển đổi lỗi ở ranh giới bằng ngôn ngữ bên ngoài
Các ngoại lệ có thể làm cho việc truyền lỗi trở nên ngắn gọn nhưng có thể khó biết được ngoại lệ nào đang được ném ra chỉ bằng cách nhìn vào phần khai báo hàm. Các kiểu kết quả làm cho lỗi tiềm ẩn rõ ràng nhưng có thể dẫn đến việc truyền mã lặp đi lặp lại nhiều hơn. Các ngôn ngữ có thể giảm những chi phí này thông qua sự kết hợp giữa các toán tử ngữ pháp, hệ thống kiểu, khớp mẫu và truyền bá.
Mô hình đồng thời và bộ nhớ
Các ngôn ngữ hiện đại phải tính đến môi trường nơi nhiều tác vụ được thực thi đồng thời. Chỉ cung cấp API để tạo thread là chưa đủ; cần có mô hình bộ nhớ để xác định thứ tự mà các đơn vị thực thi khác nhau quan sát thao tác bộ nhớ.
Thiết kế đồng thời bao gồm các lựa chọn sau:
- Nó có sử dụng bộ nhớ và luồng dùng chung không?
- Nó có tập trung vào việc gửi tin nhắn và các kênh không?
- Nó có tạo ra sự cách ly với các actor không?
- Nó có cung cấp coroutines và các hàm không đồng bộ không?
- Cách xác định các phép toán nguyên tử và thứ tự bộ nhớ
- Bạn có coi các cuộc đua dữ liệu là lỗi hoặc hành vi không xác định không?
- Cách định cấu hình hủy tác vụ và truyền bá lỗi
- Ngôn ngữ có bao gồm cấu trúc đồng thời không?
- Bạn thể hiện sự lặp lại song song và tính song song của dữ liệu như thế nào?
Trình biên dịch và CPU có thể thay đổi thứ tự thực tế của hoạt động bộ nhớ vì lý do hiệu suất. Mô hình bộ nhớ của ngôn ngữ phải xác định những di chuyển nào được phép và hình thức hoạt động đồng bộ hóa mối quan hệ thứ tự nào. Nếu mô hình bộ nhớ không rõ ràng, cùng một chương trình có thể hoạt động khác nhau tùy thuộc vào bộ xử lý và trình biên dịch.
Mô-đun và không gian tên
Hệ thống mô-đun là một hệ thống phân tách chương trình thành nhiều đơn vị và phân biệt giao diện chung cũng như triển khai nội bộ của từng đơn vị.
Thiết kế mô-đun phải xác định các yếu tố sau:
- Một tập tin và một mô-đun có phải là cùng một đơn vị không?
- Nhiều tập tin có thể tạo thành một mô-đun không?
- Các không gian tên có thể lồng nhau được không?
- Cách phân biệt khai báo công khai và khai báo riêng
- Cách giải quyết xung đột tên khi nhập mô-đun
- Bạn có cho phép phụ thuộc vòng tròn không?
- Các module có thể được biên dịch riêng biệt không?
- Thông tin thuộc loại và chức năng công khai được lưu trữ ở định dạng nào?
- Cách phân biệt giữa các gói, mô-đun và thư viện
- Các gói có phiên bản khác nhau có thể sử dụng đồng thời được không?
Một hệ thống mô-đun tốt sẽ ẩn việc triển khai, làm rõ các phần phụ thuộc và hỗ trợ quá trình biên dịch tăng dần. Mặt khác, nếu có quá nhiều chi tiết triển khai được hiển thị trong giao diện chung thì ngay cả những thay đổi nhỏ bên trong cũng có thể dẫn đến việc biên dịch lại các mô-đun phụ thuộc hoặc các vấn đề về tương thích.
Khả năng tương tác và giao diện bên ngoài
Để một ngôn ngữ lập trình mới có thể được sử dụng trong môi trường thực, nó phải có khả năng kết nối với các hệ điều hành, thư viện, định dạng dữ liệu hiện có và các ngôn ngữ khác. Khả năng tương tác ngôn ngữ đặc biệt quan trọng trong việc hình thành một hệ sinh thái sớm.
Thiết kế khả năng tương tác bao gồm các yếu tố sau:
- Có thể gọi C ABI và các hàm gốc không?
- Cấu trúc, mảng và chuỗi được truyền ở định dạng bộ nhớ nào?
- Ai sở hữu đồ vật và chịu trách nhiệm giải phóng nó?
- Lệnh gọi lại có thể được chuyển sang các ngôn ngữ khác không?
- Các trường hợp ngoại lệ có thể vượt qua ranh giới ngôn ngữ không?
- Các đối tượng được quản lý bởi thời gian chạy có thể được cố định để chúng không di chuyển không?
- Cách diễn đạt các khái quát, lớp và giao diện bằng các ngôn ngữ khác
- Thời gian chạy và tệp nhị phân của các phiên bản khác nhau có thể được sử dụng cùng nhau không?
Giao diện bên ngoài có thể là ranh giới nơi sự đảm bảo an toàn ngôn ngữ bị phá vỡ. Nếu một hàm bên ngoài trả về một con trỏ hoặc độ dài không chính xác thì lỗi có thể xảy ra ngay cả trong ngôn ngữ an toàn, do đó cần có một lớp để xác minh dữ liệu bên ngoài và chuyển đổi nó thành loại nội bộ an toàn.
Thiết kế cú pháp
Cú pháp là một cách diễn đạt các khái niệm ngôn ngữ thông qua các chữ cái và mã thông báo. Thiết kế cú pháp phải xem xét không chỉ độ dài của mã mà còn cả khả năng đọc, sự mơ hồ, khả năng phục hồi lỗi và khả năng sử dụng công cụ.
Các quyết định được đưa ra trong thiết kế cú pháp như sau:
- Các dạng định danh và từ khóa
- Cách phân biệt giữa câu và khối
- Ý nghĩa của khoảng trắng và thụt lề
- Ưu tiên người vận hành và hướng kết hợp
- Dạng khai báo hàm, kiểu, module
- Cú pháp của generic và ký hiệu kiểu
- Định dạng bình luận và bình luận tài liệu
- Ký hiệu chuỗi và số
- Cú pháp macro và siêu lập trình
- Phục hồi sau lỗi ngữ pháp
Cú pháp nên tránh các cấu trúc mơ hồ, có thể mở ra nhiều cách hiểu. Ngay cả khi trình phân tích cú pháp có thể diễn giải nó, các biểu thức mà con người dễ hiểu nhầm có thể làm giảm khả năng bảo trì. Ngoài ra, việc thêm từ khóa mới có thể phá vỡ các chương trình sử dụng cùng tên với mã định danh trong mã hiện có, do đó chính sách từ dành riêng được liên kết với sự phát triển ngôn ngữ lâu dài.
Thiết kế cú pháp cũng ảnh hưởng đến các trình định dạng tự động, IDE và trình tạo mã. Nếu ý nghĩa của mã thông báo thay đổi quá mức tùy theo ngữ cảnh hoặc nếu ngữ pháp dựa vào thông tin từ các tệp khác thì việc hỗ trợ trình soạn thảo và phân tích cú pháp độc lập có thể trở nên khó khăn.
Tách biệt cú pháp và ngữ nghĩa
Đặc tả ngôn ngữ phân biệt giữa các chương trình đúng về mặt cú pháp và các chương trình đúng về mặt ngữ nghĩa. Ngữ pháp xác định cấu trúc trong đó các mã thông báo có thể được kết hợp, nhưng tên có được khai báo hay không, tính tương thích loại, quyền truy cập và kết quả đánh giá được xác định bởi các quy tắc ngữ nghĩa riêng biệt.
Ví dụ: biểu thức sau đây có thể đúng về mặt ngữ pháp:
left + right
Tuy nhiên, hệ thống kiểu và phân tích ngữ nghĩa sẽ xác định liệu phép cộng có thể được áp dụng cho left và right hay không, cách triển khai phép cộng nào được chọn và kiểu kết quả là gì.
Thông số kỹ thuật ngôn ngữ thường được mô tả trong các lớp sau:
- Ngữ pháp từ vựng
- Ngữ pháp cú pháp
- quy tắc ngữ nghĩa tĩnh
- Ý nghĩa thực thi động
- Giao diện thư viện
- Phạm vi mà việc triển khai có thể chọn
- Những hành vi mà chương trình không nên dựa vào
Nếu chỉ có cú pháp chi tiết và ý nghĩa không rõ ràng thì nhiều lần triển khai có thể xử lý cùng một mã nguồn một cách khác nhau. Ngược lại, nếu ý nghĩa được xác định quá chặt chẽ với các quy trình triển khai thì việc sử dụng các chiến lược triển khai hoặc tối ưu hóa mới sẽ trở nên khó khăn. Đặc tả phải xác định rõ ràng các kết quả và hành vi có thể quan sát được, đồng thời không áp đặt các hạn chế không cần thiết lên cấu trúc bên trong của quá trình triển khai.
Đặc tả ngôn ngữ
Đặc tả ngôn ngữ là tài liệu quy chuẩn xác định chương trình nguồn nào là chính xác và ý nghĩa của chương trình chính xác. Một đặc tả khác với một tài liệu chỉ mô tả hành vi hiện tại của một trình biên dịch cụ thể. Việc triển khai phải tuân theo đặc tả và nếu hành vi của việc triển khai khác với đặc tả thì nó được coi là lỗi triển khai hoặc sự mơ hồ trong đặc tả.
Một đặc tả ngôn ngữ điển hình bao gồm:
- Bộ ký tự và mã hóa mã nguồn
- Ngữ pháp mã thông báo và từ vựng
- Ngữ pháp cú pháp
- Tên và phạm vi
- Các loại và chuyển đổi
- Biểu thức và tuyên bố
- Khai báo và module
- Thứ tự thực hiện và quy tắc đánh giá
- Mô hình đối tượng và bộ nhớ
- Hoạt động đồng thời và nguyên tử
- Lỗi và ngoại lệ
- Thư viện chuẩn
- Tuân thủ các điều kiện thực hiện
- Triển khai hành vi được xác định/không xác định/không xác định
- Mối quan hệ với các tiêu chuẩn khác
C (ngôn ngữ lập trình) Mục đích của tiêu chuẩn là xác định hình thức và cách diễn giải các chương trình được thể hiện bằng C và cung cấp cơ sở chung cho các chương trình di động và cách triển khai phù hợp. WG14 chịu trách nhiệm tiêu chuẩn hóa quốc tế về C và WG21 chịu trách nhiệm về tiêu chuẩn hóa quốc tế về C++.
Nội dung quy phạm và nội dung giải thích
Trong một tài liệu tiêu chuẩn, bạn có thể phân biệt giữa nội dung quy chuẩn mà việc triển khai phải tuân theo và nội dung giải thích giúp hiểu rõ.
Nội dung quy chuẩn có thể bao gồm các biểu thức như:
- Việc thực hiện phải thực hiện một thao tác cụ thể.
- Chương trình phải đáp ứng một số điều kiện nhất định.
- Nếu các điều kiện bị vi phạm, phải cung cấp chẩn đoán.
- Kết quả phải nằm trong phạm vi xác định.
Nội dung mô tả cung cấp các ví dụ, nền tảng thiết kế, khuyến nghị và tài liệu tham khảo nhưng có thể không trực tiếp xác định tính phù hợp để triển khai.
Nếu sự khác biệt này không rõ ràng thì có thể nảy sinh tranh luận về việc liệu mã ví dụ và câu giải thích có nên được hiểu là quy tắc hay không. Vì vậy, điều mong muốn là các đặc tả hình thức cần phân biệt rõ ràng giữa các yêu cầu và mô tả tham chiếu.
Chương trình phù hợp và bản triển khai phù hợp
Các tiêu chuẩn ngôn ngữ có thể xác định sự phù hợp của không chỉ các chương trình mà còn cả việc triển khai. Việc triển khai phù hợp phải hỗ trợ các chức năng cú pháp, ngữ nghĩa và thư viện theo yêu cầu của tiêu chuẩn và cung cấp các chẩn đoán cần thiết.
Việc triển khai có thể cung cấp chức năng mở rộng vượt quá tiêu chuẩn. Tuy nhiên, bạn không được thay đổi ý nghĩa của các chương trình phù hợp không sử dụng phần mở rộng hoặc từ chối các chương trình tiêu chuẩn. Việc triển khai cũng có thể cung cấp các chế độ mở rộng riêng biệt và các chế độ tiêu chuẩn nghiêm ngặt.
Sự phù hợp không có nghĩa là việc triển khai phải tạo ra cùng một ngôn ngữ máy trong tất cả các chương trình. Các tối ưu hóa, vị trí bộ nhớ và cấu trúc dữ liệu nội bộ có thể được tự do lựa chọn, nhưng kết quả có thể quan sát được của chương trình phải nằm trong phạm vi được đặc tả cho phép.
Hành vi do bản triển khai xác định, không được chỉ định và không xác định
Đặc tả ngôn ngữ có thể không khóa tất cả hành vi chi tiết thành một. Một số hoạt động có thể được chia thành nhiều loại để cho phép có sự khác biệt về phần cứng và hệ điều hành hoặc để mang lại sự tự do cho việc tối ưu hóa triển khai.
Hành vi do triển khai xác định
Hành động do triển khai xác định là các hành động trong đó quá trình triển khai có thể chọn một trong nhiều tùy chọn nhưng phải ghi lại lựa chọn mà nó đã thực hiện.
Ví dụ: sau đây có thể là định nghĩa triển khai:
- Kích thước của kiểu số nguyên cơ bản
- Ký tự có được ký hay không
- Xử lý các tập tin cụ thể và mã hóa ký tự
- Loại tiện ích mở rộng được cung cấp bởi việc triển khai
- Quy ước gọi dành riêng cho nền tảng
Một chương trình có thể dựa vào hành vi của nó bằng cách kiểm tra tài liệu triển khai nhưng kết quả có thể khác nhau khi được chuyển sang một triển khai khác.
Hành vi không được chỉ định
Hành vi không được chỉ định là một hoạt động trong đó đặc tả cho phép nhiều kết quả và việc triển khai không nhất thiết phải ghi lại kết quả mà nó đã chọn. Ngay cả trong cùng một quá trình triển khai, việc lựa chọn có thể khác nhau tùy thuộc vào điều kiện tối ưu hóa hoặc thực thi.
Các chương trình chỉ được giả định rằng một trong những kết quả được phép sẽ xảy ra và không nên dựa vào bất kỳ kết quả cụ thể nào.
Hành vi không xác định
Hành vi không xác định là hành vi mà đặc tả không quy định kết quả. Bản triển khai không bảo đảm một kết quả cụ thể cho chương trình đã phát sinh hành vi đó.
Hành vi không xác định cũng được sử dụng trong các ngôn ngữ gần như phần cứng để cho phép triển khai đa dạng và hiệu suất cao. Tuy nhiên, nếu lập trình viên khiến điều này xảy ra trong đường dẫn thực thi thông thường, nó có thể dẫn đến các lỗ hổng bảo mật và các lỗi khó lường.
Cần phải phân biệt giữa hành vi không xác định và lỗi triển khai. Nếu chương trình tuân thủ các đặc tả nhưng việc triển khai tạo ra kết quả không chính xác thì đó là lỗi triển khai. Ngược lại, nếu một chương trình thực hiện một thao tác bị cấm theo đặc tả thì nó không thể bị coi là lỗi chỉ vì việc triển khai không tạo ra kết quả cụ thể.
Mối quan hệ giữa đặc tả và triển khai
Một ngôn ngữ lập trình có thể có nhiều cách triển khai. Mỗi triển khai có thể sử dụng các cấu trúc trình biên dịch, phương pháp tối ưu hóa, máy ảo, trình thu gom rác, triển khai thư viện tiêu chuẩn và nền tảng đích khác nhau.
Sự tồn tại của nhiều triển khai có những ưu điểm sau:
- Sự phụ thuộc vào các công ty hoặc dự án cụ thể có thể được giảm bớt.
- Có thể sử dụng các chiến lược triển khai phù hợp với các môi trường thực thi khác nhau.
- Sự mơ hồ về thông số kỹ thuật có thể được phát hiện thông qua so sánh giữa các lần triển khai.
- Các lỗi xảy ra trong một lần triển khai có thể được xác nhận trong một lần triển khai khác.
- Thử nghiệm các tối ưu hóa và cấu trúc thời gian chạy mới.
Mặt khác, nếu nhiều quá trình triển khai thực sự xử lý cùng một chương trình một cách khác nhau thì hệ sinh thái có thể bị phân mảnh. Để ngăn chặn điều này, cần có các thông số kỹ thuật rõ ràng, kiểm tra sự phù hợp và kiểm tra sự khác biệt giữa các lần triển khai.
Trong các ngôn ngữ chỉ sử dụng hiệu quả một cách triển khai, hành vi của việc triển khai đó có thể đóng vai trò là một đặc tả không chính thức. Tuy nhiên, nếu không thể phân biệt được hành vi ngẫu nhiên trong quá trình triển khai với các quy tắc ngôn ngữ dự định thì việc tối ưu hóa hoặc sửa lỗi tiếp theo có thể phá vỡ chương trình hiện có.
Bản triển khai tham chiếu
Bản triển khai tham chiếu là trình biên dịch hoặc trình thông dịch đại diện triển khai đặc tả ngôn ngữ. Việc triển khai tham chiếu kiểm tra các tính năng ngôn ngữ trong thực tế và cung cấp đường cơ sở để các cách triển khai khác nhau có thể so sánh hành vi của chúng.
Tuy nhiên, việc triển khai tham chiếu và đặc tả ngôn ngữ không giống nhau. Việc triển khai tham chiếu cũng có thể có sai sót và chỉ sử dụng một trong số các phương pháp triển khai được đặc tả cho phép. Ngay cả khi một triển khai khác có cấu trúc bên trong hoặc các đặc tính hiệu suất khác với triển khai tham chiếu thì đó vẫn có thể là một triển khai phù hợp nếu nó đáp ứng các đặc tả.
Nếu việc triển khai tham chiếu trở thành một tiêu chuẩn quá mạnh thì ngay cả các chi tiết triển khai không có trong đặc tả cũng có thể trở thành một tiêu chuẩn thực tế. Vì vậy, điều mong muốn là các dự án ngôn ngữ nên phân tách rõ ràng các quy tắc dự kiến thành các thông số kỹ thuật và các bài kiểm tra.
Kiểm tra sự phù hợp
Kiểm tra tuân thủ là một tập hợp các kiểm tra nhằm đảm bảo rằng việc triển khai đáp ứng các yêu cầu của đặc tả ngôn ngữ và thư viện tiêu chuẩn.
Kiểm tra sự phù hợp có thể bao gồm:
- Xác minh rằng chương trình đúng được xử lý bình thường
- Đảm bảo rằng chương trình bị lỗi cung cấp các chẩn đoán cần thiết
- Kiểm tra kết quả thực hiện của biểu thức và luồng điều khiển
- Kiểm tra chuyển đổi loại và lựa chọn quá tải
- Kiểm tra kết quả và tình trạng lỗi trong thư viện chuẩn
- Kiểm tra mô hình bộ nhớ và hành vi đồng thời
- Kiểm tra cấu trúc file và định dạng nhị phân
- So sánh kết quả trên các nền tảng khác nhau
Việc vượt qua bài kiểm tra sự phù hợp không có nghĩa là việc triển khai hoàn hảo. Bởi vì số lượng chương trình có thể thực hiện là vô hạn nên việc kiểm thử sẽ xác minh các quy tắc đại diện và các điều kiện biên của đặc tả. Việc tạo chương trình ngẫu nhiên kết hợp với làm mờ, kiểm tra vi phân và xác minh chính thức có thể phát hiện ra nhiều lỗi triển khai hơn.
Thông số kỹ thuật Java giải quyết rõ ràng các điều kiện trong đó việc triển khai độc lập đáp ứng các yêu cầu cụ thể và vượt qua các bài kiểm tra khả năng tương thích. Đặc tả ngôn ngữ Java xác định các quy tắc tương thích nhị phân cho các lớp và giao diện cũng như ngôn ngữ nguồn trong một chương riêng.
Thư viện chuẩn
Khả năng sử dụng thực tế của một ngôn ngữ lập trình phần lớn không chỉ phụ thuộc vào cú pháp cốt lõi mà còn cả thư viện chuẩn của nó. Bạn phải quyết định mức độ mà các tính năng như chuỗi, bộ sưu tập, tệp, thời gian, toán học, tính tương tranh, mạng và tuần tự hóa sẽ được cung cấp theo tiêu chuẩn.
Một thư viện tiêu chuẩn nhỏ giúp việc triển khai và tính di động dễ dàng hơn, đồng thời cho phép nhiều thư viện bên ngoài cạnh tranh. Mặt khác, nếu ngay cả các chức năng cơ bản cũng phụ thuộc vào các gói bên ngoài thì các giao diện khác nhau có thể được sử dụng cho từng dự án và hệ sinh thái có thể bị phân mảnh.
Khi thiết kế một thư viện tiêu chuẩn, bạn nên cân nhắc những điều sau:
- Ranh giới giữa các tính năng ngôn ngữ cốt lõi và các tính năng thư viện
- Tính năng có sẵn trên tất cả các nền tảng
- Cách trừu tượng hóa sự khác biệt giữa các hệ điều hành
- Phương pháp xử lý lỗi
- API đồng thời và không đồng bộ
- Khả năng tương thích nhị phân
- Tên API và tính nhất quán
- Quy trình ngừng hoạt động các tính năng lỗi thời
- Phân chia vai trò với hệ sinh thái gói bên ngoài
API thư viện tiêu chuẩn có thể ảnh hưởng trực tiếp đến phạm vi mã người dùng rộng hơn cú pháp ngôn ngữ. Bởi vì rất khó để thay đổi tên và hoạt động của các loại và chức năng một khi chúng được công khai nên các chính sách tương thích và khả năng mở rộng phải được xem xét ngay từ đầu.
Đề xuất thay đổi ngôn ngữ
Trong các ngôn ngữ hoàn thiện, các tính năng không được thêm ngay lập tức mà phải trải qua một số quy trình đề xuất. Các tài liệu đề xuất không chỉ mô tả vấn đề và giải pháp mà còn cả mối quan hệ của chúng với các tính năng hiện có, các giải pháp thay thế, tính tương thích, tính khả thi của việc triển khai và những hạn chế.
Một tài liệu đề xuất điển hình có chứa những nội dung sau:
- Vấn đề bạn muốn giải quyết
- Các trường hợp sử dụng trong thế giới thực
- Ngữ pháp và ý nghĩa gợi ý
- Quy tắc kiểm tra và thực thi kiểu
- Khả năng tương thích với mã hiện có
- Tác động đến các thư viện và công cụ tiêu chuẩn
- Phương pháp thực hiện và chi phí thực hiện
- Ý nghĩa về an ninh và an toàn
- Các giải pháp thay thế được xem xét nhưng không được thông qua
- Vấn đề chưa được giải quyết
- Khả năng mở rộng trong tương lai
- Kế hoạch kiểm tra và tài liệu
PEP của Python là tài liệu thiết kế cung cấp thông tin cho cộng đồng Python hoặc mô tả các tính năng mới của ngôn ngữ, quy trình và môi trường, đồng thời yêu cầu các thông số kỹ thuật và cơ sở để cung cấp đề xuất.
Quy trình Rust RFC được thiết kế để đảm bảo rằng những thay đổi lớn đối với ngôn ngữ và thư viện tiêu chuẩn sẽ diễn ra theo một lộ trình nhất quán và được kiểm soát. Các đề xuất được chấp nhận hoặc từ chối thông qua thảo luận công khai và đánh giá của nhóm, đồng thời các RFC được chấp nhận không ngay lập tức trở thành các tính năng ổn định. Cần có các quy trình thực hiện, thử nghiệm và ổn định riêng biệt.
Trong quy trình đề xuất của Go, trước tiên bạn gửi vấn đề mô tả ngắn gọn vấn đề và trong quá trình xem xét, bạn có thể chấp nhận hoặc từ chối ngay lập tức hoặc yêu cầu tài liệu thiết kế chi tiết hơn. Đánh giá đề xuất nhằm mục đích xây dựng sự đồng thuận chung và các đề xuất mâu thuẫn với thiết kế cốt lõi của ngôn ngữ hoặc khó thực hiện một cách hiệu quả có thể bị từ chối.
Kho lưu trữ KEEP của Kotlin duy trì cả các đề xuất đang diễn ra cho ngôn ngữ và thư viện chuẩn cũng như tài liệu thiết kế cho những thay đổi đã được triển khai.
Swift Evolution là một quy trình công khai để xem xét các thay đổi thiết kế hiển thị cho người dùng ngôn ngữ Swift, thư viện tiêu chuẩn và các công cụ cốt lõi. Quá trình này nhằm mục đích duy trì định hướng tổng thể và tính nhất quán về khái niệm của ngôn ngữ đồng thời phản ánh quan điểm rộng rãi của cộng đồng.
Phát triển đề xuất theo từng bước
Các đặc điểm ngôn ngữ thường không đi thẳng từ một đề xuất thành một tiêu chuẩn chính thức. Bạn có thể trải qua các bước sau:
Ý tưởng và nêu vấn đề
↓
Thiết kế ban đầu và trường hợp sử dụng
↓
Tài liệu đề xuất công khai
↓
Thảo luận và sửa đổi
↓
Triển khai nguyên mẫu
↓
Thu thập kinh nghiệm kiểm thử và sử dụng thực tế
↓
Soạn thảo nội dung đặc tả
↓
Xem xét ổn định hóa
↓
Đưa vào ngôn ngữ hoặc tiêu chuẩn chính thức
Trong giai đoạn đầu, hãy cân nhắc xem vấn đề bạn đang cố gắng giải quyết có thực sự quan trọng hay không. Ngay cả các tính năng có vẻ hữu ích cũng có thể không được thêm vào ngôn ngữ nếu chúng có thể được giải quyết dễ dàng bằng cú pháp và thư viện hiện có hoặc nếu các trường hợp sử dụng bị hạn chế.
Nguyên mẫu được sử dụng để xác minh rằng một đề xuất thực sự có thể triển khai được và phát hiện các vấn đề phát sinh trong quá trình kiểm tra cú pháp và kiểu, thông báo lỗi, hiệu suất và hỗ trợ công cụ. Nếu bạn quyết định chọn một tính năng chỉ dựa trên các tài liệu mà không có kinh nghiệm triển khai, bạn có thể phát hiện ra những vấn đề phức tạp tiềm ẩn sau này.
Quy trình TC39 của ECMAScript sử dụng một số bước để chuyển đề xuất từ giai đoạn ý tưởng thông qua nghiên cứu và thiết kế, phản hồi triển khai và hoàn thành đặc tả để đưa vào đặc tả chính thức. Các đề xuất hoàn thành phải đáp ứng các điều kiện như triển khai và thử nghiệm thực tế cũng như văn bản đặc tả có thể chỉnh sửa được.
Tính năng thử nghiệm và ổn định
Nếu một tính năng mới được cung cấp ngay lập tức như một tính năng ổn định thì sẽ khó sửa nó ngay cả khi sau này phát hiện ra lỗi thiết kế. Để giảm thiểu điều này, các ngôn ngữ và nền tảng có thể sử dụng các tính năng thử nghiệm và xem trước, API vườn ươm, v.v.
Trong giai đoạn thử nghiệm, những điều sau đây có thể được cho phép:
- Ngữ pháp và ý nghĩa có thể thay đổi trong các phiên bản sau
- Yêu cầu các tùy chọn biên dịch rõ ràng
- Được cung cấp trong một không gian tên riêng của thư viện tiêu chuẩn
- Không khuyến khích sử dụng trong môi trường hoạt động
- Đảm bảo khả năng tương thích có giới hạn
- Thu thập kinh nghiệm sử dụng và phản hồi triển khai
Các mục cần xem xét trước khi ổn định như sau.
- Có đủ trường hợp sử dụng trong thế giới thực không?
- Ngữ pháp và ý nghĩa có phù hợp với chức năng hiện có không?
- Việc triển khai có thể hỗ trợ nó một cách hiệu quả không?
- Các thông báo lỗi và công cụ hỗ trợ có đầy đủ không?
- Có chỗ cho việc mở rộng trong tương lai không?
- Có vấn đề gì về an toàn, an ninh không?
- Nó có làm thay đổi ý nghĩa của chương trình hiện tại không?
- Các triển khai khác có thể triển khai độc lập không?
OpenJDK theo dõi các tính năng và thay đổi chính đối với JDK và nền tảng Java thông qua quy trình JEP. Các mô-đun vườn ươm và chức năng xem trước cũng được quản lý như một phần của quy trình JEP.
Nếu hàm thực nghiệm được duy trì trong thời gian dài, ranh giới giữa hàm ổn định và hàm thực nghiệm có thể bị mờ đi. Ngược lại, nếu không thu thập đầy đủ và ổn định trải nghiệm người dùng thực tế, bạn có thể phải duy trì các API và cú pháp không chính xác trong một thời gian dài. Vì vậy, điều quan trọng là phải xác định trước mục đích và điều kiện kết thúc của giai đoạn thử nghiệm.
Đồng thuận và ra quyết định
Những thay đổi về ngôn ngữ thường khó được quyết định bằng bỏ phiếu đa số đơn giản. Điều này là do một tính năng có thể có tác động khác nhau đối với người dùng hiện tại, người triển khai, nhà phát triển công cụ và tác giả thư viện.
Một quy trình đồng thuận điển hình sẽ xem xét các yếu tố sau:
- Tính khả thi về mặt kỹ thuật
- Định hướng ngôn ngữ lâu dài
- Nhu cầu thực sự của người dùng
- Gánh nặng thực hiện và công cụ
- Khả năng tương thích với mã hiện có
- Sự tồn tại của các lựa chọn thay thế
- Ý kiến cộng đồng
- Chi phí bảo trì
- Khả năng quản lý liên tục của nhóm phụ trách
Sự đồng thuận không có nghĩa là tất cả những người tham gia đều phải thích một đề xuất hơn. Nó có thể có nghĩa là đạt đến trạng thái mà những phản đối quan trọng đã được xem xét đầy đủ và giá trị cũng như chi phí của một đề xuất có thể được đánh giá bởi một cơ quan ra quyết định có trách nhiệm.
Các cuộc thảo luận công khai có thể giúp khám phá nhiều trường hợp sử dụng và vấn đề khác nhau, nhưng số lượng người tham gia thảo luận có thể không đại diện cho tất cả người dùng thực tế. Do đó, các nhà thiết kế ngôn ngữ không chỉ nên sử dụng các nhận xét thảo luận mà còn cả khảo sát mã, đo lường hiệu suất, khảo sát người dùng, các trường hợp lỗi hiện có và trải nghiệm tạo mẫu.
Quản trị
Quản trị là một hệ thống xác định ai là người xác định hướng kỹ thuật và những thay đổi đối với ngôn ngữ cũng như thông qua những thủ tục nào.
Quản trị ngôn ngữ lập trình có thể có các dạng sau:
- Một hoặc một vài nhà thiết kế đưa ra quyết định cuối cùng.
- Nhóm nội bộ của công ty quản lý ngôn ngữ và việc triển khai.
- Vai trò được phân chia giữa nhóm cốt lõi và các nhóm phụ của dự án công.
- Một ủy ban bao gồm các thành viên trong ngành đưa ra quyết định bằng sự đồng thuận.
- Sử dụng các quy trình tiêu chuẩn hóa quốc tế có sự tham gia của đại diện quốc gia.
- Một tổ chức từ thiện hoặc tổ chức phi lợi nhuận cung cấp hỗ trợ về hành chính và nguồn lực.
Cơ cấu quản trị ảnh hưởng đến tốc độ, tính minh bạch, tính nhất quán và sự tham gia của các quyết định. Việc để một nhà thiết kế dẫn đầu có thể giúp duy trì hướng đi nhất quán và đưa ra quyết định nhanh chóng nhưng cũng có thể làm tăng sự phụ thuộc vào người đó. Cách tiếp cận của ủy ban có thể phản ánh những lợi ích đa dạng, nhưng việc đưa ra quyết định có thể mất nhiều thời gian.
Một hệ thống quản trị tốt cần công bố rõ ràng những điều sau:
- Cách gửi đề xuất
- Cá nhân, tổ chức xem xét đề xuất
- Tiêu chí chấp nhận và từ chối
- Cách đưa ra ý kiến của mình
- Cách giải quyết xung đột lợi ích
- Phạm vi công bố quyết định và biên bản cuộc họp
- Thủ tục xem xét lại các quyết định hiện hành
- Mối quan hệ thẩm quyền giữa nhóm dự án và ủy ban tiêu chuẩn hóa
Ngay cả khi các thông số kỹ thuật của ngôn ngữ được mở, nếu quá trình ra quyết định thực tế không rõ ràng thì những người triển khai và người dùng bên ngoài khó có thể dự đoán được hướng đi dài hạn.
Tiêu chuẩn hóa quốc tế
Tiêu chuẩn hóa quốc tế là một quá trình trong đó các tổ chức tiêu chuẩn quốc gia và các chuyên gia tham gia để tạo ra các tiêu chuẩn chung. Trong lĩnh vực ngôn ngữ lập trình, SC 22 trực thuộc JTC 1, ủy ban kỹ thuật chung của ISO và IEC, đóng vai trò chính. Phạm vi của SC 22 bao gồm ngôn ngữ lập trình, môi trường thực thi và giao diện phần mềm hệ thống.
WG14 chịu trách nhiệm tiêu chuẩn hóa quốc tế về C (ngôn ngữ lập trình). WG14 là nhóm làm việc quản lý tiêu chuẩn ngôn ngữ C và các thông số kỹ thuật liên quan.
Việc tiêu chuẩn hóa quốc tế của C++ được xử lý bởi WG21. WG21 phát triển tài liệu kỹ thuật và tiêu chuẩn C++, bao gồm các quy ước cốt lõi về ngôn ngữ và thư viện tiêu chuẩn. Các đề xuất được gửi dưới dạng tài liệu của ủy ban và có thể được phản ánh trong tiêu chuẩn dự thảo sau khi xem xét nghiên cứu, sự phát triển và cách diễn đạt theo lĩnh vực chi tiết.
Thay vì chỉ định mã nguồn của một triển khai cụ thể làm tiêu chuẩn, các tiêu chuẩn quốc tế xác định trong các tài liệu các yêu cầu mà chương trình và triển khai phải đáp ứng. Các tài liệu tiêu chuẩn có thể được sửa đổi định kỳ và các thủ tục riêng biệt cũng có thể được áp dụng để giải quyết các vấn đề về báo cáo và giải thích lỗi.
Ưu điểm của tiêu chuẩn hóa quốc tế là nó cung cấp các tiêu chuẩn chung độc lập với các công ty hoặc việc triển khai cụ thể. Mặt khác, tốc độ thay đổi có thể chậm hơn so với các dự án phần mềm mở vì cần có các thủ tục, thỏa thuận và đánh giá tài liệu cụ thể theo quốc gia.
Tiêu chuẩn ngành và consortium
Một số ngôn ngữ phát triển thông qua các nhóm ngành hoặc hiệp hội hơn là thông qua các tiêu chuẩn quốc tế.
ECMAScript là đặc tả ngôn ngữ được duy trì bởi TC39 của Ecma International. TC39 duy trì các tài liệu đề xuất và văn bản đặc tả trong kho lưu trữ công cộng và thảo luận xem có nên tiến hành các đề xuất tại các cuộc họp thường kỳ hay không. Vì khả năng tương thích giữa việc triển khai JavaScript thực tế và hệ sinh thái web là quan trọng nên trải nghiệm của nhiều người triển khai trình duyệt và thời gian chạy được phản ánh trong quy trình đề xuất.
Phương pháp này có thể phát triển các chức năng nhanh hơn tiêu chuẩn hóa quốc tế và dễ dàng phản ánh trực tiếp phản hồi từ việc triển khai thực tế vào thông số kỹ thuật. Tuy nhiên, do ảnh hưởng của các công ty tham gia và những người thực hiện chủ chốt có thể tương đối lớn nên thủ tục công bố thông tin, biên bản cuộc họp và các tiêu chuẩn đồng thuận là rất quan trọng.
Quy trình phát triển dự án công khai
Ngôn ngữ được quản lý bởi các dự án phần mềm mở có thể phát triển xung quanh các kho mã, trình theo dõi vấn đề và các tài liệu đề xuất công khai.
Trong hệ thống PEP của Python, không chỉ các tính năng ngôn ngữ mà cả các quy trình phát triển, quản trị và tiêu chuẩn đóng gói đều có thể được đề cập trong các tài liệu đề xuất. Số PEP, trạng thái và lịch sử thay đổi được giữ nguyên, do đó bạn không chỉ có thể xem các tính năng được áp dụng mà còn có thể xem thông tin cơ bản về các thiết kế đã bị từ chối hoặc rút lại.
Hệ thống RFC của Rust đảm bảo rằng những thay đổi về ngôn ngữ chính và thư viện tiêu chuẩn sẽ được thực hiện thông qua tài liệu thiết kế công cộng. Các tổ chức có trách nhiệm như nhóm ngôn ngữ và nhóm thư viện xem xét đề xuất, việc triển khai và ổn định được thực hiện dưới dạng các thủ tục riêng biệt ngay cả sau khi chấp nhận RFC.
Quá trình đề xuất của Go không yêu cầu tài liệu thiết kế dài dòng cho mỗi thay đổi nhỏ. Xác định tầm quan trọng và độ phức tạp của đề xuất trong quá trình xem xét vấn đề ban đầu và chỉ yêu cầu tài liệu thiết kế chi tiết khi cần thiết. Đây là phương pháp điều chỉnh chi phí của thủ tục đề xuất theo quy mô thay đổi.
KEEP và Swift Evolution của Kotlin cũng phát triển ngôn ngữ và thư viện tiêu chuẩn thông qua các tài liệu đề xuất, các cuộc thảo luận công khai và quyết định từ các nhóm chịu trách nhiệm.
Khả năng tương thích
Khi ngôn ngữ lập trình được sử dụng rộng rãi, việc không phá vỡ các chương trình hiện có có thể trở nên quan trọng hơn việc thêm các tính năng mới. Khả năng tương thích không phải là một khái niệm duy nhất mà được chia thành nhiều lớp.
| Loại | Ý nghĩa |
|---|---|
| Khả năng tương thích nguồn | Mã nguồn hiện có có được biên dịch lại bằng trình biên dịch mới không? |
| Khả năng tương thích nhị phân | Các thư viện và chương trình không được biên dịch lại có hoạt động cùng nhau không? |
| Khả năng tương thích hoạt động | Chương trình có giữ nguyên ý nghĩa và kết quả như trước không? |
| Khả năng tương thích API | Các phương pháp sử dụng các chức năng, loại và mô-đun công cộng có được duy trì không? |
| Khả năng tương thích ABI | Quy ước gọi, ký hiệu và bố trí đối tượng có được duy trì không? |
| Khả năng tương thích dữ liệu | Tôi vẫn có thể đọc các định dạng tệp, mạng và tuần tự hóa hiện có chứ? |
| Khả năng tương thích công cụ | Công cụ build, trình gỡ lỗi, trình định dạng và trình phân tích có tiếp tục hoạt động không? |
| Khả năng tương thích hệ sinh thái | Các gói và khung có hoạt động cùng nhau trong các phiên bản ngôn ngữ mới không |
Ngay cả khi khả năng tương thích nguồn được duy trì thì khả năng tương thích nhị phân có thể bị phá vỡ. Việc xây dựng lại mã hiện có bằng trình biên dịch mới sẽ hoạt động nhưng bạn không thể liên kết trực tiếp chương trình mới với các thư viện được tạo bằng trình biên dịch cũ.
Ngược lại, ngay cả khi khả năng tương thích nhị phân được duy trì, hành vi có thể thay đổi đôi chút. Nếu việc xử lý lỗi, thứ tự sắp xếp hoặc kết quả dấu phẩy động của các hàm thư viện tiêu chuẩn thay đổi thì kết quả có thể thay đổi ngay cả khi chương trình được thực thi.
Đặc tả ngôn ngữ Java phân biệt cụ thể giữa những thay đổi duy trì khả năng tương thích với các tệp nhị phân hiện có khi các lớp và giao diện được sửa đổi và những thay đổi không duy trì.
Khả năng tương thích ngược
Khả năng tương thích ngược là thuộc tính mà ngôn ngữ hoặc phiên bản triển khai mới có thể tiếp tục xử lý các chương trình được viết cho các phiên bản trước.
Để duy trì khả năng tương thích ngược, bạn phải chú ý những điều sau:
- Từ khóa mới có xung đột với mã định danh hiện có không?
- Kết quả của việc nạp chồng và suy luận kiểu có khác nhau không?
- Liệu ngữ pháp hiện tại có trở nên mơ hồ với ngữ pháp mới không?
- Có bất kỳ xung đột tên nào khi các hàm mới được thêm vào thư viện chuẩn không?
- Không phải mã được coi là lỗi được hiểu là mã thông thường có ý nghĩa khác sao?
- Thứ tự thực hiện và kết quả tính toán có thay đổi không?
- Định dạng ABI và tuần tự hóa hiện tại có được duy trì không?
Rất khó để đảm bảo khả năng tương thích tuyệt đối. Hành vi hiện tại có thể cần phải được thay đổi để khắc phục các lỗ hổng bảo mật hoặc sửa các lỗi đặc tả rõ ràng. Ngoài ra, nếu một chương trình dựa trên hành vi không được đảm bảo bởi đặc tả, thì chương trình đó có thể bị hỏng trong quá trình triển khai mới.
Tài liệu tương thích Go 1 đặt ra kỳ vọng rằng các chương trình Go 1 hiện tại sẽ tiếp tục biên dịch và chạy trong quá trình triển khai Go 1 trong tương lai, nhưng lưu ý rằng có thể tồn tại các ngoại lệ, bao gồm các vấn đề bảo mật và hành vi không xác định.
Ngừng dùng và loại bỏ
Ngay cả khi một tính năng tốt hơn xuất hiện hoặc một tính năng không an toàn không còn được khuyến khích nữa, việc loại bỏ tính năng hiện có ngay lập tức có thể làm hỏng nhiều chương trình. Với mục đích này, quy trình Ngừng dùng được sử dụng.
Quy trình xử lý chung như sau:
Cung cấp tính năng thay thế
↓
Đánh dấu tính năng hiện có là sắp bị loại bỏ
↓
Cảnh báo trong trình biên dịch và tài liệu
↓
Cung cấp công cụ chuyển đổi tự động
↓
Duy trì thời gian chuyển đổi đầy đủ
↓
Loại bỏ trong phiên bản lớn hoặc chế độ ngôn ngữ mới
Việc loại bỏ một tính năng có nghĩa là việc sử dụng nó không được khuyến khích và không có nghĩa là nó không thể được sử dụng ngay lập tức. Cảnh báo thải bỏ tốt nhất nên bao gồm các phương pháp thay thế và thời điểm dự kiến loại bỏ.
Nếu các tính năng được giữ vĩnh viễn thay vì bị loại bỏ thì độ phức tạp của ngôn ngữ và cách triển khai sẽ tiếp tục tăng lên. Ngược lại, nếu bạn gỡ bỏ quá nhanh, hệ sinh thái sẽ không thể chuyển sang phiên bản mới. Do đó, việc sử dụng, rủi ro bảo mật, chi phí bảo trì và khả năng di chuyển tự động phải được xem xét cùng nhau.
Phiên bản và ấn bản ngôn ngữ
Một vấn đề thiết kế quan trọng khác là đơn vị mà các thay đổi ngôn ngữ sẽ được phân phối.
Các phương pháp phổ biến bao gồm:
- Phiên bản ngôn ngữ mới được phát hành đều đặn.
- Các chức năng đã hoàn thiện được phản ánh liên tục trong thông số kỹ thuật.
- Các phiên bản chính có hỗ trợ dài hạn được cung cấp riêng.
- Cho phép bạn chọn phiên bản ngôn ngữ trong tệp nguồn hoặc dự án.
- Các phiên bản ngôn ngữ khác nhau được hỗ trợ bởi cùng một trình biên dịch.
Rust đặt một số thay đổi không tương thích ngược trên các ranh giới có thể lựa chọn được gọi là phiên bản. Trừ khi một dự án chọn một phiên bản mới một cách rõ ràng, nó sẽ duy trì các quy tắc của phiên bản hiện có và các thư viện sử dụng các phiên bản khác nhau có thể được sử dụng cùng nhau trong cùng một biểu đồ phụ thuộc. Rust cung cấp các công cụ di chuyển tự động để hỗ trợ chuyển đổi nguồn sang phiên bản mới.
Cách tiếp cận này cho phép đưa ra những thay đổi không tương thích về mặt cú pháp mà không cần chia toàn bộ ngôn ngữ thành các phiên bản mới, riêng biệt cùng một lúc. Tuy nhiên, trình biên dịch và công cụ phải quản lý đồng thời sự khác biệt về cú pháp và ngữ nghĩa giữa nhiều phiên bản.
ECMAScript sử dụng phương pháp phản ánh các đề xuất đã hoàn thành theo các thông số kỹ thuật sống động mới nhất và xuất bản chúng dưới dạng ấn bản định kỳ hàng năm.
Chu kỳ phát hành tiêu chuẩn và bản triển khai
Chu kỳ sửa đổi của tài liệu tiêu chuẩn và chu kỳ phân phối của trình biên dịch có thể khác nhau. Việc triển khai có thể cung cấp thử nghiệm các tính năng chưa có trong tiêu chuẩn cuối cùng hoặc có thể triển khai các tính năng theo từng giai đoạn sau khi tiêu chuẩn mới được xuất bản.
Trong trường hợp này, cần phân biệt các trạng thái sau:
- Các tính năng chỉ mang tính gợi ý và chưa được triển khai
- Các tính năng chỉ tồn tại khi triển khai thử nghiệm
- Các tính năng có trong thông số kỹ thuật nhưng không được một số triển khai hỗ trợ
- Các tiện ích mở rộng được hỗ trợ bởi việc triển khai chính nhưng không có trong tiêu chuẩn chính thức
- Các tính năng được hỗ trợ đáng tin cậy trong cả tiêu chuẩn và triển khai chính
Người dùng không chỉ phải kiểm tra phiên bản ngôn ngữ mà còn cả phiên bản triển khai, nền tảng đích và các tùy chọn tính năng đã kích hoạt. Ngay cả khi hiển thị cùng một phiên bản ngôn ngữ, phạm vi hỗ trợ và lỗi có thể khác nhau tùy theo cách triển khai.
Phần mở rộng tiêu chuẩn
Việc triển khai thực tế có thể cung cấp các chức năng mở rộng không có trong tiêu chuẩn. Các ví dụ đại diện bao gồm các loại và thuộc tính vectơ dành riêng cho phần cứng, tập hợp nội tuyến, API dành riêng cho hệ điều hành và các hàm dựng sẵn của trình biên dịch.
Các chức năng mở rộng cho phép sử dụng các tính năng phần cứng và nền tảng khó thể hiện chỉ bằng các tiêu chuẩn. Tuy nhiên, các chương trình dựa trên tiện ích mở rộng có thể trở nên khó chuyển sang các trình biên dịch và nền tảng khác.
Việc triển khai có thể quản lý các chức năng mở rộng như sau:
- Sử dụng một không gian tên hoặc tiền tố riêng.
- Cho phép mở rộng một cách rõ ràng.
- Chẩn đoán việc sử dụng kéo dài ở chế độ tiêu chuẩn.
- Cung cấp các macro hoặc thuộc tính phát hiện chức năng.
- Đề xuất các phần mở rộng được áp dụng rộng rãi cho ủy ban tiêu chuẩn hóa.
Nếu nhiều triển khai độc lập cung cấp các phần mở rộng tương tự, chúng có thể là ứng cử viên cho tiêu chuẩn hóa. Ngược lại, các chức năng quá phụ thuộc vào cấu trúc bên trong của một triển khai cụ thể có thể khó tạo thành một tiêu chuẩn chung.
Phát hiện tính năng
Nếu một chương trình muốn hỗ trợ nhiều phiên bản và triển khai ngôn ngữ, nó cần có cách để xác định xem có sẵn một tính năng cụ thể nào không.
Việc chỉ kiểm tra số phiên bản của trình biên dịch có thể không xử lý chính xác các bản sao lưu riêng lẻ của các chức năng hoặc triển khai được phân nhánh. Do đó, ngôn ngữ có thể cung cấp các phương pháp phát hiện tính năng sau.
- Kiểm tra xem các tính năng ngữ pháp cụ thể có được hỗ trợ không
- Kiểm tra sự tồn tại của API thư viện chuẩn
- Kiểm tra xem các thuộc tính và chức năng tích hợp có được hỗ trợ không
- Kiểm tra khả năng của hệ điều hành và CPU mục tiêu
- Chọn phiên bản ngôn ngữ hoặc bộ tính năng
- Biên dịch có điều kiện
Vì việc phát hiện tính năng có thể làm tăng độ phức tạp bằng cách tăng các nhánh có điều kiện trong chương trình, nên cần đặt một phiên bản được hỗ trợ chung tối thiểu và hạn chế chỉ xử lý những khác biệt cần thiết.
Lỗi đặc tả và diễn giải
Ngay cả những thông số kỹ thuật chi tiết nhất cũng có thể chứa đựng những điều mơ hồ, mâu thuẫn và thiếu sót. Các vấn đề với đặc tả có thể được phát hiện khi quá trình triển khai được phát triển hoặc các trường hợp sử dụng mới xuất hiện.
Các lỗi đặc tả xuất hiện dưới các dạng sau:
- Ngữ pháp có thể được hiểu theo hai cách
- Quy tắc từ các điều khoản khác nhau xung đột
- Kết quả của một số điều kiện biên nhất định không được xác định
- Ví dụ và văn bản quy phạm không khớp
- Có những yêu cầu không thể thực hiện được hoặc quá kém hiệu quả.
- Các hoạt động và thông số kỹ thuật chung của việc triển khai hiện tại là khác nhau
Các tổ chức tiêu chuẩn hóa và các dự án ngôn ngữ nhận được các báo cáo lỗi, xem xét mục đích của đặc tả cũng như các triển khai và khả năng tương thích hiện có, đồng thời thực hiện các chỉnh sửa. Những thay đổi biên tập đơn giản có thể được phản ánh nhanh chóng, nhưng những sửa đổi làm thay đổi ý nghĩa của chương trình có thể cần được xem xét lại, chẳng hạn như đề xuất thay đổi ngôn ngữ.
Khi sửa các lỗi đặc tả, bạn cũng phải xem xét cách diễn giải mà chương trình hiện tại dựa vào. Ngay cả khi bạn thay đổi nó thành một quy tắc đúng về mặt lý thuyết, nếu nó làm hỏng một chương trình được sử dụng rộng rãi thì có thể cần phải thực hiện quy trình di chuyển từng bước.
Kinh nghiệm triển khai và thiết kế thực nghiệm
Các tính năng ngôn ngữ cần được xem xét không chỉ về tính nhất quán về mặt lý thuyết mà còn về khả năng sử dụng trong mã thực tế. Trình biên dịch nguyên mẫu, các nhánh thử nghiệm và thư viện bên ngoài là một cách để tích lũy kinh nghiệm trước khi đưa các tính năng vào ngôn ngữ hình thức.
Trong thiết kế thực nghiệm, dữ liệu sau có thể được sử dụng:
- Các mẫu lặp lại trong cơ sở mã thực
- Thường xuyên xảy ra lỗi biên dịch
- Độ dài và độ phức tạp của mã sử dụng hàm
- Thời gian biên dịch và hiệu suất thực hiện
- Kích thước nhị phân được tạo
- Khó triển khai IDE và máy phân tích tĩnh
- Kết quả học tập cho người dùng mới
- So sánh với các lựa chọn thay thế được tạo bằng các chức năng hiện có
- Kết quả chuyển đổi tự động cho các dự án nguồn mở
Ngay cả khi một hàm có vẻ đơn giản trong một vài ví dụ thì các thông báo lỗi và kết quả suy luận có thể trở nên phức tạp trong mã quy mô lớn. Ngược lại, một tính năng ban đầu có vẻ hạn chế có thể giải quyết được một vấn đề chung trong nhiều lĩnh vực.
Các bản sửa đổi trước đây đối với đặc tả ngôn ngữ Java cũng đã sử dụng phân tích mã trong thế giới thực trong các thay đổi đối với các ngôn ngữ hoàn thiện, chẳng hạn như đo lường tính hiệu quả của các phương pháp suy luận kiểu bằng cách sử dụng các bộ mã nguồn mở thực tế.
An ninh và an toàn
Thiết kế ngôn ngữ không chỉ phải xem xét tính biểu cảm và hiệu suất của chương trình mà còn phải xem xét liệu cấu trúc có dễ tạo ra các lỗ hổng bảo mật hay không.
Các mục tiêu đánh giá chính như sau:
- Truy cập bên ngoài giới hạn mảng
- Sử dụng bộ nhớ được giải phóng và con trỏ không hợp lệ
- Tràn số nguyên và cắt bớt
- Sự kết hợp không chính xác giữa chuỗi và lệnh
- Sử dụng các giá trị chưa được khởi tạo
- Cạnh tranh dữ liệu
- Quá trình giải tuần tự hóa không chính xác
- Ranh giới quyền lực và sự tin cậy
- Ngoại lệ và dọn dẹp tài nguyên
- Tạo mã và vệ sinh macro
- Mã thực thi chuỗi cung ứng và gói hàng
Không phải tất cả sự an toàn đều có thể được đảm bảo ở cấp độ ngôn ngữ. Tuy nhiên, việc cung cấp các giá trị mặc định an toàn và hạn chế các hoạt động nguy hiểm ở các ranh giới rõ ràng có thể làm giảm phạm vi xảy ra lỗi.
Các tính năng an toàn cũng ảnh hưởng đến chi phí triển khai và độ phức tạp của việc triển khai. Kiểm tra giới hạn, kiểm tra tràn và kiểm tra kiểu thời gian chạy có thể tạo thêm chi phí, do đó, các quy tắc ngữ nghĩa và biểu diễn trung gian phải được thiết kế sao cho trình biên dịch có thể loại bỏ các kiểm tra không cần thiết.
Tính di động
Tính di động là thuộc tính cho phép sử dụng một chương trình với những sửa đổi nhỏ trong các triển khai, hệ điều hành và bộ xử lý khác nhau. Tính di động cũng là mục tiêu chính trong các hoạt động tiêu chuẩn hóa C và SC 22.
Để tăng tính di động, bạn nên xem xét những điều sau:
- Kích thước và phạm vi của các loại cơ bản
- Các phép tính số nguyên và dấu phẩy động
- Sắp xếp theo độ bền
- Đường dẫn tập tin và mã hóa ký tự
- Mô hình luồng và bộ nhớ
- Sự khác biệt về API hệ điều hành
- Thời gian và địa điểm
- Mạng và hệ thống tập tin
- Thư viện bên ngoài và ABI
- Biên dịch có điều kiện
Rất khó để che giấu hoàn toàn tất cả sự khác biệt về nền tảng và quá trừu tượng có thể khiến các tính năng dành riêng cho nền tảng trở nên khó sử dụng. Các ngôn ngữ và thư viện chuẩn phải cung cấp chức năng chung đồng thời cung cấp cấu trúc cho phép truy cập vào các tiện ích mở rộng dành riêng cho nền tảng khi cần thiết.
Tính ổn định và sự phát triển của ngôn ngữ
Nếu ngôn ngữ lập trình không thay đổi thì khó đáp ứng được phần cứng, phương pháp phát triển và yêu cầu an toàn mới. Ngược lại, nếu mọi thứ thay đổi quá nhanh, người dùng sẽ phải liên tục sửa đổi mã của mình và các thư viện và công cụ sẽ khó duy trì ổn định.
Các yếu tố sau đây cần thiết cho sự phát triển ổn định của ngôn ngữ:
- Mục tiêu thiết kế rõ ràng
- Thủ tục thay đổi được ghi lại bằng văn bản
- Đề xuất và đánh giá của công chúng
- Kinh nghiệm triển khai và sử dụng thực tế
- Kiểm tra sự phù hợp
- Chính sách tương thích ngược
- Thủ tục thanh lý, chuyển giao
- Công cụ chuyển đổi mã tự động
- Đồng bộ hóa liên tục các thông số kỹ thuật và triển khai
- Trách nhiệm quản lý
Quyết định không chỉ thêm các tính năng vào ngôn ngữ là quan trọng mà còn là quyết định không thêm các tính năng. Sau khi ngữ pháp, ý nghĩa và API được tiêu chuẩn hóa phải được duy trì trong một thời gian dài, thì phải xem xét tính nhất quán và chi phí bảo trì của toàn bộ ngôn ngữ thay vì sự thuận tiện ngắn hạn.
Tiêu chuẩn hóa cũng không phải là một quá trình sửa chữa ngôn ngữ và ngăn chặn sự thay đổi. Đó là một quá trình xem xét các nhu cầu mới trong khi vẫn duy trì các tiêu chuẩn chung đã được thống nhất và điều chỉnh tính ổn định của các chương trình hiện tại cũng như tiềm năng phát triển trong tương lai của chúng.
Do đó, việc thiết kế và tiêu chuẩn hóa ngôn ngữ lập trình không chỉ là nhiệm vụ kỹ thuật nhằm xác định ngữ pháp và chức năng mà còn là nhiệm vụ tạo ra các quy tắc và cấu trúc ra quyết định để nhiều triển khai, người dùng, thư viện và công cụ có thể cùng nhau phát triển trong một khoảng thời gian dài. Sự thành công của một ngôn ngữ được quyết định nhiều hơn bởi tính nhất quán của các nguyên tắc thiết kế, tính chính xác của thông số kỹ thuật, chất lượng triển khai và mức độ tin cậy của chính sách tương thích và hệ sinh thái của nó được kết nối với nhau hơn là bởi số lượng các tính năng riêng lẻ.
Lĩnh vực ứng dụng
Ngôn ngữ lập trình được sử dụng trong hầu hết mọi lĩnh vực sử dụng máy tính, từ hệ điều hành và điều khiển thiết bị đến dịch vụ web, ứng dụng di động, trò chơi, tính toán khoa học, trí tuệ nhân tạo và tự động hóa tác vụ. Tuy nhiên, không phải ngôn ngữ nào cũng phù hợp như nhau cho mọi lĩnh vực. Phạm vi sử dụng thực tế sẽ khác nhau tùy thuộc vào mức độ phù hợp của mô hình thực thi, hệ thống kiểu, phương pháp quản lý bộ nhớ, nền tảng hỗ trợ, thư viện chuẩn, công cụ phát triển và hệ sinh thái hiện có của ngôn ngữ với nhu cầu của từng lĩnh vực.
Lĩnh vực ứng dụng của ngôn ngữ lập trình không chỉ được xác định bởi các chức năng của ngôn ngữ đó. Cùng một ngôn ngữ có thể được sử dụng rộng rãi trong một lĩnh vực cụ thể trong khi kết hợp với một hệ điều hành, khung, thời gian chạy hoặc hệ sinh thái công nghiệp cụ thể. Kotlin là ngôn ngữ lập trình có mục đích chung nhưng được kết hợp chặt chẽ với môi trường phát triển Android, trong khi Swift là trung tâm để phát triển ứng dụng trên nền tảng Apple. JavaScript đóng một vai trò quan trọng trong giao diện người dùng web do vị trí của nó là ngôn ngữ thực thi tiêu chuẩn cho trình duyệt web. Google tập trung phát triển Android vào Kotlin và Apple cung cấp Swift làm ngôn ngữ cho các nền tảng của Apple, bao gồm iOS, iPadOS, macOS, tvOS và watchOS.
Một hệ thống phần mềm duy nhất cũng có thể được viết bằng nhiều ngôn ngữ. Trò chơi triển khai công cụ và hệ thống định hướng hiệu suất bằng C++, đồng thời các quy tắc và công cụ trò chơi có thể được viết bằng C#, Lua hoặc kịch bản trực quan. Các dịch vụ web sử dụng JavaScript hoặc TypeScript trong trình duyệt, Java, C#, Go, Python hoặc Rust trên máy chủ và các truy vấn cơ sở dữ liệu có thể được viết bằng SQL. Hệ thống máy học định cấu hình mô hình và quy trình học bằng Python, đồng thời có thể triển khai các hoạt động tính toán lớn bằng C++, CUDA hoặc các ngôn ngữ tăng tốc khác.
Vì vậy, khi hiểu lĩnh vực sử dụng, thay vì cố định một ngôn ngữ cụ thể vào một lĩnh vực, bạn nên xem xét các điều kiện cần có trong lĩnh vực đó và vai trò của ngôn ngữ đó.
Phần mềm hệ thống
Phần mềm hệ thống là phần mềm cung cấp nền tảng để chạy các chương trình khác, chẳng hạn như hệ điều hành, trình điều khiển thiết bị, trình biên dịch, thời gian chạy, hệ thống tệp, công cụ cơ sở dữ liệu, ngăn xếp mạng và máy ảo.
Các đặc điểm sau đây rất quan trọng trong phần mềm hệ thống:
- Phải có khả năng kiểm soát chính xác vị trí và tuổi thọ của bộ nhớ.
- Phải có quyền truy cập trực tiếp vào hệ điều hành và giao diện phần cứng.
- Thời gian thực hiện và việc sử dụng bộ nhớ phải có thể dự đoán được.
- Phải có khả năng chạy mà không cần thời gian chạy quy mô lớn riêng biệt.
- Phải có khả năng hỗ trợ nhiều kiến trúc CPU và ABI.
- Phải có khả năng gọi các thư viện bên ngoài và API hệ điều hành một cách đáng tin cậy.
- Phải có khả năng duy trì hiệu suất và khả năng tương thích trong mã được duy trì trong thời gian dài.
C (ngôn ngữ lập trình) từ lâu đã được sử dụng trong các hệ điều hành, thư viện hệ thống và phần mềm nhúng, cung cấp con trỏ và cấu trúc, quản lý bộ nhớ rõ ràng, mô hình thực thi tương đối đơn giản và ABI được hỗ trợ rộng rãi. Tiêu chuẩn C xác định cách trình bày và cú pháp của chương trình C, các ràng buộc, quy tắc ngữ nghĩa và các hạn chế mà việc triển khai tuân thủ phải tuân theo.
C++ cung cấp các lớp và mẫu, lập trình tổng quát, quản lý tài nguyên dựa trên phạm vi và thư viện tiêu chuẩn trong khi vẫn duy trì khả năng kiểm soát phần cứng và hiệu suất thực thi gốc gần bằng C. Nó được sử dụng trong các hệ thống yêu cầu cả hiệu suất và tính trừu tượng phức tạp, chẳng hạn như trình duyệt, công cụ trò chơi, cơ sở dữ liệu, trình biên dịch và các chương trình máy tính để bàn lớn.
Rust là ngôn ngữ lập trình hệ thống sử dụng quyền sở hữu, phép mượn và kiểm tra vòng đời để bảo đảm an toàn bộ nhớ mà không cần bộ thu gom rác. Chương trình dòng lệnh, dịch vụ mạng, WebAssembly và thiết bị nhúng là những lĩnh vực ứng dụng chính của dự án Rust.
Lập trình hệ thống không có nghĩa là tất cả mã phải được viết bằng ngôn ngữ cấp thấp. Các phần cốt lõi của hệ điều hành và ranh giới phần cứng có thể được triển khai bằng ngôn ngữ gốc và các công cụ quản lý, trình cài đặt, hệ thống xây dựng và tự động hóa thử nghiệm có thể được viết bằng ngôn ngữ Python hoặc shell. Các ngôn ngữ ở các cấp độ khác nhau thường được kết hợp để đảm bảo độ tin cậy của hệ thống tổng thể và hiệu quả phát triển.
Hệ thống nhúng
Hệ thống nhúng là một hệ thống máy tính được nhúng trong một thiết bị hoặc sản phẩm cụ thể và thực hiện một mục đích giới hạn. Chúng bao gồm bộ vi điều khiển và cảm biến, bộ điều khiển ô tô, thiết bị công nghiệp, thiết bị gia dụng, thiết bị y tế, máy bay không người lái, thiết bị liên lạc và thiết bị Internet of Things.
Môi trường nhúng rất khác nhau tùy thuộc vào thiết bị. Một số thiết bị sử dụng bộ vi điều khiển 8 bit chỉ có vài kilobyte bộ nhớ, trong khi những thiết bị khác có thể có bộ xử lý đa lõi, hệ điều hành và vài gigabyte bộ nhớ. Tài liệu Embedded Rust cũng giải thích rằng phạm vi lập trình nhúng rất rộng, từ MCU 8 bit nhỏ đến hệ thống đa lõi và các hạn chế khác nhau được áp dụng tùy thuộc vào mục tiêu.
Các yếu tố sau đây rất quan trọng trong phần mềm nhúng:
- Dung lượng RAM và dung lượng lưu trữ hạn chế
- Tiêu thụ điện năng thấp
- Thanh ghi phần cứng và điều khiển thiết bị ngoại vi
- Xử lý ngắt
- Phản hồi theo thời gian thực
- Thực thi kim loại trần không có hệ điều hành
- Ổn định lâu dài
- Cập nhật từ xa và khắc phục thảm họa
- An toàn và bảo mật
- Biên dịch chéo cho thiết bị mục tiêu
C và C++ được sử dụng dựa trên các trình biên dịch, thư viện thiết bị và SDK được hỗ trợ rộng rãi từ các nhà sản xuất phần cứng trong môi trường nhúng. Rust cũng hỗ trợ các môi trường no_std không có hệ điều hành và thư viện chuẩn, đồng thời có thể tận dụng các tính năng an toàn về quyền sở hữu và loại ngôn ngữ trên bộ vi điều khiển. Cuốn sách Embedded Rust giải thích chính thức cách sử dụng Rust trên các bộ vi điều khiển bằng kim loại trần.
Trong môi trường nhúng, tính đồng thời không bị giới hạn ở các luồng của hệ điều hành. Trình xử lý ngắt chạy vào những thời điểm không thể đoán trước, các tác vụ thời gian thực thay phiên nhau và nhiều lõi có thể thực thi mã độc lập. Trong cấu trúc thực thi này, trạng thái chia sẻ, truy cập nguyên tử, mức độ ưu tiên ngắt và lập lịch thời gian thực phải được xem xét cùng nhau.
Hệ thống thời gian thực
Hệ thống thời gian thực không chỉ đơn giản là một hệ thống chạy nhanh mà còn là một hệ thống chỉ được công nhận là đúng nếu nó cung cấp kết quả trong một khoảng thời gian nhất định. Các lĩnh vực ứng dụng điển hình bao gồm điều khiển nhà máy, robot, điều khiển máy bay, điều khiển phương tiện, thiết bị liên lạc, thiết bị y tế và xử lý âm thanh.
Trong các hệ thống thời gian thực, thời gian thực hiện trong trường hợp xấu nhất và giới hạn trên về độ trễ, mức độ ưu tiên của tác vụ, phản hồi ngắt, tranh chấp tài nguyên và khả năng dự đoán lịch trình quan trọng hơn tốc độ xử lý trung bình. Việc thu thập rác hoặc biên dịch nhanh chóng, có thể ngừng thực thi tùy ý trong thời gian dài, có thể không phù hợp với một số môi trường thời gian thực, do đó, phương pháp quản lý bộ nhớ có thể dự đoán hoặc thời gian chạy giới hạn được sử dụng cho mục đích thời gian thực.
Ada đã phát triển để hỗ trợ các hệ thống yêu cầu thực thi tác vụ và thời gian thực, gõ mạnh và tính toàn vẹn cao. Các hệ thống có tính toàn vẹn cao nằm trong phụ lục của tiêu chuẩn Ada giải quyết các ràng buộc và môi trường triển khai cho các hệ thống yêu cầu độ tin cậy và an toàn cao.
Ngay cả các chương trình thời gian thực cũng không cần tất cả các thành phần được viết bằng một ngôn ngữ. Các vòng điều khiển có giới hạn thời gian chặt chẽ có thể được triển khai bằng C, C++, Ada hoặc Rust, trong khi các công cụ cấu hình, phân tích dữ liệu, mô phỏng và giao diện người dùng có thể được triển khai bằng các ngôn ngữ cấp cao khác.
Ứng dụng dành cho máy tính để bàn
Ứng dụng máy tính để bàn là các chương trình chạy trực tiếp trên hệ điều hành máy tính cá nhân như Windows, macOS và Linux. Nó bao gồm trình chỉnh sửa tài liệu, công cụ đồ họa, môi trường phát triển, trình nhắn tin, chương trình truyền thông, công cụ thiết kế, trình quản lý tệp và ứng dụng khách doanh nghiệp.
Đối với các ứng dụng dành cho máy tính để bàn, các yêu cầu sau rất quan trọng:
- Tích hợp với các chức năng cửa sổ, đầu vào, tập tin và thông báo của hệ điều hành
- Giao diện người dùng đồ họa phức tạp
- Truy cập các tập tin và thiết bị cục bộ
- Chạy ngoại tuyến
- Cài đặt và cập nhật tự động
- Hỗ trợ nhiều hệ điều hành
- Khả năng tiếp cận và quốc tế hóa
- Tính ổn định khi chạy trong thời gian dài
- Hiệu suất gốc và mức sử dụng bộ nhớ
Các chương trình gốc của Windows có thể sử dụng C++, C# và .NET và các chương trình macOS có thể sử dụng Swift, Objective-C và C++. Các chương trình hỗ trợ nhiều hệ điều hành có thể sử dụng C++ và Qt, khung đa nền tảng dựa trên C#, Java và JVM, thời gian chạy máy tính để bàn dựa trên JavaScript và các công cụ GUI dựa trên Rust.
Python cũng có thể được sử dụng trong một số lĩnh vực phát triển, bao gồm các ứng dụng và tập lệnh dành cho máy tính để bàn, dịch vụ web và máy tính khoa học, đồng thời có thể được kết hợp với các thư viện GUI và các mô-đun mở rộng gốc.
Một chương trình máy tính để bàn có thể có toàn bộ giao diện người dùng được viết bằng một ngôn ngữ nhưng công cụ tính toán bên trong của nó có thể bao gồm các thư viện bằng ngôn ngữ khác. Trình chỉnh sửa video và công cụ tạo 3D thường tách các lớp giao diện người dùng và tập lệnh khỏi các lớp xử lý đồ họa và phương tiện hiệu suất cao.
Ứng dụng di động
Các ứng dụng di động chạy trên điện thoại thông minh, máy tính bảng và thiết bị đeo được, đồng thời phải tính đến đầu vào cảm ứng, cảm biến, camera, thông báo, thông tin vị trí, nguồn điện hạn chế và các chính sách thực thi ở chế độ nền.
Kotlin và Java là những ngôn ngữ chính được sử dụng trong Android. Google khuyến nghị Kotlin là ưu tiên hàng đầu để phát triển Android và hỗ trợ tích hợp Kotlin với mã Java hiện có.
Coroutine trong Kotlin được dùng để thể hiện các hoạt động không đồng bộ không chặn luồng giao diện người dùng, chẳng hạn như yêu cầu mạng hoặc quyền truy cập cơ sở dữ liệu. Tài liệu chính thức của Android mô tả coroutine là phương pháp được đề xuất cho lập trình không đồng bộ, trình bày tính chất đồng thời có cấu trúc và lan truyền hủy bỏ là các đặc điểm chính.
Swift và Objective-C được sử dụng trên nền tảng Apple. Apple cung cấp Swift làm ngôn ngữ cho tất cả các nền tảng của Apple và SwiftUI là một khuôn khổ để xây dựng giao diện người dùng cho nhiều thiết bị Apple sử dụng Swift.
Phát triển di động đa nền tảng cho phép bạn sử dụng các framework hỗ trợ Android và iOS cùng nhau trong một cơ sở mã. Kotlin Multiplatform hỗ trợ chia sẻ logic kinh doanh giữa Android và iOS, đồng thời có thể kết hợp với giao diện người dùng dành riêng cho nền tảng hoặc giao diện người dùng chung nếu cần.
Trò chơi dành cho thiết bị di động và các chương trình truyền thông hiệu suất cao thường kết nối một công cụ phổ biến được viết bằng C++ với lớp Kotlin·Java hoặc Swift·Objective-C dành riêng cho nền tảng.
Frontend web
Web front-end là một khu vực phần mềm chạy trong trình duyệt web và xử lý việc trình bày màn hình tài liệu, thông tin đầu vào của người dùng, giao tiếp mạng, hoạt ảnh và đa phương tiện.
Các công nghệ web cơ bản được phân loại theo vai trò của chúng.
- HTML thể hiện cấu trúc và ý nghĩa của tài liệu.
- CSS xác định hình thức và bố cục của tài liệu.
- JavaScript xử lý thông tin đầu vào của người dùng, các thay đổi trạng thái động và việc sử dụng API trình duyệt.
JavaScript là ngôn ngữ lập trình bổ sung tính năng cập nhật nội dung động, đồ họa, điều khiển đa phương tiện và tính tương tác cho các trang web. Các trình duyệt cung cấp nhiều API Web khác nhau cho JavaScript, bao gồm mô hình đối tượng tài liệu, mạng, bộ lưu trữ, đồ họa và âm thanh.
TypeScript thêm các chức năng phân tích và ký hiệu kiểu tĩnh vào JavaScript và được chuyển đổi thành JavaScript đơn giản. Trong các chương trình giao diện người dùng quy mô lớn, các loại thuộc tính thành phần, dữ liệu mạng và cấu trúc quản lý trạng thái có thể được chỉ định để hỗ trợ điều hướng mã, tái cấu trúc và phát hiện lỗi.
Giao diện người dùng web bị giới hạn bởi môi trường trình duyệt. Quyền truy cập vào tệp, thiết bị và mạng phải tuân theo mô hình bảo mật của trình duyệt và quyền của người dùng, đồng thời phải xem xét trạng thái hỗ trợ tính năng, hiệu suất và khả năng truy cập của các trình duyệt khác nhau.
WebAssembly có thể được sử dụng làm mục tiêu để thực thi mã cấp thấp được tạo bằng các ngôn ngữ như C, C++ và Rust trong môi trường web. Có thể triển khai các mô-đun chuyên sâu về tính toán như xử lý hình ảnh và âm thanh, trò chơi, mô phỏng và nén bằng WebAssembly và JavaScript chịu trách nhiệm kết nối giao diện người dùng và API của trình duyệt.
Backend web và ứng dụng máy chủ
Web backend là phần mềm phía máy chủ nhận yêu cầu của khách hàng và thực hiện xác thực, logic nghiệp vụ, truy cập cơ sở dữ liệu, xử lý tệp và liên lạc dịch vụ bên ngoài.
Các yếu tố sau đây rất quan trọng trong máy chủ web và API:
- Xử lý nhiều kết nối đồng thời
- Thực thi không đồng bộ với đầu vào và đầu ra mạng
- Kết nối cơ sở dữ liệu
- Quản lý xác thực và cấp phép
- Xác minh dữ liệu đầu vào
- Mở rộng theo chiều ngang
- Khả năng quan sát và ghi nhật ký
- Cập nhật bảo mật
- Phân phối ổn định
- Tích hợp với các dịch vụ và thư viện hiện có
Java và Kotlin có thể sử dụng khung máy chủ và hệ sinh thái doanh nghiệp của JVM, trong khi C# kết hợp với khung web và đám mây của .NET. Python có thể được sử dụng cho các API web, dịch vụ được quản lý và phần phụ trợ tập trung vào dữ liệu nhờ vào sự phát triển nhanh chóng và các thư viện mở rộng. JavaScript và TypeScript có thể chia sẻ cấu trúc ngôn ngữ và dữ liệu trên các trình duyệt và máy chủ thông qua thời gian chạy của máy chủ như Node.js.
Go được sử dụng cho các dịch vụ mạng và công cụ đám mây dựa trên goroutines và kênh, thư viện mạng và tiêu chuẩn HTTP cũng như các triển khai gốc. Rust có thể được áp dụng cho các dịch vụ yêu cầu thời gian chạy không đồng bộ, an toàn bộ nhớ và hiệu năng gốc. Các dịch vụ mạng cũng được coi là một lĩnh vực ứng dụng chính trong dự án Rust.
Lựa chọn ngôn ngữ của máy chủ không chỉ được xác định bởi hiệu suất thực hiện của một yêu cầu. Tốc độ phát triển của dịch vụ và khả năng phản hồi với các lỗi, sự hoàn thiện của thư viện, môi trường triển khai, mức sử dụng bộ nhớ, thời gian khởi động, kinh nghiệm của nhân viên vận hành và khả năng tương thích với các hệ thống hiện có đều ảnh hưởng đến nó.
Điện toán đám mây
Trong môi trường điện toán đám mây, các chương trình chạy trên máy ảo, bộ chứa, môi trường thực thi không có máy chủ, dịch vụ dữ liệu được quản lý và cơ sở hạ tầng phân tán trên nhiều khu vực.
Phần mềm đám mây có thể được chia thành các lĩnh vực sau:
- Kiểm soát cơ sở hạ tầng và cung cấp tài nguyên
- Quản lý vùng chứa và cụm
- Dịch vụ vi mô và API
- Chức năng không có máy chủ
- Bộ sưu tập nhật ký và số liệu
- Đường dẫn xử lý dữ liệu
- Tự động hóa triển khai và vận hành
- Chính sách bảo mật và kiểm soát truy cập
Go được sử dụng cho các công cụ cơ sở hạ tầng đám mây do khả năng khởi động, chạy đồng thời và lập trình mạng tương đối nhanh và có thể thực thi được. Java và C# được sử dụng rộng rãi trong các hệ thống doanh nghiệp và dịch vụ đám mây được quản lý, đồng thời Python có thể được sử dụng để tự động hóa, xử lý dữ liệu và các chức năng không có máy chủ bằng API đám mây. Rust và C++ có thể được áp dụng cho phần mềm cơ bản yêu cầu thông lượng cao và độ trễ thấp, chẳng hạn như proxy, công cụ lưu trữ và các thành phần mạng.
Các chương trình đám mây bị ảnh hưởng bởi đặc điểm thời gian chạy và chi phí vận hành của các ngôn ngữ cụ thể. Việc sử dụng bộ nhớ và thời gian khởi động chương trình có thể ảnh hưởng đến chi phí trong môi trường không có máy chủ và triển khai vùng chứa quy mô lớn, đồng thời việc thu thập rác và biên dịch JIT có các đặc điểm khác nhau đối với các dịch vụ chạy dài và các chức năng chạy ngắn.
Hệ thống phân tán
Hệ thống phân tán là một hệ thống trong đó nhiều máy tính có bộ nhớ độc lập và có khả năng xảy ra lỗi tạo thành một dịch vụ thông qua mạng.
Các vấn đề sau đây rất quan trọng trong hệ thống phân tán:
- Sự chậm trễ và ngắt kết nối mạng
- Sao chép và sắp xếp lại tin nhắn
- Lỗi một phần khi chỉ có một số nút bị lỗi
- Sao chép dữ liệu và tính nhất quán
- Thử lại hoạt động
- Không chắc chắn về thời gian và trình tự
- Phát hiện lỗi
- Điều hướng dịch vụ
- Cân bằng tải
- Giao tiếp giữa các dịch vụ của các phiên bản khác nhau
Nếu một ngôn ngữ cung cấp khả năng truyền thông điệp và tác nhân, chức năng không đồng bộ, hoạt động nhẹ và đồng thời có cấu trúc, thì một số cấu trúc của các hệ thống này có thể được biểu diễn trực tiếp hơn. Tuy nhiên, chỉ riêng chức năng ngôn ngữ không thể tự động giải quyết các vấn đề về đồng thuận phân tán, tính nhất quán của dữ liệu và phân vùng mạng. Phương pháp lưu trữ dữ liệu, giao thức liên lạc, chính sách thử lại và mô hình lỗi phải được thiết kế riêng.
Nhiều ngôn ngữ được sử dụng cho các dịch vụ phân tán, bao gồm Java, C#, Go, Rust, Erlang, Elixir, Scala, C++ và Python. Nhóm ngôn ngữ Erlang tập trung vào các quy trình biệt lập, truyền thông báo và cấu trúc giám sát, trong khi các ngôn ngữ dựa trên JVM và .NET tận dụng máy chủ trưởng thành và hệ sinh thái công cụ quan sát.
Chương trình dòng lệnh
chương trình dòng lệnh chạy trong terminal hoặc shell, lấy các đối số và đầu vào tiêu chuẩn, đồng thời cung cấp kết quả cho đầu ra tiêu chuẩn hoặc một tệp. Nó bao gồm các trình biên dịch, công cụ quản lý phiên bản, trình chuyển đổi tệp, trình quản lý gói, công cụ xây dựng và chương trình quản lý hệ thống.
Các đặc điểm sau đây rất quan trọng trong các chương trình dòng lệnh:
- Thời gian khởi động nhanh
- Lập luận rõ ràng và trợ giúp
- Kết hợp sử dụng đường ống và dũa
- Mã thoát dễ tự động
- Triển khai trên nhiều hệ điều hành
- Tệp thực thi nhỏ và ít phụ thuộc
- Thông báo lỗi và nhật ký
- Tự động hoàn thành Shell
Ngôn ngữ Shell phù hợp để kết nối nhanh chóng các lệnh và đường dẫn của hệ điều hành, nhưng khi các chương trình phát triển, những hạn chế về loại, cấu trúc dữ liệu và xử lý lỗi có thể xuất hiện. Python có thể xử lý các đối số dòng lệnh bằng cách sử dụng argparse của thư viện tiêu chuẩn và Rust xử lý việc phát triển CLI bằng cách sử dụng các tệp thực thi gốc và hệ sinh thái gói làm khu vực ứng dụng chính thức.
Go, C++ và C# cũng được sử dụng trong các công cụ phát triển và quản lý độc lập. Ngôn ngữ thích hợp sẽ khác nhau tùy thuộc vào quá trình cài đặt, nền tảng đích, thời gian khởi động và liệu thư viện gốc có được sử dụng hay không.
Tập lệnh và tự động hóa tác vụ
Script đề cập đến một chương trình tương đối nhỏ được viết để tự động xử lý tệp, thực thi chương trình, chuyển đổi dữ liệu, cấu hình hệ thống và các tác vụ lặp đi lặp lại.
Các chức năng sau đây rất quan trọng trong lĩnh vực tự động hóa:
- Xử lý chuỗi và tập tin
- Thực thi các lệnh của hệ điều hành
- Xử lý JSON, XML, CSV
- Yêu cầu HTTP và lệnh gọi API
- Sửa nhanh và chạy
- Kết nối với các chương trình bên ngoài
- Xử lý lỗi và nhật ký
- Hỗ trợ nhiều hệ điều hành
Ngôn ngữ Shell phù hợp để kiểm soát trực tiếp các tệp, quy trình và đường dẫn. Python cung cấp các công cụ tệp, mạng, tuần tự hóa, kiểm tra và dòng lệnh thông qua các thư viện tiêu chuẩn và các gói bên ngoài. PowerShell cung cấp các quy trình dựa trên đối tượng và môi trường quản lý Windows, trong khi JavaScript được sử dụng trong các công cụ dựa trên Node.js và tự động hóa phát triển web.
Một tập lệnh đơn giản là đủ cho các tác vụ ngắn, một lần, nhưng khi một chương trình tự động chịu trách nhiệm phân phối, bảo mật và dữ liệu nhạy cảm, nó yêu cầu cùng mức độ kiểm tra, kiểm tra, xử lý lỗi, quản lý quyền và bảo trì như một ứng dụng thông thường.
DevOps và tự động hóa build
DevOps và môi trường phân phối phần mềm tự động hóa quá trình xây dựng, thử nghiệm, đóng gói và triển khai mã nguồn.
Các chương trình được sử dụng bao gồm:
- Xây dựng kịch bản
- Đường dẫn tích hợp liên tục
- Cấu hình hình ảnh vùng chứa
- Định nghĩa cơ sở hạ tầng
- Công cụ triển khai và khôi phục
- Tự động hóa thử nghiệm
- Thu thập nhật ký và trạng thái
- Cấu hình môi trường phát triển
Shell, Python, Go, JavaScript, Ruby và PowerShell được sử dụng để tạo các công cụ tự động hóa. Một số hệ thống xây dựng và cơ sở hạ tầng cung cấp các định dạng khai báo như YAML hoặc các ngôn ngữ dành riêng cho miền của chúng.
Trong môi trường tự động, khả năng tái tạo và không có hiệu lực, quản lý thông tin bí mật, dừng và phục hồi sau lỗi cũng như nhật ký thực thi là rất quan trọng. Thay vì chọn một ngôn ngữ đơn giản vì nó có ngữ pháp ngắn, chúng ta nên xem xét liệu nó có thể phát hiện chính xác các lỗi và kiểm soát sự khác biệt về môi trường hay không.
Cơ sở dữ liệu và quản lý dữ liệu
Trong trường cơ sở dữ liệu, có sự phân biệt giữa ngôn ngữ triển khai công cụ lưu trữ dữ liệu và ngôn ngữ truy vấn dữ liệu được lưu trữ.
Công cụ bên trong của hệ thống quản lý cơ sở dữ liệu thực hiện các chức năng sau:
- Quản lý trang đĩa và bộ nhớ
- chỉ mục
- Tối ưu hóa truy vấn
- Giao dịch
- Kiểm soát đồng thời
- Đăng nhập và phục hồi
- Nhân rộng
- Giao thức mạng
Vì các thành phần này phải xử lý chi tiết hiệu năng, bộ nhớ và tính đồng thời nên có thể sử dụng C, C++, Rust, Java, v.v.
Người dùng mô tả các điều kiện dữ liệu mong muốn và định dạng kết quả bằng ngôn ngữ khai báo, chẳng hạn như SQL. Việc lựa chọn chỉ mục thực tế, thứ tự nối và phương pháp xử lý song song được xác định bởi trình tối ưu hóa truy vấn cơ sở dữ liệu.
Các ứng dụng xung quanh cơ sở dữ liệu sử dụng nhiều ngôn ngữ khác nhau, bao gồm Java, C#, Python, JavaScript, Go và PHP, để thực thi SQL và chuyển đổi kết quả thành các đối tượng miền. Mặc dù có thể sử dụng các công cụ ánh xạ quan hệ đối tượng nhưng chúng không thể trừu tượng hóa hoàn toàn các giao dịch, chỉ mục và kế hoạch thực hiện truy vấn của cơ sở dữ liệu.
Kỹ thuật dữ liệu
Kỹ thuật dữ liệu là lĩnh vực thu thập, làm sạch và chuyển đổi dữ liệu từ nhiều nguồn thành dạng có thể được sử dụng để phân tích và cung cấp dịch vụ.
Các nhiệm vụ chính như sau:
- Thu thập dữ liệu
- Xử lý hàng loạt và xử lý luồng
- Chuyển đổi lược đồ
- Kiểm tra chất lượng dữ liệu
- Di chuyển giữa các kho dữ liệu
- Lập kế hoạch nhiệm vụ
- Xử lý phân tán
- Quản lý siêu dữ liệu
- Tạo mô hình dữ liệu để phân tích
Python và SQL được sử dụng để xây dựng các hệ thống phân tích và quy trình xử lý dữ liệu, trong khi Java và Scala được sử dụng trong các hệ thống xử lý dữ liệu phân tán dựa trên JVM. Azure Databricks cũng hỗ trợ nhiều ngôn ngữ lập trình cho các tác vụ kỹ thuật dữ liệu và khoa học dữ liệu, đồng thời giải thích rằng mỗi ngôn ngữ có vị trí và giới hạn sử dụng khác nhau.
Trong kỹ thuật dữ liệu, các công cụ thực thi phân tán, định dạng lưu trữ, chi phí mạng cũng như vị trí và phân vùng dữ liệu có thể có tác động lớn hơn đến hiệu suất tổng thể so với hiệu suất ngôn ngữ của các hoạt động riêng lẻ. Các tác vụ được viết bằng ngôn ngữ cấp cao có thể được thực thi nội bộ bằng cách sử dụng các phép toán JVM, C++ hoặc vectơ gốc.
Phân tích và thống kê dữ liệu
Phân tích dữ liệu là trường khám phá và tóm tắt dữ liệu, tìm mối quan hệ thống kê và trực quan hóa chúng.
Python và R (ngôn ngữ lập trình) cung cấp một hệ sinh thái để phân tích, thống kê và trực quan hóa dữ liệu một cách tương tác. SQL được sử dụng để chọn và tổng hợp dữ liệu để phân tích, đồng thời các ngôn ngữ và môi trường như Julia và MATLAB cũng được sử dụng trong tính toán và nghiên cứu số.
Các đặc điểm sau đây rất quan trọng trong ngôn ngữ phân tích:
- Xử lý dữ liệu dạng mảng và dạng bảng
- Chức năng thống kê
- Trực quan hóa
- Môi trường thực thi tương tác
- Sổ tay
- Kết nối với các thư viện khoa học
- Gọi các thư viện hiệu suất cao được viết bằng C, C++ và Fortran
- Độ lặp lại của kết quả phân tích
Mã phân tích tương tác rất phù hợp để thử nghiệm nhanh, nhưng khi được triển khai vào hệ thống sản xuất, mã này cần được tăng cường bằng việc xác thực và kiểm tra đầu vào, ghim phiên bản, sử dụng tài nguyên và xử lý lỗi.
Trí tuệ nhân tạo và học máy
Trong các trường Trí tuệ nhân tạo và Học máy, quá trình chuẩn bị dữ liệu, định nghĩa mô hình, học tập, đánh giá, suy luận và triển khai dịch vụ sẽ diễn ra.
Python được sử dụng để nghiên cứu mô hình, thử nghiệm và giao diện phía trên của quy trình học tập dựa trên biểu thức và dữ liệu ngắn gọn cũng như hệ sinh thái tính toán số. Tuy nhiên, các hoạt động ma trận quy mô lớn, xử lý tensor và nhân GPU thường được thực hiện trong C++, CUDA hoặc các triển khai gốc khác. Mã Python có thể đóng vai trò kiểm soát các thư viện và thời gian chạy hiệu suất cao này.
CUDA là một nền tảng điện toán và mô hình lập trình song song giúp tăng tốc các chương trình tính toán chuyên sâu sử dụng GPU. Tài liệu CUDA chính thức của NVIDIA giải thích rằng tính toán GPU được sử dụng cho deep learning, tính toán khoa học và tính toán hiệu năng cao, đồng thời bao gồm lập trình GPU bằng C++ và Python.
Một hệ thống máy học có thể bao gồm nhiều lớp:
Thử nghiệm và định nghĩa mô hình
Python
Framework và engine thực thi
C++ / Rust / Java
Phép toán GPU
CUDA / ngôn ngữ shader và kernel
Xử lý dữ liệu
Python / SQL / Scala / Java
API dịch vụ
Python / Go / Java / C# / Rust
Suy luận trên thiết bị di động và edge
C++ / Java / Kotlin / Swift
Lựa chọn ngôn ngữ không trực tiếp xác định tính chính xác của mô hình. Các khung được hỗ trợ, bộ tăng tốc phần cứng, môi trường xử lý dữ liệu, mục tiêu triển khai và công cụ vận hành có tác động trực tiếp hơn.
Tính toán khoa học
Tính toán khoa học là lĩnh vực tính toán máy tính của các mô hình toán học trong vật lý, hóa học, khí tượng, chất lỏng, thiên văn học, khoa học đời sống, phân tích số và mô phỏng kỹ thuật.
Các yếu tố sau đây rất quan trọng trong tính toán khoa học:
- Độ chính xác và khả năng lặp lại của phép tính dấu phẩy động
- Các phép toán mảng và ma trận
- Thuật toán số
- Xử lý song song
- Bộ nhớ và lưu trữ lớn
- Khả năng tương thích với các mã nghiên cứu hiện có
- Duy trì mã qua nhiều thế hệ
- Khả năng tương tác với các ngôn ngữ và thư viện khác
Fortran đã được sử dụng chủ yếu trong các lĩnh vực mà máy tính được giới thiệu sớm trong khoa học và kỹ thuật. Fortran-lang giải thích dự đoán thời tiết và đại dương, động lực học chất lỏng tính toán, toán học ứng dụng, thống kê và tài chính là những lĩnh vực sử dụng chính của Fortran.
C, C++, Python, Julia và MATLAB cũng được sử dụng trong máy tính khoa học. Python được sử dụng làm lớp trên để xây dựng các quy trình nghiên cứu và phân tích dữ liệu, đồng thời các phép tính thực tế có thể được giao cho các thư viện viết bằng Fortran, C và C++.
Trong phần mềm khoa học, các thuật toán, định dạng dữ liệu và mã được tích lũy trong một thời gian dài đã được chứng minh có thể có giá trị hơn ngữ pháp mới nhất. Khi giới thiệu một ngôn ngữ mới, phải xem xét sự khác biệt trong kết nối với mã hiện có, xác minh kết quả và các phép toán dấu phẩy động.
Tính toán hiệu năng cao
Điện toán hiệu năng cao là lĩnh vực sử dụng siêu máy tính, cụm tính toán, CPU đa lõi và GPU để thực hiện các phép tính quy mô lớn. Nó được sử dụng để dự đoán thời tiết, mô phỏng vật lý, phân tích bộ gen, động lực học chất lỏng, nghiên cứu vật liệu, tính toán tài chính và học máy.
Trong điện toán hiệu năng cao, các mức độ song song sau đây có thể cùng tồn tại:
- Vector hóa ở cấp độ hướng dẫn CPU
- Nhiều thread trong một nút
- Truyền tin nhắn giữa nhiều nút
- Giảm tải GPU và bộ tăng tốc
- Thực hiện không đồng nhất kết hợp nhiều đơn vị tính toán
Fortran, C và C++ được sử dụng theo truyền thống, trong khi Python và Julia có thể được sử dụng để tổ chức và phân tích nhiệm vụ cấp cao. Tính toán GPU sử dụng CUDA C++, ngôn ngữ hạt nhân và thư viện bộ tăng tốc. Mô hình lập trình của CUDA cung cấp khả năng viết mã chạy trên GPU và điều khiển bộ nhớ cũng như các cấu trúc thực thi song song.
Trong điện toán hiệu năng cao, hiệu suất không chỉ được xác định bởi tốc độ thực thi lý thuyết của ngôn ngữ. Các kiểu truy cập bộ nhớ, vị trí dữ liệu, khối lượng giao tiếp, vector hóa, cấu trúc song song của thuật toán và tối ưu hóa phù hợp với thiết bị mục tiêu là rất quan trọng.
Đồ họa máy tính
Trường Đồ họa máy tính thực hiện kết xuất, xử lý hình ảnh, tạo mô hình, hoạt hình, mô phỏng và giao diện người dùng của cảnh 2D và 3D.
Phần mềm đồ họa thường bao gồm các lớp sau:
- API đồ họa và trừu tượng hóa thiết bị
- Công cụ kết xuất
- Trình đổ bóng
- Xử lý cảnh và tài sản
- Biên tập viên và công cụ
- Tích hợp hoạt hình và vật lý
- Kịch bản nội dung
Công cụ kết xuất và trình điều khiển đồ họa thường sử dụng C và C++ để truy cập phần cứng và kiểm soát hiệu suất. Rust cũng có thể được sử dụng làm công cụ kết xuất và trình bao bọc API đồ họa.
Trình đổ bóng được viết bằng ngôn ngữ dành riêng cho miền nhắm mục tiêu đến các giai đoạn đỉnh, pixel và tính toán của GPU. Các ví dụ tiêu biểu bao gồm GLSL, HLSL và Ngôn ngữ tạo bóng kim loại. Mã Shader phải xem xét mô hình thực thi song song và cấu trúc bộ nhớ khác với các chương trình CPU thông thường.
Trong các công cụ và quy trình soạn thảo đồ họa, Python có thể được sử dụng để chuyển đổi nội dung và tự động hóa các tác vụ lặp đi lặp lại, cũng như các phần bổ trợ và công cụ soạn thảo. Do đó, ngay cả trong một chương trình đồ họa, trình kết xuất gốc, ngôn ngữ đổ bóng và tập lệnh công cụ đều được sử dụng cùng nhau.
Phát triển trò chơi
Phát triển trò chơi là một lĩnh vực phức tạp liên quan đến kết xuất thời gian thực, vật lý, âm thanh, mạng, trí tuệ nhân tạo, đầu vào, giao diện người dùng, công cụ và tạo nội dung.
Trong quá trình phát triển trò chơi, các ngôn ngữ khác nhau có thể được sử dụng tùy theo vai trò.
- Công cụ trò chơi và trình kết xuất
- Hệ thống vật lý và âm thanh
- Mã chơi trò chơi
- Máy chủ và mạng
- Công cụ biên tập và tác giả
- Xây dựng và xây dựng đường dẫn tài sản
- Trình đổ bóng
- Mod và tập lệnh người dùng
C ++ được sử dụng trong các công cụ trò chơi lớn do hiệu suất cao, kiểm soát bộ nhớ và hỗ trợ API đồ họa và bảng điều khiển. Unreal Engine cung cấp khung và API cho các nhà phát triển C++, cho phép họ sử dụng các tập lệnh trực quan C++ và Blueprint cùng nhau để xây dựng nội dung và hệ thống trò chơi.
C# được sử dụng trong một số công cụ và công cụ trò chơi do thời gian chạy và năng suất được quản lý. Lua và Python, các ngôn ngữ lập trình độc quyền, có thể được tích hợp vào công cụ gốc dành cho các quy tắc và nội dung trò chơi, tự động hóa bản mod hoặc trình soạn thảo.
Lựa chọn ngôn ngữ trong trò chơi không chỉ là vấn đề hiệu suất khung hình tối đa. Nhu cầu về mã thời gian thực và mã công cụ là khác nhau. Độ trễ thấp và mức sử dụng bộ nhớ có thể dự đoán được rất quan trọng đối với các đường dẫn quan trọng về kết xuất và vật lý, nhưng khả năng phát triển nhanh, giao diện người dùng và xử lý tệp có thể quan trọng hơn đối với trình soạn thảo và quy trình xây dựng.
Xử lý âm thanh và phương tiện
Các chương trình âm thanh và video xử lý các luồng dữ liệu lớn theo thời gian thực hoặc không theo thời gian thực. Nó bao gồm codec, bộ trộn, xử lý tín hiệu, chuyển đổi video, chỉnh sửa, phát trực tuyến và hệ thống phát lại.
Trong âm thanh thời gian thực, việc thực thi có thể dự đoán và phân bổ bộ nhớ hạn chế rất quan trọng vì việc thiếu thời hạn xử lý có thể dẫn đến gián đoạn âm thanh. C và C++ được sử dụng trong DSP, codec và công cụ âm thanh, đồng thời Rust cũng có thể được áp dụng để xử lý phương tiện gốc yêu cầu an toàn bộ nhớ.
Các chương trình tạo phương tiện có thể kết hợp các công cụ xử lý gốc với các lớp plugin và tự động hóa dựa trên Python hoặc JavaScript. CUDA và trình đổ bóng điện toán có thể được sử dụng để xử lý hình ảnh bằng GPU.
Lập trình mạng
Lập trình mạng triển khai các ổ cắm và giao thức, máy chủ và máy khách, proxy, thiết bị mạng và thư viện truyền thông.
Các yếu tố sau đây rất quan trọng trong các chương trình mạng:
- Nhiều kết nối đồng thời
- Đầu vào và đầu ra không đồng bộ
- Hết thời gian và hủy bỏ
- Ranh giới tin nhắn và tuần tự hóa
*Giao thức bảo mật
- Truyền và nhận một phần
- Chấm dứt kết nối và kết nối lại
- Quản lý bộ đệm và bộ nhớ
- API mạng theo hệ điều hành
C và C++ được sử dụng để triển khai các ngăn xếp mạng của hệ điều hành cũng như các proxy và giao thức hiệu suất cao. Go phù hợp để phát triển dịch vụ bằng cách sử dụng goroutines và thư viện tiêu chuẩn mạng, trong khi Rust có thể sử dụng các thư viện không đồng bộ và an toàn bộ nhớ. Java, C#, Python và JavaScript cũng được sử dụng trong các ứng dụng máy chủ và máy khách.
Trong các chương trình mạng, ngữ pháp không đồng bộ do ngôn ngữ cung cấp có thể giảm độ phức tạp, nhưng trạng thái giao thức, giới hạn thời gian, xác minh đầu vào và phản hồi tấn công phải được thiết kế riêng.
An ninh mạng
Trong lĩnh vực cyber security, việc phát triển sản phẩm bảo mật, phân tích lỗ hổng, giám sát mạng, triển khai mật khẩu, phân tích mã độc, điều tra kỹ thuật số và tự động hóa được thực hiện.
Ngôn ngữ được sử dụng thay đổi tùy theo nhiệm vụ.
- C và C++ được sử dụng cho hệ điều hành, nhị phân, mạng và các công cụ bảo mật cấp thấp.
- Rust có thể được sử dụng trong các công cụ bảo mật và các thành phần hệ thống nơi an toàn bộ nhớ là quan trọng.
- Python được sử dụng để phân tích, tự động hóa, kết nối API an toàn và các công cụ kiểm tra.
- Ngôn ngữ hội có thể được yêu cầu để phân tích các tệp thực thi và hướng dẫn bộ xử lý.
- JavaScript được sử dụng để bảo mật web và phân tích môi trường trình duyệt.
- SQL liên quan đến bảo mật cơ sở dữ liệu và phân tích truy vấn.
Trong lĩnh vực bảo mật, việc sử dụng ngôn ngữ an toàn không tự động đảm bảo an toàn cho toàn bộ chương trình. Xác thực và ủy quyền, giao thức mật mã, xác thực đầu vào, quản lý khóa và bảo mật chuỗi cung ứng là những vấn đề riêng biệt với bảo mật bộ nhớ của ngôn ngữ.
Mật mã
Mật mã Phần mềm thực hiện các thuật toán mã hóa, chữ ký, hàm băm, trao đổi khóa và tạo số ngẫu nhiên.
Các yếu tố sau đây rất quan trọng trong việc triển khai mật mã:
- Hoạt động số nguyên và bit chính xác
- Chạy trong một khoảng thời gian nhất định
- Quản lý bộ nhớ thông tin bí mật
- Thay đổi mã do tối ưu hóa
- Hướng dẫn tăng tốc phần cứng
- Sử dụng các thuật toán đã được chứng minh
- Kiểm tra nghiêm ngặt đầu vào bên ngoài
- Đánh giá độc lập việc thực hiện
Thư viện mật mã hiệu suất cao có thể được sử dụng cùng với ngôn ngữ C, C++, Rust và hợp ngữ. Các ứng dụng như Python, Java và C# thường gọi các thư viện tiêu chuẩn và API nền tảng đã được chứng minh thay vì triển khai trực tiếp các thuật toán mã hóa.
Hệ thống tài chính và kinh doanh
Hệ thống tài chính, bảo hiểm, chính phủ, phân phối, kế toán và kinh doanh của công ty xử lý các giao dịch, khách hàng, hợp đồng, thanh toán, sổ cái và báo cáo.
Các yếu tố sau đây rất quan trọng trong lĩnh vực này:
- Độ chính xác của dữ liệu
- Hồ sơ giao dịch và kiểm toán
- Khả năng tương thích lâu dài
- Quyền truy cập và tuân thủ
- Xử lý dữ liệu quy mô lớn
- Tích hợp với các hệ thống hiện có
- Chuyển đổi dự phòng
- Tính toán số lượng chính xác
COBOL đã được sử dụng trong các hệ thống kinh doanh hiện có trong ngân hàng, bảo hiểm, chính phủ và các tập đoàn lớn. Java, C# và SQL được sử dụng trong các máy chủ doanh nghiệp và ứng dụng kinh doanh hiện đại, trong khi JavaScript và TypeScript được sử dụng trong giao diện người dùng.
Ở những lĩnh vực mà độ trễ và thông lượng là quan trọng, chẳng hạn như giao dịch tần suất cao và tính toán rủi ro, các ngôn ngữ như C++, Java, Rust và Fortran có thể được sử dụng. Fortran coi thống kê và tài chính là một trong những lĩnh vực ứng dụng của tính toán khoa học và số.
Thay đổi ngôn ngữ trong hệ thống tài chính không phải là vấn đề chuyển đổi mã đơn giản. Quá trình hiện đại hóa và khả năng tương tác ngày càng quan trọng vì các quy tắc giao dịch, định dạng dữ liệu, quy định và hành vi hàng thập kỷ hiện tại phải được bảo tồn một cách chính xác.
Hệ thống quan trọng về an toàn
Hệ thống quan trọng về an toàn là những hệ thống trong đó lỗi phần mềm có thể dẫn đến thiệt hại đến tính mạng và sức khỏe con người, môi trường hoặc thiệt hại tài sản trên quy mô lớn. Chúng bao gồm hàng không, đường sắt, ô tô, thiết bị y tế, điều khiển công nghiệp và hệ thống năng lượng hạt nhân.
Lĩnh vực này đòi hỏi sự phát triển và xác minh nghiêm ngặt hơn các ứng dụng thông thường.
- Theo dõi yêu cầu
- Quy tắc mã hóa
- Phân tích tĩnh
- Đo phạm vi thử nghiệm
- Xác minh định dạng
- Phân tích thời gian thực hiện tồi tệ nhất
- Hạn chế sử dụng bộ nhớ động
- Công cụ được chứng nhận
- Thay đổi hồ sơ quản lý và kiểm toán
Ada có thể được sử dụng làm ngôn ngữ để hỗ trợ các ứng dụng và hệ thống có tính toàn vẹn cao yêu cầu chứng nhận tiêu chuẩn an toàn. Hệ thống gõ mạnh mẽ và hồ sơ thực thi hạn chế của Ada được thiết kế để giảm khả năng xảy ra lỗi chương trình và hỗ trợ xác minh.
C và C++ cũng được sử dụng trong môi trường ô tô, hàng không và công nghiệp, nhưng với các tập hợp con an toàn, tiêu chuẩn mã hóa, phân tích tĩnh và các ràng buộc về thời gian chạy. Quá trình phát triển, công cụ xác minh, phần cứng và thiết kế an toàn của hệ thống tổng thể có thể quan trọng hơn việc lựa chọn ngôn ngữ.
Robot và hệ thống tự hành
Hệ thống robot và tự động kết hợp đầu vào cảm biến, ước tính vị trí, lập kế hoạch, điều khiển, xử lý hình ảnh và giao tiếp phần cứng.
Các vai trò ngôn ngữ chính có thể được chia như sau:
- C và C++ được sử dụng để điều khiển thời gian thực, xử lý cảm biến và phần mềm trung gian của robot.
- Python được sử dụng để kết nối các thuật toán nghiên cứu, thí nghiệm, công cụ và mô hình học máy.
- Ngôn ngữ CUDA và GPU tăng tốc quá trình xử lý hình ảnh và mạng lưới thần kinh.
- Rust có thể được áp dụng để bảo mật các thành phần hệ thống và giao tiếp của thiết bị.
- Môi trường dựa trên MATLAB và Simulink có thể được sử dụng cho các mô hình và mô phỏng điều khiển.
Vì các chương trình rô-bốt tương tác trực tiếp với thế giới vật lý nên lỗi có thể vượt xa việc chấm dứt chương trình đơn giản và dẫn đến hành vi nguy hiểm của thiết bị. Những hạn chế về thời gian thực, sự không chắc chắn của cảm biến, dừng khẩn cấp và trạng thái an toàn phải được giải quyết bằng thiết kế hệ thống bên ngoài ngôn ngữ.
Internet vạn vật
Internet of Things là một hệ thống trong đó các cảm biến và thiết bị được kết nối qua mạng để thu thập dữ liệu và thực hiện các lệnh từ xa.
Một hệ thống IoT bao gồm nhiều lớp.
Firmware thiết bị
C / C++ / Rust
Gateway
C++ / Rust / Go / Python
Xử lý thông điệp và máy chủ
Go / Java / C# / Python / JavaScript
Lưu trữ và phân tích dữ liệu
SQL / Python / Scala
Giao diện quản trị
JavaScript / TypeScript
Đối với các thiết bị có bộ nhớ và nguồn điện hạn chế, việc mất mạng và độ tin cậy của các bản cập nhật từ xa là rất quan trọng. Trong máy chủ, việc kết nối nhiều thiết bị, xử lý tin nhắn, xác thực thiết bị và phân tích dữ liệu là rất quan trọng.
Do các thiết bị IoT có thể được cài đặt tại hiện trường trong thời gian dài và khó bảo trì trực tiếp nên các bản cập nhật an toàn, quản lý khóa, mã hóa thông tin liên lạc và khắc phục thảm họa cũng quan trọng như việc lựa chọn ngôn ngữ.
Blockchain và sổ cái phân tán
Hệ thống sổ cái phân tán và chuỗi khối bao gồm các chương trình nút, giao thức đồng thuận, giao tiếp mạng, lưu trữ và hợp đồng thông minh.
C++, Go, Rust, Java, v.v. có thể được sử dụng để triển khai nút và các ngôn ngữ dành riêng cho miền dành riêng cho nền tảng hoặc các ngôn ngữ có mục đích chung hạn chế được sử dụng cho hợp đồng thông minh.
Vì hợp đồng thông minh khó sửa đổi sau khi triển khai và chi phí thực hiện cũng như những thay đổi về trạng thái có thể liên quan trực tiếp đến giá trị kinh tế nên các yếu tố sau rất quan trọng:
- Quyết tâm thực hiện
- Xử lý số nguyên và số lượng
- Kiểm tra quyền
- Mối quan hệ quay trở lại và cuộc gọi
- Hạn chế sử dụng tài nguyên
- Tính nguyên tử của sự thay đổi trạng thái
- Xác minh chính thức và phân tích tĩnh
- Chính sách nâng cấp
Các lỗ hổng có thể phục hồi được trong một chương trình máy chủ thông thường có thể dẫn đến mất tài sản vĩnh viễn trong hợp đồng thông minh, đặt ra các hạn chế và khả năng xác minh trước tính biểu cảm của ngôn ngữ.
Giáo dục
Ngôn ngữ lập trình được sử dụng để dạy khoa học máy tính và phát triển phần mềm, tư duy logic và giải quyết vấn đề.
Khi chọn ngôn ngữ để giảng dạy, hãy cân nhắc những điều sau:
- Đơn giản và nhất quán về ngữ pháp
- Thông báo lỗi
- Thuận tiện trong lắp đặt và vận hành
- Phòng thí nghiệm tương tác
- Trực quan hóa và gỡ lỗi
- Sự liên quan đến nội dung bài học
- Liên kết đến ngôn ngữ bạn sẽ sử dụng sau này
- Sách giáo khoa và tài liệu giáo dục
- Khả năng truy cập của hệ điều hành và thiết bị
Python có thể được sử dụng cho giáo dục cơ bản và giáo dục khoa học/dữ liệu dựa trên cú pháp tương đối đơn giản và môi trường thực thi tương tác. Swift Playground cung cấp việc học viết mã và phát triển ứng dụng bằng cách sử dụng mã Swift thực và các bài học tương tác.
Ngôn ngữ dựa trên khối và cào làm giảm gánh nặng lỗi cú pháp và cho phép bạn tìm hiểu trực quan các khái niệm về sự kiện, sự lặp lại, điều kiện và biến. Trong giáo dục khoa học máy tính ở trường đại học, có thể sử dụng các ngôn ngữ khác nhau, chẳng hạn như Python, Java, C, C++, Haskell và Đề án, tùy thuộc vào mục đích giáo dục.
Ngôn ngữ nhập môn có thể không phải là ngôn ngữ tốt nhất để dạy tất cả các khái niệm nâng cao. Ngôn ngữ C và hợp ngữ có thể hữu ích cho việc học bộ nhớ và hệ điều hành, đồng thời các ngôn ngữ dựa trên Haskell hoặc ML có thể phù hợp cho việc học về chức năng và lý thuyết loại.
Nghiên cứu ngôn ngữ lập trình
Bản thân ngôn ngữ lập trình cũng là một chủ đề nghiên cứu. Cú pháp mới, hệ thống kiểu, ngữ nghĩa, tối ưu hóa trình biên dịch, xác minh chương trình và các công cụ phát triển được nghiên cứu.
Các lĩnh vực nghiên cứu chính như sau:
- Lý thuyết loại
- Ngữ nghĩa hình thức
- Trình biên dịch
- Phân tích tĩnh
- Xác minh chương trình
- An toàn bộ nhớ
- Mô hình đồng thời
- Ngôn ngữ dành riêng cho miền
- Tổng hợp và tạo mã
- Ngôn ngữ và trải nghiệm người dùng
- Thông báo lỗi và công cụ phát triển
- Ngôn ngữ lập trình lượng tử
- Ngôn ngữ lập trình xác suất
Các ngôn ngữ nghiên cứu có thể thử nghiệm các tính năng mới mà các ngôn ngữ công nghiệp có mục đích chung gặp khó khăn trong việc cung cấp. Các hàm đã được xác minh sau này có thể được đưa vào các ngôn ngữ, thư viện và trình biên dịch hiện có.
Các đặc điểm thành công trong nghiên cứu không nhất thiết phải phù hợp với các ngôn ngữ công nghiệp. Chi phí triển khai, thời gian biên dịch, chẩn đoán lỗi, khả năng tương thích với mã hiện có và chi phí học tập của người dùng phải được xem xét cùng nhau.
Ngôn ngữ chuyên biệt miền
Ngôn ngữ chuyên biệt miền là ngôn ngữ được thiết kế để thể hiện trực tiếp các khái niệm và quy tắc của một miền vấn đề cụ thể.
Ví dụ đại diện như sau:
- SQL thể hiện các truy vấn dữ liệu quan hệ.
- Biểu thức chính quy thể hiện các mẫu chuỗi.
- Ngôn ngữ Shader thể hiện các hoạt động đồ họa của GPU.
- Ngôn ngữ mô tả phần cứng thể hiện các mạch số.
- Ngôn ngữ xây dựng thể hiện sự phụ thuộc của các tệp và tác vụ.
- Ngôn ngữ cấu hình thể hiện cấu hình của chương trình và hệ thống.
- Ngôn ngữ mô hình thống kê thể hiện phân bố xác suất và mối quan hệ quan sát.
- Ngôn ngữ chính sách thể hiện quyền và quy tắc truy cập.
Các ngôn ngữ dành riêng cho miền cung cấp các biểu thức ngắn hơn, rõ ràng hơn so với các ngôn ngữ có mục đích chung trong trường và cho phép người thực thi thực hiện tối ưu hóa và xác minh bằng kiến thức về miền. Mặt khác, phạm vi ứng dụng của ngôn ngữ này rất hẹp và có thể cần có các công cụ và việc học riêng biệt.
Ngôn ngữ dành riêng cho miền có thể tồn tại dưới dạng định dạng tệp độc lập hoặc có thể được nhúng trong thư viện, toán tử và macro của ngôn ngữ có mục đích chung.
Thiết kế phần cứng
Thiết kế các mạch và bộ xử lý kỹ thuật số đòi hỏi phải thực hiện phần mềm tuần tự và các mô hình tính toán khác nhau. Nhiều phần của mạch hoạt động đồng thời và tín hiệu, đồng hồ, độ trễ và cấu trúc tài nguyên vật lý phải được biểu diễn.
Ngôn ngữ mô tả phần cứng, chẳng hạn như Verilog và VHDL, mô tả các thanh ghi, mạch tổ hợp, máy trạng thái và kết nối mô-đun. Thiết kế được tạo ra có thể được mô phỏng hoặc tổng hợp thành các mạch FPGA và ASIC.
Gần đây, các ngôn ngữ cấu hình phần cứng tạo cấu trúc mạch trên các ngôn ngữ có mục đích chung như Scala, Python và Haskell cũng được sử dụng. Trong trường hợp này, các tính năng trừu tượng hóa và siêu lập trình của các ngôn ngữ có mục đích chung được sử dụng để cuối cùng tạo ra một biểu diễn trung gian của công nghệ phần cứng.
Lập trình shader và GPU
GPU được thiết kế để thực hiện song song các hoạt động tương tự trên nhiều đơn vị tính toán. Các shader và nhân GPU nhắm vào mô hình thực thi này.
Trình đổ bóng đồ họa thực hiện các tác vụ sau:
- Chuyển đổi tọa độ đỉnh
- Tính toán vật liệu bề mặt và ánh sáng
- Lấy mẫu kết cấu
- Tính màu pixel
- Xử lý hậu kỳ
- Dò tia
- Tính toán song song cho mục đích chung
Ngôn ngữ GPU cần xem xét các luồng và nhóm làm việc, phân cấp bộ nhớ, đồng bộ hóa và phân kỳ nhánh. Mặc dù nó sử dụng cú pháp tương tự như ngôn ngữ CPU thông thường nhưng phương thức thực thi lại khác biệt đáng kể.
CUDA cung cấp một mô hình cho phép sử dụng các phép tính GPU trong C++ và Python, đồng thời API đồ họa có thể sử dụng các biểu thức đổ bóng như HLSL, GLSL và SPIR-V.
Điện toán lượng tử
Điện toán lượng tử sử dụng ngôn ngữ và SDK để thể hiện qubit, cổng lượng tử, phép đo và điều khiển cổ điển.
Các chương trình lượng tử thường có cấu trúc hỗn hợp trong đó các chương trình cổ điển xây dựng các mạch lượng tử, nhận kết quả thực thi và xử lý hậu kỳ chúng.
Chương trình host cổ điển
↓
Tạo mạch lượng tử
↓
Trình mô phỏng hoặc thiết bị lượng tử
↓
Kết quả đo
↓
Phân tích cổ điển
Các trạng thái lượng tử không thể được sao chép tự do như các giá trị thông thường và các phép đo ảnh hưởng đến trạng thái đó. Theo đó, các loại tuyến tính, hệ thống hiệu ứng và ngôn ngữ dựa trên mạch đang được nghiên cứu.
Hiện tại, lập trình lượng tử không chỉ được sử dụng trong các thiết bị thực mà còn trong mô phỏng, giáo dục và nghiên cứu thuật toán, đồng thời việc hỗ trợ SDK phần cứng, trình biên dịch và biểu thức mạch quan trọng hơn các ngôn ngữ cụ thể.
WebAssembly bên ngoài trình duyệt web
WebAssembly có thể được sử dụng không chỉ trong trình duyệt web mà còn trong máy chủ, plugin, hộp cát và môi trường thực thi mô-đun di động.
Các trường hợp sử dụng như sau:
- Chạy các mô-đun ngôn ngữ bản địa trong trình duyệt
- Các plugin được cách ly an toàn
- Chức năng không có máy chủ
- Định dạng phân phối có thể chạy trên nhiều hệ điều hành
- Hệ thống Mod cho game và ứng dụng
- Điện toán biên
- Phần phụ trợ chung cho thời gian chạy ngôn ngữ
WebAssembly tách mô hình tính toán cốt lõi khỏi các chức năng mạng và tệp của máy chủ. Để các chương trình thực tế truy cập các chức năng của hệ điều hành, cần có giao diện máy chủ như API trình duyệt hoặc WASI.
Tích hợp và mở rộng ngôn ngữ
Một ứng dụng có thể chứa trình thông dịch nội bộ hoặc máy ảo của các ngôn ngữ lập trình khác. Điều này cho phép người dùng mở rộng hoạt động của chương trình hoặc tự động hóa các tác vụ lặp đi lặp lại.
Các lĩnh vực ứng dụng chính như sau:
- Kịch bản trò chơi
- Tự động hóa trình soạn thảo
- chế độ
- Trình cắm
- Cài đặt và quy tắc
- Kịch bản mô phỏng
- Đường dẫn xử lý dữ liệu
Lua, Python, JavaScript và ngôn ngữ dành riêng cho miền của bạn có thể được sử dụng làm ngôn ngữ tích hợp. Các chương trình máy chủ chỉ có thể hiển thị các API an toàn cho các tập lệnh và hạn chế thời gian thực thi cũng như quyền truy cập bộ nhớ, tệp và mạng.
Việc nhúng ngôn ngữ làm tăng tính linh hoạt nhưng phải giải quyết vòng đời của đối tượng máy chủ, lan truyền lỗi, gỡ lỗi, tương thích phiên bản và cách ly mã không đáng tin cậy.
Tiêu chí chọn ngôn ngữ
Khi chọn ngôn ngữ để sử dụng trong một lĩnh vực cụ thể, rất khó để đánh giá chỉ dựa trên mức độ phổ biến hoặc tốc độ thực thi.
Các tiêu chí lựa chọn chính như sau:
- Nó có hỗ trợ hệ điều hành và phần cứng mục tiêu không?
- Các thư viện và framework cần thiết có sẵn không?
- Nó có đáp ứng nhu cầu về hiệu suất và độ trễ không?
- Nó có đáp ứng các giới hạn về bộ nhớ và năng lượng không?
- Nó có đáp ứng các yêu cầu về an toàn và bảo mật không?
- Nó có thể được kết nối với mã và dữ liệu hiện có không?
- Có thể đảm bảo an toàn cho các nhà phát triển và nhân viên bảo trì không?
- Có đủ công cụ xây dựng, thử nghiệm và gỡ lỗi không?
- Có chính sách hỗ trợ và tương thích lâu dài không?
- Triển khai và cập nhật có dễ dàng không?
- Nó có đáp ứng các yêu cầu về cấp phép, tiêu chuẩn và chứng nhận không?
- Có phù hợp với quy mô và thời gian phát triển của dự án không?
Lựa chọn ngôn ngữ không nhất thiết phải là một quyết định buộc một ngôn ngữ phải áp dụng cho tất cả các phần của dự án. Bằng cách tách các thành phần có nhu cầu khác nhau và kết nối chúng với các giao diện ổn định, bạn có thể sử dụng ngôn ngữ phù hợp hơn cho từng miền.
Tuy nhiên, khi số lượng ngôn ngữ tăng lên thì các chi phí sau cũng tăng lên.
- Xây dựng hệ thống và hệ thống công cụ trở nên phức tạp hơn.
- Dữ liệu phải được chuyển đổi qua ranh giới ngôn ngữ.
- Yêu cầu phối hợp giữa các thời gian chạy và phương pháp quản lý bộ nhớ khác nhau.
- Việc gỡ lỗi và lập hồ sơ có thể trở nên khó khăn.
- Nhà phát triển phải hiểu nhiều hệ sinh thái.
- Phải quản lý khả năng tương thích của ABI và phiên bản.
Do đó, cấu trúc đa ngôn ngữ phù hợp để sử dụng khi ranh giới giữa các thành phần rõ ràng và lợi ích của việc thêm ngôn ngữ lớn hơn chi phí cho khả năng tương tác.
Quan hệ giữa lĩnh vực và ngôn ngữ
Mối quan hệ giữa ngôn ngữ lập trình và lĩnh vực sử dụng không cố định. Khi các môi trường thực thi, thư viện và trình biên dịch mới xuất hiện, các ngôn ngữ hiện có có thể được mở rộng sang các trường mới. Kotlin đã mở rộng phạm vi của nó từ ngôn ngữ JVM sang phát triển Android và đa nền tảng, còn Swift thì đang mở rộng ra ngoài các ứng dụng của Apple sang máy chủ và các khu vực nhúng. Các tài liệu chính thức của Apple liên quan đến Swift cũng trình bày phạm vi ứng dụng của ngôn ngữ này không chỉ với các ứng dụng di động mà còn với các máy chủ và phần mềm nhúng.
Ngược lại, một ngôn ngữ được sử dụng rộng rãi trong một lĩnh vực cụ thể không phải lúc nào cũng được chọn vì đó là ngôn ngữ tốt nhất về mặt kỹ thuật. Mã và thư viện hiện có, tài liệu đào tạo, công cụ phát triển, sự hỗ trợ từ các nhà cung cấp phần cứng và thị trường lao động có ảnh hưởng mạnh mẽ. Mã, tiêu chuẩn và khả năng tương tác đã được chứng minh từ lâu có thể có giá trị hơn cú pháp và tính năng mới.
Việc sử dụng các ngôn ngữ lập trình đã phát triển theo hướng hợp tác giữa các ngôn ngữ ở các mức độ trừu tượng khác nhau hơn là theo hướng một ngôn ngữ thay thế tất cả các lĩnh vực. Các lớp nền tảng hướng đến hệ thống và hiệu suất, các lớp ứng dụng được quản lý, tập lệnh tự động hóa và các ngôn ngữ dành riêng cho miền cho truy vấn dữ liệu và GPU được sử dụng cùng nhau trong một hệ thống.
Do đó, việc hiểu các ứng dụng của ngôn ngữ lập trình không chỉ giới hạn ở việc liệt kê những gì có thể được tạo bằng một ngôn ngữ cụ thể. Điều này có nghĩa là xác định mô hình tính toán, độ an toàn, hiệu suất, môi trường thực thi và hệ sinh thái theo yêu cầu của từng lĩnh vực, đồng thời chọn ngôn ngữ, thời gian chạy, thư viện và công cụ phù hợp với mục đích của hệ thống tổng thể.
So sánh các ngôn ngữ lập trình
So sánh các ngôn ngữ lập trình không phải là việc xác định liệu một ngôn ngữ về tổng thể có tốt hơn ngôn ngữ khác hay không. Vì mỗi ngôn ngữ được thiết kế cho các vấn đề, môi trường thực thi, yêu cầu an toàn, phương pháp phát triển và hệ sinh thái hiện có khác nhau nên tính ưu việt của tất cả các ngôn ngữ không thể được xác định theo cùng một tiêu chí. Ngôn ngữ phù hợp với nhân hệ điều hành không nhất thiết là ngôn ngữ tốt nhất để phân tích dữ liệu và ngôn ngữ cho phép bạn viết nhanh các chương trình tự động hóa ngắn không mang lại lợi thế tương tự cho mã lõi của trình kết xuất thời gian thực.
Các đặc điểm thực tế của một ngôn ngữ không chỉ được xác định bởi ngữ pháp. Đặc tả ngôn ngữ và hệ thống kiểu, trình biên dịch hoặc trình thông dịch, thời gian chạy, trình thu gom rác, thư viện chuẩn, hệ sinh thái gói, công cụ phát triển và nền tảng đích hoạt động cùng nhau. Ngay cả đối với cùng một ngôn ngữ, hiệu suất, mức sử dụng bộ nhớ, thời gian khởi động và phương thức phân phối có thể khác nhau tùy thuộc vào cách triển khai, tùy chọn biên dịch, thư viện và môi trường thực thi.
Vì vậy, các ngôn ngữ lập trình phải được so sánh riêng biệt trên một số tiêu chí, chẳng hạn như:
| Tiêu chí so sánh | Câu hỏi chính |
|---|---|
| Trừu tượng hóa và kiểm soát | Có thể trực tiếp kiểm soát phần cứng và bộ nhớ đến mức nào? |
| mô hình thực hiện | Bạn có sử dụng mã gốc, máy ảo hoặc trình thông dịch không? |
| hệ thống kiểu | Khi nào cần kiểm tra lỗi và cách thể hiện loại |
| Quản lý bộ nhớ | Ai quản lý tuổi thọ của đồ vật và tài nguyên |
| An toàn | Ngôn ngữ và quá trình triển khai ngăn chặn những loại lỗi nào |
| Hiệu suất | Bạn có thể kiểm soát được bao nhiêu thông lượng, độ trễ và mức sử dụng bộ nhớ? |
| đồng thời | Cách thể hiện nhiều nhiệm vụ và trạng thái chia sẻ |
| Tính di động | Có thể chạy trên nhiều hệ điều hành và kiến trúc |
| Khả năng tương tác | Việc sử dụng các thư viện hiện có và các ngôn ngữ khác dễ dàng như thế nào |
| Năng suất phát triển | Thật thuận tiện khi viết, chỉnh sửa, kiểm tra và phát hiện lỗi |
| Công cụ và hệ sinh thái | Có đủ trình biên dịch, trình gỡ lỗi, gói và khung không |
| Tính ổn định và khả năng tương thích | Ngôn ngữ và nền tảng có duy trì mã hiện có trong một thời gian dài không? |
Giới hạn của việc so sánh
Khi so sánh các ngôn ngữ lập trình, chúng ta phải phân biệt giữa ngôn ngữ và cách thực hiện. Python là một ngôn ngữ và CPython là một triển khai tiêu biểu. Java là một ngôn ngữ và JVM là môi trường thực thi có thể được nhiều ngôn ngữ sử dụng, bao gồm cả Java. C# là một ngôn ngữ và thường được sử dụng với .NET, nhưng đặc tả ngôn ngữ C# không hạn chế rằng việc triển khai tuân thủ phải được xây dựng dựa trên CLI hoặc .NET. Tài liệu tham khảo ngôn ngữ hình thức của Python cũng mô tả cú pháp và ngữ nghĩa cốt lõi của ngôn ngữ và không khóa tất cả chi tiết triển khai vào CPython.
Không nên sử dụng sự khác biệt giữa “ngôn ngữ được biên dịch” và “ngôn ngữ được dịch” như một thuộc tính tuyệt đối của chính ngôn ngữ đó. Java thường dịch mã nguồn thành mã byte độc lập với máy và chạy nó trên JVM và JVM có thể diễn giải hoặc JIT biên dịch mã. Việc triển khai Python cũng có thể sử dụng các phương thức khác nhau, chẳng hạn như mã byte nội bộ, máy ảo và biên dịch JIT. Đặc tả ngôn ngữ Java mô tả mã byte độc lập với máy là kết quả của quá trình biên dịch chung, nhưng điều này không có nghĩa là ngôn ngữ Java bị khóa đối với một chiến lược thực thi cụ thể.
So sánh hiệu suất cũng không thể được kết luận chỉ dựa trên tên ngôn ngữ. Ngay cả trong cùng một ngôn ngữ, kết quả cũng khác nhau tùy thuộc vào thuật toán, cấu trúc dữ liệu, trình biên dịch, tùy chọn tối ưu hóa, chuẩn bị thời gian chạy, cài đặt trình thu gom rác cũng như cấu trúc CPU và bộ nhớ. Thời gian khởi động của một chương trình dòng lệnh ngắn, thông lượng tối đa của máy chủ chạy trong thời gian dài và độ trễ trong trường hợp xấu nhất của hệ thống âm thanh thời gian thực là các tiêu chí hiệu suất khác nhau.
Do đó, việc so sánh ngôn ngữ nên được thực hiện bằng cách xem xét “chi phí nào do tầng lớp nào gánh chịu và chi phí có thể dự đoán và kiểm soát là bao nhiêu” thay vì “chi phí nào là nhanh nhất”.
Mức độ trừu tượng và kiểm soát phần cứng
Các ngôn ngữ lập trình khác nhau tùy thuộc vào mức độ chúng thể hiện cấu trúc cụ thể của phần cứng.
C (ngôn ngữ lập trình) có thể xử lý trực tiếp các con trỏ và địa chỉ bộ nhớ, biểu diễn số nguyên, vị trí cấu trúc và quản lý bộ nhớ thủ công. Tiêu chuẩn C xác định cách diễn đạt và ngữ nghĩa của chương trình, hướng tới tính di động và thực thi hiệu quả trên các hệ thống máy tính khác nhau. Tuy nhiên, kích thước và căn chỉnh chính xác của các kiểu dữ liệu cũng như kết quả của một số thao tác có thể khác nhau tùy thuộc vào việc triển khai và môi trường đích.
C++ cung cấp khả năng kiểm soát cấp thấp tương tự như C trong khi thêm các phần trừu tượng hóa cấp cao như lớp và mẫu, lập trình tổng quát, ngữ nghĩa giá trị và quản lý tài nguyên dựa trên phạm vi. Một trong những nguyên tắc quan trọng trong thiết kế C++ là nguyên tắc trừu tượng không mất phí, trong đó nêu rõ rằng bạn không nên phải chịu chi phí cho các chức năng không được sử dụng và các phần trừu tượng mà bạn sử dụng không được yêu cầu các chi phí không cần thiết so với mã cấp thấp mà bạn tự viết khi được triển khai đúng cách.
Rust cũng có thể viết các chương trình hệ thống yêu cầu con trỏ và vị trí bộ nhớ, ABI gốc và quản lý tài nguyên trực tiếp, nhưng mã bảo mật sẽ giới hạn việc sử dụng bộ nhớ thông qua các quy tắc sở hữu, mượn và vòng đời. Cách tiếp cận độc quyền của Rust là yêu cầu trình biên dịch kiểm tra các quy tắc quản lý bộ nhớ trong quá trình thực thi mà không cần sử dụng trình thu gom rác.
Go (ngôn ngữ lập trình) là ngôn ngữ có mục đích chung dành cho lập trình hệ thống, nhưng để lại nhiều chi tiết về bộ nhớ và vị trí đối tượng trong thời gian chạy hơn C và Rust. Đặc tả Go mô tả Go là ngôn ngữ cung cấp tính năng định kiểu tĩnh và thu thập rác, hỗ trợ đồng thời rõ ràng và cấu thành chương trình dựa trên gói.
Các ngôn ngữ như Java, C#, Python và JavaScript thường không trực tiếp thao tác số học con trỏ và vị trí bộ nhớ đối tượng mà sử dụng mô hình đối tượng của máy ảo hoặc thời gian chạy. Mặc dù sự trừu tượng hóa này có thể làm tăng tính an toàn của bộ nhớ và tính di động, nhưng việc kiểm soát trực tiếp chi phí của vị trí đối tượng, thời gian thu thập và lệnh gọi động là tương đối khó khăn.
Mức độ trừu tượng không thể được đánh giá một cách đơn giản là cao hay thấp. Kiểm soát phần cứng nhiều hơn cho phép tối ưu hóa đặc biệt và truy cập thiết bị, nhưng những khác biệt về nền tảng cụ thể và lỗi bộ nhớ phải được quản lý trực tiếp. Ngược lại, việc quản lý thời gian chạy nhiều chi tiết giúp tập trung vào logic ứng dụng dễ dàng hơn nhưng có thể bị hạn chế ở những khu vực mà phản hồi thời gian thực và vị trí bộ nhớ là quan trọng.
Mô hình thực thi
Các mô hình thực thi chung của các ngôn ngữ lập trình đại diện có thể được so sánh như sau.
| ngôn ngữ | Mô hình thực hiện chung | Đặc điểm chính |
|---|---|---|
| C (ngôn ngữ lập trình) | Biên dịch bản địa AOT | Thời gian chạy nhỏ, ABI trực tiếp và truy cập hệ thống |
| C++ | Biên dịch bản địa AOT | Trừu tượng hóa cấp cao và tối ưu hóa theo mục tiêu cụ thể |
| Rust | Biên dịch bản địa AOT | Tạo mã gốc sau khi kiểm tra quyền sở hữu |
| Go (ngôn ngữ lập trình) | Biên dịch bản địa AOT | Bao gồm trình thu gom rác và thời gian chạy ngôn ngữ |
| Java | Mã byte JVM và JIT hoặc AOT | Máy ảo và Môi trường thực thi được quản lý |
| C# | Ngôn ngữ trung gian phổ biến và .NET JIT hoặc AOT | CLR và thời gian chạy được quản lý |
| Python | Giải thích mã byte, JIT, mở rộng gốc tùy thuộc vào việc triển khai | Thực thi động và phát triển tương tác |
| JavaScript | Trình thông dịch và JIT của công cụ trình duyệt/máy chủ | Khớp nối chặt chẽ với môi trường máy chủ |
| TypeScript | Chuyển đổi sang JavaScript và chạy trong môi trường JavaScript | Thông tin loại thường bị xóa trước khi thực hiện |
| Kotlin | JVM, JavaScript, Native, WebAssembly, v.v. | Tương tác với các chương trình phụ trợ được nhắm mục tiêu |
| Swift | biên dịch bản địa | Các kiểu giá trị và quản lý bộ nhớ tự động |
| Fortran | Biên dịch bản địa AOT | Tối ưu hóa tập trung vào số/mảng và các hàm song song |
Bảng này chỉ trình bày các phương pháp triển khai mang tính đại diện và không giới hạn tất cả các phương pháp triển khai mà một ngôn ngữ có thể có. Mã C và C++ có thể được biên dịch thành WebAssembly hoặc các dạng biểu diễn trung gian khác, đồng thời các chương trình Java và .NET cũng có thể được phân phối AOT. Các phần nặng về tính toán của mã Python có thể được giao cho các thư viện C, C++, Fortran hoặc GPU.
ECMAScript, tiêu chuẩn cho JavaScript, xác định các quy tắc của ngôn ngữ để tính toán và thao tác đối tượng, nhưng để lại đầu vào và đầu ra bên ngoài cho môi trường máy chủ như thời gian chạy trình duyệt hoặc máy chủ. Do đó, phạm vi chức năng thực tế của JavaScript là kết quả của sự kết hợp giữa ngôn ngữ ECMAScript và API Web của trình duyệt hoặc API Node.js.
TypeScript không thay thế môi trường thực thi của JavaScript. TypeScript phân tích tĩnh dạng và hành vi của các giá trị JavaScript và tạo ra JavaScript bằng cách loại bỏ hoặc chuyển đổi ký hiệu kiểu và một số ngữ pháp. Hệ thống kiểu của TypeScript xoay quanh các kiểu cấu trúc để mô hình hóa cách sử dụng các đối tượng trong JavaScript.
Kiểu tĩnh và kiểu động
Ngôn ngữ kiểu tĩnh chủ yếu kiểm tra mối quan hệ kiểu của biến, biểu thức và lệnh gọi hàm trước khi thực thi. Chúng bao gồm C, C++, Rust, Go, Java, C#, Kotlin, Swift và Fortran. Đặc tả Java định nghĩa Java là ngôn ngữ được định kiểu tĩnh trong đó các loại của tất cả các biến và biểu thức được biết đến tại thời điểm biên dịch.
Trong ngôn ngữ kiểu động, các giá trị có một kiểu trong khi thực thi và kiểu của giá trị thực được kiểm tra khi thực hiện một thao tác. Python và JavaScript là những ví dụ điển hình. Trong mô hình dữ liệu của Python, tất cả dữ liệu được thể hiện dưới dạng đối tượng hoặc mối quan hệ giữa các đối tượng và mỗi đối tượng có một danh tính, loại và giá trị.
Sự khác biệt giữa kiểu tĩnh và kiểu động không chỉ đơn giản là tên kiểu có được ghi trong phần khai báo biến hay không. Rust, Kotlin, Swift, C#, v.v. có thể suy ra loại biến cục bộ nhưng thực hiện kiểm tra kiểu tĩnh. Ngược lại, kiểm tra kiểu và kiểm tra đối tượng thời gian chạy cũng tồn tại trong Python và JavaScript.
Ưu điểm chính của kiểu tĩnh là:
- Một số lỗi có thể được tìm thấy trước khi thực hiện.
- Hợp đồng của các chức năng và mô-đun có thể được thể hiện dưới dạng các loại.
- IDE dễ dàng hỗ trợ điều hướng mã, tự động hoàn thành và tái cấu trúc.
- Trình biên dịch có thể sử dụng các mối quan hệ giá trị và cuộc gọi để tối ưu hóa.
- Dễ dàng nhận thấy tác động của việc thay đổi mã quy mô lớn.
Ưu điểm chính của các kiểu động là:
- Bạn có thể xử lý dữ liệu và đối tượng có hình dạng thay đổi trong quá trình thực thi một cách tự nhiên.
- Bạn có thể nhanh chóng viết các chương trình nhỏ và mã thử nghiệm.
- Sự trừu tượng có thể được xây dựng mà không cần khai báo kiểu hoặc khái quát phức tạp.
- Dễ dàng sử dụng phản ánh, siêu lập trình và tải động trực tiếp.
Ngay cả trong các ngôn ngữ kiểu tĩnh, các lỗi logic mà hệ thống kiểu không thể diễn đạt vẫn còn và các ngôn ngữ kiểu động cũng có thể đảm bảo độ tin cậy cao thông qua kiểm tra đầy đủ và phân tích tĩnh. Thời điểm kiểm tra kiểu là một trong nhiều yếu tố quyết định chất lượng phần mềm.
Kiểu danh nghĩa và kiểu cấu trúc
Các lớp và giao diện trong Java, C#, Kotlin và Swift chủ yếu tuân theo hệ thống kiểu danh nghĩa sử dụng tên được khai báo và các mối quan hệ kế thừa/triển khai rõ ràng. Ngay cả khi hai lớp có cùng trường và phương thức, chúng thường không được coi là cùng loại nếu không có mối quan hệ rõ ràng.
TypeScript chủ yếu sử dụng hệ thống kiểu cấu trúc để so sánh các kiểu thành viên mà một đối tượng có. Ngay cả khi việc triển khai giao diện không được khai báo riêng biệt, một đối tượng có tất cả các trường và phương thức cần thiết vẫn có thể tương thích với loại tương ứng.
Giao diện của Go cũng có bản chất cấu trúc, trong đó nó được coi là đã triển khai nếu nó đáp ứng một tập hợp các phương thức mà không có khai báo implements rõ ràng. Tuy nhiên, hệ thống kiểu tổng thể của Go và các kiểu đối tượng cấu trúc của TypeScript không được tạo thành từ các quy tắc giống nhau, vì vậy chúng không thể được coi là cùng một hệ thống.
Các loại danh nghĩa có lợi trong việc phân biệt rõ ràng các khái niệm miền và ranh giới API. Các kiểu cấu trúc có thể được sử dụng trong các giao diện cần thiết mà không cần sửa đổi các đối tượng hiện có, làm giảm sự ghép nối. Mặt khác, khả năng các giá trị có cùng cấu trúc vô tình rơi vào những vị trí ngoài ý muốn cũng phải được xem xét.
Generics và lập trình chung
Generics trong C++ và Rust thường có thể tạo mã phù hợp với các loại cụ thể trong quá trình biên dịch. Điều này cho phép tối ưu hóa theo từng loại cụ thể và gửi tĩnh, nhưng việc sử dụng nhiều kết hợp loại có thể làm tăng thời gian biên dịch và kích thước mã.
Generics trong Java chủ yếu sử dụng tính năng xóa kiểu, cho phép nhiều phiên bản kiểu chia sẻ một mã byte chung. Generics trong .NET bảo toàn thông tin loại ngay cả khi chạy và các chiến lược chuyên môn hóa và chia sẻ mã có thể khác nhau tùy thuộc vào kiểu giá trị và kiểu tham chiếu. Hệ thống kiểu của C# tích hợp các kiểu giá trị và kiểu tham chiếu để cung cấp một cấu trúc cuối cùng coi tất cả các giá trị là object.
Tham số loại của Go là các ràng buộc về loại giao diện xác định loại và thao tác được phép. Rust sử dụng giới hạn đặc điểm và C++ sử dụng các mẫu và khái niệm. Các ngôn ngữ họ Haskell thể hiện sự khái quát hóa xung quanh tính đa hình tham số và các lớp kiểu.
Ngay cả khi cú pháp của generics tương tự nhau, phương pháp triển khai, ràng buộc kiểu và sự hiện diện của thông tin kiểu trong quá trình thực thi là khác nhau, do đó thiết kế thư viện và đặc điểm hiệu suất cũng khác nhau.
Phương pháp quản lý bộ nhớ
Phương pháp quản lý bộ nhớ là một trong những tiêu chí quan trọng nhất khi so sánh các ngôn ngữ lập trình.
Quản lý bộ nhớ thủ công
Trong C, lập trình viên trực tiếp quyết định thời điểm phân bổ và giải phóng bộ nhớ. Điều này cho phép kiểm soát chi tiết việc sử dụng tài nguyên, nhưng yêu cầu ngăn chặn trực tiếp các lỗi như rò rỉ bộ nhớ, sử dụng sau, rảnh đôi và vượt quá phạm vi bộ đệm.
C++ cung cấp RAII, con trỏ thông minh và vùng chứa gắn kết thời gian tồn tại của một đối tượng với phạm vi của nó trong khi vẫn cho phép sử dụng phân bổ thủ công. Trong C++ thích hợp, bạn có thể giảm việc sử dụng new và delete trực tiếp và tự động dọn sạch tài nguyên khi một đối tượng nằm ngoài phạm vi. Tuy nhiên, các con trỏ thô và các chức năng quản lý tài nguyên thủ công vẫn còn, do đó, sự an toàn tổng thể bị ảnh hưởng bởi thiết kế và các quy tắc của mã.
Quản lý dựa trên quyền sở hữu
Rust đặt chủ sở hữu vào từng giá trị và dọn sạch tài nguyên khi chủ sở hữu vượt quá phạm vi. Các tham chiếu được mượn thay vì sở hữu và trình biên dịch sẽ kiểm tra mối quan hệ cũng như thời gian tồn tại của các tham chiếu có thể thay đổi và bất biến. Kiến trúc này nhằm mục đích cung cấp sự an toàn cho bộ nhớ mà không cần bộ thu gom rác.
Ưu điểm của Rust là nhiều lỗi bộ nhớ và các cuộc chạy đua dữ liệu tiềm ẩn có thể được hạn chế trong thời gian biên dịch. Mặt khác, quyền sở hữu và thời gian tồn tại phải được xem xét rõ ràng khi thể hiện các cấu trúc được chia sẻ và tự tham chiếu phức tạp cũng như thời gian tồn tại không đồng bộ và một số hoạt động cấp thấp có thể yêu cầu mã unsafe.
Thu gom rác
Go và các triển khai chính của Java, C#, Python và JavaScript cung cấp khả năng quản lý bộ nhớ tự động. Bộ thực thi tìm và truy xuất các đối tượng mà chương trình không thể truy cập được nữa, do đó không cần phải giải phóng trực tiếp các đối tượng chung.
Trình thu gom rác của .NET phân bổ các đối tượng trên vùng được quản lý và lấy lại các đối tượng không thể truy cập được từ thư mục gốc của ngăn xếp, trường tĩnh và thanh ghi CPU. Bạn cũng có thể giảm sự phân mảnh bộ nhớ bằng cách nén các đối tượng.
Việc thu gom rác làm giảm gánh nặng quản lý vòng đời của đối tượng nhưng có thể gây ra điểm thu thập và thời gian ngừng hoạt động, bộ nhớ bổ sung và độ phức tạp của thời gian chạy. Bộ thu thập hiện đại giảm các chi phí này thông qua việc thu thập thế hệ, thu thập đồng thời và xử lý gia tăng, nhưng các hệ thống yêu cầu độ trễ nghiêm ngặt trong trường hợp xấu nhất cần phải xem xét riêng.
Số lượng tham chiếu
Swift quản lý tuổi thọ của các phiên bản lớp bằng cách tính tham chiếu tự động trong khi tích cực sử dụng các kiểu giá trị như cấu trúc và bảng liệt kê. Tài liệu Swift của Apple giải thích rằng các loại phổ biến như mảng và từ điển là kiểu giá trị và Swift cung cấp khả năng khởi tạo, kiểm tra giới hạn và an toàn bộ nhớ như những phần chính của thiết kế ngôn ngữ.
Số lượng tham chiếu được dọn sạch khi tham chiếu mạnh cuối cùng của một đối tượng biến mất, giúp hiểu được tuổi thọ tương đối dễ dàng, nhưng các tham chiếu vòng tròn mạnh phải được ngăn chặn riêng biệt và chi phí cho các hoạt động tăng và giảm tham chiếu có thể phải chịu.
An toàn bộ nhớ
An toàn bộ nhớ là thuộc tính đảm bảo rằng chương trình chỉ truy cập vào bộ nhớ hợp lệ thuộc đúng loại và thời gian tồn tại.
C và C++ cung cấp nhiều khả năng kiểm soát bằng cách cho phép truy cập trực tiếp và diễn giải lại bộ nhớ, nhưng các tập hợp con an toàn của ngôn ngữ và quy ước mã hóa, phân tích tĩnh, kiểm tra thời gian chạy và thiết kế thư viện đều quan trọng. Con trỏ không hợp lệ và lỗi phạm vi có thể dẫn đến hành vi không xác định.
Rust kiểm tra các quy tắc quyền sở hữu và mượn trong mã an toàn, nhưng cung cấp unsafe cho các hàm bên ngoài, con trỏ thô và một số thao tác hệ thống. unsafe không tắt tất cả các kiểm tra của trình biên dịch mà thay vào đó cho phép một số hành vi bổ sung, chẳng hạn như hủy bỏ tham chiếu con trỏ thô và để lại các điều kiện an toàn cho lập trình viên.
Các ngôn ngữ được quản lý như Java, C# và Go hạn chế các thao tác con trỏ tùy ý trong mã thông thường và kiểm tra giới hạn mảng cũng như loại đối tượng. C# cho phép hầu hết mã được viết dưới dạng mã an toàn có thể kiểm chứng, nhưng cho phép con trỏ và truy cập bộ nhớ trực tiếp trong các vùng unsafe rõ ràng.
Swift cũng cung cấp hành vi mặc định an toàn nhưng cung cấp họ UnsafePointer để có khả năng tương tác với C và các tối ưu hóa đặc biệt. Việc sử dụng các con trỏ này khiến nhà phát triển chịu trách nhiệm trực tiếp về việc quản lý bộ nhớ tự động và một số đảm bảo về loại/tuổi thọ.
Do đó, thay vì phân biệt tuyệt đối giữa “ngôn ngữ an toàn” và “ngôn ngữ không an toàn”, chúng ta nên so sánh lỗi nào bị chặn trong mã cơ sở, cách giải quyết nào tồn tại và ranh giới an toàn có thể được vạch ra rõ ràng như thế nào.
Ngữ nghĩa giá trị và ngữ nghĩa tham chiếu
Các ngôn ngữ cũng khác nhau ở cách cơ bản mà chúng sao chép và chia sẻ các giá trị.
Cấu trúc và đối tượng chung trong C và C++ có thể được sao chép theo giá trị theo mặc định. Con trỏ hoặc tài liệu tham khảo cho phép bạn chia sẻ cùng một đối tượng. Trong C++, bạn có thể xác định cách sao chép các giá trị và chuyển quyền sở hữu thông qua việc sao chép và di chuyển các hàm tạo cũng như toán tử gán.
Rust phân biệt giữa các loại không thể sao chép được, trong đó chuyển động là mặc định và các loại có thể sao chép rõ ràng. Quyền sở hữu một giá trị sở hữu tài nguyên heap có thể được chuyển giao khi được gán hoặc chuyển cho một hàm và việc sử dụng tên trước đó bị hạn chế.
Trong Java, Python và JavaScript, các biến đối tượng thường chứa các tham chiếu đến các đối tượng. Bài tập sao chép các tham chiếu thay vì toàn bộ đối tượng. Một số giá trị, chẳng hạn như số nguyên và chuỗi, là bất biến hoặc có quy tắc giá trị riêng biệt, nhưng những thay đổi đối với đối tượng do người dùng xác định có thể được quan sát qua nhiều tham chiếu.
C# phân biệt rõ ràng giữa kiểu giá trị và kiểu tham chiếu. Cấu trúc và bảng liệt kê là các kiểu giá trị và các giá trị của các thể hiện được sao chép trong quá trình gán và truyền đối số. Một lớp là một kiểu tham chiếu.
Swift coi cấu trúc và bảng liệt kê là kiểu giá trị và thể hiện lớp là kiểu tham chiếu. Các bộ sưu tập tiêu chuẩn cũng cung cấp ngữ nghĩa có giá trị và có thể sử dụng tính năng tối ưu hóa ghi trên bản sao trong nội bộ.
Ngữ nghĩa giá trị có thể làm giảm sự lan truyền bất ngờ của các thay đổi đối tượng đến các vị trí khác và có thể thuận lợi cho việc xử lý song song. Ngữ nghĩa tham chiếu cho phép nhiều thành phần chia sẻ cùng một trạng thái mà không cần sao chép các đối tượng lớn, nhưng yêu cầu quản lý bí danh và mối quan hệ đồng bộ hóa.
Mô hình hướng đối tượng
Java và C# hỗ trợ lập trình hướng đối tượng, tập trung vào các lớp và giao diện, kế thừa, phương thức ảo và kiểu danh nghĩa. Ngoài các lớp, C# còn cung cấp cấu trúc và cấu trúc bản ghi, là các kiểu giá trị và xử lý tất cả các kiểu trong một hệ thống kiểu tích hợp.
C++ cung cấp các lớp, đa kế thừa và các hàm ảo cũng như ngữ nghĩa giá trị và đa hình tĩnh dựa trên mẫu. Lập trình hướng đối tượng là một trong một số mô hình trong C++ và không phải tất cả các khái niệm trừu tượng đều yêu cầu sử dụng tính kế thừa và lệnh gọi ảo.
Rust không cung cấp tính kế thừa lớp truyền thống. Xác định các phương thức trong cấu trúc và bảng liệt kê cũng như hành vi chung trừu tượng với các đặc điểm. Mặc dù bạn có thể sử dụng các đối tượng đặc điểm động, nhưng bạn thường sử dụng kết hợp các đối tượng tổng quát và công văn tĩnh.
Go cũng không cung cấp tính kế thừa lớp mà sử dụng các cấu trúc, phương thức, giao diện và thành phần. Một giao diện có thể được thỏa mãn bằng cách cung cấp tập hợp các phương thức được yêu cầu mà không cần khai báo rõ ràng việc triển khai giao diện.
JavaScript sử dụng mô hình đối tượng dựa trên nguyên mẫu. Ngữ pháp class được cung cấp nhưng mối quan hệ kế thừa của các đối tượng dựa trên kết nối nguyên mẫu. Đặc tả ECMAScript định nghĩa các đối tượng là tập hợp các thuộc tính và đặt các đối tượng, hàm và hoạt động nguyên mẫu ở trung tâm ngữ nghĩa của ngôn ngữ.
Mô hình đối tượng nào tốt hơn phụ thuộc vào sự trừu tượng mà bạn muốn sử dụng. Các lớp và giao diện có thể phù hợp với các hợp đồng API danh nghĩa ổn định, đồng thời các kiểu giá trị và kiểu tổng có thể phù hợp với các mô hình dựa trên dữ liệu. Trong các môi trường mà phần mở rộng đối tượng động và siêu lập trình là quan trọng, các nguyên mẫu và thuộc tính động có thể có lợi.
Xử lý lỗi
Các ngôn ngữ cũng khác nhau trong cách truyền đạt lỗi.
C thường sử dụng các giá trị trả về, mã lỗi và trạng thái lỗi chung. Việc xử lý lỗi rất rõ ràng nhưng lỗi có thể bị bỏ qua nếu người gọi không kiểm tra giá trị trả về.
C++, Java, C#, Python, JavaScript, Kotlin và Swift cung cấp các cấu trúc xử lý ngoại lệ. Các ngoại lệ có thể được tự động truy tìm lại một số cấp độ lệnh gọi, nhưng có thể khó biết được lỗi nào đã xảy ra nếu chỉ dựa trên kiểu trả về chung của hàm.
Rust chủ yếu biểu thị các lỗi có thể phục hồi dưới dạng các kiểu tổng, chẳng hạn như Result<T, E> và có thể sử dụng panic đối với các lỗi không thể khôi phục. Go tập trung vào việc trả về lỗi dưới dạng giá trị thông thường. Cả hai phương pháp đều bộc lộ rõ ràng khả năng xảy ra lỗi trong mã gọi nhưng yêu cầu cú pháp ngôn ngữ và mẫu thành ngữ để giảm mã truyền lặp lại.
Swift có thể sử dụng throws và do·try·catch, giá trị lựa chọn và kiểu kết quả, đồng thời Kotlin tương tác với Java nhưng không thực thi các ngoại lệ đã kiểm tra trong hệ thống kiểu ngôn ngữ.
Xử lý lỗi không chỉ là vấn đề ưu tiên cú pháp. Nó ảnh hưởng đến API thư viện, dọn dẹp tài nguyên, thực thi không đồng bộ, ranh giới giao dịch và chiến lược khôi phục chương trình.
An toàn null
Trong các ngôn ngữ như Java và C# đời đầu, hầu hết các kiểu tham chiếu đều chứa null và bạn phải kiểm tra riêng xem giá trị đó có phải là một đối tượng thực tế hay không. C# hiện đại có thể phân biệt giữa string và string? ở cấp nguồn thông qua phân tích kiểu tham chiếu có thể rỗng, nhưng hai ký hiệu này được sử dụng để phân tích tĩnh của trình biên dịch thay vì tạo các loại thời gian chạy riêng biệt.
Kotlin phân biệt về mặt cú pháp giữa loại không null và loại null. Về nguyên tắc, null không thể thay thế cho String và chỉ được phép trong String?. Tuy nhiên, khi tương tác với Java, các loại nền tảng và chú thích bên ngoài phải được xử lý cùng nhau vì các khai báo Java có thể không chứa đủ thông tin null.
Rust không bao gồm giá trị rỗng trong các tham chiếu thông thường và thể hiện sự thiếu vắng giá trị Option<T>. Swift cũng sử dụng các kiểu lựa chọn để phân biệt khả năng không có giá trị theo kiểu.
Trong Python và JavaScript, các giá trị như None, null và undefined được sử dụng trong quá trình thực thi động và có thể kiểm tra một số khả năng thông qua máy phân tích tĩnh và TypeScript.
Các tính năng an toàn null có thể làm giảm đáng kể phạm vi lỗi tham chiếu null, nhưng lỗi vẫn có thể xảy ra thông qua mã bên ngoài và quy trình khởi tạo, xác nhận bắt buộc và giao diện không an toàn.
Đồng thời và song song
Hỗ trợ đồng thời bị ảnh hưởng rất nhiều bởi cú pháp của ngôn ngữ cũng như thư viện chuẩn và thời gian chạy.
C và C++ sử dụng các luồng hệ điều hành, các phép toán nguyên tử, các mô hình khóa và bộ nhớ. Có thể kiểm soát ở mức độ thấp nhưng bạn phải trực tiếp quản lý trạng thái chia sẻ, thời gian tồn tại và độ chính xác đồng bộ hóa.
Rust cũng hỗ trợ các luồng, khóa, hoạt động nguyên tử và thực thi không đồng bộ. Các mối quan hệ quyền sở hữu và loại, chẳng hạn như Send và Sync có thể hạn chế tĩnh xem liệu một số giá trị nhất định có thể được di chuyển hoặc chia sẻ giữa các luồng hay không. Đồng thời bộ nhớ dùng chung vẫn yêu cầu khóa và thiết kế nguyên tử, nhưng có thể ngăn chặn một số cuộc chạy đua dữ liệu ở giai đoạn kiểm tra kiểu.
Go cung cấp goroutines và các kênh làm mô hình đồng thời cốt lõi của ngôn ngữ và thời gian chạy. Goroutine là một đơn vị công việc nhẹ hơn thread của hệ điều hành và các kênh được sử dụng để truyền giá trị và đồng bộ hóa giữa các goroutine. Tuy nhiên, Go cũng có thể sử dụng bộ nhớ dùng chung và khóa nên các kênh không phải là phương thức tương tranh duy nhất.
Java và C# cung cấp nhóm luồng, tương lai, tác vụ, async·await, bộ sưu tập đồng thời và thời gian chạy được quản lý. Hệ sinh thái máy chủ lâu đời của JVM và .NET cung cấp các công cụ và thư viện cần thiết để soạn thảo các dịch vụ đồng thời quy mô lớn.
Các coroutine Kotlin kết hợp chức năng tạm dừng của ngôn ngữ với mô hình thực thi dựa trên thư viện. Bộ lập lịch thực thi, kênh, luồng, v.v. chủ yếu được cung cấp bởi thư viện kotlinx.coroutines. Bối cảnh thực thi của một coroutine bao gồm các thành phần như tác vụ và bộ điều phối.
JavaScript thường sử dụng vòng lặp sự kiện và lời hứa, async·await để xử lý đầu vào và đầu ra không đồng bộ. Thời gian chạy của trình duyệt và máy chủ có thể cung cấp công việc song song hoặc không đồng bộ thực sự thông qua hàng đợi tác vụ, Công nhân web và hệ thống đầu vào/đầu ra gốc.
Chức năng tương tranh đơn giản không tự động giải quyết các điều kiện tương tranh, bế tắc, hủy bỏ và lỗi một phần. Quan trọng hơn là cách bạn thiết kế trạng thái chia sẻ, thời gian hoạt động, lan truyền lỗi và áp suất ngược.
Hiệu năng và khả năng dự đoán
Hiệu suất được chia thành nhiều loại, bao gồm thông lượng và độ trễ, thời gian khởi động, mức sử dụng bộ nhớ, kích thước nhị phân và mức tiêu thụ điện năng.
C, C++ và Rust thường sử dụng ngôn ngữ máy gốc và các ranh giới thực thi nhỏ để cho phép kiểm soát chi tiết hơn về vị trí, phân bổ và phương thức gọi bộ nhớ. Điều này thuận lợi cho các ứng dụng yêu cầu mức độ tối ưu hóa thấp và độ trễ có thể dự đoán được. Tuy nhiên, hiệu suất cao không được đảm bảo tự động và cấu trúc dữ liệu cũng như quyền truy cập bộ nhớ không chính xác có thể gây ra sự suy giảm hiệu suất đáng kể ở bất kỳ ngôn ngữ nào.
Go tạo mã gốc nhưng bao gồm trình thu thập rác và trình lập lịch trình goroutine. Các chương trình máy chủ và mạng có thể được cấu hình đơn giản nhưng phải tính đến chi phí thu thập và lập kế hoạch.
Trong Java và C#, trình biên dịch JIT có thể tối ưu hóa mã dựa trên thông tin thực thi thực tế. Các tối ưu hóa như nội tuyến động và chuyên môn hóa loại có thể thực hiện được trong các chương trình chạy dài, nhưng các giai đoạn khởi động và chuẩn bị chương trình, bộ đệm mã và thu thập rác ảnh hưởng đến hiệu suất.
Các ngôn ngữ động như Python và JavaScript có thể phải chịu chi phí khi xử lý loại và hoạt động của từng giá trị trong quá trình thực thi. Công cụ JIT JavaScript có thể chuyên biệt hóa các loại và hình dạng đối tượng lặp lại, nhưng việc hủy tối ưu hóa là cần thiết khi các giả định bị phá vỡ. Python được sử dụng rộng rãi để giảm chi phí lặp đi lặp lại của các trình thông dịch ngôn ngữ bằng cách thực hiện các thao tác hàng loạt trong các thư viện viết bằng C, C++ và Fortran.
Fortran đã phát triển xung quanh mảng, phép toán số và tính toán khoa học, đồng thời hỗ trợ các phương pháp hướng mảng, hướng đối tượng và chức năng cũng như các phương pháp bắt buộc và thủ tục. Cấu trúc ngôn ngữ và trình biên dịch của Fortran làm cho nó trở thành một lựa chọn quan trọng cho tính toán số và lập trình song song.
Khi so sánh hiệu suất, bạn nên đo lường việc triển khai thực tế và khối lượng công việc thay vì hình ảnh trung bình của ngôn ngữ. Xử lý chuỗi, xếp hàng cơ sở dữ liệu, mảng số, cấp phát bộ nhớ và I/O mạng yêu cầu các đặc điểm ngôn ngữ và thời gian chạy khác nhau.
Thời gian biên dịch và thời gian khởi động
Thời gian biên dịch cho C và C++ có thể khác nhau rất nhiều tùy thuộc vào việc đưa tiêu đề, khởi tạo mẫu và mức độ tối ưu hóa. Đặc biệt, các mẫu phức tạp và tối ưu hóa toàn cục của C++ có thể yêu cầu chi phí biên dịch cao.
Thời gian biên dịch cũng có thể là một yếu tố quan trọng cần cân nhắc trong các dự án lớn do tính đơn hình chung, kiểm tra quyền sở hữu/loại và tối ưu hóa LLVM của Rust.
Go có khả năng biên dịch nhanh và các mối quan hệ phụ thuộc đơn giản làm hướng thiết kế chính của nó và quản lý các phần phụ thuộc thông qua cấu trúc gói.
Java và C# có thể tạo mã byte hoặc ngôn ngữ trung gian từ nguồn tương đối nhanh chóng, nhưng có thể thêm trình biên dịch JIT trong quá trình thực thi. AOT và hình ảnh thời gian chạy đã chuẩn bị sẵn cho phép bạn điều chỉnh các đặc điểm khởi động.
Python và JavaScript có lợi cho việc phát triển và viết các tập lệnh ngắn vì chúng cho phép bạn chạy chương trình ngay lập tức mà không cần bản dựng gốc đầy đủ riêng biệt. Tuy nhiên, thời gian khởi động có thể bao gồm việc tải mô-đun, khởi tạo thời gian chạy và chuẩn bị JIT.
Thời gian khởi động rất quan trọng đối với các chương trình dòng lệnh chạy ngắn và các chức năng không có máy chủ, đồng thời thông lượng ở trạng thái ổn định có thể quan trọng hơn chi phí thiết lập ban đầu cho các máy chủ chạy dài.
Phương thức triển khai
Các chương trình trong C, C++, Rust và Go thường được phân phối dưới dạng tệp thực thi gốc phù hợp với hệ điều hành và CPU mục tiêu. Liên kết tĩnh làm giảm sự phụ thuộc bên ngoài nhưng yêu cầu phải xem xét kích thước tệp, cấp phép và chính sách thư viện hệ thống. Liên kết động có thể chia sẻ thư viện nhưng phụ thuộc vào ABI và phiên bản thư viện của hệ thống đích.
Các chương trình Java yêu cầu một JVM và một lớp hoặc tệp JAR và thời gian chạy có thể được đóng gói cùng với chương trình hoặc có thể sử dụng một JVM được cài đặt trong môi trường đích. C# và .NET cũng có thể sử dụng một số phương pháp, bao gồm triển khai phụ thuộc vào khung, triển khai độc lập và AOT gốc.
Các chương trình Python yêu cầu phải có trình thông dịch và gói được chuẩn bị cùng nhau. Nó có thể được đóng gói dưới dạng một gói độc lập, nhưng các tiện ích mở rộng gốc và phần phụ thuộc dành riêng cho hệ điều hành có thể yêu cầu quy mô triển khai và quản lý khả năng tương thích.
Đối với giao diện người dùng JavaScript, trình duyệt thường hoạt động như một thời gian chạy và trình đóng gói sẽ định cấu hình mô-đun thành một tệp để phân phối. Server JavaScript yêu cầu thời gian chạy và gói như Node.js.
Swift và Kotlin có các phương pháp triển khai khác nhau tùy thuộc vào nền tảng mục tiêu. Kotlin/JVM sử dụng JVM và Kotlin/Native có thể tạo các tệp nhị phân gốc. Swift có thể tạo các tệp thực thi gốc cho nền tảng Apple, Linux và Windows.
Tính di động của nền tảng
C và C++ được triển khai trong hầu hết các hệ điều hành và bộ xử lý chính, đồng thời dễ dàng truy cập các API phần cứng và hệ điều hành. Tuy nhiên, các chương trình có thể phải xử lý thủ công các khác biệt về kích thước loại, ABI, hệ thống tệp và API hệ thống dành riêng cho nền tảng.
Rust và Go cũng hỗ trợ máy tính để bàn và máy chủ lớn cũng như một số kiến trúc nhúng. Mức độ hỗ trợ không chỉ phụ thuộc vào đặc tả ngôn ngữ mà còn phụ thuộc vào sự hoàn thiện của phần phụ trợ trình biên dịch, thư viện chuẩn và chuỗi công cụ dành riêng cho nền tảng.
Java được thiết kế để chạy cùng loại lớp trên nhiều hệ thống bằng cách sử dụng lớp thực thi chung được gọi là JVM. Tuy nhiên, sự khác biệt về nền tảng có thể xuất hiện trở lại khi sử dụng thư viện gốc, đồ họa và tích hợp hệ điều hành.
C# và .NET hỗ trợ nhiều hệ điều hành, nhưng các khung giao diện người dùng cụ thể và API hệ điều hành có thể dành riêng cho nền tảng.
JavaScript có một môi trường thực thi chung rất rộng được gọi là trình duyệt, nhưng phải tính đến sự khác biệt trong hỗ trợ tính năng dành riêng cho trình duyệt và API máy chủ.
Mặc dù các ngôn ngữ như Kotlin và Swift hỗ trợ nhiều nền tảng nhưng các khung và thư viện có sẵn cho mỗi nền tảng có thể khác nhau. Việc một ngôn ngữ có tính đa nền tảng là một chuyện, nhưng việc toàn bộ ứng dụng chạy trên tất cả các nền tảng mà không cần sửa đổi lại là chuyện khác.
Khả năng tương tác với ngôn ngữ khác
Bởi vì C ABI tương đối đơn giản và được hỗ trợ bởi nhiều ngôn ngữ và hệ điều hành nên nó được sử dụng rộng rãi như một ranh giới chung giữa các ngôn ngữ. C++, Rust, Swift, Python, Java, C#, v.v. có thể gọi các hàm C hoặc cung cấp các giao diện có thể được gọi từ C.
C++ ABI bị ảnh hưởng nhiều hơn bởi cách trang trí tên và lớp, các ngoại lệ, loại thư viện tiêu chuẩn và phiên bản trình biên dịch. Các giao diện ổn định để tiếp xúc với thế giới bên ngoài thường sử dụng C ABI và các tay cầm mờ đục.
Kotlin lấy khả năng tương tác với Java làm mục tiêu thiết kế chính, vì vậy hầu hết mã Java có thể được sử dụng trực tiếp trong Kotlin và mã Kotlin cũng có thể được hiển thị với Java.
C# chia sẻ hệ thống kiểu chung và ngôn ngữ trung gian với các ngôn ngữ khác trong .NET và cho phép khả năng tương tác nguyên gốc với P/Invoke.
Python có thể kết nối các thư viện gốc thông qua API C và các mô-đun mở rộng. Đây là cơ sở để sử dụng Python làm lớp điều khiển cao hơn trong tính toán khoa học và học máy, đồng thời giao các phần chuyên sâu về tính toán cho các thư viện gốc.
Swift cung cấp khả năng tương tác giữa Objective-C và C, đồng thời phạm vi tích hợp với C++ cũng đang mở rộng. Tuy nhiên, khi sử dụng các giao diện bên ngoài không an toàn, bạn phải tự mình kiểm tra các quy tắc về bộ nhớ và vòng đời.
Tiêu chuẩn hóa và ổn định ngôn ngữ
Các thông số kỹ thuật của C và C++ được quản lý thông qua tiêu chuẩn ISO/IEC. Hiện tại, tiêu chuẩn hóa C được xử lý bởi WG14 và tiêu chuẩn hóa C++ được xử lý bởi WG21. Các tiêu chuẩn quốc tế cung cấp cơ sở cho nhiều triển khai độc lập tuân theo các quy tắc ngôn ngữ chung.
Java cung cấp các đặc tả ngôn ngữ và đặc tả JVM riêng biệt, đồng thời phân biệt rõ ràng mối quan hệ giữa ngôn ngữ nguồn và máy ảo.
ECMAScript phát triển dựa trên thông số kỹ thuật sống động của TC39, với các đề xuất hoàn chỉnh được phản ánh trong thông số kỹ thuật mới nhất.
Python, Rust, Go, Kotlin và Swift đều nâng cao ngôn ngữ của họ thông qua các đề xuất công khai và quản trị dự án. Ngay cả khi không có các tiêu chuẩn quốc tế, khả năng tương thích thực tế vẫn có thể được cung cấp thông qua các tài liệu tham khảo rõ ràng, các chính sách ổn định và chất lượng của các hoạt động triển khai chính.
Mặc dù đây là ngôn ngữ được tiêu chuẩn hóa nhưng không phải tất cả các triển khai đều luôn hỗ trợ tất cả các tính năng mới nhất cùng một lúc. Ngược lại, các ngôn ngữ được quản lý bởi một dự án có thể triển khai các tính năng mới một cách nhanh chóng nhưng có thể làm tăng sự phụ thuộc vào việc triển khai và quản trị.
Năng suất của nhà phát triển
Năng suất phát triển không thể chỉ được đo bằng số dòng mã. Ngôn ngữ phát hiện lỗi sớm như thế nào, tốc độ xây dựng và kiểm tra của nó như thế nào, các thư viện của nó dễ sử dụng như thế nào cũng như các thông báo lỗi và công cụ của nó dễ hiểu như thế nào.
Python phù hợp cho việc tự động hóa và phát triển thử nghiệm thông qua các đối tượng động, cú pháp ngắn gọn và thực thi tương tác. Mặt khác, trong mã quy mô lớn, gợi ý kiểu, phân tích tĩnh, kiểm tra và ranh giới mô-đun có thể trở nên quan trọng.
Java và C# có thể quản lý các ứng dụng quy mô lớn thông qua các mô hình đối tượng và kiểu rõ ràng, các thư viện tiêu chuẩn mở rộng và các IDE hoàn thiện. Mặc dù mã có thể tương đối dài dòng nhưng nó cung cấp nhiều thông tin để tái cấu trúc và phân tích công cụ.
Kotlin cung cấp tính năng an toàn rỗng, suy luận kiểu, hàm mở rộng và biểu diễn dữ liệu ngắn gọn trong khi vẫn duy trì khả năng tương thích với hệ sinh thái Java. Vì nó sử dụng cùng một nền tảng với Java nên có thể sử dụng các thư viện hiện có nhưng phải xem xét đến sự khác biệt về thông tin rỗng và dung sai API giữa Java và Kotlin.
TypeScript duy trì hệ sinh thái JavaScript hiện có trong khi thêm phân tích trình soạn thảo và định kiểu tĩnh. Mặc dù hệ thống kiểu rất linh hoạt để lập mô hình các mẫu động có thể được biểu thị bằng JavaScript, một số loại bị loại bỏ mà không cần kiểm tra thời gian chạy thực tế, do đó dữ liệu bên ngoài phải được xác thực riêng.
Trong Rust, trình biên dịch kiểm tra các ràng buộc về quyền sở hữu, thời gian tồn tại và đặc điểm để phát hiện nhiều lỗi trước khi thực thi, nhưng chi phí thiết kế và đào tạo ban đầu có thể cao. Trong mã hệ thống được duy trì lâu dài, chi phí cho việc kiểm tra tĩnh này có thể dẫn đến giảm lỗi và tái cấu trúc ổn định.
C và C++ có khả năng truy cập tuyệt vời vào phần cứng và các thư viện hiện có, nhưng yêu cầu quản lý bộ nhớ, ABI, cài đặt bản dựng và sự khác biệt về nền tảng theo cách thủ công. Bởi vì C++ cung cấp rất nhiều tính năng trừu tượng và hiện có khác nhau nên các tập hợp con ngôn ngữ và quy ước mã hóa trong toàn nhóm có thể quan trọng.
So sánh các họ ngôn ngữ đại diện
C, C++ và Rust
Cả ba ngôn ngữ đều có thể được sử dụng để lập trình hệ thống gốc và điều khiển cấp thấp, nhưng chúng khác nhau về độ an toàn và tính trừu tượng.
| tiêu chuẩn | C (ngôn ngữ lập trình) | C++ | Rust |
|---|---|---|---|
| Trừu tượng cốt lõi | Hàm, cấu trúc, con trỏ | Lớp, kiểu giá trị, mẫu, RAII | cấu trúc, liệt kê, đặc điểm, quyền sở hữu |
| Quản lý bộ nhớ | Chủ yếu là thủ công | RAII và quản lý thủ công song song | Quyền sở hữu và mượn |
| thu gom rác | Không có | Không có | Không có |
| generic | Macro·void* căn giữa | Mẫu và khái niệm | generic và đặc điểm |
| thừa kế | Không có | Hỗ trợ kế thừa lớp | Không có sự kế thừa lớp |
| Truy cập không an toàn | Tính năng ngôn ngữ đơn giản | Tính năng ngôn ngữ đơn giản | unsafe được phân tách bằng ranh giới |
| ABI | C ABI được sử dụng rộng rãi | Phụ thuộc nhiều vào việc triển khai/nền tảng | Ranh giới bên ngoài thông qua C ABI là phổ biến |
| Ưu điểm chính | Mô hình thực thi đơn giản và hỗ trợ rộng rãi | Tính biểu cảm cao và hệ sinh thái hiện có | Bộ nhớ tĩnh/an toàn đồng thời |
| gánh nặng lớn | Trực tiếp quản lý an toàn bộ nhớ | Sự phức tạp về chức năng và cấu trúc kế thừa không an toàn | Mô hình sở hữu và chi phí tổng hợp |
C phù hợp với mã ranh giới trong đó thời gian chạy nhỏ và ABI đơn giản là quan trọng. C++ có thế mạnh khi nói đến các ứng dụng hiệu suất cao, trừu tượng hóa quy mô lớn và các thư viện mở rộng hiện có. Rust có lợi khi cố gắng nâng cao tính an toàn của bộ nhớ trong các thành phần hệ thống mới, nhưng việc thay thế hệ sinh thái C++ hiện tại và mô hình đối tượng phức tạp đòi hỏi phải trả thêm chi phí.
Go, Java và C#
Ba ngôn ngữ này thường được so sánh trong các ứng dụng máy chủ và doanh nghiệp cũng như các dịch vụ mạng, nhưng chúng có mô hình thực thi và triết lý ngôn ngữ khác nhau.
| Tiêu chí | Go (ngôn ngữ lập trình) | Java | C# |
|---|---|---|---|
| Thực thi | Mã máy bản địa và runtime Go | JVM | .NET CLR |
| Bộ nhớ | Thu gom rác | Thu gom rác | Thu gom rác |
| Mô hình đối tượng | Cấu trúc, phương thức và interface cấu trúc | Lớp và interface định danh | Lớp, cấu trúc và interface định danh |
| Tương tranh | Goroutine và channel | Thread, future, virtual thread, v.v. | Task cùng async và await |
| Generic | Tham số kiểu và ràng buộc | Generic dựa trên xóa kiểu | Generic giữ thông tin kiểu tại runtime |
| Triển khai | Tệp thực thi bản địa | JVM và bytecode | Phụ thuộc runtime, tự chứa hoặc AOT |
| Xu hướng thiết kế | Ngôn ngữ nhỏ gọn và công cụ tích hợp | Hệ sinh thái JVM ổn định | Tính năng ngôn ngữ và tích hợp .NET |
Go giới hạn tương đối các tính năng ngôn ngữ và nhấn mạnh tính đơn giản của chương trình mạng đồng thời cũng như quy trình build và triển khai. Thế mạnh của Java nằm ở JVM cùng hệ sinh thái máy chủ và doanh nghiệp được tích lũy trong thời gian dài. C# tích hợp chặt chẽ với .NET và cung cấp nhiều tính năng ngôn ngữ như kiểu giá trị, LINQ, hàm bất đồng bộ và khớp mẫu.
Java và Kotlin
Kotlin được thiết kế theo hướng hiện đại hóa cách biểu đạt của ngôn ngữ trong khi tận dụng JVM và các thư viện Java, thay vì thay thế Java bằng một hệ sinh thái hoàn toàn tách biệt.
| Tiêu chí | Java | Kotlin |
|---|---|---|
| Môi trường thực thi chính | JVM | JVM, JavaScript, Native, WebAssembly |
| Xử lý null | Tham chiếu truyền thống có thể là null | Phân biệt kiểu nullable |
| Biểu đạt mã | Tường minh và thận trọng | Suy luận kiểu và cú pháp ngắn gọn |
| Bất đồng bộ | Thread, future và virtual thread | Coroutine và thực thi dựa trên thư viện |
| Khả năng tương tác | Nền tảng của hệ sinh thái JVM | Tương tác trực tiếp với Java |
| Phương thức phát triển | JLS và JEP | Đặc tả Kotlin, KEEP và quy trình dự án |
Nếu tổ chức và thư viện Java hiện có cùng khả năng tương thích lâu dài là yếu tố quan trọng, bản thân Java có thể là lựa chọn đơn giản. Nếu muốn sử dụng phân tích null mạnh hơn, API ngắn gọn và coroutine trong cùng hệ sinh thái JVM, Kotlin có thể có lợi thế. Khi dùng hai ngôn ngữ cùng nhau, cần xem xét thông tin null và checked exception của Java cũng như sự khác biệt trong cách biểu diễn phương thức tĩnh và property.
JavaScript và TypeScript
TypeScript không hẳn là một ngôn ngữ runtime riêng cạnh tranh với JavaScript, mà là một lớp ngôn ngữ phân tích tĩnh chương trình JavaScript rồi chuyển đổi thành JavaScript.
| Tiêu chí | JavaScript | TypeScript |
|---|---|---|
| Kiểm tra kiểu | Chủ yếu khi thực thi | Bổ sung phân tích tĩnh trước khi biên dịch |
| Thực thi | Engine trình duyệt hoặc máy chủ | Thực thi JavaScript đã chuyển đổi |
| Hệ thống kiểu | Kiểu đối tượng động | Kiểu tĩnh cấu trúc |
| Dữ liệu bên ngoài | Xử lý trực tiếp khi thực thi | Cần kiểm tra tại runtime độc lập với khai báo kiểu |
| Mã hiện có | Có thể thực thi trực tiếp | Có thể dần dần bao gồm JavaScript |
| Ưu điểm chính | Ngôn ngữ nền tảng của mọi môi trường web | Hỗ trợ khám phá và thay đổi codebase lớn |
Đối với script nhỏ và mã động, chỉ JavaScript có thể đã đủ. Trong ứng dụng lớn do nhiều nhà phát triển quản lý, TypeScript có thể hỗ trợ hợp đồng API và tái cấu trúc. Tuy nhiên, kiểu TypeScript thường bị xóa khi thực thi nên không tự động bảo đảm rằng JSON nhận qua mạng thực sự thỏa mãn kiểu đã khai báo.
Python và ngôn ngữ biên dịch tĩnh
Python thường bị phân loại đơn giản là có hiệu năng thực thi thấp hơn các ngôn ngữ tĩnh bản địa, nhưng trong hệ thống thực tế, việc phân chia vai trò mới là điều quan trọng.
Python có thể được dùng để tổ chức chương trình, tự động hóa, phân tích tương tác và định nghĩa mô hình, còn các phép tính chuyên sâu có thể giao cho C, C++, Fortran, Rust hoặc thư viện GPU. Trong cấu trúc này, cách chuyển các đơn vị công việc lớn cho thư viện bản địa quan trọng hơn tốc độ của vòng lặp viết bằng Python.
Ngôn ngữ tĩnh bản địa có lợi thế trong các phép toán nhỏ lặp đi lặp lại, bố trí bộ nhớ và kiểm soát độ trễ. Python có thể thuận lợi cho tốc độ phát triển và thử nghiệm nhờ cách biểu đạt ngắn gọn, đối tượng động và hệ thống thư viện phong phú.
Vì vậy, thay vì chỉ chọn một trong Python và C++, một cấu trúc kết hợp trong đó Python đảm nhiệm luồng công việc cấp cao còn C++ đảm nhiệm phép tính cốt lõi có thể phù hợp hơn.
Swift và Kotlin
Swift và Kotlin thường được so sánh tương ứng trong hệ sinh thái Apple và Android, nhưng phạm vi thực thi của bản thân các ngôn ngữ này không chỉ giới hạn ở thiết bị di động.
Swift tập trung vào biên dịch bản địa, kiểu giá trị, đếm tham chiếu tự động, an toàn bộ nhớ và tích hợp với framework Apple. Kotlin cung cấp khả năng tương tác với JVM, thu gom rác, null safety, coroutine và backend đa nền tảng.
Nếu UI bản địa và API hệ thống trên nền tảng Apple là trọng tâm, Swift là lựa chọn tự nhiên; trong hệ sinh thái Android và JVM, Kotlin là lựa chọn tự nhiên. Nếu muốn chia sẻ logic nghiệp vụ giữa hai nền tảng, có thể xem xét một cấu trúc riêng như Kotlin Multiplatform hoặc lõi chung dựa trên C++ hay Rust.
Những sai lầm thường gặp khi so sánh
Đồng nhất tốc độ của ngôn ngữ với tốc độ của bản triển khai
Đặc tả ngôn ngữ xác định ý nghĩa của chương trình nhưng không quyết định toàn bộ mã máy cụ thể và mức tối ưu hóa. Cùng một ngôn ngữ có thể cho hiệu năng khác nhau giữa các bản triển khai.
Cho rằng mã ngắn luôn đồng nghĩa với năng suất cao
Mã ngắn có thể giúp viết nhanh hơn nhưng cũng có thể làm tăng hành vi ẩn, lỗi động và chi phí bảo trì. Ngược lại, kiểu tường minh và xử lý lỗi làm mã dài hơn nhưng có thể hỗ trợ những thay đổi quy mô lớn.
Cho rằng kiểu tĩnh loại bỏ mọi lỗi
Kiểu tĩnh chỉ chặn những lỗi mà hệ thống kiểu có thể biểu đạt. Thuật toán sai, chính sách bảo mật, lỗi giao dịch cơ sở dữ liệu và vấn đề đầu vào bên ngoài phải được xử lý riêng.
Cho rằng thu gom rác luôn chậm
Bộ thu gom rác có thể xử lý cấp phát nhanh và nén đối tượng để tăng tính cục bộ. Mặt khác, thời điểm thu gom và khoảng trống bộ nhớ trở thành những chi phí quan trọng. Kết quả phụ thuộc vào tải công việc và thiết kế của bộ thu gom.
Cho rằng ngôn ngữ bản địa luôn nhanh hơn ngôn ngữ được quản lý
Trình biên dịch JIT có thể dùng thông tin thực thi thực tế để tối ưu hóa động. Ngược lại, trình biên dịch bản địa có thể phân tích toàn bộ chương trình và tối ưu hóa theo đích trước khi thực thi. Phương án nào có lợi hơn phụ thuộc vào chương trình, thời gian chạy và bản triển khai.
Cho rằng ngôn ngữ mới luôn thay thế ngôn ngữ cũ
Mã và thư viện hiện có, ABI, nhân lực và hồ sơ kiểm chứng đều là những tài sản lớn. Fortran, COBOL và C tiếp tục được sử dụng không chỉ vì độ hiện đại của cú pháp mà còn vì các hệ thống và hệ sinh thái đã tích lũy.
Cho rằng hỗ trợ nhiều mô hình luôn tốt hơn
Nhiều tính năng giúp biểu đạt nhiều loại vấn đề nhưng cũng có thể làm tăng độ phức tạp của ngôn ngữ và codebase. Một ngôn ngữ nhỏ và có giới hạn có lợi thế là giúp cả nhóm sử dụng một cách tiếp cận nhất quán.
Cách lựa chọn ngôn ngữ
Khi chọn ngôn ngữ lập trình, trước hết cần tách các điều kiện bắt buộc của dự án khỏi những điều kiện có thể thay đổi.
Môi trường thực thi
Trước tiên cần xác định hệ điều hành và CPU đích, trình duyệt, nền tảng di động, máy chơi game và thiết bị nhúng. Ngôn ngữ và SDK được một nền tảng hỗ trợ chính thức trên thực tế có thể quyết định phạm vi lựa chọn.
Yêu cầu hiệu năng
Không chỉ thông lượng trung bình mà cả độ trễ trong trường hợp xấu nhất, thời gian khởi động, giới hạn bộ nhớ, kích thước tệp nhị phân và mức tiêu thụ điện năng đều phải được xác định cụ thể. Cần đo nguyên mẫu bằng tải công việc gần với thực tế.
An toàn và độ tin cậy
Cần xem xét an toàn bộ nhớ, lỗi kiểu, data race, khả năng null, xử lý lỗi và các yêu cầu chứng nhận. Không chỉ tính năng ngôn ngữ mà cả tiêu chuẩn viết mã, phân tích tĩnh và công cụ kiểm thử cũng phải được đánh giá cùng nhau.
Hệ sinh thái hiện có
Cần xác định những ngôn ngữ được hỗ trợ bởi driver cơ sở dữ liệu, API đồ họa, framework machine learning, SDK thiết bị và hệ thống doanh nghiệp cần thiết. Nếu thiếu thư viện cốt lõi, chi phí tự triển khai có thể lớn hơn lợi thế cú pháp của ngôn ngữ.
Tổ chức phát triển
Cần cân nhắc kinh nghiệm và khả năng tuyển dụng nhà phát triển, chi phí đào tạo, quy trình review mã và vận hành. Việc dùng một ngôn ngữ chỉ một số ít chuyên gia hiểu cho toàn bộ hệ thống cốt lõi có thể tạo ra rủi ro bảo trì dài hạn.
Triển khai và vận hành
Cần xem xét cách cài đặt, cập nhật và quan sát chương trình. Việc cài runtime, kích thước container, cold start, cùng công cụ gỡ lỗi và profiling ảnh hưởng đến chi phí vận hành.
Bảo trì dài hạn
Cần kiểm tra cơ chế quản trị của ngôn ngữ và bản triển khai, chính sách tương thích, tình trạng tiêu chuẩn hóa và thời gian hỗ trợ phiên bản. Khả năng cập nhật mã và dependency trong nhiều năm có thể quan trọng hơn tốc độ phát triển ngắn hạn.
Bảng đánh giá thực tiễn để so sánh
| Yêu cầu | Đặc tính cần ưu tiên xem xét | Các lựa chọn ngôn ngữ tiêu biểu |
|---|---|---|
| Hệ điều hành và driver | Thực thi bản địa, ABI, kiểm soát bộ nhớ | C, C++, Rust |
| Game và rendering hiệu năng cao | Độ trễ có thể dự đoán, hệ sinh thái đồ họa | C++, Rust, C# |
| Dịch vụ mạng | Tương tranh, triển khai, hệ sinh thái máy chủ | Go, Java, C#, Rust, Kotlin |
| Hệ thống nghiệp vụ doanh nghiệp | Khả năng tương thích, runtime được quản lý, framework | Java, C#, Kotlin |
| Frontend web | Thực thi trong trình duyệt và API web | JavaScript, TypeScript |
| Tự động hóa và công cụ | Viết nhanh, xử lý tệp và tiến trình | Python, shell, PowerShell |
| Phân tích dữ liệu | Thực thi tương tác, hệ sinh thái thống kê và số học | Python, R, Julia |
| Tính toán khoa học và số học | Phép toán mảng, tính toán song song, mã hiện có | Fortran, C++, Python, Julia |
| Ứng dụng di động | SDK nền tảng và framework UI | Kotlin, Swift |
| Thiết bị nhúng | Runtime nhỏ, truy cập phần cứng | C, C++, Rust, Ada |
| Hệ thống trọng yếu về an toàn | Khả năng kiểm chứng, mô hình thực thi hạn chế | Ada, C, C++, Rust |
| Plugin và mod | Khả năng nhúng, sandbox, API đơn giản | Lua, JavaScript, WebAssembly |
| Tạo nguyên mẫu nhanh | Khả năng thực thi và sửa đổi tức thời | Python, JavaScript |
| Mã web tĩnh quy mô lớn | Kiểu cấu trúc và hệ sinh thái JS | TypeScript |
Bảng này không phải là danh sách khuyến nghị tuyệt đối. Ngay trong cùng một lĩnh vực, lựa chọn khác có thể phù hợp hơn tùy theo công nghệ hiện có, kinh nghiệm của nhóm, nền tảng và giấy phép.
Tổng hợp
Sự khác biệt giữa các ngôn ngữ lập trình không chỉ nằm ở cách biểu đạt cú pháp. Cách mô hình hóa giá trị và đối tượng, thời điểm kiểm tra kiểu, bên quản lý bộ nhớ và tài nguyên, cùng môi trường thực thi của chương trình làm thay đổi phương thức phát triển, loại lỗi và đặc tính hiệu năng.
C và C++ cung cấp khả năng kiểm soát rộng đối với phần cứng và hệ sinh thái hệ thống hiện có. Rust tăng cường an toàn dựa trên quyền sở hữu trong một lĩnh vực tương tự. Go cung cấp một ngôn ngữ tương đối nhỏ cùng khả năng tương tranh dựa trên runtime. Java và C# tập trung vào nền tảng được quản lý và hệ sinh thái ứng dụng quy mô lớn. Python và JavaScript cung cấp thực thi động, phát triển nhanh và môi trường ứng dụng rộng, còn TypeScript bổ sung phân tích tĩnh cho hệ sinh thái JavaScript. Kotlin và Swift kết hợp các tính năng kiểu và an toàn hiện đại với những hệ sinh thái nền tảng lớn. Fortran có một hệ sinh thái khoa học và kỹ thuật lâu đời tập trung vào mảng và tính toán số.
Không ngôn ngữ nào đồng thời vượt trội theo mọi tiêu chí so sánh. Tăng cường kiểm soát cấp thấp mở rộng phạm vi trách nhiệm của lập trình viên, còn tăng cường quản lý tự động mở rộng phần việc do runtime và bản triển khai đảm nhiệm. Kiểm tra tĩnh mạnh hơn có thể ngăn trước một số lỗi nhưng cũng có thể làm hệ thống kiểu và quá trình biên dịch phức tạp hơn; thực thi động linh hoạt hơn nhưng làm tăng trách nhiệm kiểm tra và kiểm thử trong khi chạy.
Vì vậy, mục đích của việc so sánh ngôn ngữ lập trình không phải là chọn ra một người chiến thắng cuối cùng. Mục đích là hiểu mỗi ngôn ngữ đã chọn những chi phí và ràng buộc nào để giải quyết vấn đề gì, từ đó đưa ra lựa chọn phù hợp nhất với môi trường thực thi, yêu cầu an toàn, hiệu năng, tổ chức phát triển và hệ sinh thái hiện có của dự án.
Tài liệu liên quan
- Lập trình
- Mã nguồn
- Mã máy
- Hợp ngữ
- Ngôn ngữ lập trình bậc cao
- Ngôn ngữ lập trình bậc thấp
- Ngôn ngữ lập trình đa dụng
- Ngôn ngữ chuyên biệt miền
- Ngôn ngữ kịch bản
- Mô hình lập trình
- Lập trình mệnh lệnh
- Lập trình hướng đối tượng
- Lập trình hàm
- Lập trình khai báo
- Lập trình logic
- Hệ thống kiểu
- Kiểu tĩnh
- Kiểu động
- Cú pháp
- Ngữ nghĩa ngôn ngữ lập trình
- Trình biên dịch
- Trình thông dịch
- Phân tích từ vựng
- Phân tích cú pháp
- Cây cú pháp trừu tượng
- Biểu diễn trung gian
- Mã byte
- Máy ảo
- Hệ thống thời gian chạy
- Thư viện chuẩn
- Đặc tả ngôn ngữ
- Tiêu chuẩn hóa ngôn ngữ lập trình
- C (ngôn ngữ lập trình)
- C++
- Fortran
- COBOL
- LISP
- Smalltalk
- Pascal
- Prolog
- Haskell
- Java
- JavaScript
- Python
- Ruby
- PHP
- C#
- Go (ngôn ngữ lập trình)
- Rust
- Kotlin
- Swift
- TypeScript
- SQL
- WebAssembly