其實使用C#這麼多年,我時不時會遇到一些令人不爽的設計缺陷。這些缺陷大都是些限制,雖說無傷大雅,也很容易避免,但一旦遇到這些情況,總會令人心生不快,畢竟都是些無謂的限制。而且令人遺憾的是,雖說去除這些限制也不會帶來什麼問題,但我認為C#設計團隊也基本不會去修復這些問題了,畢竟它們大都是些細枝末節。作為一名用C#的純種碼農,我突然一時興起也要把這些設計缺陷記錄下,也方便和大伙一起討論下。那麼這次就先從實現接口內的事件說起,當我們需要顯式實現一個接口內的事件時,會發現我們必須提供add和remove訪問器,這還會稍許影響到事件常用的使用模式。
這個問題聽上去有些繞,不過看代碼便一清二楚。例如,在項目中我會定義一個這樣的INotifyPropertyChanged接口,其中包含一個PropertyChanged事件:
public interface INotifyPropertyChanged<TPropertyIdentity>
{
event EventHandler<PropertyChangedEventArgs<TPropertyIdentity>> PropertyChanged;
}
public class PropertyChangedEventArgs<TPropertyIdentity> : EventArgs
{
public PropertyChangedEventArgs(TPropertyIdentity propertyIdentity)
{
this.PropertyIdentity = propertyIdentity;
}
public TPropertyIdentity PropertyIdentity { get; private set; }
}
可以看出這個接口和.NET內置的INotifyPropertyChanged事件可謂如出一轍,其實他們的目的也一樣,就是向外通知該對象的某個屬性發生了改變。不同的是,系統內置的PropertyChangedEventArgs對象使用屬性名,也就是一個字符串標識一個屬性,而在如上帶泛型的PropertyChangedEventArgs裡,則可以使用任意類型的對象來標識屬性,這無疑帶來的更多的靈活性。例如,我們可以使用連續的整型數值來標識對象,這樣我們就可以使用數組來創建一個索引,它的性能會比使用字符串為鍵值的字典要高出一些。
不過,我們實現系統自帶的INotifyPropertyChanged屬性時,並非是要“自行使用”,而往往是想讓通知其他組件,例如ORM框架或是UI控件。因此,它其實已經是.NET平台上的統一約定,即便有所不足,也不能捨棄它。因此,我們往往需要在一個對象上同時實現兩種INotifyPropertyChanged接口,例如:
public class Item : INotifyPropertyChanged<int>, INotifyPropertyChanged
{
public event EventHandler<PropertyChangedEventArgs<int>> PropertyChanged;
event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
{
add { throw new NotImplementedException(); }
remove { throw new NotImplementedException(); }
}
}
以上是Visual Studio為兩個事件實現自動生成的代碼框架,且看第二個事件,它要求我們提供add和remove訪問器。為什麼?我不知道,C#開發團隊自己可能也已經不太清楚這麼規定的原因:
Interesting question. I did some poking around the language notes archive and I discovered that this decision was made on the 13th of October, 1999, but the notes do not give a justification for the decision.
Off the top of my head I don't see any theoretical or practical reason why we could not have field-like explicitly implemented events. Nor do I see any reason why we particularly need to. This may have to remain one of the mysteries of the unknown.
Eric Lippert是老牌C#團隊成員了,經常在Stack Overflow或是博客上寫一些C#的設計內幕,可惜在這個問題上連他也認為是個“不解之謎”。此外,“自動屬性”讓這個限制進一步顯得“無厘頭”了,因為我們完全可以這麼顯式實現接口裡的屬性:
public interface INameProvider
{
string Name { get; set; }
}
public class MyNameProvider : INameProvider
{
string INameProvider.Name { get; set; }
}
既然如此,事件跟它又有什麼本質區別呢?
順便一提,我們知道,在C#裡不能把顯式實現的接口成員標注為抽象成員,這對於事件來說還存在一些額外的問題。且看以下代碼片段:
public abstract class Base : INotifyPropertyChanged<MyIdentity>
{
public EventHandler<PropertyChangedEventArgs<MyIdentity>> PropertyChanged;
protected void OnPropertyChanged(PropertyChangedEventArgs<MyIdentity> args)
{
var propertyChanged = this.PropertyChanged;
if (propertyChanged != null)
{
propertyChanged(this, args);
}
}
}
Base是個基類,因此它往往會暴露個OnXyz方法,以便子類觸發Xyz事件。在OnPropertyChanged方法中,我們會先判斷_propertyChanged是否為null,因為null表示還沒有人注冊過事件——這是事件使用時的常見模式。事件本身沒有注冊任何處理器,則意味著事件本身不觸發亦可,同樣意味著我們甚至可以不去創建事件所需的EventArgs參數。但是,如果我們是要在子類裡觸發事件(即調用OnXxx方法),則沒有辦法檢查該事件有沒有注冊處理器。假如這個EventArgs對象創建起來成本較高,就會造成一定的性能損失。
解決方法倒也簡單,例如,在基類裡增加一個事件:
public abstract class Base : INotifyPropertyChanged<MyIdentity>
{
public abstract event EventHandler<PropertyChangedEventArgs<MyIdentity>> MyIdentityPropertyChanged;
event EventHandler<PropertyChangedEventArgs<MyIdentity>> INotifyPropertyChanged<MyIdentity>.PropertyChanged
{
add { this.MyIdentityPropertyChanged += value; }
remove { this.MyIdentityPropertyChanged -= value; }
}
}
或干脆加一個“延遲”構造EventArgs的重載:
public abstract class Base : INotifyPropertyChanged<MyIdentity>
{
private EventHandler<PropertyChangedEventArgs<MyIdentity>> _propertyChanged;
event EventHandler<PropertyChangedEventArgs<MyIdentity>> INotifyPropertyChanged<MyIdentity>.PropertyChanged
{
add { this._propertyChanged += value; }
remove { this._propertyChanged -= value; }
}
protected void OnPropertyChanged(PropertyChangedEventArgs<MyIdentity> args) { ... }
protected void OnPropertyChanged(Func<PropertyChangedEventArgs<MyIdentity>> argsFactory) { ... }
}
於是在基類裡觸發事件時即可:
this.OnPropertyChanged(() => new PropertyChangedEventArgs<MyIdentity>(new MyIdentity()));
如果您覺得在沒有事件處理器的情況下創建一個委托對象也是一種浪費,那麼就自己想辦法解決咯。沒什麼困難的,不應該想不出。
作者 zhaojie