Giới thiệu
Kiểu dữ liệu chuỗi là một trong những kiểu dữ liệu cơ bản, cùng với kiểu số (int, long, double) và logic (Boolean). Bạn khó có thể hình dung ra ít nhất một chương trình hữu ích không sử dụng loại này.
Trên nền tảng .NET, kiểu chuỗi được trình bày dưới dạng lớp Chuỗi bất biến. Ngoài ra, nó được tích hợp mạnh mẽ vào môi trường CLR và cũng được hỗ trợ bởi trình biên dịch C #.
Bài viết này dành cho phép nối - một phép toán được thực hiện trên chuỗi thường xuyên như phép toán cộng trên chữ số. Bạn có thể nghĩ:“Còn gì để nói nữa?”, Rốt cuộc thì chúng ta đều biết về toán tử chuỗi “+”, nhưng hóa ra, nó có những điều kỳ quặc riêng.
Đặc tả ngôn ngữ cho toán tử chuỗi “+”
Đặc tả ngôn ngữ C # cung cấp ba cách nạp chồng cho toán tử chuỗi “+”:
string operator + (string x, string y) string operator + (string x, object y) string operator + (object x, string y)
Nếu một trong các toán hạng của nối chuỗi là NULL, thì chuỗi trống sẽ được chèn vào. Nếu không, bất kỳ đối số nào, không phải là một chuỗi, được biểu diễn dưới dạng một chuỗi bằng cách gọi phương thức ảo ToString. Nếu phương thức ToString trả về NULL, thì một chuỗi trống sẽ được chèn vào. Cần lưu ý rằng theo đặc điểm kỹ thuật, thao tác này sẽ không bao giờ trả về NULL.
Mô tả toán tử đã đủ rõ ràng, tuy nhiên, nếu chúng ta xem xét việc triển khai lớp String, chúng ta thấy định nghĩa rõ ràng chỉ có hai toán tử “==” và “! =”. Một câu hỏi hợp lý được đặt ra:điều gì xảy ra đằng sau quá trình nối chuỗi? Trình biên dịch xử lý toán tử chuỗi “+” như thế nào?
Câu trả lời cho câu hỏi này hóa ra không quá khó. Chúng ta hãy xem xét kỹ hơn phương thức String.Concat tĩnh. Phương thức String.Concat kết hợp một hoặc nhiều thể hiện của lớp String hoặc các dạng xem dưới dạng giá trị Chuỗi của một hoặc nhiều thể hiện của Đối tượng. Có các cách nạp chồng sau của phương thức này:
public static String Concat (String str0, String str1) public static String Concat (String str0, String str1, String str2) public static String Concat (String str0, String str1, String str2, String str3) public static String Concat (params String[] values) public static String Concat (IEnumerable <String> values) public static String Concat (Object arg0) public static String Concat (Object arg0, Object arg1) public static String Concat (Object arg0, Object arg1, Object arg2) public static String Concat (Object arg0, Object arg1, Object arg2, Object arg3, __arglist) public static String Concat <T> (IEnumerable <T> values)
Chi tiết
Giả sử chúng ta có biểu thức sau s =a + b, trong đó a và b là các chuỗi. Trình biên dịch chuyển đổi nó thành một cuộc gọi của một phương thức tĩnh Concat, tức là
s = string.Concat (a, b)
Thao tác nối chuỗi, giống như bất kỳ thao tác cộng nào khác trong ngôn ngữ C #, là phép liên kết trái.
Mọi thứ đều rõ ràng với hai hàng, nhưng nếu có nhiều hàng hơn thì sao? Biểu thức s =a + b + c, với tính chất kết hợp trái của phép toán, có thể được thay thế bằng:
s = string.Concat(string.Concat (a, b), c)
Tuy nhiên, do quá tải có ba đối số, nó sẽ được chuyển đổi thành:
s = string.Concat (a, b, c)
Tình huống tương tự là với việc nối bốn chuỗi. Để nối 5 chuỗi trở lên, chúng ta có quá tải string.Concat (chuỗi tham số []), vì vậy cần phải tính đến chi phí liên quan đến cấp phát bộ nhớ cho một mảng.
Cũng cần lưu ý rằng toán tử nối chuỗi là liên kết hoàn toàn :không quan trọng chúng ta nối các chuỗi theo thứ tự nào, vì vậy biểu thức s =a + (b + c), mặc dù mức độ ưu tiên thực hiện nối được chỉ định rõ ràng, sẽ được xử lý như sau
s = (a + b) + c = string.Concat (a, b, c)
thay vì dự kiến:
s = string.Concat (a, string.Concat (b, c))
Như vậy, tổng kết ở trên:thao tác nối chuỗi luôn được biểu diễn từ trái sang phải và gọi phương thức String.Concat tĩnh.
Tối ưu hóa trình biên dịch cho chuỗi ký tự
Trình biên dịch C # có các tối ưu hóa liên quan đến các chuỗi ký tự. Ví dụ:biểu thức s =“a” + “b” + c, với tính chất kết hợp trái của toán tử “+”, tương đương với s =(“a” + “b”) + c được chuyển thành
s = string.Concat ("ab", c)
Biểu thức s =c + “a” + “b”, bất chấp tính kết hợp trái của phép toán nối (s =(c + “a”) + “b”) được chuyển đổi thành
s = string.Concat (c, "ab")
Nói chung, vị trí của các ký tự không quan trọng, trình biên dịch nối mọi thứ có thể và chỉ sau đó cố gắng chọn một quá tải thích hợp của phương thức Concat. Biểu thức s =a + “b” + “c” + d được chuyển đổi thành
s = string.Concat (a, "bc", d)
Các tối ưu hóa liên quan đến chuỗi rỗng và NULL cũng nên được đề cập. Trình biên dịch biết rằng việc thêm một dấu chấm trống không ảnh hưởng đến kết quả của phép nối, vì vậy biểu thức s =a + “” + b được chuyển thành
s = string.Concat (a, b),
thay vì dự kiến
s = string.Concat (a, "", b)
Tương tự, với chuỗi const, giá trị của nó là NULL, chúng ta có:
const string nullStr = null; s = a + nullStr + b;
được chuyển đổi thành
s = string.Concat (a, b)
Biểu thức s =a + nullStr được chuyển thành s =a ?? “”, Nếu a là một chuỗi và lệnh gọi của chuỗi. .
Một tính năng thú vị liên quan đến việc tối ưu hóa xử lý theo nghĩa đen và tính liên kết trái của toán tử chuỗi “+”.
Hãy xem xét biểu thức:
var s1 = 17 + 17 + "abc";
xét đến tính liên kết trái, nó tương đương với
var s1 = (17 + 17) + "abc"; // сalling the string.Concat method (34, "abc")
Do đó, tại thời điểm biên dịch, các chữ số được thêm vào, do đó kết quả sẽ là 34abc.
Mặt khác, biểu thức
var s2 = "abc" + 17 + 17;
tương đương với
var s2 = ( "abc" + 17) + 17; // calling the string.Concat method ("abc", 17, 17)
kết quả sẽ là abc1717.
Như vậy, bạn hiểu rồi đấy, cùng một toán tử ghép dẫn đến các kết quả khác nhau.
String.Concat VS StringBuilder.Append
Cần phải nói đôi lời về sự so sánh này. Hãy xem xét đoạn mã sau:
string name = "Timur"; string surname = "Guev"; string patronymic = "Ahsarbecovich"; string fio = surname + name + patronymic;
Nó có thể được thay thế bằng mã sử dụng StringBuilder:
var sb = new StringBuilder (); sb.Append (surname); sb.Append (name); sb.Append (patronymic); string fio = sb.ToString ();
Tuy nhiên, trong trường hợp này, chúng ta sẽ khó nhận được lợi ích từ việc sử dụng StringBuilder. Ngoài thực tế là mã trở nên ít đọc hơn, nó đã trở nên ít nhiều hiệu quả, vì việc triển khai phương thức Concat sẽ tính toán độ dài của chuỗi kết quả và cấp phát bộ nhớ chỉ một lần, trái ngược với StringBuilder không biết gì về độ dài của chuỗi kết quả.
Triển khai phương thức Concat cho 3 chuỗi:
public static string Concat (string str0, string str1, string str2) { if (str0 == null && str1 == null && str2 == null) return string.Empty; if (str0 == null) str0 = string.Empty; if (str1 == null) str1 = string.Empty; if (str2 == null) str2 = string.Empty; string dest = string.FastAllocateString (str0.Length + str1.Length + str2.Length); // Allocate memory for strings string.FillStringChecked (dest, 0, str0); / string.FillStringChecked (dest, str0.Length, str1); string.FillStringChecked (dest, str0.Length + str1.Length, str2); return dest; }
Toán tử “+” trong Java
Vài lời về toán tử chuỗi “+” trong Java. Mặc dù tôi không lập trình bằng Java, nhưng tôi quan tâm đến cách nó hoạt động ở đó. Trình biên dịch Java tối ưu hóa toán tử “+” để nó sử dụng lớp StringBuilder và gọi phương thức append.
Mã trước đó được chuyển đổi thành
String fio = new StringBuilder(String.valueOf(surname)).append(name).append (patronymic).ToString()
Điều đáng chú ý là họ cố tình từ chối từ việc tối ưu hóa như vậy trong C #, Eric Lippert có một bài về chủ đề này. Vấn đề là tối ưu hóa như vậy không phải là tối ưu hóa như vậy, nó là viết lại mã. Bên cạnh đó, những người sáng tạo ra ngôn ngữ C # tin rằng các nhà phát triển nên làm quen với các khía cạnh làm việc với lớp String và nếu cần, hãy chuyển sang StringBuilder.
Nhân tiện, Eric Lippert là người đã làm việc để tối ưu hóa trình biên dịch C # liên quan đến việc nối các chuỗi.
Kết luận
Thoạt nhìn, có vẻ lạ khi lớp String không xác định toán tử “+” cho đến khi chúng ta nghĩ đến khả năng tối ưu hóa của trình biên dịch liên quan đến khả năng hiển thị của một đoạn mã lớn hơn. Ví dụ:nếu toán tử “+” được định nghĩa trong lớp String, biểu thức s =a + b + c + d sẽ dẫn đến việc tạo ra hai chuỗi trung gian, một lệnh gọi duy nhất của chuỗi. c, d) phương pháp cho phép thực hiện nối hiệu quả hơn.