out参数不用赋值?这么神奇吗!
首先提醒大家一下,docs.microsoft.com上的《C# 指南》是這樣描述out 參數修飾符[1]的:
作為 out 參數傳遞的變量在方法調用中傳遞之前不必進行初始化。但是,被調用的方法需要在返回之前賦一個值。
請注意上面加粗的話,然后看看下面的代碼片段,你覺得它能否編譯通過:
private?void?Test(out?System.Reflection.ParameterModifier?obj) {? //什么也不做 }如果你很肯定地回答“不能”,那么恭喜你——答錯了。我當初看到這段代碼的第一感覺也是不能,但發現代碼確實能夠編譯通過。
分析原因
難道是語法改變了,官方文檔沒更新? 我又測試了一下:
private?void?Test2(out?string?obj)//編譯失敗 {? } private?void?Test3(out?int?obj)//編譯失敗 {? }難道這個類型有什么特殊之處? 我把dotnet/runtime中的ParameterModifier源代碼[2]復制到本地項目,編譯同樣提示CS0177錯誤,WTF!!!
private?void?Test(out?ParameterModifier?obj) {? } public?readonly?struct?ParameterModifier {private?readonly?bool[]?_byRef;public?ParameterModifier(int?parameterCount){if?(parameterCount?<=?0)throw?new?ArgumentException();_byRef?=?new?bool[parameterCount];}public?bool?this[int?index]{get?=>?_byRef[index];set?=>?_byRef[index]?=?value;}#if?CORECLRinternal?bool[]?IsByRefArray?=>?_byRef; #endif }深入Roslyn
應該是編譯器做了什么特殊處理!
于是我clone了dotnet/roslyn源代碼[3],本來想調試源代碼的,結果由于編譯時依賴包一直下載不下來,干脆直接讀源代碼了。
通過查找錯誤提示"must be assigned to before control leaves the current method",定位到CSharpResources.resx,確認錯誤編碼為ERR_ParamUnassigned:
??<data?name="ERR_ParamUnassigned"?xml:space="preserve"><value>The?out?parameter?'{0}'?must?be?assigned?to?before?control?leaves?the?current?method</value></data>查找ERR_ParamUnassigned,定位到了編譯錯誤信息被添加的位置(DefiniteAssignment.cs文件內的ReportUnassignedOutParameter方法);
protected?virtual?void?ReportUnassignedOutParameter(ParameterSymbol?parameter,?SyntaxNode?node,?Location?location) {......if?(Diagnostics?!=?null?&&?this.State.Reachable){......if?(!reported){Debug.Assert(!parameter.IsThis);Diagnostics.Add(ErrorCode.ERR_ParamUnassigned,?location,?parameter.Name);}} }因為同樣的方法定義,只是參數類型不一樣導致編譯報錯,因此猜測這個方法肯定進入了,只是this.State.Reachable值不同的原因,Reachable的代碼如下:
public?bool?Reachable {get{return?Assigned.Capacity?<=?0?||?!IsAssigned(0);} } public?bool?IsAssigned(int?slot) {return?/*(slot?==?-1)?||?*/Assigned[slot]; }public?void?Assign(int?slot) {if?(slot?==?-1)return;Assigned[slot]?=?true; }繼續查找Assign的調用位置,發現一段很有意思的代碼:
Debug.Assert(!_emptyStructTypeCache.IsEmptyStructType(type)); ...... state.Assign(slot);IsEmptyStructType是不是意味著空Struct不檢查?立馬來試試:
private?void?Test(out?EmptyStruct?obj)///編譯通過 {? }public?struct?EmptyStruct {? }繼續探究
但是ParameterModifier明顯不是空Struct,而且更奇怪的是為什么將源代碼復制到本地項目又不能編譯了。 帶著這個疑問,我們繼續深挖:
private?bool?IsEmptyStructType(TypeSymbol?type,?ConsList<NamedTypeSymbol>?typesWithMembersOfThisType) {......result?=?CheckStruct(typesWithMembersOfThisType,?nts);......return?result; }private?bool?CheckStruct(ConsList<NamedTypeSymbol>?typesWithMembersOfThisType,?NamedTypeSymbol?nts) {if?(!typesWithMembersOfThisType.ContainsReference(nts)){......return?CheckStructInstanceFields(typesWithMembersOfThisType,?nts);}return?true; } private?bool?CheckStructInstanceFields(ConsList<NamedTypeSymbol>?typesWithMembersOfThisType,?NamedTypeSymbol?type) {//?PERF:?we?get?members?of?the?OriginalDefinition?to?not?create?substituted?members/types?//???????unless?necessary.foreach?(var?member?in?type.OriginalDefinition.GetMembersUnordered()){if?(member.IsStatic){continue;}var?field?=?GetActualField(member,?type);if?((object)field?!=?null){var?actualFieldType?=?field.Type;if?(!IsEmptyStructType(actualFieldType,?typesWithMembersOfThisType)){return?false;}}}return?true; }代碼檢查每個字段的類型是否是“空Struct”。這意味著如果所有實例字段都是“空Struct”,則原始類型也被視為“空Struct”,否則為“非空Struct”。看來關鍵就在GetActualField了:
private?FieldSymbol?GetActualField(Symbol?member,?NamedTypeSymbol?type) {switch?(member.Kind){case?SymbolKind.Field:var?field?=?(FieldSymbol)member;//?Do?not?report?virtual?tuple?fields.//?They?are?additional?aliases?to?the?fields?of?the?underlying?struct?or?nested?extensions.//?and?as?such?are?already?accounted?for?via?the?nonvirtual?fields.if?(field.IsVirtualTupleField){return?null;}return?(field.IsFixedSizeBuffer?||?ShouldIgnoreStructField(field,?field.Type))???null?:?field.AsMember(type);case?SymbolKind.Event:var?eventSymbol?=?(EventSymbol)member;return?(!eventSymbol.HasAssociatedField?||?ShouldIgnoreStructField(eventSymbol,?eventSymbol.Type))???null?:?eventSymbol.AssociatedField.AsMember(type);}return?null; }private?bool?ShouldIgnoreStructField(Symbol?member,?TypeSymbol?memberType) {return?_dev12CompilerCompatibility?&&?????????????????????????????//?when?we're?trying?to?be?compatible?with?the?native?compiler,?we?ignore((object)member.ContainingAssembly?!=?_sourceAssembly?||???//?imported?fieldsmember.ContainingModule.Ordinal?!=?0)?&&??????????????????????//?????(an?added?module?is?imported)IsIgnorableType(memberType)?&&?????????????????????????????????//?of?reference?type?(but?not?type?parameters,?looking?through?arrays)!IsAccessibleInAssembly(member,?_sourceAssembly);??????????//?that?are?inaccessible?to?our?assembly. }必須是Struct和代碼不在同一個程序集(((object)member.ContainingAssembly != _sourceAssembly),字段類型必須是引用類型或數組(IsIgnorableType),并且是私有的(!IsAccessibleInAssembly)。我們來驗證一下,將ParameterModifier源代碼復制到類庫中:
//ConsoleApp1.csproj private?void?Test(out?ClassLibrary1.ParameterModifier?obj) { }//ClassLibrary1.csproj namespace?ClassLibrary1 {public?readonly?struct?ParameterModifier{private?readonly?bool[]?_byRef;?//編譯通過//private?readonly?string?_byRef;?//編譯通過//private?readonly?int?_byRef;?//編譯失敗//public?readonly?bool[]?_byRef;?//編譯失敗} }結論
今天我們深入了編譯器的源代碼分析了一個簡單問題的成因:
一般來說,out參數必須在被調用方法將控制返回給調用方之前初始化。然而,編譯器可以進行優化,在某些情況下,如類型是沒有Public字段的Struct,將不會顯示編譯錯誤。
雖然感覺知道了也并沒什么鳥用,但至少說明了好的代碼風格還是非常重要的!希望這篇文章能夠對你有所啟發。
歡迎關注我的個人公眾號”My IO“
參考資料
[1]
out 參數修飾符: https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/out-parameter-modifier
[2]ParameterModifier源代碼: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Reflection/ParameterModifier.cs
[3]dotnet/roslyn源代碼: https://github.com/dotnet/roslyn
總結
以上是生活随笔為你收集整理的out参数不用赋值?这么神奇吗!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 被同事嘲笑说技术方案没深度?
- 下一篇: 程序员过关斩将--错误的IOC和DI