改善C#编程的50个建议(21-25)

-------------------------翻译 By Cryking-----------------------------
-----------------------转载请注明出处,谢谢!------------------------ 

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);
            };
        }
    }

你可能感兴趣的:(改善C#编程的50个建议(21-25))