程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> 改善C#編程的50個建議(31-35)

改善C#編程的50個建議(31-35)

編輯:C#入門知識

-------------------------翻譯 By Cryking-----------------------------
-----------------------轉載請注明出處,謝謝!------------------------

題外話:關於C#_Effective(Covers C#4.0)一書的翻譯也快已完結,由於水平有限,錯誤在所難免,尤其很多地方有所省略(一來英文水平有限,木辦法;二來自己感覺原作者有時太啰嗦了大笑)或加上了自己的一些見解,所以還請各位自己明辨。

PS:由於一些無德網站惡意轉載本博客文章(連轉字都不帶一個的),所以加上了上面的鏈接文字,防君子不防小人,還請尊重鄙人的勞動成果,感謝!


31 使用IComparable和IComparer實現順序關系
你的類型需要順序關系來描述集合元素怎麼排序和查詢。.NET框架定義了IComparable和IComparer兩種接口來描述這種順序關系。你可以定義你自己的關系操作實現(>,<,<=,>=)來提供指定類型的比較。
IComparable接口包含了一個方法:CompareTo(),IComparable用在.NET的最新的API中,而舊的API使用IComparable接口.因此為了保持兼容性,當你實現IComparable時,你也應該實現IComparable.IComparable帶有System.Object類型的參數:

  public struct Customer : IComparable, IComparable
    {
        private readonly string name;
        public Customer(string name)
        {
            this.name = name;
        }
        #region IComparable 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泛型的意義所在。你應該永遠不為值類型增加支持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、數組、以及其他支持IList接口的序列。
第二個是按塊分區。該算法的內部會隨時間持續改變。當任務需要更多工作時,它隨時都會給每個任務一塊輸入項。
另兩個分區方案是針對特定的查詢操作。一個是條紋分區,條紋分區是一種特殊的范圍分區,用來優化處理序列的開始元素。每個處理的工作線程會跳躍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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved