10

我有一段用流利的語法編寫的軟件。方法鏈有一個明確的「結束」,在此之前,代碼中實際上沒有任何有用的東西(比如NBuilder,或者Linq-to-SQL的查詢生成在我們使用ToList()遍歷我們的對象之前並不實際觸及數據庫) )。如何在C#中強制使用方法的返回值?

我遇到的問題是其他開發人員對正確使用代碼存在困惑。他們忽略稱之爲「結局」的方法(因此從來沒有真正「做任何事情」)!

我對執行我的一些方法的返回值的使用感興趣,以便我們永遠不要「調用」Finalize()「或」Save()「方法來」結束鏈「工作。

考慮下面的代碼:

//The "factory" class the user will be dealing with 
public class FluentClass 
{ 
    //The entry point for this software 
    public IntermediateClass<T> Init<T>() 
    { 
     return new IntermediateClass<T>(); 
    } 
} 

//The class that actually does the work 
public class IntermediateClass<T> 
{ 
    private List<T> _values; 

    //The user cannot call this constructor 
    internal IntermediateClass<T>() 
    { 
     _values = new List<T>(); 
    } 

    //Once generated, they can call "setup" methods such as this 
    public IntermediateClass<T> With(T value) 
    { 
     var instance = new IntermediateClass<T>() { _values = _values }; 
     instance._values.Add(value); 
     return instance; 
    } 

    //Picture "lazy loading" - you have to call this method to 
    //actually do anything worthwhile 
    public void Save() 
    { 
     var itemCount = _values.Count(); 
     . . . //save to database, write a log, do some real work 
    } 
} 

正如你所看到的,這個代碼的正確用法是這樣的:

new FluentClass().Init<int>().With(-1).With(300).With(42).Save(); 

的問題是,人們正在使用這種方式(思維它達到了與上面相同):

new FluentClass().Init<int>().With(-1).With(300).With(42); 

這麼普遍的是這個問題,完全好的意圖,另一位開發人員實際上曾改變過「Init」方法的名稱,以表明該方法正在做軟件的「真正的工作」。

像這樣的邏輯錯誤是非常難以發現的,當然,它編譯,因爲它是完全可以接受的調用返回值的方法,只是「假裝」它返回無效。 Visual Studio不關心你是否這樣做;你的軟件仍然會編譯和運行(儘管在某些情況下我相信它會引發警告)。當然,這是一個很棒的功能。設想一個簡單的「InsertToDatabase」方法,它將新行的ID作爲整數返回 - 很容易看出有些情況下我們需要該ID,還有一些情況下我們可以不用它。

就這款軟件而言,絕對沒有任何理由避開方法鏈末尾的「保存」功能。這是一個非常專業化的工具,唯一的收穫來自最後一步。

如果他們調用「With()」而不是「Save()」,我希望某人的軟件在編譯器級別上失敗

這似乎是一個傳統手段不可能的任務 - 但這就是爲什麼我來找你們。是否有一個屬性,我可以用來防止一個方法被「鑄造成無效」或一些這樣的?

注:已經被建議我實現這一目標的另一種方法是編寫了一套單元測試來執行這一規則,並使用類似http://www.testdriven.net東西,它們綁定到編譯器。這是一個可以接受的解決方案,但我希望有更優雅的東西。

+0

+1 [return-value] ;-) – 2011-03-23 20:14:11

+0

在黑暗中拍攝的總數,我不知道這是可能的,但也許.NET代碼合同?我認爲它們被內置到.NET 4.0中 – Roly 2011-03-23 20:14:23

+1

Code Contracts沒有屬性或斷言,它需要檢查返回值。 – user7116 2011-03-23 20:46:59

回答

1

經過極大的思考和反覆試驗,事實證明,從Finalize()方法拋出一個異常對我來說不起作用。顯然,你根本無法做到這一點;異常會被吃掉,因爲垃圾收集操作是非確定性的。我無法讓軟件從析構函數中自動調用Dispose()。傑克V.的評論很好地解釋了這一點;這裏是他發佈的鏈接,以備不時之需/強調:

The difference between a destructor and a finalizer?

更改語法使用的回調是一個聰明的辦法,使行爲萬無一失,但商定的語法是固定的,和我有與它一起工作。我們的公司都是關於流暢的方法鏈。說實話,我也是「out參數」解決方案的粉絲,但是底線是方法簽名根本無法改變。

關於我的具體問題有用的信息包括事實證明我的軟件是只有永遠要運行的一套單元測試的一部分 - 所以效率不是問題。

我最終做的是使用Mono.Cecil來反映調用程序集(調用我的軟件的代碼)。請注意,System.Reflection不足以達到我的目的,因爲它無法精確定位方法引用,但我仍然需要(?)使用它來獲取「調用程序集」本身(Mono.Cecil仍然沒有記錄,所以可能我只是需要得到更多熟悉它,以便與完全的System.Reflection做了;這還有待觀察....)

我把Mono.Cecil能代碼在的init()方法,結構現在看起來像這樣:

public IntermediateClass<T> Init<T>() 
{ 
    ValidateUsage(Assembly.GetCallingAssembly()); 
    return new IntermediateClass<T>(); 
} 

void ValidateUsage(Assembly assembly) 
{ 
    // 1) Use Mono.Cecil to inspect the codebase inside the assembly 
    var assemblyLocation = assembly.CodeBase.Replace("file:///", ""); 
    var monoCecilAssembly = AssemblyFactory.GetAssembly(assemblyLocation); 

    // 2) Retrieve the list of Instructions in the calling method 
    var methods = monoCecilAssembly.Modules...Types...Methods...Instructions 
    // (It's a little more complicated than that... 
    // if anybody would like more specific information on how I got this, 
    // let me know... I just didn't want to clutter up this post) 

    // 3) Those instructions refer to OpCodes and Operands.... 
    // Defining "invalid method" as a method that calls "Init" but not "Save" 
    var methodCallingInit = method.Body.Instructions.Any 
     (instruction => instruction.OpCode.Name.Equals("callvirt") 
        && instruction.Operand is IMethodReference 
        && instruction.Operand.ToString.Equals(INITMETHODSIGNATURE); 

    var methodNotCallingSave = !method.Body.Instructions.Any 
     (instruction => instruction.OpCode.Name.Equals("callvirt") 
        && instruction.Operand is IMethodReference 
        && instruction.Operand.ToString.Equals(SAVEMETHODSIGNATURE); 

    var methodInvalid = methodCallingInit && methodNotCallingSave; 

    // Note: this is partially pseudocode; 
    // It doesn't 100% faithfully represent either Mono.Cecil's syntax or my own 
    // There are actually a lot of annoying casts involved, omitted for sanity 

    // 4) Obviously, if the method is invalid, throw 
    if (methodInvalid) 
    { 
     throw new Exception(String.Format("Bad developer! BAD! {0}", method.Name)); 
    } 
} 

相信我,實際的代碼甚至ugli呃看起來比我的僞代碼....:-)

但是Mono.Cecil可能是我最喜歡的玩具。

我現在有一個方法,拒絕運行它的主體,除非調用代碼「承諾」以後也調用第二種方法。這就像一種奇怪的代碼契約。我實際上正在考慮製作這種通用的,可重複使用的。你們有沒有用這種東西?說,如果它是一個屬性?

9

我不知道在編譯器級別執行此操作的方法。它經常被要求實現IDisposable的對象,但不是真正可執行的。

但是,一個可能的選擇是幫助建立你的課程,在DEBUG只有,有一個終結日誌/拋出/等。如果Save()從未被調用過。這可以幫助您在調試過程中發現這些運行時問題,而不是依靠搜索代碼等。

但是,請確保在發佈模式下不使用此操作,因爲這會導致性能開銷一個不必要的終結器在GC性能上非常糟糕。

+0

+1,對於Debug中的聰明的終結器技巧,以前用它來提醒自己關於代碼中的關鍵託管本機故障。 – user7116 2011-03-23 20:13:36

+0

我越想越想越喜歡它。尤其是因爲,這裏是我在原帖中沒有提到的踢球者,我的軟件**本身**僅用於測試;我甚至不必指定「僅調試」,因爲代碼已經在主類庫中沒有位置。我很快就會嘗試一下,看看我能否實現它,但經過一段時間的討論後,我懷疑你已經釘了它(至少對我而言)。謝謝! – Bokonon 2011-03-24 19:16:27

4

您可能需要特定的方法使用回調,像這樣:

new FluentClass().Init<int>(x => 
{ 
    x.Save(y => 
    { 
     y.With(-1), 
     y.With(300) 
    }); 
}); 

的與方法返回一些特定對象,並獲取該對象的唯一途徑是通過調用x.Save(),這本身有一個回調,可以讓你設置你的不確定數量的語句。所以初始化需要這樣的:

public T Init<T>(Func<MyInitInputType, MySaveResultType> initSetup) 
0

在調試模式下實現了IDisposable,你可以設置一個計時器1秒後,將拋出一個異常,如果resultmethod尚未調用旁邊。

+0

如果系統過載,一秒可能會危險短路。您可以在終結器中引發異常。這兩種技術都會增加開銷,但計時器可能更昂貴。 – Qwertie 2011-03-23 20:21:24

1

如果你這麼做InitWith不返回FluentClass類型的對象?讓他們回來,例如,UninitializedFluentClass包裝FluentClass對象。然後在UnitializedFluentClass對象上調用.Save(0對象在包裝的FluentClass對象上調用它並返回它。如果他們不打電話Save他們沒有得到一個FluentClass對象。

0

使用out參數!必須使用所有的out

編輯:我不知道它會幫助,但... 它會打破流利的語法。

1

我能想到三個幾個解決方案,不太理想。

  1. AIUI你想要的是當臨時變量超出範圍,這就是所謂的功能(如,當它成爲可進行垃圾回收,但可能不會被垃圾收集一段時間未定)。 (見:The difference between a destructor and a finalizer?)這個假設的函數會說「如果你在這個對象中構造了一個查詢但沒有調用保存,則產生一個錯誤」。 C++/CLI調用這個RAII,並且在C++/CLI中,當對象不再被使用時存在「析構函數」的概念,並且在最終被垃圾收集時調用「終結器」。非常容易混淆的是,C#只有一個所謂的析構函數,但這只是只有被垃圾收集器調用(這對於框架更早調用它是有效的,就好像它是立即部分清除對象一樣,但AFAIK它不會不要做那樣的事情)。所以你想要的是一個C++/CLI析構函數。不幸的是,AIUI映射到IDisposable的概念,該概念公開了一個dispose()方法,可以在調用C++/CLI析構函數時調用,或者調用C#析構函數時調用 - 但是AIUI仍然需要調用「dispose 「手動,哪個打敗了這一點?

  2. 稍微重構接口以更準確地傳達概念。調用類似「prepareQuery」或「AAA」或「initRememberToCallSaveOrThisWontDoAnything」的init函數。 (最後一個是誇張的,但可能有必要表明這一點)。

  3. 這是一個比技術問題更多的社會問題。界面應該讓做事容易,但程序員必須知道如何使用代碼!讓所有的程序員在一起。簡單地解釋一下這個簡單的事實。如果有必要,讓他們在一張紙上簽名,表明他們理解,如果他們故意繼續編寫不做任何標記的代碼,那麼對公司來說就會更糟,而且會被解僱。

  4. 操作員鏈接的方式,例如。讓每個intermediateClass函數組裝一個包含所有參數的聚合中間類對象(你大部分都是這樣做的(它已經是(?)),但需要類初始化函數來將它作爲參數,而不是讓它們作爲參數然後你可以保存並且其他函數返回兩個不同的類類型(基本上具有相同的內容),並且init只接受一個正確類型的類。

,它是仍然問題表明,無論是你的同事需要一個有益的提醒,或者他們寧願低於平均水平,或接口不是很清楚(也許是其完美的,但事實作者沒有意識到,如果你只是用它來傳遞而不是去了解它)或者你自己誤解了這種情況。一個技術解決方案會很好,但你應該考慮問題的原因以及如何更清楚地溝通,可能會問某位老人的意見。