改善C#编程的50个建议(26-30)

来源:互联网 发布:杰昆.菲尼克斯 知乎 编辑:程序博客网 时间:2024/06/06 02:30

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

26 避免返回内部类对象的引用
  你可能会想一个只读属性应该是只读的,调用者不能修改它.但不幸的是,不总是你想的那样.如果你创建了一个返回引用类型的属性,调用者则可以访问对象的任何公共成员,包括可以修改属性状态的成员方法.如:
public class MyBusinessObject{    // Read Only property providing access to a    // private data member:    private BindingList<ImportantData> listOfData =    new BindingList<ImportantData>();    public BindingList<ImportantData> Data    {        get { return listOfData; }    }    // other details elided}// Access the collection:BindingList<ImportantData> stuff = bizObj.Data;// Not intended, but allowed:stuff.Clear(); // Deletes all data.

现在MyBusinessObject的任何公共客户端都能修改你的内部数据集。你本意是通过属性来隐藏内部数据结构,提供方法来让客户端通过已知的方法来操作数据。此时只读属性破坏了类的封装性。你可以使用四种不同的策略来保护你的内部数据不会遭到意想不到的修改:值类型、不可变类型、接口、封装器。
  很显然当客户端通过属性访问时值类型是传递的一份副本。所以任何改变都不会影响到对象的内部状态。
  不可变类型,如System.String,也是安全的。你可以返回string,或者任何不可变类型,因为没有客户端能够修改不可变类型,你的对象的内部状态是非常安全的。
  定义接口来访问内部成员的功能是允许的。通过这些接口来暴露功能,你可以尽可能的减少无意的内部数据的改变。客户端可以通过你提供的接口来访问内部对象,这个接口将不包含类的整个功能。如暴露List<T>的IEnumerable<T>接口指针就是这种策略。使用接口的MyBusinessObject如下:
public class MyBusinessObject{    // Read Only property providing access to a    // private data member:    private BindingList<ImportantData> listOfData = newBindingList<ImportantData>();    public IBindingList BindingData    {        get { return listOfData; }    }    public ICollection<ImportantData> CollectionOfData    {        get { return listOfData; }    }    // other details elided}

在我们讲如何创建完全的只读数据视图之前,来短暂地看看当你允许公共客户端修改它时如何响应数据的改变。BindingList<T>类支持IBindingList接口,以便你能在展示给用户之前响应集合项目的任何增加、修改、删除。当你的数据存储在BindingList<T>,你可以在BindingList对象上强制设置各种不同的属性(AddEdit, AllowNew, AllowRemove等).UI控件基于这些属性的值启用和禁用不同的行为.这些都是公共属性,所以能修改你的集合的行为,那也就是说你不应暴露BindingList<T>对象作为一个公共的属性.
  最后的选择是提供一个封装器对象并且导出封装器,最大限度减少包含的对象.System
.Collections.ObjectModel.ReadOnlyCollection<T>类型就是一个标准的方式封装了一个集合并且导出一个数据的只读版本:
    public class MyBusinessObject    {        // Read Only property providing access to a        // private data member:        private BindingList<ImportantData> listOfData = new        BindingList<ImportantData>();        public IBindingList BindingData        {            get { return listOfData; }        }        public ReadOnlyCollection<ImportantData> CollectionOfData        {            get            {                return new ReadOnlyCollection<ImportantData>                (listOfData);            }        }        // other details elided    }

通过公共接口暴露引用类型,允许使用你的对象的用户来修改它的内部而不是通过你定义的方法或属性.最终你通过接口、封装器对象或值类型等来限制访问暴露的私有内部数据。


27 让你的类型可序列化
持久化是一个类型的核心特征。它是一种基础的元素以至于没人会注意到你忽视了去支持它。如果你忘记了对你的类型支持可序列化,你将会创建更多的工作给使用你的类型的开发者。.NET序列化是非常简单的,以致于你没有任何借口来忘记去支持它。很多情况下,增加可序列化特性就足够了:
    [Serializable]
    public class MyType
    {
        private string label;
        private int value;
    }
因为string和int都支持可序列化,因此这里简单的增加可序列化特性即可。增加支持可序列化的重要性在当你增加一个自定义类型到可序列化类型的时是显而易见的:
    [Serializable]
    public class MyType
    {
        private string label;
        private int value;
        private OtherClass otherThing;
    }
如果OtherClass 类支持.NET的可序列化,那么这里可序列化特性将会工作。否则你将会获得一个运行时错误,并且你需要编写你自己的额外代码来序列化MyType。
  .NET序列化保存你的对象中所有的成员变量到输出流。.NET序列化支持任意对象路径:甚至如果在你的对象中有循环引用,序列化和反序列化方法将保存和恢复每个实际的对象仅仅一次。序列化特性还支持二进制和SOAP的序列化。
  增加序列化特性是最简单的方法来支持对象序列化,但是最简单的不一定是最正确的。有时你不想序列化对象的所有成员,你可以附加[NonSerialized]特性到任何不想序列化的数据成员:
    [Serializable]    public class MyType    {        private string label;        [NonSerialized]        private int cachedValue;        private OtherClass otherThing;    }


28 创建大粒度的网络服务API
通信协议的费用和不便决定你应如何使用媒介。这里演示一个糟糕的服务接口定义的常见缺点。无论你使用web服务、.NET远程或者基于Azure编程,你必须记得最昂贵的部分是当你在不同机器上传递对象。你必须停止简单地包装本地接口来创建远程API。它会工作,但是非常没效率。
  以客户订单为例,我们设计一个由中心服务器和桌面客户端组成的客户订单处理系统。客户端要通过web服务来访问信息。其中一个类就是客户类,如果你忽略了传输问题,可能的代码如下:
 public class Customer
    {
        public Customer()
        {
        }
        // Properties to access and modify customer fields:
        public string Name { get; set; }
        public Address ShippingAddr { get; set; }
        public Account CreditCardInfo { get; set; }
    }
Customer类没有包含远程调用的API,调用一个远程的customer结果将会花费过多的传输时间。
// create customer on the server.
Customer c = Server.NewCustomer();
// round trip to set the name.
c.Name = dlg.Name;
// round trip to set the addr.
c.ShippingAddr = dlg.ShippingAddr;
// round trip to set the cc card.
c.CreditCardInfo = dlg.CreditCardInfo;
相应地,你需要创建一个本地的Customer对象,在设置好所有字段后将它传递到服务端:
// create customer on the client.
Customer c2 = new Customer();
// Set local copy
c2.Name = dlg.Name;
// set the local addr.
c2.ShippingAddr = dlg.ShippingAddr;
// set the local cc card.
c2.CreditCardInfo = dlg.CreditCardInfo;
// send the finished object to the server. (one trip)
Server.AddCustomer(c2);
上面的Customer演示了一个简单的例子:在客户端和服务器之前来回传递对象。但是为了书写更有效的代码,你需要扩展简单例子来包含正确的关联对象。远程调用这样单个属性的设置粒度太小.
  假设这个系统支持超过100万的客户,你不能再用上面的简单模式,你需要设计一个更有效的方式来处理客户端和服务器之间的传输。
public CustomerSet RetrieveCustomerData(AreaCode theAreaCode)
{
// Find all customers for a given area code.
// Foreach customer in that area code:
// Find all orders by that customer.
// Filter out those that have already
// been received.
// Return the result.
}
一次性找到所有需要传输的数据,以一个大粒度的方式来传输这样能提高传输效率。
与远程机器通信,你会想最小化访问次数和传输的大小来提高效率,这两个目标是相对的,次数少了必然每次传输的内容将变大,你需要做的是如何找到一个平衡
点,你应该避免走两个极端(最大化访问次数或最大化传输大小),但应注意选择传输大粒度少次数会减少错误的发生。


29 支持泛型的协变和逆变
协变和逆变是指在一定条件下一个类型能代替另一个类型。只要有可能,你应该使用接口和委托来支持通用的协变和逆变,这样可以使你的API安全地使用在不同的方式。协变和逆变是两种不同方式的类型替换。
如果你能比类型声明更能替代派生类型的话,这个返回类型就是协变的。
如果你能比类型声明更能替代基础参数类型的话,这个参数类型就是逆变的。
面向对象的语言一般都支持参数类型的协变。你能传递一个派生类对象在任何需要基类类型参数的方法上。如:
Console.WriteLine()方法有一个重载方法需要一个System.Object类型的参数,所以你可以传递任何该类型(System.Object)的派生类的实例到该方法上。C#4.0之前,所有泛型都是不变的,Array被认为是协变的,但它不支持安全的协变。在4.0,新的关键字(in和out)使你可以使用泛型的协变和逆变。
如:
    abstract public class CelestialBody
    {
        public double Mass { get; set; }
        public string Name { get; set; }
        // elided
    }
    public class Planet : CelestialBody
    {
        // elided
    }
    public class Moon : CelestialBody
    {
        // elided
    }
    public class Asteroid : CelestialBody
    {
        // elided
    }
    public static void CoVariantArray(CelestialBody[] baseItems)
    {
        foreach (var thing in baseItems)
            Console.WriteLine("{0} has a mass of {1} Kg",
            thing.Name, thing.Mass);
    }
CoVariantArray方法以CelestialBody对象数组为参数,实现了安全的协变,下面的方法是不安全的协变:
    public static void UnsafeVariantArray(CelestialBody[] baseItems)
    {
        baseItems[0] = new Asteroid { Name = "Hygiea", Mass = 8.85e19 };
    }
赋值语句可能会抛出异常。4.0之前的泛型被认为是不变的,它必须要有严格的匹配。但在4.0,泛型接口被认为可能是协变或逆变的。
泛型的协变:
    public static void CoVariantGeneric(IEnumerable<CelestialBody> baseItems)
    {
        foreach (var thing in baseItems)
            Console.WriteLine("{0} has a mass of {1} Kg",
            thing.Name, thing.Mass);
    }
这个方法可以以List<Planet>参数类型来调用,因为IEnumerable<T>增强了T的输出限制。
    public interface IEnumerable<out T> : IEnumerable
    {
        IEnumerator<T> GetEnumerator();
    }
    public interface IEnumerator<out T> :IDisposable, IEnumerator
    {
        T Current { get; }
        // MoveNext(), Reset() inherited from IEnumerator
    }
IEnumerator<T>有一个重要的限制,注意IEnumerator<T>现在有一个带out的参数类型T,这强制编译器限制T的输出位置,输出现在被限制到函数返回值,属性访问器,以及委托。因此,使用IEnumerable<out T>,编译器知道你将查看序列中的每个T,但是不会修改序列源的内容。IEnumerable<T>只能被协变,因为IEnumerator<T>也是协变的。
  当然你也可以创建逆变的泛型接口和委托(也就是使用in代替out)。
public interface IComparable<in T>//支持逆变
{
  int CompareTo(T other);
}
创建协变和逆变的泛型委托:
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T, out TResult>(T arg);


30 偏向重写事件句柄(Event Handler)
大多数.NET类提供了2种不同的方式来处理事件,你可以附加事件处理程序,或者重写基类的virtual函数.为什么要提供两种方式来完成同样的事件?这是因为不同的情况会使用不同的方法。在派生类,你应总是重写virtual函数。在不相关的对象中使用事件处理程序来响应。  
重写virtual方法:
    public partial class Window1 : Window
    {
        // other code elided
        public Window1()
        {
            InitializeComponent();
        }
        protected override void OnMouseDown(MouseButtonEventArgs e)
        {
            DoMouseThings(e);
            base.OnMouseDown(e);
        }
    }  
你也可以附加事件处理如下(WPF程序):
<!-- XAML File -->
<Window x:Class="Item36_OverridesAndEvent.Window1"
  xmlns=
  "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Window1" Height="300" Width="300"
  MouseDown="OnMouseDown">
<Grid>
</Grid>
</Window>


// C Sharp file:public partial class Window1 : Window{// other code elided    public Window1()    {        InitializeComponent();    }    private void OnMouseDown(object sender,MouseButtonEventArgs e)    {        DoMouseThings(e);    }    private void DoMouseThings(MouseButtonEventArgs e)    {        throw new NotImplementedException();    }}

第一种方式(重写virtual)是首选。如果一个事件处理抛出了一个异常,事件链中没有其他的处理程序来调用。使用重写virtual,你的处理程序会首先调用,基类版本的virtual函数用来响应其他特定事件的处理,意思是如果你想基类的事件处理被调用,你必须调用base类。
使用override也比附加事件处理更有效率。
0 0