.Net令人糾結的Null
從我們剛學.Net編程起,我們的程序不斷被從天而降NullReferenceException打斷。直到今天,我們仍然時常為C#的Null或者VB的Nothing困惑。什么情況下我們該返回null,如果參數是null代表什么。許多類型,有兩種不同意義的空狀態,一種是null,一種是其本身或其某個屬性集合中沒有元素,這就更容易產生誤用。常聽有人說,Null這個概念在編程語言中根本不應該存在。但是,從C++到Java到.Net,它從未離開過。
最近,注意到.Net Framework在讀取程序配置文件的一個小Bug。比如我在配置文件中,自定義了名為ReviewPeriod的節點:
<configSections>
<section name="reviewPeriod" type="WordPadTest.ReviewPeriod,WordPadTest"/>
</configSections>
<reviewPeriod>
<Periods>
<period id="1" />
<period id="2" name="d" />
<period id="3" name="m" />
</Periods>
</reviewPeriod>
</configuration>
注意第一個period節點沒有定義name屬性。.Net Framework中關于程序配置處理的類都在System.Configuration命名空間下,應用很廣泛,比如Asp.Net和WCF項目,還有許多第三方框架如NHibernate。下面定義三個類,分別描述自定義節點配置,自定義節點集合,自定義節點實體,這也是最普通的程序自定義配置處理方式。然后就可以遍歷ConfigurationManager.GetSection("reviewPeriod") as ReviewPeriod 訪問每個period對象。
{
class ReviewPeriod : ConfigurationSection
{
[ConfigurationProperty("Periods")]
public Periods Periods
{
get { return this["Periods"] as Periods; }
}
}
class Periods: ConfigurationElementCollection
{
protected override ConfigurationElement CreateNewElement()
{
return new Period();
}
protected override object GetElementKey(ConfigurationElement element)
{
return (element as Period).Id;
}
public override ConfigurationElementCollectionType CollectionType
{
get { return ConfigurationElementCollectionType.BasicMap; }
}
protected override string ElementName
{
get { return "period"; }
}
}
public class Period:ConfigurationElement
{
[ConfigurationProperty("name")]
public string Name
{
get { return (string)this["name"]; }
}
[ConfigurationProperty("id")]
public int Id
{
get { return (int)this["id"]; }
}
}
}
這段程序運行沒問題,意外的是在第一個period節點上,它的Name屬性是什么呢,一直以來我都弄錯了,可能還有許多人也弄錯了。正確答案是String.Empty,而不是null。而且即使我在ConfigurationProperty加上DefaultValue=null,Name返回的依然是String.Empty。
在我看來,null應該用于表示一種在我們預料之外的狀態,包括傳入的參數和返回的結果。比如一段從數據庫讀取記錄的代碼,如果執行中出現異常,則返回null,和正常執行后未找到任何匹配記錄的情況截然不同。同理,對于節點的name屬性未定義的情況,顯然也不同于定義了name=”"的情況,返回null是合理的。而ConfigurationElement不允許string屬性為null,可能有某方面的考慮,但這應該是個Bug,因為它返回值含混不清,是對string空值的誤解。
請再來看另一個例子:如果這個返回值變成了參數的情況。在實際軟件中,讀取XML數據源時,通常要進行架構驗證,比如像下面這樣:
settings.Schemas.Add(targetNamespace, "postlist.xsd"); //targetNamespace是傳入方法的參數
settings.ValidationType = ValidationType.Schema;
XmlReader reader = XmlReader.Create("postlist.xml", settings);
這段代碼有一個問題,它未判斷targetNamespace的是Null還是String.Empty。對于XmlSchemaSet的Add方法,targetNamespace參數如果為null,則表示使用xsd文件中定義的命名空間,而如果傳入String.Empty,XmlSchemaSet依然認為,是我們手動指定了一個空的命名空間,而這完全不是我們想要的。
如果在xsd文件中定義了targetNamespace,我們手動指定的namespace必須與之一致,否則將拋出異常。而xsd文件中的targetNameSpace屬性,你可以不加它,但加了就不允許空值。換句話說,我們傳入的為String.Empty時targetNameSpace參數時,xsd中只要有targetNamespace的定義,程序必然出錯。下圖也是上面代碼執行的結果,和傳null值的情況有天壤之別。
我們不能怪罪寫Xml驗證的人太馬虎,微軟一個內部框架中就有這個問題。歸根究底,是.Net Framework對null的誤用,導致了我們的誤解。
首先,ConfigurationElement類對返回值的誤用,屬性值該是null卻返回String.Empty。
其次,XmlSchemaSet類的Add方法有一定問題,應該提供一個重載函數,不需要傳入為null的targetNamespace參數,直接用xsd文件中的對應屬性。 不但方便,也減少出錯的機會。當然,它正確的區分了null和empty的含義,這點還是值得稱贊的。
連.Net Framework對null使用上都有如此的問題,我們寫程序時,應該更小心翼翼,對付這個難以捉摸、令人糾結的null。