Dịch từ bài viết: The convenience of .NET của tác giả Richard Lander
Có các tùy chọn thuận tiện cho gần như mọi công việc trong cuộc sống, từ việc đến sân bay đến viết mã nguồn. Sự thuận tiện chính là ý tưởng rằng một giải pháp tuyệt vời có sẵn khi bạn muốn và nó hoạt động đối với bạn. Như những người thiết kế của nền tảng .NET, chúng tôi nhằm mục tiêu cung cấp các giải pháp thuận tiện cho nhiều công việc và cải thiện sự thuận tiện khi viết ứng dụng với mỗi phiên bản mới.
Bài viết này bắt đầu một series mới, khám phá các giải pháp thuận tiện cho các công việc phổ biến. Năng suất, hiệu suất, bảo mật và đáng tin cậy là các điểm thiết kế đặc trưng của nền tảng .NET. Chúng tôi đã mô tả chúng chi tiết trong bài viết gần đây của chúng tôi “Tại sao .NET?“. Stephen Toub cũng đã công bố bài viết hàng năm của mình về hiệu suất, Cải Thiện Hiệu Suất trong .NET 8. Bài viết này (và những bài viết sẽ tiếp theo) khám phá các ý tưởng và tính năng được thảo luận trong các bài viết khác đó theo hình thức các giải pháp thuận tiện. Bạn sẽ thấy một sự kết hợp của các API tiện ích ở mức độ cao cung cấp một sự cân bằng tốt giữa các điểm thiết kế đó và các API ở mức độ thấp hơn giúp bạn đạt được một sự cân bằng khác nhau phù hợp với nhu cầu của bạn.
Các bài viết tiếp theo sẽ đi vào chi tiết hơn về các họ API cụ thể, với rất nhiều mã nguồn và số liệu hiệu suất, để khám phá đầy đủ các giải pháp thuận tiện này.
Hãy bắt đầu series này với một khám phá tổng quan hơn về cách nền tảng .NET đem lại sự thuận tiện.
Thuận tiện là một phạm vi (Convenience is a spectrum)
Tôi thích sử dụng các thuật ngữ “thuận tiện” (convenience) và “kiểm soát” (control), để mô tả hai đầu của “phạm vi thuận tiện” (convenience spectrum). Thuận tiện là miêu tả cho trải nghiệm viết mã nguồn và kiểm soát là khả năng của bạn để định nghĩa hành vi của nó.
Mã nguồn thuận tiện nhất là mã nguồn ngắn gọn và đơn giản, thường chỉ có một vài tùy chọn để thay đổi hành vi (vì “lựa chọn” chính là một dạng phức tạp). File.ReadAllText() là một ví dụ tốt. Nó trả về nội dung của một tập tin (văn bản) dưới dạng một chuỗi bạn có thể đọc và xử lý. Mã nguồn ở mức độ thấp nhất cho phép nhiều linh hoạt, kiểm soát và tối ưu hóa hiệu suất, nhưng đòi hỏi việc sử dụng cẩn thận hơn. Như là File.OpenHandle() và RandomAccess.Read(), chúng cung cấp một trình xử lý hệ điều hành (operating system handle) với rất ít hạn chế để đọc qua một tập tin (dưới dạng byte) với hiệu suất tối đa.
Tôi sẽ cho bạn thấy một vài danh sách các API. Không sao nếu chúng không quen thuộc. Những API này bắt đầu từ các khái niệm và định dạng cơ bản nhất (mức độ kiểm soát cao nhất) và kết thúc với các khái niệm được đóng gói và tinh tế nhất (mức độ thuận tiện nhất).
Phạm vi thuận tiện cho việc đọc một tập tin văn bản:
- Kiểm soát cao nhất:
File.OpenHandle + RandomAccess.Read - Thuận tiện hơn:
File.Open + FileStream.Read - Thuận tiện hơn nữa:
File.OpenText + StreamReader.ReadLine - Thuận tiện hơn nữa:
File.ReadLines + IEnumerable - Thuận tiện hơn nữa:
File.ReadAllLines + string[] - Thuận tiện nhất:
File.ReadAllText + string
Phạm vi thuận tiện cho việc đọc văn bản JSON:
- Kiểm soát cao nhất:
Utf8JsonReader + PipelineshoặcStream - Thuận tiện hơn:
JsonDocument + Stream - Thuận tiện hơn nữa:
JsonSerializer + Stream - Thuận tiện nhất:
JsonSerializer + string
Lưu ý: Các API được liệt kê là API chính + API hoặc kiểu dữ liệu đồng hành có khả năng cao nhất của chúng.
Một điểm quan trọng là không có sự chia rõ ràng giữa các mẫu thuận tiện và kiểm soát trong các danh sách này. Đoạn kết thúc của một mẫu thuận tiện chồng lấn với đoạn bắt đầu của một mẫu kiểm soát tiếp theo. Sự thuận tiện của một người có thể là kiểm soát của người khác. Đó chính là định nghĩa của một phạm vi.
Sự thuận tiện bắt đầu từ sự lựa chọn
Có thể bạn tự hỏi tại sao chúng ta cần tất cả những API này. Chúng đều làm cùng một việc, phải không? Lý do đầu tiên là mỗi lựa chọn trong số này là công cụ phù hợp cho công việc trong các tình huống khác nhau và thuận tiện cho tình huống đó. Trên thực tế, cộng đồng phát triển .NET luôn yêu cầu chúng tôi cung cấp một loạt rộng các API, và chúng tôi rất vui lòng đáp ứng yêu cầu đó. Lý do thứ hai là chúng tôi phải xây dựng các API ở mức độ thấp để tạo ra các API ở mức độ cao. Điều này giống như việc xây dựng các tháp gạch Lego. Lý thuyết, chúng tôi có thể chỉ tiết lộ các API ở mức độ cao bằng cách đặt các API ở mức độ thấp là private, nhưng điều này không hợp lý và không thực tế trong trường hợp tổng quát.
Một số ngăn xếp phát triển chủ yếu chỉ tiết lộ các API ở mức độ cao được xây dựng trên các thư viện mã nguồn gốc, nhưng thiếu các API ở mức độ thấp hữu ích. Các thư viện mã nguồn gốc thường được viết một cách khiến việc tiết lộ các API ở mức độ thấp cho một ngôn ngữ quản lý trở nên không thực tế, nên chúng không được tiết lộ. Điều này hạn chế rất nhiều. Trong .NET, chúng tôi có một triết lý mạnh mẽ rằng chức năng thư viện chúng tôi xây dựng nên được viết bằng C#, điều này có nghĩa là cả các API ở cấp độ cao và thấp đều có sẵn để bạn sử dụng. Điều này cũng có nghĩa là bạn có thể đọc mã nguồn của tất cả các API bạn sử dụng trong C# (trên GitHub), như File class đã được đề cập trước đó.
Tất nhiên, có những nơi mà chúng tôi không tiết lộ tất cả các lớp; mỗi API mới chúng tôi tiết lộ sẽ tồn tại mãi mãi, và đòi hỏi thiết kế, kiểm tra và bảo dưỡng và tài liệu và các hạn chế về tương thích và v.v. Chúng tôi do đó phải chọn lọc lớp nào chúng tôi tiết lộ vào thời điểm nào, và liên tục đánh giá xem liệu chúng tôi nên tiết lộ thêm hỗ trợ nào không. Ví dụ, hàm File.ReadAllText đã tồn tại trong rất nhiều năm, trong khi hàm RandomAccess.Read đã được giới thiệu gần đây. Xu hướng dài hạn của chúng tôi là khi nào có một lý do thuyết phục, chúng tôi sẽ tiết lộ các API ở mức độ thấp.
Thuận tiện tạo điều kiện cho sự hợp tác
Thư viện .NET tiết lộ một tập hợp đa dạng các chức năng cho bạn sử dụng. Trong nhiều trường hợp (như với kiểu dữ liệu File), nhiều chức năng liên quan được tiết lộ tại một nơi và được thiết kế để hoạt động như một hệ thống lớn và hợp nhất. Điều đó có nghĩa là bạn có thể sử dụng các API thuận tiện hơn ở một phần của mã nguồn của bạn và các API kiểm soát cao ở những nơi khác và tất cả chúng có thể hoạt động cùng nhau một cách hài hòa.
“Ở đây… tôi sẽ viết dữ liệu này vào một Stream với các API mà chúng tôi cần cho dịch vụ của chúng tôi. Bạn có thể sử dụng StreamReader.ReadLineAsync để đọc nó. Nếu điều đó không hoạt động, tôi sẽ tiết lộ một IAsyncEnumerable cho mỗi dòng và bạn có thể sử dụng await foreach như một giải pháp truyền dữ liệu liên tục. Cả hai lựa chọn đều phù hợp với tôi. Tôi thích cách đơn giản mà tất cả các lựa chọn này mang lại. Rất dễ dàng kết nối mã nguồn của chúng ta với nhau và mọi thứ đều nhanh chóng và thuận tiện.” – Nhà phát triển .NET tại Công ty ACME Solutions.
Các nhà phát triển làm việc trong các nhóm có thể đưa ra các lựa chọn thuận tiện khác nhau (và tốt như nhau) ở các tầng khác nhau trong một mã nguồn lớn hơn, với các mẫu đơn giản để kết nối những tầng đó.
Tôi có kiểm soát
Đúng vậy. Điểm ở đây không phải là chọn một điểm nào đó trên phạm vi này và giữ chặt lấy nó cho tất cả mã nguồn bạn viết. Thay vào đó, mục tiêu là chọn các API thỏa mãn yêu cầu của thuật toán đang xử lý, ngay cả khi bạn có kỹ năng để viết mã nguồn phức tạp hơn có thể tốt hơn trên một số tiêu chí (có thể quan trọng hoặc không). Người tiếp theo duy trì mã nguồn của bạn có thể không có kỹ năng giống như bạn và có thể (sai lầm) kết luận rằng mẫu bạn chọn là một yêu cầu khi thực sự không phải vậy.
Chúng tôi sử dụng các API thuận tiện trong một số nơi trong thư viện .NET, ngay cả khi chúng không đạt tốc độ tối đa. Chúng làm cho mã nguồn ngắn gọn, đơn giản và dễ hiểu và điều đó có thể có giá trị hơn là tốc độ tối đa.
Đó chính là điều mà một trong số các kiến trúc sư của chúng tôi nói về cách tiếp cận của chúng tôi đối với mã nguồn của chúng tôi, ngay cả trong một nhóm chuyên về hiệu suất cao. Chúng tôi thích viết mã nguồn thuận tiện mỗi khi chúng tôi có thể. Chúng tôi muốn tập trung nỗ lực của mình vào việc xây dựng thêm tính năng và tối ưu hóa các API có khả năng được gọi trong một vòng lặp nóng.
Mặt khác của đồng xu là rằng, mà càng hiệu quả các API thuận tiện là, chúng tôi càng có thể sử dụng chúng mà không phải lo lắng trong mã nguồn của chúng tôi. Điều này làm cho toàn bộ nhóm trở nên hiệu quả hơn. Chúng tôi cố gắng làm cho các API thuận tiện càng hiệu quả càng tốt trong phạm vi của những gì mà hình dạng của API cho phép.
Phá vỡ phạm vi
Có một số trường hợp mà một API duy nhất bao phủ hầu hết các trường hợp sử dụng. Điều này chỉ xảy ra khi một API với một hợp đồng đơn giản là một công cụ đa nhiệm tuyệt vời và được yêu cầu trong nhiều tình huống.
Các API của lớp string là một ví dụ quan trọng. IndexOf và IndexOfAny là hai yêu thích. Chúng tôi sử dụng những API này rộng rãi trên nền tảng .NET và chúng cũng được sử dụng nhiều bởi các nhà phát triển .NET. Bạn có thể thấy có bao nhiêu yêu cầu (PRs) đã được gửi đến những API đó.
Nhiều cuộc gọi IndexOf{Any} thực sự hiện đã được chuyển sang spans, thay vì các cuộc gọi trực tiếp đến string.IndexOf{Any}. Mặc dù spans thường trỏ vào chuỗi, nhưng những API này thường hoạt động trên các phần được cắt ra (sau khi gọi string.AsSpan, ở mức độ nội bộ).
Gia đình các API này đã được cải thiện rất nhiều, sử dụng nhiều kỹ thuật để tăng hiệu suất. Ví dụ, những API này sử dụng các hướng dẫn CPU vector để tìm kiếm các thuật ngữ tìm kiếm trong một chuỗi. Trong .NET 8, hỗ trợ cho AVX512 đã được thêm vào. Điều này hiện vẫn chưa phổ biến đối với hầu hết các phần cứng, tuy nhiên điều này có nghĩa là IndexOf sẽ sẵn sàng cho phần cứng mới khi bạn có nó.
Chúng tôi sẽ tập trung vào IndexOfAny chi tiết hơn trong bài viết về System.IO. Đó là một API tuyệt vời.
Kết luận
Nhóm .NET theo đuổi triết lý “chiếc lều rộng” (a “big tent” philosophy). Chúng tôi muốn mỗi nhà phát triển tìm thấy các API mà họ có thể tiếp cận và phù hợp. Nếu bạn mới bắt đầu lập trình, chúng tôi có các API dành cho bạn. Nếu bạn quen thuộc hơn với các API cấp thấp, chúng tôi có các API mà bạn có thể đã biết.
Tôi đang mong đợi chia sẻ một số phân tích sâu hơn và khám phá về phạm vi thuận tiện trong các bài viết sắp tới và hy vọng rằng điều này sẽ dẫn đến một cuộc thảo luận thú vị. Nếu không có gì, việc này đã giúp tôi hiểu biết được mức độ tôi đánh giá cao về phạm vi của các API này. Có lẽ tài liệu của chúng tôi nên được cập nhật để mô tả mỗi lĩnh vực theo cụ thể của phạm vi này.