Khi nào sử dụng String.Format so với nối chuỗi thì tốt hơn?


120

Tôi có một đoạn mã nhỏ đang phân tích cú pháp một giá trị chỉ mục để xác định đầu vào ô vào Excel. Tôi đang nghĩ ...

Sự khác biệt giữa

xlsSheet.Write("C" + rowIndex.ToString(), null, title);

xlsSheet.Write(string.Format("C{0}", rowIndex), null, title);

Cái này tốt hơn những cái khác phải không? Và tại sao?


4
Điều này tương tự với stackoverflow.com/questions/16432/…
Jonathan S.

Câu trả lời:


115

Trước C # 6

Thành thật mà nói, tôi nghĩ phiên bản đầu tiên đơn giản hơn - mặc dù tôi muốn đơn giản hóa nó thành:

xlsSheet.Write("C" + rowIndex, null, title);

Tôi nghi ngờ các câu trả lời khác có thể nói về cú đánh hiệu suất, nhưng thành thật mà nói thì nó sẽ rất nhỏ nếu có - và phiên bản nối này không cần phải phân tích cú pháp chuỗi định dạng.

Chuỗi định dạng là rất tốt cho mục đích bản địa hóa, v.v., nhưng trong trường hợp như thế này thì việc nối đơn giản hơn và hoạt động tốt.

Với C # 6

Nội suy chuỗi làm cho rất nhiều thứ đơn giản hơn để đọc trong C # 6. Trong trường hợp này, mã thứ hai của bạn trở thành:

xlsSheet.Write($"C{rowIndex}", null, title);

mà có lẽ là lựa chọn tốt nhất, IMO.



Tôi biết rồi mà. Nó được thực hiện trong jest (đã đọc liên kết btw trước đây, đó là một bài đọc tốt)
nawfal

Jon. Tôi luôn là một fan hâm mộ của ông Richter, và đã tuân theo hướng dẫn về quyền anh, v.v. về mặt tôn giáo. Tuy nhiên, sau khi đọc bài viết (cũ) của bạn, tôi bây giờ là một người chuyển đổi. Cảm ơn
stevethethread

3
@ mbomb007: Bây giờ là codeblog.jonskeet.uk/2008/10/08/…
Jon Skeet

4
Bây giờ C # 6 có sẵn, bạn có thể sử dụng cú pháp chuỗi suy mới cho những gì tôi nghĩ là có thể đọc dễ dàng hơn:xlsSheet.Write($"C{rowIndex}", null, title);
HotN

158

Sở thích ban đầu của tôi (đến từ nền tảng C ++) là dành cho String.Format. Tôi đã bỏ điều này sau đó vì những lý do sau:

  • Nối chuỗi được cho là "an toàn hơn". Nó đã xảy ra với tôi (và tôi đã thấy nó xảy ra với một số nhà phát triển khác) khi loại bỏ một tham số hoặc nhầm lẫn thứ tự tham số do nhầm lẫn. Trình biên dịch sẽ không kiểm tra các tham số so với chuỗi định dạng và bạn sẽ gặp phải lỗi thời gian chạy (nghĩa là, nếu bạn đủ may mắn không mắc phải nó trong một phương pháp khó hiểu, chẳng hạn như ghi lỗi). Với phép nối, việc loại bỏ một tham số ít bị lỗi hơn. Bạn có thể cho rằng khả năng sai sót là rất nhỏ, nhưng nó có thể xảy ra.

- Nối chuỗi cho phép giá trị null, String.Formatkhông. Viết " s1 + null + s2" không bị vỡ, nó chỉ coi giá trị null là String.Empty. Chà, điều này có thể phụ thuộc vào tình huống cụ thể của bạn - có những trường hợp bạn muốn nhận lỗi thay vì im lặng bỏ qua FirstName rỗng. Tuy nhiên, ngay cả trong tình huống này, cá nhân tôi vẫn thích tự mình kiểm tra null và đưa ra các lỗi cụ thể thay vì ArgumentNullException tiêu chuẩn mà tôi nhận được từ String.Format.

  • Nối chuỗi hoạt động tốt hơn. Một số bài viết ở trên đã đề cập đến vấn đề này (mà không thực sự giải thích tại sao, điều đó quyết định tôi viết bài này :).

Ý tưởng là trình biên dịch .NET đủ thông minh để chuyển đổi đoạn mã này:

public static string Test(string s1, int i2, int i3, int i4, 
        string s5, string s6, float f7, float f8)
{
    return s1 + " " + i2 + i3 + i4 + " ddd " + s5 + s6 + f7 + f8;
}

đến điều này:

public static string Test(string s1, int i2, int i3, int i4,
            string s5, string s6, float f7, float f8)
{
    return string.Concat(new object[] { s1, " ", i2, i3, i4, 
                    " ddd ", s5, s6, f7, f8 });
}

Điều gì xảy ra dưới lớp vỏ của String.Concat rất dễ đoán (sử dụng Reflector). Các đối tượng trong mảng được chuyển đổi thành chuỗi của chúng thông qua ToString (). Sau đó, tổng chiều dài được tính và chỉ một chuỗi được phân bổ (với tổng chiều dài). Cuối cùng, mỗi chuỗi được sao chép vào chuỗi kết quả thông qua wstrcpy trong một số đoạn mã không an toàn.

Lý do String.Concatlà cách nhanh hơn? Chà, tất cả chúng ta đều có thể biết String.Formatđược những gì đang làm - bạn sẽ ngạc nhiên về số lượng mã cần thiết để xử lý chuỗi định dạng. Trên hết (tôi đã thấy các bình luận liên quan đến việc tiêu thụ bộ nhớ), hãy String.Formatsử dụng một StringBuilder bên trong. Đây là cách thực hiện:

StringBuilder builder = new StringBuilder(format.Length + (args.Length * 8));

Vì vậy, đối với mỗi đối số được thông qua, nó có 8 ký tự. Nếu đối số là giá trị một chữ số thì quá tệ, chúng ta có một số không gian lãng phí. Nếu đối số là một đối tượng tùy chỉnh trả về một số văn bản dài ToString(), thì thậm chí có thể cần một số phân bổ lại (tất nhiên là trong trường hợp xấu nhất).

So với điều này, việc nối chỉ lãng phí không gian của mảng đối tượng (không quá nhiều, vì nó là một mảng tham chiếu). Không có phân tích cú pháp cho các chỉ định định dạng và không có StringBuilder trung gian. Chi phí quyền anh / unboxing có trong cả hai phương pháp.

Lý do duy nhất tôi muốn sử dụng String.Format là khi bản địa hóa có liên quan. Đặt các chuỗi định dạng trong tài nguyên cho phép bạn hỗ trợ các ngôn ngữ khác nhau mà không làm rối mã (hãy nghĩ về các trường hợp trong đó các giá trị được định dạng thay đổi thứ tự tùy thuộc vào ngôn ngữ, tức là "sau {0} giờ và {1} phút" có thể trông khá khác trong tiếng Nhật: ).


Tổng hợp bài đăng đầu tiên (và khá dài) của tôi:

  • cách tốt nhất (về hiệu suất so với khả năng bảo trì / khả năng đọc) đối với tôi là sử dụng nối chuỗi mà không cần bất kỳ ToString()lệnh gọi nào
  • nếu bạn đang sau khi biểu diễn, hãy tự thực hiện các ToString()cuộc gọi để tránh đấm bốc (tôi hơi thiên về tính dễ đọc) - giống như lựa chọn đầu tiên trong câu hỏi của bạn
  • nếu bạn đang hiển thị các chuỗi được bản địa hóa cho người dùng (không phải trường hợp ở đây), String.Format()có một lợi thế.

5
1) string.Formatlà "an toàn" khi sử dụng ReSharper; nghĩa là, nó an toàn như bất kỳ mã nào khác có thể được sử dụng [không chính xác]. 2) string.Format không cho phép một "an toàn" null: string.Format("A{0}B", (string)null)kết quả là "AB". 3) Tôi ít khi quan tâm đến mức độ thành quả (và vì mục đích đó, nó là một ngày hiếm khi tôi kéo ra StringBuilder) ...

Đồng ý trên 2), tôi sẽ sửa bài. Không thể xác minh xem điều này có an toàn trở lại trong 1.1 hay không, nhưng khung công tác mới nhất thực sự là không an toàn.
Dan C.

Có phải string.Concat vẫn được sử dụng nếu một trong các toán hạng là một cuộc gọi phương thức với giá trị trả về, thay vì là một tham số hoặc biến?
Richard Collette

2
@RichardCollette Có, String.Concat được sử dụng ngay cả khi bạn nối các giá trị trả về của các cuộc gọi phương thức, ví dụ: string s = "This " + MyMethod(arg) + " is a test";được biên dịch thành một String.Concat()cuộc gọi trong chế độ Phát hành.
Dan C.

Câu trả lời tuyệt vời; rất tốt được viết và giải thích.
Frank V

6

Tôi nghĩ rằng tùy chọn đầu tiên dễ đọc hơn và đó phải là mối quan tâm chính của bạn.

xlsSheet.Write("C" + rowIndex.ToString(), null, title);

string.Format sử dụng StringBuilder dưới mui xe (kiểm tra bằng phản xạ ) vì vậy nó sẽ không có bất kỳ lợi ích hiệu suất nào trừ khi bạn đang thực hiện một lượng lớn quá trình nối. Nó sẽ chậm hơn đối với kịch bản của bạn nhưng thực tế là quyết định tối ưu hóa hiệu suất vi mô này hầu như không phù hợp và bạn thực sự nên tập trung vào khả năng đọc mã của mình trừ khi bạn đang ở trong một vòng lặp.

Dù bằng cách nào, trước tiên hãy viết để dễ đọc và sau đó sử dụng trình mô tả hiệu suất để xác định các điểm phát sóng của bạn nếu bạn thực sự nghĩ rằng mình có mối quan tâm về hiệu suất.



5

Đối với một trường hợp đơn giản, đó là một phép nối đơn đơn giản, tôi cảm thấy rằng nó không có giá trị phức tạp string.Format(và tôi chưa thử nghiệm, nhưng tôi nghi ngờ rằng đối với trường hợp đơn giản như thế này, string.Format thể chậm hơn một chút, với phân tích chuỗi định dạng và tất cả). Giống như Jon Skeet, tôi không muốn gọi rõ ràng .ToString(), vì điều đó sẽ được thực hiện ngầm bởi string.Concat(string, object)quá tải và tôi nghĩ rằng mã trông sạch hơn và dễ đọc hơn nếu không có nó.

Nhưng đối với nhiều từ nối (bao nhiêu là chủ quan), tôi chắc chắn thích hơn string.Format. Tại một thời điểm nhất định, tôi nghĩ rằng cả khả năng đọc và hiệu suất đều bị ảnh hưởng không cần thiết với sự ghép nối.

Nếu có nhiều tham số đối với chuỗi định dạng (một lần nữa, "nhiều" là chủ quan), tôi thường thích bao gồm các chỉ số được nhận xét trên các đối số thay thế, vì tôi không biết giá trị nào chuyển đến tham số nào. Một ví dụ tiếp theo:

Console.WriteLine(
    "Dear {0} {1},\n\n" +

    "Our records indicate that your {2}, \"{3}\", is due for {4} {5} shots.\n" +
    "Please call our office at 1-900-382-5633 to make an appointment.\n\n" +

    "Thank you,\n" +
    "Eastern Veterinary",

    /*0*/client.Title,
    /*1*/client.LastName,
    /*2*/client.Pet.Animal,
    /*3*/client.Pet.Name,
    /*4*/client.Pet.Gender == Gender.Male ? "his" : "her",
    /*5*/client.Pet.Schedule[0]
);

Cập nhật

Tôi nhận ra rằng ví dụ tôi đã đưa ra hơi khó hiểu, vì có vẻ như tôi đã sử dụng cả phép nối và string.Formatở đây. Và vâng, về mặt logic và từ vựng, đó là những gì tôi đã làm. Nhưng tất cả các đoạn nối sẽ được tối ưu hóa bởi trình biên dịch 1 , vì chúng đều là chuỗi ký tự. Vì vậy, tại thời điểm chạy, sẽ có một chuỗi duy nhất. Vì vậy, tôi đoán tôi nên nói rằng tôi muốn tránh nhiều kết nối trong thời gian chạy .

Tất nhiên, hầu hết chủ đề này hiện đã lỗi thời, trừ khi bạn vẫn gặp khó khăn khi sử dụng C # 5 trở lên. Bây giờ chúng ta có các chuỗi nội suy , để dễ đọc, vượt trội hơn nhiều so với string.Format, trong hầu hết các trường hợp. Ngày nay, trừ khi tôi chỉ nối một giá trị trực tiếp vào đầu hoặc cuối của một chuỗi ký tự, tôi hầu như luôn sử dụng phép nội suy chuỗi. Hôm nay, tôi sẽ viết ví dụ trước đó của tôi như thế này:

Console.WriteLine(
    $"Dear {client.Title} {client.LastName},\n\n" +

    $"Our records indicate that your {client.Pet.Animal}, \"{client.Pet.Name}\", " +
    $"is due for {(client.Pet.Gender == Gender.Male ? "his" : "her")} " +
    $"{client.Pet.Schedule[0]} shots.\n" +
    "Please call our office at 1-900-382-5633 to make an appointment.\n\n" +

    "Thank you,\n" +
    "Eastern Veterinary"
);

Bạn không mất thời gian biên dịch nối theo cách này. Mỗi chuỗi nội suy được string.Formattrình biên dịch chuyển thành lệnh gọi và kết quả của chúng được nối với nhau tại thời điểm chạy. Điều đó có nghĩa là đây là một sự hy sinh hiệu suất thời gian chạy để có thể đọc được. Hầu hết thời gian, đó là một sự hy sinh đáng giá, vì hình phạt thời gian chạy là không đáng kể. Tuy nhiên, trong mã quan trọng về hiệu suất, bạn có thể cần lập hồ sơ các giải pháp khác nhau.


1 Bạn có thể thấy điều này trong đặc tả C # :

... các cấu trúc sau được cho phép trong các biểu thức không đổi:

...

  • Toán tử nhị phân + ... được xác định trước ...

Bạn cũng có thể xác minh nó bằng một đoạn mã nhỏ:

const string s =
    "This compiles successfully, " +
    "and you can see that it will " +
    "all be one string (named `s`) " +
    "at run time";

1
fyi, bạn có thể sử dụng @ "... chuỗi nhiều dòng" thay vì tất cả các từ nối.
Aaron Palmer

Có, nhưng sau đó bạn phải căn trái chuỗi của mình. @ chuỗi bao gồm tất cả các dòng mới và ký tự tab giữa các dấu ngoặc kép.
P Daddy

Tôi biết điều này đã cũ, nhưng đây là trường hợp tôi muốn nói rằng hãy đặt chuỗi định dạng trong tệp resx.
Andy

2
Chà, mọi người tập trung vào chuỗi ký tự, thay vì trọng tâm của vấn đề.
P Daddy

heheh - Tôi chỉ nhận thấy sự nối bên trong chuỗi của bạnString.Format()
Kristopher

3

Nếu chuỗi của bạn phức tạp hơn với nhiều biến được nối, thì tôi sẽ chọn string.Format (). Nhưng đối với kích thước của chuỗi và số biến được nối trong trường hợp của bạn, tôi muốn đi với phiên bản đầu tiên của bạn, đó là hơn Spartan .


3

Tôi đã xem qua String.Format (sử dụng Reflector) và nó thực sự tạo ra một StringBuilder sau đó gọi AppendFormat trên đó. Vì vậy, nó là nhanh hơn so với concat cho nhiều bàn khuấy. Nhanh nhất (tôi tin là) sẽ tạo một StringBuilder và thực hiện các lệnh gọi đến Nối theo cách thủ công. Tất nhiên số lượng "nhiều" là để đoán. Tôi sẽ sử dụng + (thực sự và vì tôi hầu hết là một lập trình viên VB) cho một cái gì đó đơn giản như ví dụ của bạn. Khi nó phức tạp hơn, tôi sử dụng String.Format. Nếu có RẤT NHIỀU biến thì tôi sẽ sử dụng StringBuilder và Append, ví dụ: chúng tôi có mã xây dựng mã, ở đó tôi sử dụng một dòng mã thực để xuất ra một dòng mã đã tạo.

Dường như có một số suy đoán về số lượng chuỗi được tạo cho mỗi hoạt động này, vì vậy chúng ta hãy lấy một vài ví dụ đơn giản.

"C" + rowIndex.ToString();

"C" đã là một chuỗi.
rowIndex.ToString () tạo một chuỗi khác. (@manohard - không có quyền chọn rowIndex sẽ xảy ra)
Sau đó, chúng tôi nhận được chuỗi cuối cùng.
Nếu chúng ta lấy ví dụ về

String.Format("C(0)",rowIndex);

thì chúng ta có "C {0}" dưới dạng một chuỗi
rowIndex được đóng hộp để chuyển đến hàm
Một trình tạo
chuỗi mới được tạo AppendFormat được gọi trên trình tạo chuỗi - Tôi không biết chi tiết về cách thức hoạt động của AppendFormat nhưng hãy giả sử rằng nó là cực kỳ hiệu quả, nó vẫn sẽ phải chuyển đổi rowIndex đóng hộp thành một chuỗi.
Sau đó chuyển đổi trình tạo chuỗi thành một chuỗi mới.
Tôi biết rằng StringBuilders cố gắng ngăn các bản sao bộ nhớ vô nghĩa diễn ra nhưng String.Format vẫn có thêm chi phí so với nối đơn thuần.

Nếu bây giờ chúng ta lấy một ví dụ với một vài chuỗi khác

"a" + rowIndex.ToString() + "b" + colIndex.ToString() + "c" + zIndex.ToString();

chúng ta có 6 chuỗi để bắt đầu, sẽ giống nhau cho mọi trường hợp.
Sử dụng phép nối chúng ta cũng có 4 chuỗi trung gian cộng với kết quả cuối cùng. Đó là những kết quả trung gian được loại bỏ bằng cách sử dụng Chuỗi, Định dạng (hoặc một StringBuilder).
Hãy nhớ rằng để tạo mỗi chuỗi trung gian, chuỗi trước đó phải được sao chép sang vị trí bộ nhớ mới, nó không chỉ là việc cấp phát bộ nhớ có khả năng chậm.


4
Xoi mói. Trong "a" + ... + "b" + ... + "c" + ..., bạn sẽ không thực sự có 4 chuỗi trung gian. Trình biên dịch sẽ tạo một lệnh gọi đến phương thức tĩnh String.Concat (params string [] giá trị) và tất cả chúng sẽ được nối cùng một lúc. Tuy nhiên, tôi vẫn thích string.Format vì lợi ích của nó.
P Daddy

2

Tôi thích String.Format vì nó có thể làm cho văn bản được định dạng của bạn dễ đọc và theo dõi hơn nhiều so với nối nội dòng, nó cũng linh hoạt hơn rất nhiều cho phép bạn định dạng các tham số của mình, tuy nhiên đối với các mục đích sử dụng ngắn như của bạn, tôi không thấy vấn đề gì khi nối.

Đối với các phép nối bên trong các vòng lặp hoặc trong các chuỗi lớn, bạn nên cố gắng sử dụng lớp StringBuilder.


2

Ví dụ đó có lẽ quá tầm thường để nhận thấy sự khác biệt. Trên thực tế, tôi nghĩ trong hầu hết các trường hợp, trình biên dịch có thể tối ưu hóa bất kỳ sự khác biệt nào.

Tuy nhiên, nếu tôi phải đoán, tôi sẽ đưa ra string.Format()một lợi thế cho các tình huống phức tạp hơn. Nhưng đó là cảm giác tuyệt vời hơn vì nó có khả năng làm tốt hơn khi sử dụng bộ đệm thay vì tạo ra nhiều chuỗi bất biến và không dựa trên bất kỳ dữ liệu thực nào.


1

Tôi đồng ý với rất nhiều điểm ở trên, một điểm khác mà tôi tin rằng cần được đề cập là khả năng bảo trì mã. string.Format cho phép thay đổi mã dễ dàng hơn.

tức là tôi có một tin nhắn "The user is not authorized for location " + locationhoặc "The User is not authorized for location {0}"

nếu tôi từng muốn thay đổi thông báo thành: location + " does not allow this User Access"hoặc "{0} does not allow this User Access"

với string.Format, tất cả những gì tôi phải làm là thay đổi chuỗi. để nối, tôi phải sửa đổi thông báo đó

nếu được sử dụng ở nhiều nơi có thể tiết kiệm toàn bộ thời gian.


1

Tôi có ấn tượng rằng string.format nhanh hơn và có vẻ chậm hơn 3 lần trong thử nghiệm này

string concat = "";
        System.Diagnostics.Stopwatch sw1 = new System.Diagnostics.Stopwatch    ();
        sw1.Start();
        for (int i = 0; i < 10000000; i++)
        {
            concat = string.Format("{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}","1", "2" , "3" , "4" , "5" , "6" , "7" , "8" , "9" , "10" , i);
        }
        sw1.Stop();
        Response.Write("format: "  + sw1.ElapsedMilliseconds.ToString());
        System.Diagnostics.Stopwatch sw2 = new System.Diagnostics.Stopwatch();
        sw2.Start();
        for (int i = 0; i < 10000000; i++)
        {
            concat = "1" + "2" + "3" + "4" + "5" + "6" + "7" + "8" + "9" + "10" + i;
        }
        sw2.Stop();

string.format mất 4,6 giây và khi sử dụng '+' mất 1,6 giây.


7
Trình biên dịch nhận "1" + "2" + "3" + "4" + "5" + "6" + "7" + "8" + "9" + "10"như một chuỗi chữ, vì vậy dòng trở nên có hiệu quả "12345678910" + iđó là nhanh hơn trướcstring.Format(...)
wertzui

0

string.Format có lẽ là lựa chọn tốt hơn khi mẫu định dạng ("C {0}") được lưu trữ trong tệp cấu hình (chẳng hạn như Web.config / App.config)


0

Tôi đã làm một chút về cấu hình của các phương thức chuỗi khác nhau bao gồm string.Format, StringBuilder và nối chuỗi. Nối chuỗi hầu như luôn luôn tốt hơn các phương pháp xây dựng chuỗi khác. Vì vậy, nếu hiệu suất là chìa khóa, thì nó sẽ tốt hơn. Tuy nhiên, nếu hiệu suất không quan trọng thì cá nhân tôi thấy string.Format dễ theo dõi hơn trong mã. (Nhưng đó là một lý do chủ quan) Tuy nhiên, StringBuilder có lẽ là hiệu quả nhất đối với việc sử dụng bộ nhớ.



-1

Việc nối chuỗi chiếm nhiều bộ nhớ hơn so với String.Format. Vì vậy, cách tốt nhất để nối các chuỗi là sử dụng Đối tượng String.Format hoặc System.Text.StringBuilder.

Hãy lấy trường hợp đầu tiên: "C" + rowIndex.ToString () Giả sử rowIndex là một kiểu giá trị vì vậy phương thức ToString () phải Box để chuyển giá trị thành String và sau đó CLR tạo bộ nhớ cho chuỗi mới với cả hai giá trị được bao gồm.

Trong trường hợp string.Format mong đợi tham số đối tượng và nhận vào rowIndex như một đối tượng và chuyển đổi nó thành chuỗi bên trong offcourse sẽ có Boxing nhưng nó thực chất và nó sẽ không chiếm nhiều bộ nhớ như trong trường hợp đầu tiên.

Đối với các chuỗi ngắn, nó sẽ không thành vấn đề nhiều như vậy, tôi đoán ...

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.