我相信,每個開發人員都希望寫出優質的代碼。不會有人希望所創建的系統錯誤百出、不可 維護、需要沒完沒了地添加功能或解決問題。我曾經參與過一些項目,感覺如同總是處於混 亂狀態,毫無樂趣可言。因方法不一致而導致難以理解基本代碼,從而浪費了很多時間。我 希望在所從事的項目中,層次經過良好的定義、單元測試豐富充足並且生成服務器持續運行 以確保所有情況正常。此類項目通常會制訂由開發人員嚴格遵守的一組准則和標准。
我已見過有團隊制訂了此類准則。可能由於已將某些方法視為有疑問,因此開發人員應避免 在其代碼中調用這些方法。或者,他們可能要確保代碼在某些情況下遵循相同的模式。例如 ,項目中的開發人員可能會同意如下准則:
任何人都不應使用當地 DateTime 值。所有 DateTime 值都應采用協調世界時 (UTC)。
應避免使用在值類型上找到的 Parse 方法(如 int.Parse);應改用 int.TryParse。
所創建的所有實體類都應支持等同性,即都應重寫 Equals 和 GetHashCode 並實現 == 和 != 運算符以及 IEquatable<T> 接口。
我確信您已在某個標准文檔中看到過類似的規則。達成一致是件好事情,如果每個人都遵 循同一做法,那麼維護代碼就會變得更輕松。其中的竅門在於用一種可重用、有效的方式向 團隊中的所有開發人員快速地傳達這些知識。
代碼審閱是一種發現潛在問題的方式。 旁觀者清,對於給定實現視角新穎的人員經常能發現原作者意識不到的問題。讓另一方審閱 您的開發工作可能大有裨益,在審閱者不熟悉此項工作時尤為如此。但是,仍然很容易在開 發過程中忽視一些問題。此外,代碼審閱耗時漫長 - 開發人員不得不花費數小時審閱代碼並 與其他開發人員開會,交流雙方發現的問題。我需要這個過程更加快捷。我希望在出錯後盡 可能快地得知。盡快發現故障從長遠來看可節省時間和資金。
Visual Studio 中有多 種工具(如代碼分析)可分析您的代碼並向您通知潛在的問題。代碼分析有許多預定義規則 ,可揭示未銷毀對象或未使用方法參數等情況。遺憾的是,直到編譯完畢後,代碼分析才運 行其規則,而這可不夠快!我希望根據我的標准在鍵入的新代碼中出錯時盡快了解這一情況 。盡可能快地發現故障是件好事情。既可節省時間(並因此節省資金),又可避免交付將來 可能會導致無數問題的代碼。為此,我需要能夠將我的規則編為代碼,以使這些規則在我鍵 入時得以執行,而這正是 Microsoft“Roslyn”CTP 發揮的作用。
.NET 開發人員可用於分析其代碼的最佳工具之一就是編譯器。 它了解如何從語法上將代碼分析成各種標記,然後根據這些標記在代碼中的位置將其變為有 意義的內容。為此,編譯器以其輸出的形式將一個程序集發送到磁盤。可在編譯管道中搜集 到許多來之不易的知識,而您樂於能夠使用這些知識,但是,唉,在 .NET 環境中做不到這 一點,因為 C# 和 Visual Basic 編譯器不提供 API 供您訪問。Roslyn 使這一情況得到改 觀。Roslyn 是一組編譯器 API,通過它可靠完整地訪問編譯器經歷的每個階段。圖 1 是 Roslyn 當前在編譯器進程中提供的不同階段的圖。
圖 1:Roslyn 編譯器管道
盡管 Roslyn 仍為 CTP 模式(本文中使用的是 2012 年 9 月版),但還 是值得花時間研究其程序集中提供的功能以及了解通過 Roslyn 可做到的事情。首先最好著 眼於其腳本功能。通過 Roslyn,現在可為 C# 和 Visual Basic 代碼編寫腳本。即 Roslyn 中提供一個腳本引擎,可向該引擎中輸入代碼段。通過 ScriptEngine 類處理此功能。以下 是一個示例,其中演示此引擎可怎樣返回當前的 DateTime 值:
class Program { static void Main(string[] args) { var engine = new ScriptEngine(); engine.ImportNamespace("System"); var session = engine.CreateSession(); Console.Out.WriteLine(session.Execute<string>( "DateTime.Now.ToString();")); } }
在這段代碼中,引擎創建並導入 System 命名空間,因此 Roslyn 將可分析出 DateTime 的含義。創建會話後,它只需調用 Execute,然後 Roslyn 將分析給定的代碼。如 果它可正確地分析這段代碼,則它將運行這段代碼並返回結果。
使 C# 成為一種腳本語言是一個強大的概念。雖然 Roslyn 仍處於 CTP 模式,但人們使 用其少量功能即創造出令人驚歎的項目和框架,如 scriptcs (scriptcs.net)。不過,我認為 Roslyn 真正的亮點在於 可創建 Visual Studio 擴展以在編寫代碼時告知問題。在前一代碼段中,我使用了 DateTime.Now。如果我所從事的項目實施了我在本文開頭以項目符號列出的第一點,那麼我 將違反該標准。以後我將探討可怎樣使用 Roslyn 實施這項規則。但在我這樣做之前,我將 介紹編譯的第一個階段: 分析代碼以獲得標記。
當 Roslyn 分析一行代碼後,它返回一個不可變的語法樹。此樹包含有關給定代碼的任何 信息,包括空格和制表符等細枝末節。即使這段代碼有錯,代碼樹仍將盡可能嘗試向您提供 盡可能多的信息。
這固然很好,但您是否明白相關信息在樹中何處?當前,有關 Roslyn 的文檔還很少,由 於它仍處於 CTP,這一點可以理解。可使用 Roslyn 論壇張貼問題 (bit.ly/16qNf7w),或在 Twitter 上的推文中使用 #RoslynCTP 標簽。在安裝文件時,還有一個名為 SyntaxVisualizerExtension 的示例,它 是 Visual Studio 的一個擴展。在 IDE 中鍵入代碼時,可視化工具自動隨樹的當前版本一 起更新。
要搞清您在尋找什麼以及如何在樹中導航,此工具不可或缺。在對 DateTime 類使用 .Now 時,我搞清了我需要找到 MemberAccessExpression(或者更精確地說,需要找到 基於 MemberAccessExpressionSyntax 的對象),其中最後一個 IdentifierName 值等 於 Now。當然,這適用於輸入“var now = DateTime.Now;”的簡單情況,而您可能會在 DateTime 前面放置“System.”,或使用“using DT = System.DateTime;”;此外,系統中 的其他類中可能有一個名為 Now 的屬性。必須正確處理所有這些情況。
既然知道要查找什麼,那麼需要創建一個基於 Roslyn 的 Visual Studio 擴展以查尋 DateTime.Now 屬性的使用情況。為此,只需在 Visual Studio 中的“Roslyn”選項下選擇 “代碼問題”模板。
這樣做後,將得到一個項目,其中包含一個名為 CodeIssueProvider 的類。 此類實現 ICodeIssueProvider 接口,但您不必實現其四個成員中的每個。在這種情況 下,僅使用處理 SyntaxNode 類型的成員;而其他成員可能會引發 NotImplementedException。通過指定要用相應 GetIssues 方法處理的語法節點類型,實現 SyntaxNodeTypes 屬性。如上一個示例提到的那樣,MemberAccessExpressionSyntax 類型才 是重要的類型。以下代碼段演示如何實現 SyntaxNodeTypes:
public IEnumerable<Type> SyntaxNodeTypes { get { return new[] { typeof(MemberAccessExpressionSyntax) }; } }
這是 Roslyn 的一項優化。通過讓您更詳細地指定您要檢查的類型,Roslyn 不必 對每種語法類型都調用 GetIssues 方法。如果 Roslyn 未配備此篩選機制並對樹中的每個節 點調用了您的代碼提供程序,則性能將令人震驚。
現在只剩下實現 GetIssues,以使其將僅報告 Now 屬性的使用情況。如同我在前一 節提到的那樣,您只想查找已對 DateTime 使用 Now 的情況。使用標記時,除了文本之外沒 有多少信息。但是,Roslyn 提供一個所謂的語義模型,後者可提供有關被檢查代碼的更多信 息。圖 2 中的代碼演示可怎樣查找 DateTime.Now 的使用情況。
圖 2:查找 DateTime.Now 使用情況
public IEnumerable<CodeIssue> GetIssues( IDocument document, CommonSyntaxNode node, CancellationToken cancellationToken) { var memberNode = node as MemberAccessExpressionSyntax; if (memberNode.OperatorToken.Kind == SyntaxKind.DotToken && memberNode.Name.Identifier.ValueText == "Now") { var symbol = document.GetSemanticModel() .GetSymbolInfo(memberNode.Name).Symbol; if (symbol != null && symbol.ContainingType.ToDisplayString() == Values.ExpectedContainingDateTimeTypeDisplayString && symbol.ContainingAssembly.ToDisplayString().Contains( Values.ExpectedContainingAssemblyDisplayString)) { return new [] { new CodeIssue(CodeIssueKind.Error, memberNode.Name.Span, "Do not use DateTime.Now", new ChangeNowToUtcNowCodeAction(document, memberNode))}; } } return null; }
您將注意到未使用 cancellationToken 參數,並且本文附帶的示例項目中也未使 用它。這是一個慎重的選擇,因為向示例中放置不斷檢查標記以了解處理是否應停止的代碼 可能會分散精力。但是,如果將創建適合生產環境的基於 Roslyn 的擴展,則應確保經常檢 查標記,如果標記處於已取消狀態,則停止。
一旦判斷成員訪問表達式正在嘗試獲得一個名為 Now 的屬性,即可獲取該標記的符號信 息。為此,請獲得樹的語義模型,然後通過 Symbol 屬性獲得對基於 ISymbol 的對象的引用 。然後,只需獲得包含類型,然後查看其名稱是否為 System.DateTime,以及其包含程序集 名稱是否包括 mscorlib。如果是這樣,則這就是所尋找的問題,可通過返回一個 CodeIssue 對象,將其標為錯誤。
到現在為止進展順利,因為您將在 IDE 中 Now 文本的下方看到一個紅色彎曲的錯誤行。 但這還不夠深入。編譯器告知代碼中缺少冒號或大括號顯然不錯。獲得錯誤信息比毫無提示 好,而對於簡單的錯誤,通常可比較輕松地根據錯誤消息糾正這些錯誤。但是,如果工具本 身即可找出錯誤豈不更好?我喜歡在出錯時收到通知,而當錯誤消息給出解釋可怎樣解決問 題的詳細信息時,我會更加高興。而如果可自動處理該信息,使某個工具可替我解決問題, 那麼我在問題上所用的時間就會更少。節省的時間越多越好。
因此,您在上一個代碼段中看到對一個名為 ChangeNowToUtcNowCodeAction 類的引用。 此類實現 ICodeAction 接口,而其作用是將 Now 改為 UtcNow。必須實現的主方法稱為 GetEdit。在本例中,需要將 MemberAccessExpressionSyntax 對象中的 Name 標記改為一個 新標記。如以下代碼所示,進行此替換比較輕松:
public CodeActionEdit GetEdit(CancellationToken cancellationToken) { var nameNode = this. nowNode.Name; var utcNowNode = Syntax.IdentifierName("UtcNow"); var rootNode = this.document. GetSyntaxRoot(cancellationToken); var newRootNode = rootNode.ReplaceNode(nameNode, utcNowNode); return new CodeActionEdit( document.UpdateSyntaxRoot(newRootNode)); }
只需創建一個具有 UtcNow 文本的新標識符,然後通過 ReplaceNode 將 Now 標 記替換為這個新標識符。請記住,語法樹是不可變的,因此請勿更改當前的文檔樹。新建一 個樹,然後從方法調用中返回該樹。
這段代碼就位後,在 Visual Studio 中按 F5 即可測試它。此操作將啟動一個新的 Visual Studio 實例,其中自動裝有擴展。
以上是個好的開頭,但還有更多情況需要處理。DateTime 類定義了許多構造函數,而這 可能導致問題。有兩種情況尤其要注意:
構造函數不能采用 DateTimeKind 枚舉類型作為其某個參數,這意味著所得的 DateTime 將處於 Unspecified 狀態。
構造函數可對其某個參數采用 DateTimeKind 值,這意味著可指定 Utc 以外的枚舉值。
可編寫代碼以同時查找這兩個條件。但是,我將僅創建用於後者的代碼操作。
圖 3 列出基於 ICodeIssue 的類中 GetIssues 方法的代碼,該方法 將查找不正確的 DateTime 構造函數調用。
圖 3:查找不正確的 DateTime 構造函數調用
public IEnumerable<CodeIssue> GetIssues( IDocument document, CommonSyntaxNode node, CancellationToken cancellationToken) { var creationNode = node as ObjectCreationExpressionSyntax; var creationNameNode = creationNode.Type as IdentifierNameSyntax; if (creationNameNode != null && creationNameNode.Identifier.ValueText == "DateTime") { var model = document.GetSemanticModel(); var creationSymbol = model.GetSymbolInfo(creationNode).Symbol; if (creationSymbol != null && creationSymbol.ContainingType.ToDisplayString() == Values.ExpectedContainingDateTimeTypeDisplayString && creationSymbol.ContainingAssembly.ToDisplayString().Contains( Values.ExpectedContainingAssemblyDisplayString)) { var argument = FindingNewDateTimeCodeIssueProvider .GetInvalidArgument(creationNode, model); if (argument != null) { if (argument.Item2.Name == "Local" || argument.Item2.Name == "Unspecified") { return new [] { new CodeIssue(CodeIssueKind.Error, argument.Item1.Span, "Do not use DateTimeKind.Local or DateTimeKind.Unspecified", new ChangeDateTimeKindToUtcCodeAction(document, argument.Item1)) }; } } else { return new [] { new CodeIssue(CodeIssueKind.Error, creationNode.Span, "You must use a DateTime constuctor that takes a DateTimeKind") }; } } } return null; }
查看本欄目
它與另一個問題非常類似。得知構造函數來自 DateTime 後,即需要計算參數值 。(我馬上就會介紹 GetInvalidArgument 的作用。) 如果找到 DateTimeKind 類型的參數 ,並且它未指定 Utc,則有問題。否則,即得知所用構造函數的 DateTime 將不是 Utc 格式 ,因此這又是一個要報告的問題。圖 4 展示 GetInvalidArgument 的外 觀。
圖 4:GetInvalidArgument 方法
private static Tuple<ArgumentSyntax, ISymbol> GetInvalidArgument( ObjectCreationExpressionSyntax creationToken, ISemanticModel model) { foreach (var argument in creationToken.ArgumentList.Arguments) { if (argument.Expression is MemberAccessExpressionSyntax) { var argumentSymbolNode = model .GetSymbolInfo(argument.Expression).Symbol; if (argumentSymbolNode.ContainingType.ToDisplayString() == Values.ExpectedContainingDateTimeKindTypeDisplayString) { return new Tuple<ArgumentSyntax,ISymbol>(argument, argumentSymbolNode); } } } return null; }
此搜索與其他搜索非常類似。如果參數類型為 DateTimeKind,則得知某個參數值 可能無效。為了糾正該參數,這段代碼幾乎與您看到的第一個代碼操作完全相同,因此我在 此不再贅述。現在,如果其他開發人員嘗試規避 DateTime.Now 限制,則您可將其抓個現行 ,並可糾正構造函數調用!
想到將用 Roslyn 創建的所有工具,感覺特棒,但工作還是需要完成。我認為 Roslyn 現 在最大的一個不利因素是缺少文檔。網上和安裝文件中有許多好的示例,但 Roslyn 是一個 龐大的 API 集,因此難以搞清從何處開始以及使用什麼完成特定任務。必須研究一段時間才 能搞清要使用的正確調用,這種現象並不少見。而令人鼓舞的一面是,我在 Roslyn 中編程 時經常能夠碰到一種現象,起初看起來比較復雜,但最後代碼只有不到 100 或 200 行。
我認為隨著 Roslyn 發布日期的臨近,它周圍的一切都會得到改善。我還堅信,Roslyn 擁有鞏固 .NET 生態系統中許多框架和工具的潛力。我並未看到每個 .NET 開發人員在日常 直接使用 Roslyn API,但您最終很有可能使用在某個級別使用 Roslyn 的文件。而這正是我 鼓勵您深入研究 Roslyn 並了解運行原理的原因。能夠將習慣用語編為團隊中每個開發人員 可利用的可重用規則可幫助每個人快速地產出質量更高的代碼。
下載代碼示例