改善C#编程的50个建议(31-35)

来源:互联网 发布:西安软件公寓软件新城 编辑:程序博客网 时间:2024/06/03 09:59

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

题外话:关于C#_Effective(Covers C#4.0)一书的翻译也快已完结,由于水平有限,错误在所难免,尤其很多地方有所省略(一来英文水平有限,木办法;二来自己感觉原作者有时太啰嗦了大笑)或加上了自己的一些见解,所以还请各位自己明辨。

PS:由于一些无德网站恶意转载本博客文章(连转字都不带一个的),所以加上了上面的链接文字,防君子不防小人,还请尊重鄙人的劳动成果,感谢!


31 使用IComparable<T>和IComparer<T>实现顺序关系
你的类型需要顺序关系来描述集合元素怎么排序和查询。.NET框架定义了IComparable<T>和IComparer<T>两种接口来描述这种顺序关系。你可以定义你自己的关系操作实现(>,<,<=,>=)来提供指定类型的比较。
IComparable接口包含了一个方法:CompareTo(),IComparable<T>用在.NET的最新的API中,而旧的API使用IComparable接口.因此为了保持兼容性,当你实现IComparable<T>时,你也应该实现IComparable.IComparable带有System.Object类型的参数:

  public struct Customer : IComparable<Customer>, IComparable    {        private readonly string name;        public Customer(string name)        {            this.name = name;        }        #region IComparable<Customer> Members        public int CompareTo(Customer other)        {            return name.CompareTo(other.name);        }        #endregion        #region IComparable Members        int IComparable.CompareTo(object obj)        {            if (!(obj is Customer))                throw new ArgumentException("Argument is not a Customer", "obj");            Customer otherCustomer = (Customer)obj;            return this.CompareTo(otherCustomer);        }        #endregion    }


注意这里IComparable是显示实现的,由于其参数类型是object,你需要去检查运行时的参数类型.(obj is Customer),而且不适当的参数会产生拆箱和装箱,从而带来额外的运行时开销。
Customer c1;
Employee e1;
if (c1.CompareTo(e1) > 0)
Console.WriteLine("Customer one is greater");
此处会调用 public int CompareTo(Customer other)方法,而由于Employee类无法转换为Customer类型,从而导致这段代码不能成功编译。你必须通过显示转换来调用IComparable.CompareTo(object obj)这个方法来进行比较:
Customer c1 = new Customer();
Employee e1 = new Employee();
if ((c1 as IComparable).CompareTo(e1) > 0)
  Console.WriteLine("Customer one is greater");
对Customer增加操作符支持:
    // Relational Operators.    public static bool operator <(Customer left,    Customer right)    {        return left.CompareTo(right) < 0;    }    public static bool operator <=(Customer left,    Customer right)    {        return left.CompareTo(right) <= 0;    }    public static bool operator >(Customer left,    Customer right)    {        return left.CompareTo(right) > 0;    }    public static bool operator >=(Customer left,Customer right)    {        return left.CompareTo(right) >= 0;    }


32 避免ICloneable接口
ICloneable接口听起来感觉不错:你可以实现该接口以支持类型复制。一旦一个类型支持了ICloneable,所有它的派生类也必须支持它,所有它的成员类型也必须支持ICloneable或者有其他方式来创建复制。最终,当你创建包含web对象的设计时,支持深拷贝是非常有问题的。ICloneable它既支持深拷贝也支持浅拷贝。浅拷贝是创建复制所有成员变量的新对象,如果这些成员变量是引用类型,该新对象指向原对象所引用的。深拷贝也是创建复制所有成员变量的新对象,所有引用类型成员也按此方法递归复制(会复制引用类型变量的内容)。对于内置类型(如integer),深拷贝和浅拷贝返回的结果是相同的。很多时候,避免ICloneable可以使类简单化,而且使得类更容易实现、更容易使用。
  任何仅包含内置类型的值类型不需要支持ICloneable,一个简单的赋值会拷贝所有的值,这比Clone()更有效率。Clone()必须装箱它的返回类型到System.Object引用。但是如果值类型包含了引用类型成
呢?
    public struct ErrorMessage
    {
        private int errCode;
        private int details;
        private string msg;
        // details elided
    }
这里string类型是一个特殊的例子,因为它是不可变类型。一般情况下创建一个包含任意引用类型的struct是更复杂的,也相当少见。struct内置的赋值会创建一个浅拷贝,如果要创建深拷贝,你需要克隆其包含的引用类型成员,而且你需要这些引用类型成员支持深拷贝(有Clone()方法)。即使如此,它也只能在其引用类型成员支持ICloneable,包含的Clone()方法实现了深拷贝的情况下工作。
我们来考虑引用类型:
    class BaseType : ICloneable    {        private string label = "class name";        private int[] values = new int[10];        public object Clone()        {            BaseType rVal = new BaseType();            rVal.label = label;            for (int i = 0; i < values.Length; i++)                rVal.values[i] = values[i];            return rVal;        }    }    class Derived : BaseType    {        private double[] dValues = new double[10];        static void Main(string[] args)        {            Derived d = new Derived();            Derived d2 = d.Clone() as Derived;            if (d2 == null)                Console.WriteLine("null");        }    }

如果你运行此程序,你会发现d2的值是null。派生类继承了基类的ICloneable.Clone()方法,但是对于派生类来说这个实现是不正确的,因为它只克隆了基类,而基类的Clone()方法只创建了基类对象,而不是派生类对象。你可以在基类创建一个抽象的Clone()方法,这样来强制所有派生类来实现它,这样你需要为派生类定义一种方式来创建基类成员的复制,通常使用定义一个protected的复制构造函数,如:
class BaseType    {        private string label;        private int[] values;        protected BaseType()        {            label = "class name";            values = new int[10];        }        // Used by devived values to clone        protected BaseType(BaseType right)        {            label = right.label;            values = right.values.Clone() as int[];        }    }    sealed class Derived : BaseType, ICloneable    {        private double[] dValues = new double[10];        public Derived()        {            dValues = new double[10];        }        // Construct a copy        // using the base class copy ctor        private Derived(Derived right) :base(right)        {            dValues = right.dValues.Clone()            as double[];        }        public object Clone()        {            Derived rVal = new Derived(this);            return rVal;        }    }


这里基类不需要实现ICloneable.所有叶子类应是密封的,当有必要的时候实现ICloneable.
ICloneable有它的用处,但这只是例外而不是规则。这也是.NET框架更新支持泛型时没有增加ICloneable<T>泛型的意义所在。你应该永远不为值类型增加支持ICloneable。当一个复制操作真的需要时,你应为叶子类增加ICloneable支持。其他情况应避免使用ICloneable.


33 使用new修饰符仅仅在更新对应的基类方法时
  你使用new修饰符在一个从基类继承的非虚成员上,用来重定义成员。你能做什么事情
不是意味着你应该这样做。重定义非虚方法将会创建一个模糊的行为。
假如有以下代码执行相同的事情:
object c = MakeObject();
// Call through MyClass reference:
MyClass cl = c as MyClass;
cl.MagicMethod();
// Call through MyOtherClass reference:
MyOtherClass cl2 = c as MyOtherClass;
cl2.MagicMethod();
当new修饰符涉及时,事实就不是这样了:
    public class MyClass    {        public void MagicMethod()        {            // details elided.        }    }    public class MyOtherClass : MyClass    {        // Redefine MagicMethod for this class.        public new void MagicMethod()        {            // details elided        }    }


这种做法导致很多开发人员混淆了。如果你在同样的对象上调用同样的函数,你期待同样的代码被执行。而事实改变了,你使用new修改了函数的行为,它导致了和基类不一致。new修饰符它使得你增加了一个不同的方法在你的类的命名空间里。
非虚方法是静态绑定的,虚函数是动态绑定的,它是在运行时来动态调用对象类型的正确的函数的。
  只有一个情况例外,此时你可能需要使用new修饰符。你增加new修饰符来合并基类的新版本
该基类可能包含了你已经使用了的方法名,如:
public class MyWidget : BaseWidget{  public void new NormalizeValues() {    // details elided.    // Call the base class only if (by luck)    // the new method does the same operation.    base.NormalizeValues(); }}

34 避免重载基类中已定义的方法
当一个基类已经定义好成员的名字,它也就赋予了该名字相应的含义。派生类不应使用与此相同的名字来完成不同的功能。然而在某些情况下派生类可能仍然希望使用与基类成员相同的名字,它想用相同的名字不同的参数或方式来实现与基类成员名字相同的含义(virtual方法就是设计以不同的方式来实现相同含义的)。你不应重载基类已声明的方法。
  C#语言中,重载的规则一定是复杂的,因为重载方法可以声明在目标派生类,基类,任何扩展方法,接口等等
再加上泛型方法和泛型扩展方法,那就更复杂了。所以为了减少这种复杂,我们应尽量避免重载基类的方法。为什么一定要重载呢?我们完全可以使用一个不同的方法名来解决。注意区分重载和重写的区别:重写是覆盖基类的virtual方法,而重载是创建了多个相同名称不同参数类型或个数的方法。
让我们来看看重载基类方法存在的问题:
假设类层次结构如下:
public class B2 { }
public class D2 : B2 {}
然后有一个类如下:
    public class B
    {
        public void Foo(D2 parm)
        {
            Console.WriteLine("In B.Foo");
        }
    }
var obj1 = new D();
obj1.Foo(new D2());//输出In B.Foo
现在让我们增加派生类来重载基类方法:
    public class D : B
    {
        public void Foo(B2 parm)
        {
            Console.WriteLine("In D.Foo");
        }
    }
然后执行代码:
var obj2 = new D();
obj2.Foo(new D2());
obj2.Foo(new B2());
你期望的输出是什么呢?这里两次调用都是输出"In D.Foo".你可能希望的是第二次调用输出"In B.Foo",但结果却并非如此,都是调用了D的方法。
B obj3 = new D();
obj3.Foo(new D2());
上面的代码会输出"In B.Foo"。因为它是实例化D之后转换为了B类型,后面的方法自然会调用了B的。
var obj4 = new D();
((B)obj4).Foo(new D2());
obj4.Foo(new B2());
这样写就可以输出"In B.Foo"和"In D.Foo"了.
对于上面的代码,很多人都会迷糊,所以不应重载基类的方法,以免增加不必要的复杂性,只需要更换一个名字就可以很清楚的解决问题,何乐而不为呢?
    public class B
    {
        public void Foo(D2 parm)
        {
            Console.WriteLine("In B.Foo");
        }
        public void Bar(B2 parm)
        {
            Console.WriteLine("In B.Bar");
        }
    }


35 学习如何实现PLINQ的并行算法
多核编程本身是不简单的,但PLINQ使得其变简单了。
如计算前150个数的阶乘如下:
var nums = data.Where(m => m < 150).Select(n => Factorial(n));
改写成并行如下:
var numsParallel = data.AsParallel().Where(m => m < 150).Select(n => Factorial(n));
即简单地增加一个AsParallel()方法,改写为LINQ语法如下:
var nums = from n in data where n < 150 select Factorial(n);
var numsParallel = from n in data.AsParallel() where n < 150 select Factorial(n);
一旦你使用了AsParallel(),随后的操作将使用多进程处理。AsParallel()返回的是IParallelEnumerable(),而不是IEnumerable()。PLNQ是作为IParallelEnumerable的一组扩展方法来实现的。
上面的例子由于没有共享数据,对于返回结果的顺序也就没有要求,所以是非常简单的。
  每个并行查询都是先分区,PLINQ按数量分区输入元素并创建执行查询。分区是PLINQ中最重要的部分之一.
首先,分区不能花费很多时间,太多的时间花在分区上的话,真正处理数据的时间就少了。PLINQ使用四种不同的分区算法,它基于输入源和你创建的查询类型。最简单的分区是按范围分割,它按所给的输入任务数顺序地划分。
譬如:一个1000项的输入序列在一个四核的机器上将会创建每个250项的分割。范围分区用在当查询源支持索引序列并且能报告序列中有多少项的时候。也就是说按范围分区的查询源是类似List<T>、数组、以及其他支持IList<T>接口的序列。
  第二个是按块分区。该算法的内部会随时间持续改变。当任务需要更多工作时,它随时都会给每个任务一块输入项。
  另两个分区方案是针对特定的查询操作。一个是条纹分区,条纹分区是一种特殊的范围分区,用来优化处理序列的开始元素。每个处理的工作线程会跳跃N个项目然后处理M个项目,如此循环直到结束。还有一个是哈希分区。哈希分区是一个用来处理连接查询、分组连接查询、分组、去重、联合、交集等的特殊算法。那些耗时的操作加上特定的分区算法能实现更高的查询并行化。
哈希分区确保所有处理相同任务的项生成相同的哈希值。
  除了分区,这里还有三种算法被PLINQ用在并行任务中:管道、停止和启动、反向枚举。默认是使用管道的。
  使用管道,一个线程来处理枚举,多个线程来处理查询序列的每个元素。在管道模式,有多少数量的CPU内核,则使用多少数量的线程。
  停止和启动意思是开始枚举的线程将假如所有运行查询表达式的线程中去。这种方法用在当你使用ToList()或ToArray()或者任何随时都需要返回全部结果的PLINQ中。
如下:
var stopAndGoArray = (from n in data.AsParallel()
 where n < 150
 select Factorial(n)).ToArray();
var stopAndGoList = (from n in data.AsParallel()
where n < 150
select Factorial(n)).ToList();
使用停止和启动模式,在高内存使用率的时候,你可以得到些微的性能提高。
  反向枚举不产生一个结果。它会在每个查询表达式的结果上执行一些动作。如:
var nums2 = from n in data.AsParallel()
where n < 150
select Factorial(n);
nums2.ForAll(item => Console.WriteLine(item));
反向枚举将比停止和启动使用更少的内存,它通常是最快的枚举方法。
  所有的LINQ查询都是被动执行的。它们是在当你访问查询的结果元素时才去执行。PLINQ的工作方式有所不同,它更像LINQ到SQL,或者实体框架。
LINQ到SQL模式是指当你访问第一个元素时,整个结果序列就已经生成。
  并行算法受限于Amdahl规则。使用多处理器的程序的加速比受制于程序的连续运行。更多的PLINQ内容请查询MSDN: http://msdn.microsoft.com/zh-cn/library/dd460688(v=vs.110).aspx

0 1