2009-04-29 18 views
33

Bối cảnh: Tôi có một chuỗi các chuỗi mà tôi nhận được từ một cơ sở dữ liệu và tôi muốn trả lại chúng. Theo truyền thống, nó sẽ là một cái gì đó như thế này:C# IEnumerator/cấu trúc năng suất có khả năng xấu?

public List<string> GetStuff(string connectionString) 
{ 
    List<string> categoryList = new List<string>(); 
    using (SqlConnection sqlConnection = new SqlConnection(connectionString)) 
    { 
     string commandText = "GetStuff"; 
     using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection)) 
     { 
      sqlCommand.CommandType = CommandType.StoredProcedure; 

      sqlConnection.Open(); 
      SqlDataReader sqlDataReader = sqlCommand.ExecuteReader(); 
      while (sqlDataReader.Read()) 
      { 
       categoryList.Add(sqlDataReader["myImportantColumn"].ToString()); 
      } 
     } 
    } 
    return categoryList; 
} 

Nhưng sau đó tôi tìm được người tiêu dùng sẽ muốn lặp qua các mục và không quan tâm đến nhiều thứ khác nữa, và tôi muốn không hộp bản thân mình trong vào Danh sách, mỗi lần, vì vậy nếu tôi trả về mọi thứ IEnumerable thì tốt/linh hoạt. Vì vậy, tôi đã suy nghĩ tôi có thể sử dụng một thiết kế "yield return" loại để xử lý này ... một cái gì đó như thế này:

public IEnumerable<string> GetStuff(string connectionString) 
{ 
    using (SqlConnection sqlConnection = new SqlConnection(connectionString)) 
    { 
     string commandText = "GetStuff"; 
     using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection)) 
     { 
      sqlCommand.CommandType = CommandType.StoredProcedure; 

      sqlConnection.Open(); 
      SqlDataReader sqlDataReader = sqlCommand.ExecuteReader(); 
      while (sqlDataReader.Read()) 
      { 
       yield return sqlDataReader["myImportantColumn"].ToString(); 
      } 
     } 
    } 
} 

Nhưng bây giờ mà tôi đang đọc thêm một chút về năng suất (trên các trang web như thế này .. .msdn dường như không đề cập đến điều này), nó dường như là một người đánh giá lười biếng, giữ cho trạng thái của populator xung quanh, với dự đoán ai đó yêu cầu giá trị tiếp theo, và sau đó chỉ chạy nó cho đến khi nó trả về giá trị tiếp theo.

Điều này có vẻ tốt trong hầu hết các trường hợp, nhưng với cuộc gọi DB, điều này nghe có vẻ hơi khó chịu. Như một ví dụ hơi khó hiểu, nếu ai đó yêu cầu một IEnumerable từ đó tôi đang cư trú từ một cuộc gọi DB, được thông qua một nửa của nó, và sau đó bị mắc kẹt trong một vòng lặp ... như xa như tôi có thể thấy kết nối DB của tôi sẽ để mở mãi mãi.

Âm thanh như yêu cầu sự cố trong một số trường hợp nếu trình lặp không kết thúc ... tôi có thiếu gì đó không?

+0

Cảm ơn đã sửa, Jon ... đó là những gì tôi nhận được để gõ một cách nhanh chóng. – Beska

+1

Miễn là người tiêu dùng gọi 'Vứt bỏ' trên IEnumerator, bạn an toàn. Xem bài đăng của tôi dưới đây. – tofi9

Trả lời

44

Hành động cân bằng: bạn có muốn buộc tất cả dữ liệu vào bộ nhớ ngay lập tức để bạn có thể giải phóng kết nối hay bạn muốn hưởng lợi từ việc truyền dữ liệu, với chi phí kết nối tất cả thời gian?

Cách tôi nhìn vào nó, quyết định đó có khả năng sẽ là tùy thuộc vào người gọi, người biết nhiều hơn về những gì họ muốn làm. Nếu bạn viết mã sử dụng một khối iterator, người gọi có thể rất dễ dàng quay rằng streaming hình thành một hình thức hoàn toàn đệm:

List<string> stuff = new List<string>(GetStuff(connectionString)); 

Nếu, mặt khác, bạn làm như đệm cho mình, không có cách người gọi có thể quay lại mô hình phát trực tuyến.

Vì vậy, tôi có thể sử dụng mô hình truyền trực tuyến và nói rõ ràng trong tài liệu hướng dẫn, và khuyên người gọi quyết định một cách thích hợp. Bạn thậm chí có thể muốn cung cấp một phương thức trợ giúp về cơ bản gọi phiên bản được truyền trực tiếp và chuyển đổi nó thành một danh sách.Tất nhiên, nếu bạn không tin tưởng người gọi của mình để đưa ra quyết định thích hợp và bạn có lý do chính đáng để tin rằng họ sẽ không bao giờ thực sự muốn truyền dữ liệu (ví dụ như nó sẽ không bao giờ quay trở lại nhiều), sau đó đi cho cách tiếp cận danh sách. Dù bằng cách nào, hãy ghi lại nó - nó có thể ảnh hưởng rất tốt đến cách sử dụng giá trị trả về.

Một tùy chọn khác để xử lý lượng lớn dữ liệu là sử dụng các đợt, tất nhiên - đó là suy nghĩ cách xa câu hỏi gốc, nhưng đó là một cách tiếp cận khác để xem xét trong trường hợp phát trực tuyến thường hấp dẫn.

+0

Sự lựa chọn bạn phác thảo là đúng, nhưng tôi nghĩ rằng trọng lượng hơn nên được đưa ra để mặc định quyết định KHÔNG phát trực tuyến. Việc rời khỏi các kết nối hoặc tài nguyên bị trói buộc sẽ dẫn đến các vấn đề về khả năng mở rộng. Hành vi mặc định phải lành mạnh và không gây ra vấn đề gì. –

8

Bạn không thiếu gì cả. Mẫu của bạn cho thấy cách KHÔNG sử dụng lợi nhuận. Thêm các mục vào danh sách, đóng kết nối và trả về danh sách. Chữ ký phương thức của bạn vẫn có thể trả về IEnumerable.

Chỉnh sửa: Điều đó nói rằng, Jon có một điểm (rất ngạc nhiên!): Có những trường hợp hiếm hoi khi phát trực tuyến thực sự là điều tốt nhất để làm từ góc độ hiệu suất. Xét cho cùng, nếu đó là 100.000 (1.000.000? 10.000.000?) Hàng chúng ta đang nói đến ở đây, bạn không muốn nạp tất cả vào bộ nhớ trước.

+1

Vâng ... Tôi chỉ làm nổi bật khía cạnh IEnumerable của nó bởi vì đó là những gì làm cho tôi nghĩ đến việc sử dụng năng suất ở nơi đầu tiên. Và cảm ơn câu trả lời ... vui mừng khi thấy rằng tôi không sủa hoàn toàn cây sai. – Beska

+0

Đừng lo lắng, vui mừng vì đã giúp đỡ. Nếu điều này đã trả lời câu hỏi của bạn, đừng quên đánh dấu câu hỏi đó là câu trả lời để nó giảm danh sách câu hỏi chưa được trả lời. –

+0

Ồ, tôi hầu như luôn luôn đánh dấu câu hỏi của mình là đã trả lời ... nhưng tôi muốn giữ lại câu hỏi này một chút, vì Jon đã cân nhắc với một quan điểm hơi khác, và tôi muốn xem nó như thế nào ngoài. – Beska

1

Không, bạn đang trên con đường đúng đắn ... năng suất sẽ khóa người đọc ... bạn có thể kiểm tra nó làm một cuộc gọi cơ sở dữ liệu trong khi gọi IEnumerable

+0

Bạn bật MARS trong chuỗi kết nối để cho phép nhiều SqlDataReaders mở tại một lần truy cập hiệu suất. Nhưng vẫn còn, mô hình này có vấn đề. – spoulson

-2

năng suất sử dụng Dont đây. mẫu của bạn là tốt.

+0

Eh? Có gì sai với câu trả lời này? –

0

Tôi đã va vào bức tường này vài lần. Truy vấn cơ sở dữ liệu SQL không dễ dàng phát trực tuyến như tệp. Thay vào đó, truy vấn chỉ nhiều như bạn nghĩ bạn sẽ cần và trả lại nó như bất kỳ vùng chứa nào bạn muốn (IList<>, DataTable, v.v.). IEnumerable sẽ không giúp bạn ở đây.

-1

Điều bạn có thể làm là sử dụng SqlDataAdapter thay vào đó và điền vào một DataTable. Một cái gì đó như thế này:

public IEnumerable<string> GetStuff(string connectionString) 
{ 
    DataTable table = new DataTable(); 
    using (SqlConnection sqlConnection = new SqlConnection(connectionString)) 
    { 
     string commandText = "GetStuff"; 
     using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection)) 
     { 
      sqlCommand.CommandType = CommandType.StoredProcedure; 
      SqlDataAdapter dataAdapter = new SqlDataAdapter(sqlCommand); 
      dataAdapter.Fill(table); 
     } 

    } 
    foreach(DataRow row in table.Rows) 
    { 
     yield return row["myImportantColumn"].ToString(); 
    } 
} 

Bằng cách này, bạn đang truy vấn mọi thứ trong một lần và ngắt kết nối ngay lập tức, nhưng bạn vẫn đang lười biếng lặp lại kết quả. Hơn nữa, người gọi phương thức này không thể đưa kết quả vào Danh sách và làm điều gì đó mà họ không nên làm.

+3

Tôi không hiểu điểm "lười biếng lặp lại kết quả" là gì trong ví dụ này. – mquander

+0

Tôi nghĩ rằng điểm là OP sẽ không bị ràng buộc vào Danh sách <> (đó là lý do tại sao anh ta đi với cách tiếp cận lợi nhuận ở nơi đầu tiên), nhưng đồng thời điều này sẽ không giữ kết nối cơ sở dữ liệu mở. – Andy

+0

Vâng, với một trong hai cách tiếp cận, tôi không cần phải gắn với Danh sách <>; Tôi có thể trả về IEnumerable <> một trong hai cách. Tôi chỉ đang nghĩ đến việc chuyển sang một cái gì đó chung chung hơn Danh sách <>, và đó là điều khiến tôi suy nghĩ về năng suất, và những hậu quả tiềm năng của nó. – Beska

10

Bạn không phải lúc nào cũng không an toàn với IEnumerable. Nếu bạn rời khỏi khung gọi GetEnumerator (đó là những gì hầu hết mọi người sẽ làm), thì bạn an toàn. Về cơ bản, bạn an toàn như sự cẩn trọng của mã bằng phương pháp của mình:

class Program 
{ 
    static void Main(string[] args) 
    { 
     // safe 
     var firstOnly = GetList().First(); 

     // safe 
     foreach (var item in GetList()) 
     { 
      if(item == "2") 
       break; 
     } 

     // safe 
     using (var enumerator = GetList().GetEnumerator()) 
     { 
      for (int i = 0; i < 2; i++) 
      { 
       enumerator.MoveNext(); 
      } 
     } 

     // unsafe 
     var enumerator2 = GetList().GetEnumerator(); 

     for (int i = 0; i < 2; i++) 
     { 
      enumerator2.MoveNext(); 
     } 
    } 

    static IEnumerable<string> GetList() 
    { 
     using (new Test()) 
     { 
      yield return "1"; 
      yield return "2"; 
      yield return "3"; 
     } 
    } 

} 

class Test : IDisposable 
{ 
    public void Dispose() 
    { 
     Console.WriteLine("dispose called"); 
    } 
} 

Cho dù bạn có thể yêu cầu rời khỏi kết nối cơ sở dữ liệu mở hay không tùy thuộc vào kiến ​​trúc của bạn. Nếu người gọi tham gia vào một giao dịch (và kết nối của bạn được tự động gia nhập), thì kết nối sẽ vẫn được mở bởi khung công tác.

Một ưu điểm khác của yield là (khi sử dụng con trỏ phía máy chủ), mã của bạn không phải đọc tất cả dữ liệu (ví dụ: 1.000 mục) từ cơ sở dữ liệu, nếu người tiêu dùng của bạn muốn thoát khỏi vòng lặp trước đó (ví dụ: sau mục thứ 10). Điều này có thể tăng tốc độ truy vấn dữ liệu. Đặc biệt trong môi trường Oracle, nơi con trỏ phía máy chủ là cách phổ biến để truy xuất dữ liệu.

+3

+1 để biết chi tiết về xử lý, nhưng tôi không nghĩ đó là mối quan tâm - tôi tin * Beska lo lắng về một số lần lặp của vòng lặp của người gọi mất một thời gian rất dài để xử lý, để kết nối cơ sở dữ liệu mở khi nó không ' t thực sự cần phải. –

+0

Cảm ơn, cập nhật với tầm nhìn của tôi về việc giữ kết nối mở. – tofi9

1

Cách duy nhất điều này sẽ gây ra sự cố là nếu người gọi lạm dụng giao thức IEnumerable<T>. Cách chính xác để sử dụng nó là gọi Dispose trên đó khi không còn cần thiết nữa.

Việc thực hiện tạo ra bởi yield return lấy Dispose cuộc gọi như một tín hiệu để thực hiện bất kỳ mở finally khối, mà trong ví dụ của bạn sẽ gọi Dispose trên các đối tượng mà bạn đã tạo ra trong using báo cáo.

Có một số tính năng ngôn ngữ (cụ thể là foreach) giúp bạn dễ dàng sử dụng IEnumerable<T> chính xác.

+0

Nếu bạn có thể trang web một số tài liệu về cách Vứt bỏ được sử dụng bởi/trong các điều tra viên được thực hiện thông qua các từ khóa trả về lợi nhuận thì sẽ hữu ích. – jpierson

6

Một cách lưu ý rằng cách tiếp cận IEnumerable<T>về cơ bản là nhà cung cấp LINQ (LINQ-to-SQL, LINQ-to-Entities) làm gì để kiếm sống. Cách tiếp cận này có lợi thế, như Jon nói. Tuy nhiên, có những vấn đề nhất định quá - đặc biệt (đối với tôi) về (sự kết hợp của) tách | trừu tượng.

Những gì tôi có ý nghĩa ở đây là:

  • trong một kịch bản MVC (ví dụ) mà bạn muốn bạn "có được dữ liệu" bước để thực sự có được dữ liệu, do đó bạn có thể kiểm tra nó hoạt động ở điều khiển, không phải là xem (mà không cần phải nhớ để gọi .ToList() vv)
  • bạn không thể đảm bảo rằng một thi Dal sẽ thể stream dữ liệu (ví dụ, một cuộc gọi POX/WSE/SOAP có thể chúng tôi hồ sơ luồng trực tiếp); và bạn không nhất thiết muốn làm cho hành vi gây nhầm lẫn khác nhau (tức là kết nối vẫn mở trong khi lặp lại với một lần triển khai và đóng cho một lần khác)

Quan hệ này theo một chút với suy nghĩ của tôi ở đây: Pragmatic LINQ.

Nhưng tôi nên nhấn mạnh - có những thời điểm nhất định khi phát trực tuyến là rất mong muốn. Nó không phải là một điều "luôn luôn vs không bao giờ" đơn giản ...

0

Bạn luôn có thể sử dụng một chuỗi riêng biệt để đệm dữ liệu (có thể là một hàng đợi) trong khi cũng làm một yeild để trả lại dữ liệu. Khi người dùng yêu cầu dữ liệu (được trả về qua yeild), một mục sẽ bị xóa khỏi hàng đợi. Dữ liệu cũng được liên tục được thêm vào hàng đợi thông qua chuỗi riêng biệt. Bằng cách đó, nếu người dùng yêu cầu dữ liệu đủ nhanh, hàng đợi không bao giờ hết và bạn không phải lo lắng về vấn đề bộ nhớ. Nếu không, thì hàng đợi sẽ lấp đầy, có thể không quá tệ. Nếu có một số hạn chế mà bạn muốn áp đặt vào bộ nhớ, bạn có thể thực thi kích thước hàng đợi tối đa (tại thời điểm đó, luồng khác sẽ đợi các mục bị xóa trước khi thêm vào hàng đợi). Đương nhiên, bạn sẽ muốn chắc chắn rằng bạn xử lý các tài nguyên (tức là hàng đợi) một cách chính xác giữa hai luồng.

Thay vào đó, bạn có thể buộc người dùng chuyển vào boolean để cho biết liệu dữ liệu có được đệm hay không. Nếu đúng, dữ liệu được đệm và kết nối sẽ bị đóng càng sớm càng tốt. Nếu sai, dữ liệu không được đệm và kết nối cơ sở dữ liệu vẫn mở miễn là người dùng cần nó. Việc có tham số boolean buộc người dùng thực hiện lựa chọn, điều này đảm bảo họ biết về sự cố.

3

cách Hơi ngắn gọn hơn để buộc đánh giá của iterator:

using System.Linq; 

//... 

var stuff = GetStuff(connectionString).ToList();