今天在 Code Review 的時候抓到一支程式臭蟲,這個功能是某 ASP.NET MVC 專案中的一個匯出下載檔案功能,該功能的 Action 已經寫完很久了,之前測試都正常,但是最近卻突然爆發錯誤,經程式追查之下才發現原來是程式在執行之前就已經把資源給釋放掉了,導致 ASP.NET MVC 無法正確回應資料到用戶端,以致於發生【System.InvalidOperationException: 作業無效。已經關閉連接。】的錯誤。
問問有開發經驗的你,看到這段程式碼,你會不會很自然的加上 using 陳述式來避免記憶體洩漏呢?
public ActionResult Download(Guid uid)
{
Stream s = new MemoryStream(Encoding.UTF8.GetBytes(GetTextByUid(uid)));
return File(s, "application/ms-excel", "Export.xls");
}
我相信很多人會很直覺的替上述的這個 s 物件加上 using 陳述式,而我同事就是看到這段 Code 就直覺的加上去了,變成以下的程式碼 (此乃錯誤的寫法,容後在述):
public ActionResult Download(Guid? uid)
{
using (Stream s = new MemoryStream(ASCIIEncoding.GetEncoding("big5")
.GetBytes(GetUTF8TextByUid(uid))))
{
return File(s, "application/ms-excel", "Export.xls");
}
}
要瞭解這段 Code 會發生錯誤的原因,就必須先瞭解 ASP.NET MVC 的執行生命週期,如下圖是我在【ASP.NET MVC 2 開發實戰】一書中在從「第6章 Controller 相關技術」節錄的一段 Controller 在執行動作過濾器 (Action Filters) 的流程圖,從此圖應該很容易能理解為何以上程式會發生【作業無效。已經關閉連接。】的錯誤:
當我們的 Action 在執行時,其實只是為了取得一個 ActionResult 物件回來而已,當 ASP.NET MVC 得到 ActionResult 物件之後就會進一步執行 ActionResult 物件的 ExecuteResult 方法。
以我們上述的程式為例,由於傳入的是一個 MemoryStream 物件,所以 Controller 透過 Controller 類別的 File(Stream, String, String) 輔助方法回傳的是 FileStreamResult 型別,之後才會走道「執行檢視」的動作,而執行檢視就是執行 FileStreamResult 的 ExecuteResult 方法,由於 FileStreamResult 繼承自 System.Web.Mvc.FileResult 類別,而該類別又定義了一個 WriteFile 抽象方法,我們可以看一下在 FileStreamResult 裡的 WriteFile 方法是如何寫的:
protected override void WriteFile(HttpResponseBase response)
{
Stream outputStream = response.OutputStream;
using (this.FileStream)
{
byte[] buffer = new byte[0x1000];
while (true)
{
int count = this.FileStream.Read(buffer, 0, 0x1000);
if (count == 0)
{
return;
}
outputStream.Write(buffer, 0, count);
}
}
}
在這裡讀取到的 this.FileStream 就是我們在 Action 傳入的 MemoryStream 物件,在這裡會使用 using 陳述式來確保資料在寫入 Response.OutputStream 之後會自動清除記憶體空間。
結論
- 使用 FileStreamResult 時不能使用 using 陳述式,否則執行檢視時就會發生失敗。
- 「執行動作」與「執行檢視」是分開的,回應任何資料到用戶端是「執行檢視」在做的事,所以在執行動作時,不應該把傳進 View 的資料給 Dispose 掉。