2017-02-09 35 views
41

我試圖找出爲什麼我的單元測試失敗(下面的第三個斷言):路線插值發出

var date = new DateTime(2017, 1, 1, 1, 0, 0); 

var formatted = "{countdown|" + date.ToString("o") + "}"; 

//Works 
Assert.AreEqual(date.ToString("o"), $"{date:o}"); 
//Works 
Assert.AreEqual(formatted, $"{{countdown|{date.ToString("o")}}}"); 
//This one fails 
Assert.AreEqual(formatted, $"{{countdown|{date:o}}}"); 

AFAIK,這應能正常工作,但現在看來,它不通過格式化正確的參數,它顯示爲代碼只是{countdown|o}。任何想法爲什麼這是失敗?

+0

它似乎(雖然我討厭說)它是一個編譯器錯誤。 – DavidG

+5

@DavidG:可能是一個編譯器錯誤,或者可能是底層格式化庫中的一個錯誤,但我同意這裏的東西味道不好。至少應該調查。 –

+0

它似乎是關閉關閉插值支架的方式。使用上面的代碼時,外括號關閉插值'{{countdown | ** {** date:o}} **} **',括號之間的空格會導致它計算到內括號'{{countdown | **:{**日期:○**} ** _}}'。 – Equalsk

回答

21

這一行

Assert.AreEqual(formatted, $"{{countdown|{date:o}}}"); 

的問題是,你有3個彎引號後進行轉義變量的format string,並開始從左至右逃脫,因此它把第2個彎引號爲格式字符串的一部分和第三個引用作爲結束字符串。

因此它將o轉換成o}並且它無法對其進行插值。

這應該工作

Assert.AreEqual(formatted, $"{{countdown|{date:o}"+"}"); 

注意,簡單$"{date}}}"(即3個變量沒有一個format string後捲髮)不工作,因爲它認識到,第一個花報價是關閉一個,而解釋:打破正確的右括號標識後格式說明符的格式說明符。

爲了證明格式字符串逃過像繩子,考慮以下

$"{date:\x6f}" 

$"{date:o}" 

最後被處理,這是完全可能的,雙逃脫捲曲引號是自定義日期格式的一部分,因此編譯器的行爲是絕對合理的。再次,一個具體的例子

$"{date:MMM}}dd}}yyy}" // it's a valid feb}09}2017 

解析是一個基於表達式語法規則的正式過程,不能通過只看一眼就可以完成。

+0

大括號的數目是不是'$「{{{date:o}}}的問題'''仍然產生無效輸出。」 – DavidG

+1

@DavidG * 3捲起來的引號*在格式字符串'o' * do *產生無效輸出後,就像我在我的答案中寫的那樣 - 現在**正確** – 2017-02-09 18:13:06

+2

很好的分析。我很驚訝地發現,詞法分析器將格式說明符後面的'}}}作爲一個轉義的'}'後跟一個有意義的'}'。我會天真地希望規則是「一旦你找到一個有意義的'{',解析格式化的表達式,直到找到匹配的'}',然後恢復常規字符串lexing。下次我看到Neal時,我會問什麼是led –

2

問題似乎是在使用字符串插值時插入圓括號,您需要通過複製它來轉義它。如果添加用於插值本身的括號,我們結束了一個三重括號,例如你在該行有一個,讓你異常:

Assert.AreEqual(formatted, $"{{countdown|{date:o}}}"); 

現在,如果我們觀察到的「}} }「,我們可以注意到第一個括號包含了字符串插值,而最後兩個意味着被視爲字符串轉義的括號字符。

但是,編譯器將前兩個視爲已掃描的字符串字符,因此它將在插值分隔符之間插入一個字符串。基本上,編譯器做這樣的事情:

Assert.AreEqual(formatted, $"{{countdown|{$"{date:o}"}}}"); 
6

這是一個後續到我原來的答覆,以便

string str = "a string"; 
$"{str'}'}"; //this would obviously generate a compile error which is bypassed by this bug 

您可以通過格式化行這樣解決這個

確保這是預期的行爲

就官方來源而言,我們應該參考msdn的Interpolated Strings

內插字符串的結構是

$ " <text> { <interpolation-expression> <optional-comma-field-width> <optional-colon-format> } <text> ... } " 

每個單插正式與語法

single-interpolation: 
    interpolation-start 
    interpolation-start : regular-string-literal 

interpolation-start: 
    expression 
    expression , expression 

這裏真正重要的是,

  1. optional-colon-format定義定義作爲regular-string-literal語法=>即它可以包含escape-sequence,根據的C# Language Specification 5.0
  2. paragraph 2.4.4.5 String literals您可以使用插值的字符串的任何地方,你可以使用一個​​
  3. 要包括大括號({})在插入串用兩個大括號,{{}} = >即逸出在optional-colon-format
  4. 兩個花括號編譯器掃描包含插值expressions作爲平衡文本,直到找到一個逗號,冒號,或靠近大括號=>即,結腸斷裂編譯器平衡文本以及密切的花括號

只是要清楚,這說明$"{{{date}}}"之間的區別在哪裏dateexpression,所以它被標記化,直到第一個花括號與$"{{{date:o}}}"其中date是再次expression現在它被標記化,直到第一個冒號,在這之後,規則字符串開始,編譯器繼續逃逸兩個花括號,等...

也有從MSDN,在這種情況下,String Formatting FAQ是明確處理。

int i = 42; 
string s = String.Format(「{{{0:N}}}」, i); //prints ‘{N}’ 

問題是,爲什麼這最後一次嘗試失敗呢?有 你需要知道,以瞭解這一結果兩件事情:

當提供格式說明,字符串格式化採取這些 步驟:

確定是否符長於單個字符:如果是這樣, 然後假定說明符是自定義格式。自定義格式 將對您的格式使用合適的替換項,但如果它不知道如何處理某些字符,則它將簡單地將其作爲 文字寫入,格式爲:確定單個字符 說明符是否爲支持說明符(例如格式爲 的'N')。如果是,則格式適當。如果不是,拋出 ArgumnetException

當試圖確定一個大括號是否應該 逃出,花括號只是順序處理他們收到 。因此,{{{將跳過前兩個字符,而 將打印字面值{,並且第三個大括號將從 格式化部分開始。在此基礎上,在}}}前面兩個曲線的 括號將被轉義,因此一個字面}將被寫入 的格式字符串,然後最後一個大括號將被認爲是 結束格式化部分有了這些信息,我們現在可以 找出我們的{{{0:N}}}情況中發生了什麼。第一個 兩個花括號被轉義,然後我們有一個格式化部分。 但是,我們還可以在格式化部分關閉 之前轉義結束的大括號。因此,我們的格式化部分實際上是 ,解釋爲包含0:N}。現在,格式化程序查看 格式說明符,並且它看到指定符的N}。因此,它將 解釋爲自定義格式,既然N或}對於自定義數字格式均不意味着 ,這些字符僅僅是 寫出來的,而不是被引用的變量的值。

+0

感謝您的詳細跟進。 – FrankerZ

1

這是獲得斷言工作的最簡單的方法...

Assert.AreEqual(formatted, "{" + $"countdown|{date:o}" + "}"); 

在這種形式下...

Assert.AreEqual(formatted, $"{{countdown|{date:o}}}"); 

第2個結束括號被解釋爲一個文字關閉大括號,第三個關閉格式表達式。

這不像插入字符串的語法限制那麼多。該錯誤(如果有的話)是格式化文本的輸出可能應該是「o」而不是「o」。

我們在C,C#和C++中使用運算符「+ =」而不是「= +」的原因是,在形式= +中,某些情況下不能說出「+」是否是運算符的一部分或一個「+」。