2010-08-29 38 views
22

Để bắt đầu, tôi đồng ý rằng các câu lệnh goto phần lớn không liên quan đến cấu trúc cấp cao hơn trong ngôn ngữ lập trình hiện đại và không được sử dụng khi thay thế phù hợp có sẵn.Các cách khác để giải quyết "khởi tạo vòng lặp" trong C#

Tôi đã đọc lại phiên bản gốc của Bộ luật Steve McConnell hoàn thành gần đây và đã quên mất đề xuất của mình về một vấn đề mã hóa phổ biến. Tôi đã đọc nó nhiều năm trước đây khi tôi mới bắt đầu và không nghĩ rằng tôi nhận ra công thức nấu ăn sẽ hữu ích như thế nào. Vấn đề mã hóa là như sau: khi thực hiện một vòng lặp, bạn thường cần thực hiện một phần của vòng lặp để khởi tạo trạng thái và sau đó thực hiện vòng lặp với một số logic khác và kết thúc mỗi vòng lặp với cùng một logic khởi tạo. Một ví dụ cụ thể là thực hiện phương thức String.Join (delimiter, array).

Tôi nghĩ rằng mọi thứ đầu tiên là về vấn đề này. Giả sử phương thức chắp thêm được định nghĩa để thêm đối số vào giá trị trả lại của bạn.

bool isFirst = true; 
foreach (var element in array) 
{ 
    if (!isFirst) 
    { 
    append(delimiter); 
    } 
    else 
    { 
    isFirst = false; 
    } 

    append(element); 
} 

Lưu ý: Tối ưu hóa một chút là loại bỏ và đặt nó ở cuối vòng lặp. Một nhiệm vụ thường là một lệnh đơn và tương đương với một lệnh khác và giảm số khối cơ bản xuống 1 và tăng kích thước khối cơ bản của phần chính. Kết quả là thực hiện một điều kiện trong mỗi vòng lặp để xác định xem bạn có nên thêm dấu phân tách hay không.

Tôi cũng đã xem và sử dụng các tính năng khác để giải quyết vấn đề vòng lặp chung này. Bạn có thể thực thi mã phần tử ban đầu đầu tiên bên ngoài vòng lặp, sau đó thực hiện vòng lặp của bạn từ phần tử thứ hai đến cuối. Bạn cũng có thể thay đổi logic để luôn nối thêm phần tử sau đó dấu tách và khi vòng lặp hoàn tất, bạn có thể chỉ cần xóa dấu phân cách cuối cùng mà bạn đã thêm vào.

Giải pháp thứ hai có xu hướng là giải pháp mà tôi chỉ thích vì nó không trùng lặp với bất kỳ mã nào. Nếu logic của trình tự khởi tạo thay đổi, bạn không phải nhớ sửa nó ở hai nơi. Tuy nhiên nó đòi hỏi thêm "công việc" để làm một cái gì đó và sau đó hoàn tác nó, gây ra ít nhất thêm chu kỳ CPU và trong nhiều trường hợp như ví dụ String.Join của chúng tôi yêu cầu thêm bộ nhớ là tốt.

Tôi vô cùng phấn khích sau đó để đọc xây dựng

var enumerator = array.GetEnumerator(); 
if (enumerator.MoveNext()) 
{ 
    goto start; 
    do { 
    append(delimiter); 

    start: 
    append(enumerator.Current); 
    } while (enumerator.MoveNext()); 
} 

Lợi ích ở đây là bạn không nhận được mã trùng lặp và bạn không nhận được công việc bổ sung. Bạn bắt đầu vòng lặp một nửa của bạn vào việc thực hiện vòng lặp đầu tiên của bạn và đó là khởi tạo của bạn. Bạn bị giới hạn để mô phỏng các vòng lặp khác với cấu trúc trong khi thực hiện nhưng bản dịch dễ dàng và việc đọc nó không khó.

Vì vậy, bây giờ là câu hỏi. Tôi hạnh phúc đã đi thử thêm vào một số mã tôi đã làm việc trên và thấy nó không hoạt động. Hoạt động tốt trong C, C++, Cơ bản nhưng hóa ra trong C# bạn không thể nhảy đến một nhãn bên trong một phạm vi từ vựng khác mà không phải là phạm vi gốc. Tôi đã rất thất vọng. Vì vậy, tôi đã tự hỏi, cách tốt nhất để đối phó với vấn đề mã hóa rất phổ biến này là gì (tôi thấy nó chủ yếu trong thế hệ dây) trong C#?

Để có lẽ cụ thể hơn với các yêu cầu:

  • Đừng lặp lại đang
  • Đừng làm việc không cần thiết
  • Đừng chậm hơn 2 hoặc 3 lần so với các mã khác
  • Hãy đọc

tôi nghĩ khả năng đọc là điều duy nhất mà cho là có thể chịu đau khổ với công thức tôi đã nêu. Tuy nhiên nó không hoạt động trong C# vậy điều tốt nhất tiếp theo là gì?

* Chỉnh sửa * Tôi đã thay đổi tiêu chí hiệu suất của mình vì một số cuộc thảo luận. Hiệu suất nói chung không phải là một yếu tố hạn chế ở đây, vì vậy mục tiêu chính xác hơn nên được để không được bất hợp lý, không phải là nhanh nhất bao giờ hết.

Lý do tôi không thích triển khai thay thế mà tôi đề xuất là vì chúng trùng lặp mã rời khỏi phòng để thay đổi một phần và không phải phần kia hoặc đối với trường hợp tôi thường chọn yêu cầu "hoàn tác" thao tác. để hoàn tác điều bạn vừa làm. Với thao tác chuỗi cụ thể, điều này thường khiến bạn mở cho một lỗi hoặc không tính đến một mảng trống và cố gắng hoàn tác một cái gì đó không xảy ra.

+0

Nhưng một khi bạn biết String.Join, bao lâu thì bạn thấy mình thực sự viết vòng lặp như vậy nữa? –

+1

Tôi tìm thấy tất cả các loại lý do không sử dụng nó. Hiện tại tôi đang làm việc trên một thứ gì đó thực hiện rất nhiều thao tác chuỗi với một cây đối tượng. Tôi phải vượt qua một nhà xây dựng chuỗi xung quanh để không có nhiều phân bổ không cần thiết khi tôi dịch một biểu đồ cây tùy ý thành một chuỗi. Sử dụng quá tải này là không thể bởi vì nó không tồn tại cho stringbuilder, và thậm chí nếu nó đã làm bạn không thể sử dụng một cái gì đó như sb.Join ("delim", Children.Select (child => child.Build (sb)) –

+2

@Peter Oehlert: Bạn có thể viết một hàm trả về IEnumerable, sau đó sử dụng trả về lợi nhuận để trả lại kết quả của bạn theo bất cứ thứ tự nào bạn muốn. –

Trả lời

11

Ví dụ cụ thể của bạn có giải pháp chuẩn: string.Join. Điều này xử lý thêm dấu phân cách một cách chính xác để bạn không phải tự viết vòng lặp.

Nếu bạn thực sự muốn viết cho mình một cách tiếp cận này, bạn có thể sử dụng như sau:

string delimiter = ""; 
foreach (var element in array) 
{ 
    append(delimiter); 
    append(element); 
    delimiter = ","; 
} 

này nên được hợp lý hiệu quả và tôi nghĩ rằng đó là hợp lý để đọc. Chuỗi liên tục "," được tập trung sao cho điều này sẽ không dẫn đến một chuỗi mới được tạo trên mỗi lần lặp. Tất nhiên nếu hiệu suất là rất quan trọng cho ứng dụng của bạn, bạn nên chuẩn hơn là đoán.

+8

Anh ta chỉ sử dụng 'String.Join' làm ví dụ ... –

+0

để sử dụng' String.Join() 'trước tiên bạn cần có một chuỗi các chuỗi, nếu bạn không có chuỗi đó thì bạn đang xem xét việc xây dựng' Danh sách 'sau đó chuyển đổi nó thành' string [] ', điều này sẽ trở nên tồi tệ hơn so với khi bắt đầu. –

+0

Như thế này. Luôn luôn khó chịu bởi "đầu tiên" ... Làm thế nào chắc chắn bạn có thể được rằng trình biên dịch sẽ tối ưu hóa các bài tập lặp đi lặp lại để phân cách? – Nicolas78

0

Tôi thích phương pháp biến số first. Nó có lẽ không phải là cách sạch nhất nhưng hiệu quả nhất. Hoặc bạn có thể sử dụng Length của thứ bạn thêm vào và so sánh nó với số không. Hoạt động tốt với StringBuilder.

18

Cá nhân tôi thích lựa chọn Đánh dấu Byer, nhưng bạn luôn có thể viết phương pháp chung của riêng bạn cho việc này:

public static void IterateWithSpecialFirst<T>(this IEnumerable<T> source, 
    Action<T> firstAction, 
    Action<T> subsequentActions) 
{ 
    using (IEnumerator<T> iterator = source.GetEnumerator()) 
    { 
     if (iterator.MoveNext()) 
     { 
      firstAction(iterator.Current); 
     } 
     while (iterator.MoveNext()) 
     { 
      subsequentActions(iterator.Current); 
     } 
    } 
} 

Đó là tương đối đơn giản ... đưa ra một đặc biệt hành động cuối cùng là hơi khó:

public static void IterateWithSpecialLast<T>(this IEnumerable<T> source, 
    Action<T> allButLastAction, 
    Action<T> lastAction) 
{ 
    using (IEnumerator<T> iterator = source.GetEnumerator()) 
    { 
     if (!iterator.MoveNext()) 
     { 
      return; 
     }    
     T previous = iterator.Current; 
     while (iterator.MoveNext()) 
     { 
      allButLastAction(previous); 
      previous = iterator.Current; 
     } 
     lastAction(previous); 
    } 
} 

EDIT: Khi nhận xét của bạn liên quan đến hiệu suất này, tôi sẽ nhắc lại nhận xét của tôi trong câu trả lời này: trong khi vấn đề chung này khá phổ biến, đó là không phải phổ biến cho nó là một nút cổ chai hiệu suất mà nó có giá trị vi-tối ưu hóa xung quanh. Thật vậy, tôi không thể nhớ bao giờ đi qua một tình huống mà máy móc vòng lặp trở thành một nút cổ chai. Tôi chắc chắn điều đó xảy ra, nhưng rằng không phải là "phổ biến". Nếu tôi từng sử dụng nó, tôi sẽ đặc biệt viết mã cụ thể đó và giải pháp tốt nhất sẽ phụ thuộc vào chính xác mã cần làm gì.

Nói chung, tuy nhiên, tôi coi trọng khả năng đọc và khả năng sử dụng lại nhiều hơn nhiều hơn tối ưu hóa vi mô.

+2

+1 để hiển thị đúng cách để sử dụng một điều tra viên trực tiếp: 'using'. –

+1

Nhưng gọi một đại biểu là hành động khá tốn kém (ủy nhiệm 'Hành động') để giải pháp này có nghĩa là dễ đọc nhưng đó là chi phí cho hiệu suất. Tôi có đúng hay có lừa không? –

+1

@MartyIX: Câu hỏi của bạn nói về vấn đề * mã hóa * phổ biến, nhưng cũng chỉ định hiệu suất làm yêu cầu quan trọng. Chỉ cần làm thế nào thường vụ này lên theo cách mà chi phí thực hiện một đại biểu sẽ là một nút cổ chai? Tôi không thể nghĩ rằng * bao giờ * xảy ra với tôi. Nếu tôi gặp phải điều đó, tôi sẽ đối xử với tình huống cụ thể đó như một sự bất thường, và viết mã cụ thể cho nó. Tôi sẽ không cố gắng nghĩ về một giải pháp mục đích chung cho một tình huống bất thường như vậy. "Mục đích chung" thường phản đối loại tối ưu hóa vi mô có vẻ như bạn đang theo dõi. –

0

Tại sao không di chuyển giao dịch với phần tử đầu tiên bên ngoài vòng lặp?

StringBuilder sb = new StrindBuilder() 
sb.append(array.first) 
foreach (var elem in array.skip(1)) { 
    sb.append(",") 
    sb.append(elem) 
} 
+2

Thực ra, Peter đã đề cập ngắn gọn về giải pháp này trong câu hỏi của anh ấy. –

+0

Nếu 'mảng' thực sự là truy vấn DB, phương thức này sẽ không thực hiện truy vấn * hai lần *? – Gabe

+0

@Gabe: có, như được viết, nó sẽ. Trong trường hợp đó, bạn có thể lưu kết quả truy vấn trước bằng một cuộc gọi đến 'ToList()'. –

7

Bạn đã sẵn sàng bỏ cuộc. Vì vậy, đây nên được phù hợp:

 using (var enumerator = array.GetEnumerator()) { 
      if (enumerator.MoveNext()) { 
       for (;;) { 
        append(enumerator.Current); 
        if (!enumerator.MoveNext()) break; 
        append(delimiter); 
       } 
      } 
     } 
+0

Tôi nhớ đã thấy cấu trúc này ở một ngôn ngữ ở đâu đó: vòng lặp với séc, không phải lúc đầu, không phải ở cuối, nhưng ở giữa. Không thể nhớ nó là ngôn ngữ gì ... –

+0

Điều này hoạt động ở hầu hết mọi ngôn ngữ. –

+1

+1 Đây được gọi là vòng lặp lặp-thoát trong Hoàn thành mã và là giải pháp ưa thích của tôi cho loại sự cố này. –

4

Đôi khi tôi sử dụng LINQ .First().Skip(1) để xử lý này ... Điều này có thể đưa ra một giải pháp tương đối sạch (và rất có thể đọc được).

Sử dụng bạn Ví dụ,

append(array.First()); 
foreach(var x in array.Skip(1)) 
{ 
    append(delimiter); 
    append (x); 
} 

[này giả định có ít nhất một phần tử trong mảng, một bài kiểm tra dễ dàng để thêm nếu đó là cần phải tránh.]

Sử dụng F # sẽ là gợi ý khác :-)

+0

Tôi đã sử dụng foreach để "kiểm tra" cho mục đầu tiên và được sử dụng "Take (1)" thay vì đầu tiên. một chút sôi nổi, nhưng hoàn thành công việc. –

+0

Tôi thú nhận tôi đã không hoàn toàn nhận được xung quanh để học tập F # (đó là trong danh sách); làm thế nào điều này sẽ được thực hiện trong F #? –

+0

F # có mẫu phù hợp. 'Cons Pattern' cho phép bạn phân tách một danh sách thành' head :: tail'. Xem http://msdn.microsoft.com/en-us/library/dd547125.aspx –

2

Có những cách bạn "có thể" nhận được xung quanh mã tăng gấp đôi, nhưng trong hầu hết các trường hợp, mã trùng lặp ít nhiều xấu xí/nguy hiểm hơn các giải pháp có thể. Tôi không thực sự nghĩ rằng bạn thực sự đạt được bất cứ điều gì đáng kể (compactness, dễ đọc hoặc hiệu quả) bằng cách sử dụng nó, trong khi bạn tăng nguy cơ của một lập trình viên nhận được một cái gì đó sai tại một số thời điểm trong vòng đời của mã.

Nói chung tôi có xu hướng đi cho cách tiếp cận:

  • Một trường hợp đặc biệt đối với người đầu tiên (hoặc cuối) hành động
  • vòng lặp cho các hành động khác.

Điều này sẽ loại bỏ sự thiếu hiệu quả được giới thiệu bằng cách kiểm tra xem vòng lặp có lặp lại đầu tiên trên mọi thời gian hay không và rất dễ hiểu. Đối với các trường hợp không tầm thường, sử dụng phương thức ủy nhiệm hoặc trợ giúp để áp dụng hành động có thể giảm thiểu sự sao chép mã.

Hoặc một cách tiếp cận tôi sử dụng đôi khi nơi hiệu quả là không quan trọng:

  • vòng lặp, và kiểm tra xem chuỗi rỗng để xác định xem một dấu phân cách là bắt buộc.

Điều này có thể được viết gọn gàng hơn và dễ đọc hơn so với phương pháp tiếp cận goto, và không yêu cầu thêm bất kỳ biến/lưu trữ/kiểm tra nào để phát hiện "trường hợp đặc biệt" iteraiton.

Nhưng tôi nghĩ cách tiếp cận của Mark Byers là một giải pháp sạch sẽ tốt cho ví dụ cụ thể của bạn.

5

Bạn chắc chắn có thể tạo ra một giải pháp goto trong C# (lưu ý: Tôi không thêm null kiểm tra):

string Join(string[] array, string delimiter) { 
    var sb = new StringBuilder(); 
    var enumerator = array.GetEnumerator(); 
    if (enumerator.MoveNext()) { 
    goto start; 
    loop: 
     sb.Append(delimiter); 
     start: sb.Append(enumerator.Current); 
     if (enumerator.MoveNext()) goto loop; 
    } 
    return sb.ToString(); 
} 

Đối cụ ví dụ của bạn, điều này có vẻ khá straighforward với tôi (và đó là một trong các giải pháp mà bạn mô tả):

string Join(string[] array, string delimiter) { 
    var sb = new StringBuilder(); 
    foreach (string element in array) { 
    sb.Append(element); 
    sb.Append(delimiter); 
    } 
    if (sb.Length >= delimiter.Length) sb.Length -= delimiter.Length; 
    return sb.ToString(); 
} 

Nếu bạn muốn để có được chức năng, bạn có thể thử sử dụng cách tiếp cận gấp này:

012.
string Join(string[] array, string delimiter) { 
    return array.Aggregate((left, right) => left + delimiter + right); 
} 

Mặc dù nó đọc thật sự tốt đẹp, nó không sử dụng một StringBuilder, vì vậy bạn có thể muốn lạm dụng Aggregate một chút để sử dụng nó:

string Join(string[] array, string delimiter) { 
    var sb = new StringBuilder(); 
    array.Aggregate((left, right) => { 
    sb.Append(left).Append(delimiter).Append(right); 
    return ""; 
    }); 
    return sb.ToString(); 
} 

Hoặc bạn có thể sử dụng này (mượn ý tưởng từ câu trả lời khác ở đây):

string Join(string[] array, string delimiter) { 
    return array. 
    Skip(1). 
    Aggregate(new StringBuilder(array.FirstOrDefault()), 
     (acc, s) => acc.Append(delimiter).Append(s)). 
    ToString(); 
} 
+3

Thay thế vòng lặp bằng goto cực kỳ xấu xí và không bao giờ được sử dụng nếu không có lý do * thực sự * tốt, nhưng +1 cho suy nghĩ bên ngoài và tìm cách triển khai cấu trúc trong C#. – Heinzi

0

Nếu bạn muốn đi theo tuyến chức năng, bạn có thể xác định String.Join như LINQ xây dựng có thể sử dụng lại trên nhiều loại.

Cá nhân, tôi hầu như luôn luôn tìm hiểu rõ ràng về mã để lưu một số thực thi mã opcode.

EG:

namespace Play 
{ 
    public static class LinqExtensions { 
     public static U JoinElements<T, U>(this IEnumerable<T> list, Func<T, U> initializer, Func<U, T, U> joiner) 
     { 
      U joined = default(U); 
      bool first = true; 
      foreach (var item in list) 
      { 
       if (first) 
       { 
        joined = initializer(item); 
        first = false; 
       } 
       else 
       { 
        joined = joiner(joined, item); 
       } 
      } 
      return joined; 
     } 
    } 

    class Program 
    { 

     static void Main(string[] args) 
     { 
      List<int> nums = new List<int>() { 1, 2, 3 }; 
      var sum = nums.JoinElements(a => a, (a, b) => a + b); 
      Console.WriteLine(sum); // outputs 6 

      List<string> words = new List<string>() { "a", "b", "c" }; 
      var buffer = words.JoinElements(
       a => new StringBuilder(a), 
       (a, b) => a.Append(",").Append(b) 
       ); 

      Console.WriteLine(buffer); // outputs "a,b,c" 

      Console.ReadKey(); 
     } 

    } 
} 
+0

LINQ có điều này, nó được gọi là 'Tổng hợp ' – joshperry

+0

@ josh, điều này là khác nhau một cách tinh tế để Tổng hợp: words.Aggregate ( mới StringBuilder(), (a, b) => a.Append (", "). b) ); ... trả về (", a, b, c") –

Các vấn đề liên quan