21 限制类型的可见性
不是每个类型都需要Public。你应给你的类型最少的可见度来达到你的目的,内部或私有类能实现公共的接口。很多独立的类应当创建为内部的。你可以使用protected或私有嵌套类进一步来限制其可见性,可见性越小,当你之后更新时,整个系统的更改也就越少,越少的地方访问你的代码,你修改的地方也就越少。只暴露那些需要暴露的。对于可见性小的类,尝试使用公共接口来暴露。
// For illustration, not complete source
public class List<T> : IEnumerable<T>
{
private class Enumerator<T> : IEnumerator<T>
{
// Contains specific implementation of
// MoveNext(), Reset(), and Current.
public Enumerator(List<T> storage)
{
// elided
}
}
public IEnumerator<T> GetEnumerator()
{
return new Enumerator<T>(this);
}
// other List members.
}
客户端代码不需要知道类的Enumerator<T>。
.NET很多集合类也是这样设计的,如Dictionary<T>包含了私有的DictionaryEnumerator<T>,Queue<T>包含了QueueEnumerator<T>.
创建internal类是一种被忽视的限制类型范围的方法。缺省情况下,大部分程序员总是创建Public类,没有去考虑其他,因为这些都是VS .NET向导做的事。在接收默认形式的时候,你应留心你的新创建的类会用在哪里?是所有客户端还是主要在程序集内部使用?
使用接口暴露你的函数使你更容易创建internal类,如:
public interface IPhoneValidator
{
bool ValidateNumber(PhoneNumber ph);
}
internal class USPhoneValidator : IPhoneValidator
{
public bool ValidateNumber(PhoneNumber ph)
{
// perform validation.
// Check for valid area code, exchange.
return true;
}
}
22 偏爱定义和实现接口而不是继承
抽象基类提供了类层次的祖先。一个接口描述了一个能被类实现的原子功能。
接口以一种契约的方式来设计:一个实现了接口的类必须提供一个预期的方法实现。抽象基类为一系列相关的类型提供了一个公共抽象方法。继承意思是是什么,接口意思是行为像什么.这都是老生常谈的问题,但也是它们的工作。它们描述两个不同的概念:基类描述的是对象是什么,接口描述的是对象的一种行为方式。
接口描述了一组功能或者说是一个契约。你可以在接口中创建方法、属性、索引器、事件等。你也可以创建接口所没有的方法,因为方法没有具体的数据成员,你可以使用扩展方法来扩充接口功能。如扩展泛型接口IEnumerable<T>:
public static class Extensions
{
public static void ForAll<T>(
this IEnumerable<T> sequence,
Action<T> action)
{
foreach (T item in sequence)
action(item);
}
}
// usage
foo.ForAll((n) => Console.WriteLine(n.ToString()));
抽象基类可以为它的派生类提供一些公共的实现方法,它通过virtual方法来为派生类指定数据成员、具体方法等。这个基类实现带了一个好处:如果你给基类增加一个方法,那么它的所有派生类都自动隐式地增加.但是给接口增加一个方法将会导致实现它的所有类都产生错误.选择一个抽象基类还是一个接口,这涉及到如何能最好地总是支持你的抽象功能.接口总是固定的:你发行一个接口作为一系列任何类都能实现的功能的契约.基类总是能被扩展的.这些扩展也会成为其派生类的一部分.
这两种模型是可以混合使用的.一个例子是.NET框架中的IEnumerable<T>接口和System.Linq.Enumerable类.System.Linq.Enumerable类包含了基于System.Collections.Generic.IEnumerable<T>接口的大量扩展方法。
接口可以被任意数量的不相关类型来实现。编写接口给编写基类的开发者提供了非常大的灵活性。因为.NET环境中只支持单继承,而接口解决了多继承问题。使用接口还可以使你偶尔能减少structs类型拆箱的代价。如:
public struct URLInfo : IComparable<URLInfo>, IComparable
{
private string URL;
private string description;
#region IComparable<URLInfo> Members
public int CompareTo(URLInfo other)
{
return URL.CompareTo(other.URL);
}
#endregion
#region IComparable Members
int IComparable.CompareTo(object obj)
{
if (obj is URLInfo)
{
URLInfo other = (URLInfo)obj;
return CompareTo(other);
}
else
throw new ArgumentException(
"Compared object is not URLInfo");
}
#endregion
}
任何基于IComparable的代码将会减少装箱和拆箱,因为可以调用IComparable.CompareTo方法而不用拆箱对象.
23 理解接口方法和虚拟方法的不同
刚开始,可能以为实现一个接口和重写一个虚拟方法一样,但实际上实现一个接口和重写虚拟方法是大不相同的.首先在接口中的成员方法并不是默认以virtual声明的.派生类不能重写基类中已实现的接口成员,接口可以显示实现,这样可以隐藏从类继承而来的公共接口。它们是在不同的用途是不同的概念。
你可以在派生类中修改接口的实现。如:
interface IMsg
{
void Message();
}
public class MyClass : IMsg
{
public void Message()
{
Console.WriteLine("MyClass");
}
}
Message()方法现在是MyClass类的公共接口。Message也可以通过IMsg指针来访问。现在我们来增加一个派生类:
public class MyDerivedClass : MyClass
{
public void Message()
{
Console.WriteLine("MyDerivedClass");
}
}
因为MyClass.Message()不是虚拟的,派生类是不能提供一个重写的Message()版本的。此时派生类创建了一个新的Message()方法,它把基类的Message()方法给隐藏了,相当于new void Message()。通过IMsg引用MyClass.Message仍然是可以用的:
MyDerivedClass d = new MyDerivedClass();
d.Message(); // prints "MyDerivedClass".
IMsg m = d as IMsg;
m.Message(); // prints "MyClass"
当你实现一个接口,你实际在类中声明了一个特殊契约的具体实现。但是通常我们想在基类创建、实现接口,然后在派生类中修改它:
public class MyClass : IMsg
{
public virtual void Message()
{
Console.WriteLine("MyClass");
}
}
public class MyDerivedClass : MyClass
{
public override void Message()
{
Console.WriteLine("MyDerivedClass");
}
}
MyDerivedClass以及所有从MyClass派生的类都能声明它们自己的Message方法。这个重写版本能被派生类引用访问、能被接口引用访问、能被基类引用访问,如果你不喜欢虚函数的概念,你可以这样来重新定义基类:
public abstract class MyClass : IMsg
{
public abstract void Message();
}
对,你可以在基类中不实际实现一个接口,声明它为抽象方法。这样所有派生类必须实现这个接口方法。派生类可以通过密封该方法来阻止它自己的派生重写它:
public class MyDerivedClass2 : MyClass
{
public sealed override void Message()
{
Console.WriteLine("MyDerivedClass");
}
}
另一种解决方法是实现接口,然后包含一个调用该虚方法的方法,如:
public class MyClass2 : IMsg
{
protected virtual void OnMessage()
{
}
public void Message()
{
OnMessage();
Console.WriteLine("MyClass");
}
}
任何派生类都能重写OnMessage方法,还可以在Message方法中增加它们自己的工作。.NET事件方法几乎都用这种模式。
基类能提供一个缺省的接口方法实现,然后派生类可以通过继承基类声明并实现接口方法,如:
public class DefaultMessageGenerator
{
public void Message()
{
Console.WriteLine("This is a default message");
}
}
public class AnotherMessageGenerator :
DefaultMessageGenerator, IMsg
{
// No explicit Message() method needed.
}
注意派生类能声明这个接口作为它自己契约的一部分,即使它没有提供IMsg方法的实现,它通过继承基类的方法,隐式实现了该接口方法。
24 使用委托来表示回调
回调函数用来从服务器向客户端异步地提供一个反馈。它们可能涉及多线程,或者简单提供一个同步更新的入口点。回调函数在C#中通过委托来表现。
委托提供一个类型安全的回调函数定义。虽然委托用的最多的是事件,但这不是委托唯一能用的地方。委托让你在运行时配置目标,并且通知多个客户端。一个委托就是一个包含方法引用的对象。包含的方法可以是静态方法或者实例方法。使用委托,你可以在运行时与一个或多个客户端对象交流、配置。
C#提供了一个紧凑的语法(lambda表达式)来表达委托。.NET框架库定义了很多常见的委托形式,如:Predicate<T>, Action<>, and Func<>.predicate是一个用来测试条件的布尔函数。Func<>需要一些数值参数,然后产生一个结果。也就是Func<T, bool>和Predicate<T>有相同的形式。Action<>输入任何数量的参数,然后返回一个void类型。LINQ就是建立在这些概念上的。
List<T>类也包含了很多利用回调函数的方法,如:
List<int> numbers = Enumerable.Range(1, 200).ToList();
var oddNumbers = numbers.Find(n => n % 2 == 1);
var test = numbers.TrueForAll(n => n < 50);
numbers.RemoveAll(n => n % 2 == 0);
numbers.ForEach(item => Console.WriteLine(item));
Find()方法接收一个委托,以Predicate<int>的形式来测试列表中的每一个元素。这是一种简单的回调。List.ForEach()方法在每个列表元素中执行指定的动作。实际上编译器将lambda表达式转换为一个方法,然后创建一个委托来指向该方法.所有的LINQ都是建立在委托上的。在WPF和Windows Form中,回调是用来处理跨线程的封送处理的。
由于历史原因,所有的委托都是多播委托。多播委托封装所有的目标函数然后添加在一个调用里。这种构造存在两个问题:一个是在面对异常时不安全,另一个是返回值将是最后一个目标函数调用的返回值。
25 实现事件模式的通知
.NET的事件模式,无非是观察者模式的一个语法转换而已。事件定义了你的类的通知。事件基于委托提供了一个类型安全的函数签名。
考虑一个简单的例子。你已经建立了一个log类,它是一个应用的所有消息的调度器。它将接收应用的所有消息,并且调度这些消息到对其感兴趣的监听者上。这些监听者可能是依附在控制台、数据库、系统日志等等,定义如下:
public class LoggerEventArgs : EventArgs
{
public string Message { get; private set; }
public int Priority { get; private set; }
public LoggerEventArgs(int p, string m)
{
Priority = p;
Message = m;
}
}
public class Logger
{
static Logger()
{
theOnly = new Logger();
}
private Logger()
{
}
private static Logger theOnly = null;
public static Logger Singleton
{
get { return theOnly; }
}
// Define the event:
public event EventHandler<LoggerEventArgs> Log;
// add a message, and log it.
public void AddMsg(int priority, string msg)
{
// This idiom discussed below.
EventHandler<LoggerEventArgs> l = Log;
if (l != null)
l(this, new LoggerEventArgs(priority, msg));
}
}
AddMsg方法展示了合适的抛出异常的方式。在多线程程序的静态条件上引用log事件句柄的临时变量是很重要的保护措施。C#编译器为事件创建了add和remove访问器:
public event EventHandler<LoggerEventArgs> Log
{
add { log = log + value; }
remove { log = log - value; }
}
log的使用:
class ConsoleLogger
{
static ConsoleLogger()
{
Logger.Singleton.Log += (sender, msg) =>
{
Console.Error.WriteLine("{0}:\t{1}",
msg.Priority.ToString(),
msg.Message);
};
}
}